mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search: Implement basic improved UI (#46758)
This commit is contained in:
parent
851c54b3b1
commit
4449439a41
@ -146,6 +146,7 @@
|
|||||||
"@types/react-loadable": "5.5.6",
|
"@types/react-loadable": "5.5.6",
|
||||||
"@types/react-redux": "7.1.23",
|
"@types/react-redux": "7.1.23",
|
||||||
"@types/react-router-dom": "5.3.3",
|
"@types/react-router-dom": "5.3.3",
|
||||||
|
"@types/react-table": "^7",
|
||||||
"@types/react-test-renderer": "17.0.1",
|
"@types/react-test-renderer": "17.0.1",
|
||||||
"@types/react-transition-group": "4.4.4",
|
"@types/react-transition-group": "4.4.4",
|
||||||
"@types/react-virtualized-auto-sizer": "1.0.1",
|
"@types/react-virtualized-auto-sizer": "1.0.1",
|
||||||
@ -353,6 +354,7 @@
|
|||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-select": "5.2.2",
|
"react-select": "5.2.2",
|
||||||
"react-split-pane": "0.1.92",
|
"react-split-pane": "0.1.92",
|
||||||
|
"react-table": "^7.7.0",
|
||||||
"react-transition-group": "4.4.2",
|
"react-transition-group": "4.4.2",
|
||||||
"react-use": "17.3.2",
|
"react-use": "17.3.2",
|
||||||
"react-virtualized-auto-sizer": "1.0.6",
|
"react-virtualized-auto-sizer": "1.0.6",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DataFrame } from '../types/dataFrame';
|
import { DataFrame, Field } from '../types/dataFrame';
|
||||||
import { DisplayProcessor } from '../types';
|
import { DisplayProcessor } from '../types';
|
||||||
import { FunctionalVector } from '../vector/FunctionalVector';
|
import { FunctionalVector } from '../vector/FunctionalVector';
|
||||||
|
|
||||||
@ -16,13 +16,22 @@ import { FunctionalVector } from '../vector/FunctionalVector';
|
|||||||
export class DataFrameView<T = any> extends FunctionalVector<T> {
|
export class DataFrameView<T = any> extends FunctionalVector<T> {
|
||||||
private index = 0;
|
private index = 0;
|
||||||
private obj: T;
|
private obj: T;
|
||||||
|
readonly fields: {
|
||||||
|
readonly [Property in keyof T]: Field<T[Property]>;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private data: DataFrame) {
|
constructor(private data: DataFrame) {
|
||||||
super();
|
super();
|
||||||
const obj = {} as unknown as T;
|
const obj = {} as unknown as T;
|
||||||
|
const fields = {} as any;
|
||||||
|
|
||||||
for (let i = 0; i < data.fields.length; i++) {
|
for (let i = 0; i < data.fields.length; i++) {
|
||||||
const field = data.fields[i];
|
const field = data.fields[i];
|
||||||
|
if (!field.name) {
|
||||||
|
continue; // unsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
fields[field.name] = field;
|
||||||
const getter = () => field.values.get(this.index);
|
const getter = () => field.values.get(this.index);
|
||||||
|
|
||||||
if (!(obj as any).hasOwnProperty(field.name)) {
|
if (!(obj as any).hasOwnProperty(field.name)) {
|
||||||
@ -41,6 +50,7 @@ export class DataFrameView<T = any> extends FunctionalVector<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.obj = obj;
|
this.obj = obj;
|
||||||
|
this.fields = fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
get dataFrame() {
|
get dataFrame() {
|
||||||
|
@ -21,4 +21,7 @@ export enum DataFrameType {
|
|||||||
* All values in the grid exist and have regular spacing
|
* All values in the grid exist and have regular spacing
|
||||||
*/
|
*/
|
||||||
HeatmapScanlines = 'heatmap-scanlines',
|
HeatmapScanlines = 'heatmap-scanlines',
|
||||||
|
|
||||||
|
/** Directory listing */
|
||||||
|
DirectoryListing = 'directory-listing',
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ export function getColumns(
|
|||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent {
|
export function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent {
|
||||||
switch (displayMode) {
|
switch (displayMode) {
|
||||||
case TableCellDisplayMode.ColorText:
|
case TableCellDisplayMode.ColorText:
|
||||||
case TableCellDisplayMode.ColorBackground:
|
case TableCellDisplayMode.ColorBackground:
|
||||||
|
@ -12,7 +12,7 @@ func logf(format string, a ...interface{}) {
|
|||||||
|
|
||||||
// nolint:gocyclo
|
// nolint:gocyclo
|
||||||
// ReadDashboard will take a byte stream and return dashboard info
|
// ReadDashboard will take a byte stream and return dashboard info
|
||||||
func ReadDashboard(stream io.Reader, datasource DatasourceLookup) *DashboardInfo {
|
func ReadDashboard(stream io.Reader, lookup DatasourceLookup) *DashboardInfo {
|
||||||
iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024)
|
iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024)
|
||||||
dash := &DashboardInfo{}
|
dash := &DashboardInfo{}
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ func ReadDashboard(stream io.Reader, datasource DatasourceLookup) *DashboardInfo
|
|||||||
|
|
||||||
case "panels":
|
case "panels":
|
||||||
for iter.ReadArray() {
|
for iter.ReadArray() {
|
||||||
dash.Panels = append(dash.Panels, readPanelInfo(iter))
|
dash.Panels = append(dash.Panels, readPanelInfo(iter, lookup))
|
||||||
}
|
}
|
||||||
|
|
||||||
case "rows":
|
case "rows":
|
||||||
@ -129,16 +129,29 @@ func ReadDashboard(stream io.Reader, datasource DatasourceLookup) *DashboardInfo
|
|||||||
logf("All dashbaords should have a UID defined")
|
logf("All dashbaords should have a UID defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targets := newTargetInfo(lookup)
|
||||||
|
for _, panel := range dash.Panels {
|
||||||
|
targets.addPanel(panel)
|
||||||
|
}
|
||||||
|
dash.Datasource = targets.GetDatasourceInfo()
|
||||||
|
|
||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
|
|
||||||
// will always return strings for now
|
// will always return strings for now
|
||||||
func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
|
func readPanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelInfo {
|
||||||
panel := PanelInfo{}
|
panel := PanelInfo{}
|
||||||
|
|
||||||
|
targets := newTargetInfo(lookup)
|
||||||
|
|
||||||
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
|
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
|
||||||
// Skip null values so we don't need special int handling
|
|
||||||
if iter.WhatIsNext() == jsoniter.NilValue {
|
if iter.WhatIsNext() == jsoniter.NilValue {
|
||||||
|
if l1Field == "datasource" {
|
||||||
|
targets.addDatasource(iter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip null values so we don't need special int handling
|
||||||
iter.Skip()
|
iter.Skip()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -160,13 +173,11 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
|
|||||||
panel.PluginVersion = iter.ReadString() // since 7x (the saved version for the plugin model)
|
panel.PluginVersion = iter.ReadString() // since 7x (the saved version for the plugin model)
|
||||||
|
|
||||||
case "datasource":
|
case "datasource":
|
||||||
v := iter.Read()
|
targets.addDatasource(iter)
|
||||||
logf(">>Panel.datasource = %v\n", v) // string or object!!!
|
|
||||||
|
|
||||||
case "targets":
|
case "targets":
|
||||||
for iter.ReadArray() {
|
for iter.ReadArray() {
|
||||||
v := iter.Read()
|
targets.addTarget(iter)
|
||||||
logf("[Panel.TARGET] %v\n", v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "transformations":
|
case "transformations":
|
||||||
@ -183,7 +194,7 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
|
|||||||
// Rows have nested panels
|
// Rows have nested panels
|
||||||
case "panels":
|
case "panels":
|
||||||
for iter.ReadArray() {
|
for iter.ReadArray() {
|
||||||
panel.Collapsed = append(panel.Collapsed, readPanelInfo(iter))
|
panel.Collapsed = append(panel.Collapsed, readPanelInfo(iter, lookup))
|
||||||
}
|
}
|
||||||
|
|
||||||
case "options":
|
case "options":
|
||||||
@ -201,5 +212,7 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
panel.Datasource = targets.GetDatasourceInfo()
|
||||||
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
|
@ -12,19 +12,36 @@ import (
|
|||||||
|
|
||||||
func TestReadDashboard(t *testing.T) {
|
func TestReadDashboard(t *testing.T) {
|
||||||
inputs := []string{
|
inputs := []string{
|
||||||
"all-panels.json",
|
"check-string-datasource-id",
|
||||||
"panel-graph/graph-shared-tooltips.json",
|
"all-panels",
|
||||||
|
"panel-graph/graph-shared-tooltips",
|
||||||
}
|
}
|
||||||
|
|
||||||
// key will allow name or uid
|
// key will allow name or uid
|
||||||
ds := func(key string) *DatasourceInfo {
|
ds := func(ref *DataSourceRef) *DataSourceRef {
|
||||||
return nil // TODO!
|
if ref == nil || ref.UID == "" {
|
||||||
|
return &DataSourceRef{
|
||||||
|
UID: "default.uid",
|
||||||
|
Type: "default.type",
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
devdash := "../../../../devenv/dev-dashboards/"
|
||||||
|
|
||||||
for _, input := range inputs {
|
for _, input := range inputs {
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
// We can ignore the gosec G304 warning because this is a test with hardcoded input values
|
// We can ignore the gosec G304 warning because this is a test with hardcoded input values
|
||||||
f, err := os.Open("../../../../devenv/dev-dashboards/" + input)
|
f, err := os.Open(filepath.Join(devdash, input) + ".json")
|
||||||
|
if err == nil {
|
||||||
|
input = "devdash-" + filepath.Base(input)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// nolint:gosec
|
||||||
|
// We can ignore the gosec G304 warning because this is a test with hardcoded input values
|
||||||
|
f, err = os.Open(filepath.Join("testdata", input) + ".json")
|
||||||
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dash := ReadDashboard(f, ds)
|
dash := ReadDashboard(f, ds)
|
||||||
@ -32,7 +49,7 @@ func TestReadDashboard(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
update := false
|
update := false
|
||||||
savedPath := "testdata/" + filepath.Base(input)
|
savedPath := "testdata/" + input + "-info.json"
|
||||||
saved, err := os.ReadFile(savedPath)
|
saved, err := os.ReadFile(savedPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
update = true
|
update = true
|
||||||
|
81
pkg/services/searchV2/extract/targets.go
Normal file
81
pkg/services/searchV2/extract/targets.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package extract
|
||||||
|
|
||||||
|
import (
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type targetInfo struct {
|
||||||
|
lookup DatasourceLookup
|
||||||
|
uids map[string]*DataSourceRef
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTargetInfo(lookup DatasourceLookup) targetInfo {
|
||||||
|
return targetInfo{
|
||||||
|
lookup: lookup,
|
||||||
|
uids: make(map[string]*DataSourceRef),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *targetInfo) GetDatasourceInfo() []DataSourceRef {
|
||||||
|
keys := make([]DataSourceRef, len(s.uids))
|
||||||
|
i := 0
|
||||||
|
for _, v := range s.uids {
|
||||||
|
keys[i] = *v
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// the node will either be string (name|uid) OR ref
|
||||||
|
func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
|
||||||
|
switch iter.WhatIsNext() {
|
||||||
|
case jsoniter.StringValue:
|
||||||
|
key := iter.ReadString()
|
||||||
|
ds := s.lookup(&DataSourceRef{UID: key})
|
||||||
|
s.addRef(ds)
|
||||||
|
|
||||||
|
case jsoniter.NilValue:
|
||||||
|
s.addRef(s.lookup(nil))
|
||||||
|
iter.Skip()
|
||||||
|
|
||||||
|
case jsoniter.ObjectValue:
|
||||||
|
ref := &DataSourceRef{}
|
||||||
|
iter.ReadVal(ref)
|
||||||
|
ds := s.lookup(ref)
|
||||||
|
s.addRef(ds)
|
||||||
|
|
||||||
|
default:
|
||||||
|
v := iter.Read()
|
||||||
|
logf("[Panel.datasource.unknown] %v\n", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *targetInfo) addRef(ref *DataSourceRef) {
|
||||||
|
if ref != nil && ref.UID != "" {
|
||||||
|
s.uids[ref.UID] = ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *targetInfo) addTarget(iter *jsoniter.Iterator) {
|
||||||
|
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
|
||||||
|
switch l1Field {
|
||||||
|
case "datasource":
|
||||||
|
s.addDatasource(iter)
|
||||||
|
|
||||||
|
case "refId":
|
||||||
|
iter.Skip()
|
||||||
|
|
||||||
|
default:
|
||||||
|
v := iter.Read()
|
||||||
|
logf("[Panel.TARGET] %s=%v\n", l1Field, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *targetInfo) addPanel(panel PanelInfo) {
|
||||||
|
for idx, v := range panel.Datasource {
|
||||||
|
if v.UID != "" {
|
||||||
|
s.uids[v.UID] = &panel.Datasource[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
pkg/services/searchV2/extract/testdata/check-string-datasource-id-info.json
vendored
Normal file
29
pkg/services/searchV2/extract/testdata/check-string-datasource-id-info.json
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"id": 250,
|
||||||
|
"uid": "K2X7hzwGk",
|
||||||
|
"title": "fast streaming",
|
||||||
|
"tags": null,
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Panel Title",
|
||||||
|
"type": "timeseries",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 27,
|
||||||
|
"linkCount": 0,
|
||||||
|
"timeFrom": "now-30s",
|
||||||
|
"timeTo": "now",
|
||||||
|
"timezone": ""
|
||||||
|
}
|
67
pkg/services/searchV2/extract/testdata/check-string-datasource-id.json
vendored
Normal file
67
pkg/services/searchV2/extract/testdata/check-string-datasource-id.json
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations \u0026 Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 250,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"fieldConfig": {},
|
||||||
|
"gridPos": {},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"graph": {},
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltipOptions": {
|
||||||
|
"mode": "single"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"channel": "stream/telegraf/cpu",
|
||||||
|
"filter": {
|
||||||
|
"fields": []
|
||||||
|
},
|
||||||
|
"queryType": "measurements",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Panel Title",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 27,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-30s",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "fast streaming",
|
||||||
|
"uid": "K2X7hzwGk",
|
||||||
|
"version": 8
|
||||||
|
}
|
122
pkg/services/searchV2/extract/testdata/devdash-graph-shared-tooltips-info.json
vendored
Normal file
122
pkg/services/searchV2/extract/testdata/devdash-graph-shared-tooltips-info.json
vendored
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"uid": "TX2VU59MZ",
|
||||||
|
"title": "Panel Tests - shared tooltips",
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"panel-tests",
|
||||||
|
"graph-ng"
|
||||||
|
],
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "two units",
|
||||||
|
"type": "timeseries",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"title": "Speed vs Temperature (XY)",
|
||||||
|
"type": "xychart",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transformations": [
|
||||||
|
"seriesToColumns",
|
||||||
|
"organize"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Cursor info",
|
||||||
|
"type": "debug",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "Only temperature",
|
||||||
|
"type": "timeseries",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"title": "Only Speed",
|
||||||
|
"type": "timeseries",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"title": "Panel Title",
|
||||||
|
"type": "timeseries",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"title": "flot panel (temperature)",
|
||||||
|
"type": "graph",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"title": "flot panel (no units)",
|
||||||
|
"type": "graph",
|
||||||
|
"pluginVersion": "7.5.0-pre",
|
||||||
|
"datasource": [
|
||||||
|
{
|
||||||
|
"uid": "default.uid",
|
||||||
|
"type": "default.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 28,
|
||||||
|
"linkCount": 0,
|
||||||
|
"timeFrom": "2020-09-14T16:13:20.000Z",
|
||||||
|
"timeTo": "2020-09-15T20:00:00.000Z",
|
||||||
|
"timezone": ""
|
||||||
|
}
|
@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"uid": "TX2VU59MZ",
|
|
||||||
"title": "Panel Tests - shared tooltips",
|
|
||||||
"tags": [
|
|
||||||
"gdev",
|
|
||||||
"panel-tests",
|
|
||||||
"graph-ng"
|
|
||||||
],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "two units",
|
|
||||||
"type": "timeseries",
|
|
||||||
"pluginVersion": "7.5.0-pre"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 13,
|
|
||||||
"title": "Speed vs Temperature (XY)",
|
|
||||||
"type": "xychart",
|
|
||||||
"pluginVersion": "7.5.0-pre",
|
|
||||||
"transformations": [
|
|
||||||
"seriesToColumns",
|
|
||||||
"organize"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Cursor info",
|
|
||||||
"type": "debug",
|
|
||||||
"pluginVersion": "7.5.0-pre"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Only temperature",
|
|
||||||
"type": "timeseries",
|
|
||||||
"pluginVersion": "7.5.0-pre"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 9,
|
|
||||||
"title": "Only Speed",
|
|
||||||
"type": "timeseries",
|
|
||||||
"pluginVersion": "7.5.0-pre"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 11,
|
|
||||||
"title": "Panel Title",
|
|
||||||
"type": "timeseries",
|
|
||||||
"pluginVersion": "7.5.0-pre"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 8,
|
|
||||||
"title": "flot panel (temperature)",
|
|
||||||
"type": "graph",
|
|
||||||
"pluginVersion": "7.5.0-pre"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"title": "flot panel (no units)",
|
|
||||||
"type": "graph",
|
|
||||||
"pluginVersion": "7.5.0-pre"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"schemaVersion": 28,
|
|
||||||
"linkCount": 0,
|
|
||||||
"timeFrom": "2020-09-14T16:13:20.000Z",
|
|
||||||
"timeTo": "2020-09-15T20:00:00.000Z",
|
|
||||||
"timezone": ""
|
|
||||||
}
|
|
@ -1,13 +1,11 @@
|
|||||||
package extract
|
package extract
|
||||||
|
|
||||||
type DatasourceLookup = func(key string) *DatasourceInfo
|
// empty everything will return the default
|
||||||
|
type DatasourceLookup = func(ref *DataSourceRef) *DataSourceRef
|
||||||
|
|
||||||
type DatasourceInfo struct {
|
type DataSourceRef struct {
|
||||||
UID string `json:"uid"`
|
UID string `json:"uid,omitempty"`
|
||||||
Name string `json:"name"`
|
Type string `json:"type,omitempty"`
|
||||||
Type string `json:"type"` // plugin name
|
|
||||||
Version string `json:"version"`
|
|
||||||
Access string `json:"access,omitempty"` // proxy, direct, or empty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PanelInfo struct {
|
type PanelInfo struct {
|
||||||
@ -16,8 +14,7 @@ type PanelInfo struct {
|
|||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Type string `json:"type,omitempty"` // PluginID
|
Type string `json:"type,omitempty"` // PluginID
|
||||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||||
Datasource []string `json:"datasource,omitempty"` // UIDs
|
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||||
DatasourceType []string `json:"datasourceType,omitempty"` // PluginIDs
|
|
||||||
Transformations []string `json:"transformations,omitempty"` // ids of the transformation steps
|
Transformations []string `json:"transformations,omitempty"` // ids of the transformation steps
|
||||||
|
|
||||||
// Rows define panels as sub objects
|
// Rows define panels as sub objects
|
||||||
@ -25,15 +22,13 @@ type PanelInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DashboardInfo struct {
|
type DashboardInfo struct {
|
||||||
ID int64 `json:"id,omitempty"`
|
ID int64 `json:"id,omitempty"` // internal ID
|
||||||
UID string `json:"uid,omitempty"`
|
UID string `json:"uid,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Tags []string `json:"tags"` // UIDs
|
Tags []string `json:"tags"`
|
||||||
Datasource []string `json:"datasource,omitempty"` // UIDs
|
|
||||||
DatasourceType []string `json:"datasourceType,omitempty"` // PluginIDs
|
|
||||||
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
|
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
|
||||||
|
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||||
Panels []PanelInfo `json:"panels"` // nesed documents
|
Panels []PanelInfo `json:"panels"` // nesed documents
|
||||||
SchemaVersion int64 `json:"schemaVersion"`
|
SchemaVersion int64 `json:"schemaVersion"`
|
||||||
LinkCount int64 `json:"linkCount"`
|
LinkCount int64 `json:"linkCount"`
|
||||||
|
@ -89,7 +89,7 @@ func (s *StandardSearchService) applyAuthFilter(user *models.SignedInUser, dash
|
|||||||
// create a list of all viewable dashboards for this user
|
// create a list of all viewable dashboards for this user
|
||||||
res := make([]dashMeta, 0, len(dash))
|
res := make([]dashMeta, 0, len(dash))
|
||||||
for _, dash := range dash {
|
for _, dash := range dash {
|
||||||
if filter(dash.dash.UID) {
|
if filter(dash.dash.UID) || (dash.is_folder && dash.dash.UID == "") { // include the "General" folder
|
||||||
res = append(res, dash)
|
res = append(res, dash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,15 +106,38 @@ type dashDataQueryResult struct {
|
|||||||
Updated time.Time
|
Updated time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dsQueryResult struct {
|
||||||
|
UID string `xorm:"uid"`
|
||||||
|
Type string `xorm:"type"`
|
||||||
|
Name string `xorm:"name"`
|
||||||
|
IsDefault bool `xorm:"is_default"`
|
||||||
|
}
|
||||||
|
|
||||||
func loadDashboards(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) ([]dashMeta, error) {
|
func loadDashboards(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) ([]dashMeta, error) {
|
||||||
meta := make([]dashMeta, 0, 200)
|
meta := make([]dashMeta, 0, 200)
|
||||||
|
|
||||||
|
// Add the root folder ID (does not exist in SQL)
|
||||||
|
meta = append(meta, dashMeta{
|
||||||
|
id: 0,
|
||||||
|
is_folder: true,
|
||||||
|
folder_id: 0,
|
||||||
|
slug: "",
|
||||||
|
created: time.Now(),
|
||||||
|
updated: time.Now(),
|
||||||
|
dash: &extract.DashboardInfo{
|
||||||
|
ID: 0,
|
||||||
|
UID: "",
|
||||||
|
Title: "General",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// key will allow name or uid
|
// key will allow name or uid
|
||||||
lookup := func(key string) *extract.DatasourceInfo {
|
lookup, err := loadDatasoureLookup(ctx, orgID, sql)
|
||||||
return nil // TODO!
|
if err != nil {
|
||||||
|
return meta, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
err = sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
rows := make([]*dashDataQueryResult, 0)
|
rows := make([]*dashDataQueryResult, 0)
|
||||||
|
|
||||||
sess.Table("dashboard").
|
sess.Table("dashboard").
|
||||||
@ -146,6 +169,64 @@ func loadDashboards(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) ([
|
|||||||
return meta, err
|
return meta, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadDatasoureLookup(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) (extract.DatasourceLookup, error) {
|
||||||
|
byUID := make(map[string]*extract.DataSourceRef, 50)
|
||||||
|
byName := make(map[string]*extract.DataSourceRef, 50)
|
||||||
|
var defaultDS *extract.DataSourceRef
|
||||||
|
|
||||||
|
err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
rows := make([]*dsQueryResult, 0)
|
||||||
|
sess.Table("data_source").
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Cols("uid", "name", "type", "is_default")
|
||||||
|
|
||||||
|
err := sess.Find(&rows)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
ds := &extract.DataSourceRef{
|
||||||
|
UID: row.UID,
|
||||||
|
Type: row.Type,
|
||||||
|
}
|
||||||
|
byUID[row.UID] = ds
|
||||||
|
byName[row.Name] = ds
|
||||||
|
if row.IsDefault {
|
||||||
|
defaultDS = ds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup by UID or name
|
||||||
|
return func(ref *extract.DataSourceRef) *extract.DataSourceRef {
|
||||||
|
if ref == nil {
|
||||||
|
return defaultDS
|
||||||
|
}
|
||||||
|
key := ""
|
||||||
|
if ref.UID != "" {
|
||||||
|
ds, ok := byUID[ref.UID]
|
||||||
|
if ok {
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
key = ref.UID
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
return defaultDS
|
||||||
|
}
|
||||||
|
ds, ok := byUID[key]
|
||||||
|
if ok {
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
return byName[key]
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
type simpleCounter struct {
|
type simpleCounter struct {
|
||||||
values map[string]int64
|
values map[string]int64
|
||||||
}
|
}
|
||||||
@ -173,10 +254,12 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
|||||||
folderID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
folderID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||||
folderUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
folderUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||||
folderName := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
folderName := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||||
|
folderDashCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||||
|
|
||||||
folderID.Name = "ID"
|
folderID.Name = "id"
|
||||||
folderUID.Name = "UID"
|
folderUID.Name = "uid"
|
||||||
folderName.Name = "Name"
|
folderName.Name = "name"
|
||||||
|
folderDashCount.Name = "DashCount"
|
||||||
|
|
||||||
dashID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
dashID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||||
dashUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
dashUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||||
@ -188,22 +271,28 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
|||||||
dashUpdated := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
|
dashUpdated := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
|
||||||
dashSchemaVersion := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
dashSchemaVersion := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||||
dashTags := data.NewFieldFromFieldType(data.FieldTypeNullableString, 0)
|
dashTags := data.NewFieldFromFieldType(data.FieldTypeNullableString, 0)
|
||||||
|
dashPanelCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||||
|
dashVarCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||||
|
dashDSList := data.NewFieldFromFieldType(data.FieldTypeNullableString, 0)
|
||||||
|
|
||||||
dashID.Name = "ID"
|
dashID.Name = "id"
|
||||||
dashUID.Name = "UID"
|
dashUID.Name = "uid"
|
||||||
dashFolderID.Name = "FolderID"
|
dashFolderID.Name = "folderID"
|
||||||
dashName.Name = "Name"
|
dashName.Name = "name"
|
||||||
dashDescr.Name = "Description"
|
dashDescr.Name = "description"
|
||||||
dashTags.Name = "Tags"
|
dashTags.Name = "tags"
|
||||||
dashSchemaVersion.Name = "SchemaVersion"
|
dashSchemaVersion.Name = "SchemaVersion"
|
||||||
dashCreated.Name = "Created"
|
dashCreated.Name = "Created"
|
||||||
dashUpdated.Name = "Updated"
|
dashUpdated.Name = "Updated"
|
||||||
dashURL.Name = "URL"
|
dashURL.Name = "url"
|
||||||
dashURL.Config = &data.FieldConfig{
|
dashURL.Config = &data.FieldConfig{
|
||||||
Links: []data.DataLink{
|
Links: []data.DataLink{
|
||||||
{Title: "link", URL: "${__value.text}"},
|
{Title: "link", URL: "${__value.text}"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
dashPanelCount.Name = "panelCount"
|
||||||
|
dashVarCount.Name = "varCount"
|
||||||
|
dashDSList.Name = "datasource"
|
||||||
|
|
||||||
dashTags.Config = &data.FieldConfig{
|
dashTags.Config = &data.FieldConfig{
|
||||||
Custom: map[string]interface{}{
|
Custom: map[string]interface{}{
|
||||||
@ -218,11 +307,11 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
|||||||
panelDescr := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
panelDescr := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||||
panelType := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
panelType := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||||
|
|
||||||
panelDashID.Name = "DashboardID"
|
panelDashID.Name = "dashboardID"
|
||||||
panelID.Name = "ID"
|
panelID.Name = "id"
|
||||||
panelName.Name = "Name"
|
panelName.Name = "name"
|
||||||
panelDescr.Name = "Description"
|
panelDescr.Name = "description"
|
||||||
panelType.Name = "Type"
|
panelType.Name = "type"
|
||||||
|
|
||||||
panelTypeCounter := simpleCounter{
|
panelTypeCounter := simpleCounter{
|
||||||
values: make(map[string]int64, 30),
|
values: make(map[string]int64, 30),
|
||||||
@ -232,12 +321,14 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
|||||||
values: make(map[string]int64, 30),
|
values: make(map[string]int64, 30),
|
||||||
}
|
}
|
||||||
|
|
||||||
var tags *string
|
folderCounter := make(map[int64]int64, 20)
|
||||||
|
|
||||||
for _, row := range meta {
|
for _, row := range meta {
|
||||||
if row.is_folder {
|
if row.is_folder {
|
||||||
folderID.Append(row.id)
|
folderID.Append(row.id)
|
||||||
folderUID.Append(row.dash.UID)
|
folderUID.Append(row.dash.UID)
|
||||||
folderName.Append(row.dash.Title)
|
folderName.Append(row.dash.Title)
|
||||||
|
folderDashCount.Append(int64(0)) // filled in later
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,22 +341,23 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
|||||||
dashCreated.Append(row.created)
|
dashCreated.Append(row.created)
|
||||||
dashUpdated.Append(row.updated)
|
dashUpdated.Append(row.updated)
|
||||||
|
|
||||||
|
// Increment the folder counter
|
||||||
|
fcount, ok := folderCounter[row.folder_id]
|
||||||
|
if !ok {
|
||||||
|
fcount = 0
|
||||||
|
}
|
||||||
|
folderCounter[row.folder_id] = fcount + 1
|
||||||
|
|
||||||
url := fmt.Sprintf("/d/%s/%s", row.dash.UID, row.slug)
|
url := fmt.Sprintf("/d/%s/%s", row.dash.UID, row.slug)
|
||||||
dashURL.Append(url)
|
dashURL.Append(url)
|
||||||
|
|
||||||
// stats
|
// stats
|
||||||
schemaVersionCounter.add(strconv.FormatInt(row.dash.SchemaVersion, 10))
|
schemaVersionCounter.add(strconv.FormatInt(row.dash.SchemaVersion, 10))
|
||||||
|
|
||||||
// Send tags as JSON array
|
dashTags.Append(toJSONString(row.dash.Tags))
|
||||||
tags = nil
|
dashPanelCount.Append(int64(len(row.dash.Panels)))
|
||||||
if len(row.dash.Tags) > 0 {
|
dashVarCount.Append(int64(len(row.dash.TemplateVars)))
|
||||||
b, err := json.Marshal(row.dash.Tags)
|
dashDSList.Append(dsAsJSONString(row.dash.Datasource))
|
||||||
if err == nil {
|
|
||||||
s := string(b)
|
|
||||||
tags = &s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dashTags.Append(tags)
|
|
||||||
|
|
||||||
// Row for each panel
|
// Row for each panel
|
||||||
for _, panel := range row.dash.Panels {
|
for _, panel := range row.dash.Panels {
|
||||||
@ -278,11 +370,47 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the folder counts
|
||||||
|
for i := 0; i < folderID.Len(); i++ {
|
||||||
|
id, ok := folderID.At(i).(int64)
|
||||||
|
if ok {
|
||||||
|
folderDashCount.Set(i, folderCounter[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data.Frames{
|
return data.Frames{
|
||||||
data.NewFrame("folders", folderID, folderUID, folderName),
|
data.NewFrame("folders", folderID, folderUID, folderName, folderDashCount),
|
||||||
data.NewFrame("dashboards", dashID, dashUID, dashURL, dashFolderID, dashName, dashDescr, dashTags, dashSchemaVersion, dashCreated, dashUpdated),
|
data.NewFrame("dashboards", dashID, dashUID, dashURL, dashFolderID,
|
||||||
|
dashName, dashDescr, dashTags,
|
||||||
|
dashSchemaVersion,
|
||||||
|
dashPanelCount, dashVarCount, dashDSList,
|
||||||
|
dashCreated, dashUpdated),
|
||||||
data.NewFrame("panels", panelDashID, panelID, panelName, panelDescr, panelType),
|
data.NewFrame("panels", panelDashID, panelID, panelName, panelDescr, panelType),
|
||||||
panelTypeCounter.toFrame("panel-type-counts"),
|
panelTypeCounter.toFrame("panel-type-counts"),
|
||||||
schemaVersionCounter.toFrame("schema-version-counts"),
|
schemaVersionCounter.toFrame("schema-version-counts"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toJSONString(vals []string) *string {
|
||||||
|
if len(vals) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(vals)
|
||||||
|
if err == nil {
|
||||||
|
s := string(b)
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dsAsJSONString(vals []extract.DataSourceRef) *string {
|
||||||
|
if len(vals) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(vals)
|
||||||
|
if err == nil {
|
||||||
|
s := string(b)
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -6,9 +6,11 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { SearchPageDashboards } from './SearchPageDashboards';
|
|
||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
import { getGrafanaSearcher } from '../service';
|
import { getGrafanaSearcher, QueryFilters } from '../service';
|
||||||
|
import { Table } from './table/Table';
|
||||||
|
import { TagFilter, TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||||
|
import { getTermCounts } from '../service/backend';
|
||||||
|
|
||||||
const node: NavModelItem = {
|
const node: NavModelItem = {
|
||||||
id: 'search',
|
id: 'search',
|
||||||
@ -20,29 +22,43 @@ const node: NavModelItem = {
|
|||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
|
|
||||||
const results = useAsync(() => {
|
const results = useAsync(() => {
|
||||||
return getGrafanaSearcher().search(query);
|
const filters: QueryFilters = {
|
||||||
}, [query]);
|
tags,
|
||||||
|
};
|
||||||
|
return getGrafanaSearcher().search(query, tags.length ? filters : undefined);
|
||||||
|
}, [query, tags]);
|
||||||
|
|
||||||
if (!config.featureToggles.panelTitleSearch) {
|
if (!config.featureToggles.panelTitleSearch) {
|
||||||
return <div className={styles.unsupported}>Unsupported</div>;
|
return <div className={styles.unsupported}>Unsupported</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTagOptions = (): Promise<TermCount[]> => {
|
||||||
|
const tags = results.value?.body.fields.find((f) => f.name === 'tags');
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
return Promise.resolve(getTermCounts(tags));
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={{ node: node, main: node }}>
|
<Page navModel={{ node: node, main: node }}>
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
<Input value={query} onChange={(e) => setQuery(e.currentTarget.value)} autoFocus spellCheck={false} />
|
<Input value={query} onChange={(e) => setQuery(e.currentTarget.value)} autoFocus spellCheck={false} />
|
||||||
<br /> <br />
|
<br />
|
||||||
{results.loading && <Spinner />}
|
{results.loading && <Spinner />}
|
||||||
{results.value?.body && (
|
{results.value?.body && (
|
||||||
<div>
|
<div>
|
||||||
<AutoSizer style={{ width: '100%', height: '1000px' }}>
|
<TagFilter isClearable tags={tags} tagOptions={getTagOptions} onChange={setTags} /> <br />
|
||||||
|
<AutoSizer style={{ width: '100%', height: '2000px' }}>
|
||||||
{({ width }) => {
|
{({ width }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<SearchPageDashboards dashboards={results.value!.body} width={width} />
|
<Table data={results.value!.body} width={width} />
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { DataFrame, LoadingState } from '@grafana/data';
|
|
||||||
import { PanelRenderer } from '@grafana/runtime';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
dashboards: DataFrame;
|
|
||||||
width: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SearchPageDashboards = ({ dashboards, width }: Props) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Dashboards ({dashboards.length})</h1>
|
|
||||||
<PanelRenderer
|
|
||||||
pluginId="table"
|
|
||||||
title="Dashboards"
|
|
||||||
data={{ series: [dashboards], state: LoadingState.Done } as any}
|
|
||||||
options={{}}
|
|
||||||
width={width}
|
|
||||||
height={300}
|
|
||||||
fieldConfig={{ defaults: {}, overrides: [] }}
|
|
||||||
timeZone="browser"
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,114 +0,0 @@
|
|||||||
import { ArrayVector, DataFrame, Field, FieldType } from '@grafana/data';
|
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
|
||||||
import { GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
|
|
||||||
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
|
||||||
import { lastValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
export interface DashboardData {
|
|
||||||
dashboards: DataFrame;
|
|
||||||
panels: DataFrame;
|
|
||||||
panelTypes: DataFrame;
|
|
||||||
schemaVersions: DataFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDashboardData(): Promise<DashboardData> {
|
|
||||||
const ds = (await getDataSourceSrv().get('-- Grafana --')) as GrafanaDatasource;
|
|
||||||
const rsp = await lastValueFrom(
|
|
||||||
ds.query({
|
|
||||||
targets: [
|
|
||||||
{ refId: 'A', queryType: GrafanaQueryType.Search }, // gets all data
|
|
||||||
],
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
const data: DashboardData = {} as any;
|
|
||||||
for (const f of rsp.data) {
|
|
||||||
switch (f.name) {
|
|
||||||
case 'dashboards':
|
|
||||||
data.dashboards = f;
|
|
||||||
break;
|
|
||||||
case 'panels':
|
|
||||||
data.panels = f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data.panelTypes = buildStatsTable(data.panels.fields.find((f) => f.name === 'Type'));
|
|
||||||
data.schemaVersions = buildStatsTable(data.dashboards.fields.find((f) => f.name === 'SchemaVersion'));
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterDataFrame(query: string, frame: DataFrame, ...fields: string[]): DataFrame {
|
|
||||||
if (!frame || !query?.length) {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
query = query.toLowerCase();
|
|
||||||
|
|
||||||
const checkIndex: number[] = [];
|
|
||||||
const buffer: any[][] = [];
|
|
||||||
const copy = frame.fields.map((f, idx) => {
|
|
||||||
if (f.type === FieldType.string && fields.includes(f.name)) {
|
|
||||||
checkIndex.push(idx);
|
|
||||||
}
|
|
||||||
const v: any[] = [];
|
|
||||||
buffer.push(v);
|
|
||||||
return { ...f, values: new ArrayVector(v) };
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < frame.length; i++) {
|
|
||||||
let match = false;
|
|
||||||
for (const idx of checkIndex) {
|
|
||||||
const v = frame.fields[idx].values.get(i) as string;
|
|
||||||
if (v && v.toLowerCase().indexOf(query) >= 0) {
|
|
||||||
match = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
for (let idx = 0; idx < buffer.length; idx++) {
|
|
||||||
buffer[idx].push(frame.fields[idx].values.get(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: copy,
|
|
||||||
length: buffer[0].length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildStatsTable(field?: Field): DataFrame {
|
|
||||||
if (!field) {
|
|
||||||
return { length: 0, fields: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const counts = new Map<any, number>();
|
|
||||||
for (let i = 0; i < field.values.length; i++) {
|
|
||||||
const k = field.values.get(i);
|
|
||||||
const v = counts.get(k) ?? 0;
|
|
||||||
counts.set(k, v + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort largest first
|
|
||||||
counts[Symbol.iterator] = function* () {
|
|
||||||
yield* [...this.entries()].sort((a, b) => b[1] - a[1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const keys: any[] = [];
|
|
||||||
const vals: number[] = [];
|
|
||||||
|
|
||||||
for (let [k, v] of counts) {
|
|
||||||
keys.push(k);
|
|
||||||
vals.push(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: [
|
|
||||||
{ ...field, values: new ArrayVector(keys) },
|
|
||||||
{ name: 'Count', type: FieldType.number, values: new ArrayVector(vals), config: {} },
|
|
||||||
],
|
|
||||||
length: keys.length,
|
|
||||||
};
|
|
||||||
}
|
|
223
public/app/features/search/page/table/Table.tsx
Normal file
223
public/app/features/search/page/table/Table.tsx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useTable, useBlockLayout, Column, TableOptions, Cell } from 'react-table';
|
||||||
|
import { DataFrame, DataFrameType, DataFrameView, DataSourceRef, Field, GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
import { FixedSizeList } from 'react-window';
|
||||||
|
import { TableCell } from '@grafana/ui/src/components/Table/TableCell';
|
||||||
|
import { getTableStyles } from '@grafana/ui/src/components/Table/styles';
|
||||||
|
|
||||||
|
import { LocationInfo } from '../../service';
|
||||||
|
import { generateColumns } from './columns';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: DataFrame;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TableColumn = Column & {
|
||||||
|
field?: Field;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FieldAccess {
|
||||||
|
kind: string; // panel, dashboard, folder
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
url: string; // link to value (unique)
|
||||||
|
type: string; // graph
|
||||||
|
tags: string[];
|
||||||
|
location: LocationInfo[]; // the folder name
|
||||||
|
score: number;
|
||||||
|
|
||||||
|
// Count info
|
||||||
|
panelCount: number;
|
||||||
|
datasource: DataSourceRef[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Table = ({ data, width }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const tableStyles = useStyles2(getTableStyles);
|
||||||
|
|
||||||
|
const memoizedData = useMemo(() => {
|
||||||
|
if (!data.fields.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// as we only use this to fake the length of our data set for react-table we need to make sure we always return an array
|
||||||
|
// filled with values at each index otherwise we'll end up trying to call accessRow for null|undefined value in
|
||||||
|
// https://github.com/tannerlinsley/react-table/blob/7be2fc9d8b5e223fc998af88865ae86a88792fdb/src/hooks/useTable.js#L585
|
||||||
|
return Array(data.length).fill(0);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// React-table column definitions
|
||||||
|
const access = useMemo(() => new DataFrameView<FieldAccess>(data), [data]);
|
||||||
|
const memoizedColumns = useMemo(() => {
|
||||||
|
const isDashboardList = data.meta?.type === DataFrameType.DirectoryListing;
|
||||||
|
return generateColumns(access, isDashboardList, width, styles);
|
||||||
|
}, [data.meta?.type, access, width, styles]);
|
||||||
|
|
||||||
|
const options: TableOptions<{}> = useMemo(
|
||||||
|
() => ({
|
||||||
|
columns: memoizedColumns,
|
||||||
|
data: memoizedData,
|
||||||
|
}),
|
||||||
|
[memoizedColumns, memoizedData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useBlockLayout);
|
||||||
|
|
||||||
|
const RenderRow = React.useCallback(
|
||||||
|
({ index: rowIndex, style }) => {
|
||||||
|
const row = rows[rowIndex];
|
||||||
|
prepareRow(row);
|
||||||
|
|
||||||
|
const url = access.fields.url?.values.get(rowIndex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...row.getRowProps({ style })} className={styles.rowContainer}>
|
||||||
|
{row.cells.map((cell: Cell, index: number) => {
|
||||||
|
if (cell.column.id === 'column-checkbox') {
|
||||||
|
return (
|
||||||
|
<div key={index} className={styles.cellWrapper}>
|
||||||
|
<TableCell
|
||||||
|
key={index}
|
||||||
|
tableStyles={tableStyles}
|
||||||
|
cell={cell}
|
||||||
|
columnIndex={index}
|
||||||
|
columnCount={row.cells.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={url} key={index}>
|
||||||
|
<div className={styles.cellWrapper}>
|
||||||
|
<TableCell
|
||||||
|
key={index}
|
||||||
|
tableStyles={tableStyles}
|
||||||
|
cell={cell}
|
||||||
|
columnIndex={index}
|
||||||
|
columnCount={row.cells.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[rows, prepareRow, access.fields.url?.values, styles.rowContainer, styles.cellWrapper, tableStyles]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...getTableProps()} style={{ width }} aria-label={'Search result table'} role="table">
|
||||||
|
<div>
|
||||||
|
{headerGroups.map((headerGroup) => {
|
||||||
|
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} {...headerGroupProps} className={styles.headerRow}>
|
||||||
|
{headerGroup.headers.map((column) => {
|
||||||
|
const { key, ...headerProps } = column.getHeaderProps();
|
||||||
|
return (
|
||||||
|
<div key={key} {...headerProps} role="columnheader" className={styles.headerCell}>
|
||||||
|
{column.render('Header')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div {...getTableBodyProps()}>
|
||||||
|
{rows.length > 0 ? (
|
||||||
|
<FixedSizeList
|
||||||
|
height={500}
|
||||||
|
itemCount={rows.length}
|
||||||
|
itemSize={tableStyles.rowHeight}
|
||||||
|
width={'100%'}
|
||||||
|
className={styles.tableBody}
|
||||||
|
>
|
||||||
|
{RenderRow}
|
||||||
|
</FixedSizeList>
|
||||||
|
) : (
|
||||||
|
<div className={styles.noData}>No data</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03);
|
||||||
|
|
||||||
|
return {
|
||||||
|
noData: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
|
table: css`
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
tableBody: css`
|
||||||
|
overflow: 'hidden auto';
|
||||||
|
`,
|
||||||
|
cellIcon: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
cellWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
pointer-events: none;
|
||||||
|
`,
|
||||||
|
headerCell: css`
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-left: 10px;
|
||||||
|
`,
|
||||||
|
headerRow: css`
|
||||||
|
background-color: ${theme.colors.background.secondary};
|
||||||
|
height: 36px;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
rowContainer: css`
|
||||||
|
&:hover {
|
||||||
|
background-color: ${rowHoverBg};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
typeIcon: css`
|
||||||
|
margin-right: 9.5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: ${theme.v1.spacing.xxs};
|
||||||
|
fill: ${theme.colors.text.secondary};
|
||||||
|
`,
|
||||||
|
typeText: css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
`,
|
||||||
|
locationItem: css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
margin-right: 12px;
|
||||||
|
`,
|
||||||
|
checkboxHeader: css`
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: flex-start;
|
||||||
|
`,
|
||||||
|
checkbox: css`
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 5px;
|
||||||
|
`,
|
||||||
|
infoWrap: css`
|
||||||
|
span {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
tagList: css`
|
||||||
|
justify-content: flex-start;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
278
public/app/features/search/page/table/columns.tsx
Normal file
278
public/app/features/search/page/table/columns.tsx
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DataFrameView, DataSourceRef, Field } from '@grafana/data';
|
||||||
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import SVG from 'react-inlinesvg';
|
||||||
|
import { Checkbox, Icon, IconName, TagList } from '@grafana/ui';
|
||||||
|
import { DefaultCell } from '@grafana/ui/src/components/Table/DefaultCell';
|
||||||
|
|
||||||
|
import { FieldAccess, TableColumn } from './Table';
|
||||||
|
import { LocationInfo } from '../../service';
|
||||||
|
|
||||||
|
export const generateColumns = (
|
||||||
|
data: DataFrameView<FieldAccess>,
|
||||||
|
isDashboardList: boolean,
|
||||||
|
availableWidth: number,
|
||||||
|
styles: { [key: string]: string }
|
||||||
|
): TableColumn[] => {
|
||||||
|
const columns: TableColumn[] = [];
|
||||||
|
const urlField = data.fields.url!;
|
||||||
|
const access = data.fields;
|
||||||
|
|
||||||
|
availableWidth -= 8; // ???
|
||||||
|
let width = 50;
|
||||||
|
|
||||||
|
// TODO: Add optional checkbox support
|
||||||
|
if (false) {
|
||||||
|
// checkbox column
|
||||||
|
columns.push({
|
||||||
|
id: `column-checkbox`,
|
||||||
|
Header: () => (
|
||||||
|
<div className={styles.checkboxHeader}>
|
||||||
|
<Checkbox onChange={() => {}} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
Cell: () => (
|
||||||
|
<div className={styles.checkbox}>
|
||||||
|
<Checkbox onChange={() => {}} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
accessor: 'check',
|
||||||
|
field: urlField,
|
||||||
|
width: 30,
|
||||||
|
});
|
||||||
|
availableWidth -= width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name column
|
||||||
|
width = Math.max(availableWidth * 0.2, 200);
|
||||||
|
columns.push({
|
||||||
|
Cell: DefaultCell,
|
||||||
|
id: `column-name`,
|
||||||
|
field: access.name!,
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: (row: any, i: number) => {
|
||||||
|
const name = access.name!.values.get(i);
|
||||||
|
return name;
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
availableWidth -= width;
|
||||||
|
|
||||||
|
const TYPE_COLUMN_WIDTH = 130;
|
||||||
|
const DATASOURCE_COLUMN_WIDTH = 200;
|
||||||
|
const INFO_COLUMN_WIDTH = 100;
|
||||||
|
const LOCATION_COLUMN_WIDTH = 200;
|
||||||
|
|
||||||
|
width = TYPE_COLUMN_WIDTH;
|
||||||
|
if (isDashboardList) {
|
||||||
|
columns.push({
|
||||||
|
Cell: DefaultCell,
|
||||||
|
id: `column-type`,
|
||||||
|
field: access.name!,
|
||||||
|
Header: 'Type',
|
||||||
|
accessor: (row: any, i: number) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.typeText}>
|
||||||
|
<Icon name={'apps'} className={styles.typeIcon} />
|
||||||
|
Dashboard
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
availableWidth -= width;
|
||||||
|
} else {
|
||||||
|
columns.push(makeTypeColumn(access.kind, access.type, width, styles.typeText, styles.typeIcon));
|
||||||
|
availableWidth -= width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show datasources if we have any
|
||||||
|
if (access.datasource && hasFieldValue(access.datasource)) {
|
||||||
|
width = DATASOURCE_COLUMN_WIDTH;
|
||||||
|
columns.push(makeDataSourceColumn(access.datasource, width, styles.typeIcon));
|
||||||
|
availableWidth -= width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDashboardList) {
|
||||||
|
width = INFO_COLUMN_WIDTH;
|
||||||
|
columns.push({
|
||||||
|
Cell: DefaultCell,
|
||||||
|
id: `column-info`,
|
||||||
|
field: access.url!,
|
||||||
|
Header: 'Info',
|
||||||
|
accessor: (row: any, i: number) => {
|
||||||
|
const panelCount = access.panelCount?.values.get(i);
|
||||||
|
return <div className={styles.infoWrap}>{panelCount != null && <span>Panels: {panelCount}</span>}</div>;
|
||||||
|
},
|
||||||
|
width: width,
|
||||||
|
});
|
||||||
|
availableWidth -= width;
|
||||||
|
} else {
|
||||||
|
columns.push({
|
||||||
|
Cell: DefaultCell,
|
||||||
|
id: `column-location`,
|
||||||
|
field: access.location ?? access.url,
|
||||||
|
Header: 'Location',
|
||||||
|
accessor: (row: any, i: number) => {
|
||||||
|
const location = access.location?.values.get(i) as LocationInfo[];
|
||||||
|
if (location) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{location.map((v, id) => (
|
||||||
|
<span
|
||||||
|
key={id}
|
||||||
|
className={styles.locationItem}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('CLICK: ' + v.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={getIconForKind(v.kind)} /> {v.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
width: LOCATION_COLUMN_WIDTH,
|
||||||
|
});
|
||||||
|
availableWidth -= width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show tags if we have any
|
||||||
|
if (access.tags && hasFieldValue(access.tags)) {
|
||||||
|
width = Math.max(availableWidth, 250);
|
||||||
|
columns.push(makeTagsColumn(access.tags, width, styles.tagList));
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
function hasFieldValue(field: Field): boolean {
|
||||||
|
for (let i = 0; i < field.values.length; i++) {
|
||||||
|
const v = field.values.get(i);
|
||||||
|
if (v && v.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconForKind(v: string): IconName {
|
||||||
|
if (v === 'dashboard') {
|
||||||
|
return 'apps';
|
||||||
|
}
|
||||||
|
if (v === 'folder') {
|
||||||
|
return 'folder';
|
||||||
|
}
|
||||||
|
return 'question-circle';
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDataSourceColumn(field: Field<DataSourceRef[]>, width: number, iconClass: string): TableColumn {
|
||||||
|
return {
|
||||||
|
Cell: DefaultCell,
|
||||||
|
id: `column-datasource`,
|
||||||
|
field,
|
||||||
|
Header: 'Data source',
|
||||||
|
accessor: (row: any, i: number) => {
|
||||||
|
const dslist = field.values.get(i);
|
||||||
|
if (dslist?.length) {
|
||||||
|
const srv = getDataSourceSrv();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{dslist.map((v, i) => {
|
||||||
|
const settings = srv.getInstanceSettings(v);
|
||||||
|
const icon = settings?.meta?.info?.logos?.small;
|
||||||
|
if (icon) {
|
||||||
|
return (
|
||||||
|
<span key={i}>
|
||||||
|
<SVG src={icon} width={14} height={14} title={settings.type} className={iconClass} />
|
||||||
|
{settings.name}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span key={i}>{v.type}</span>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTypeColumn(
|
||||||
|
kindField: Field<string>,
|
||||||
|
typeField: Field<string>,
|
||||||
|
width: number,
|
||||||
|
typeTextClass: string,
|
||||||
|
iconClass: string
|
||||||
|
): TableColumn {
|
||||||
|
return {
|
||||||
|
Cell: DefaultCell,
|
||||||
|
id: `column-type`,
|
||||||
|
field: kindField,
|
||||||
|
Header: 'Type',
|
||||||
|
accessor: (row: any, i: number) => {
|
||||||
|
const kind = kindField.values.get(i);
|
||||||
|
let icon = 'public/img/icons/unicons/apps.svg';
|
||||||
|
let txt = 'Dashboard';
|
||||||
|
if (kind) {
|
||||||
|
txt = kind;
|
||||||
|
switch (txt) {
|
||||||
|
case 'dashboard':
|
||||||
|
txt = 'Dashboard';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'folder':
|
||||||
|
icon = 'public/img/icons/unicons/folder.svg';
|
||||||
|
txt = 'Folder';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'panel':
|
||||||
|
icon = 'public/img/icons/unicons/graph-bar.svg';
|
||||||
|
const type = typeField.values.get(i);
|
||||||
|
if (type) {
|
||||||
|
txt = type;
|
||||||
|
const info = config.panels[txt];
|
||||||
|
if (info?.name) {
|
||||||
|
const v = info.info?.logos.small;
|
||||||
|
if (v && v.endsWith('.svg')) {
|
||||||
|
icon = v;
|
||||||
|
}
|
||||||
|
txt = info.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={typeTextClass}>
|
||||||
|
<SVG src={icon} width={14} height={14} title={txt} className={iconClass} />
|
||||||
|
{txt}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTagsColumn(field: Field<string[]>, width: number, tagListClass: string): TableColumn {
|
||||||
|
return {
|
||||||
|
Cell: DefaultCell,
|
||||||
|
id: `column-tags`,
|
||||||
|
field: field,
|
||||||
|
Header: 'Tags',
|
||||||
|
accessor: (row: any, i: number) => {
|
||||||
|
const tags = field.values.get(i);
|
||||||
|
if (tags) {
|
||||||
|
return <TagList className={tagListClass} tags={tags} onClick={(v) => alert('CLICKED TAG: ' + v)} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
}
|
@ -1,17 +0,0 @@
|
|||||||
import { Dispatch } from 'react';
|
|
||||||
import { Action } from 'redux';
|
|
||||||
|
|
||||||
export interface DashboardResult {
|
|
||||||
UID: string;
|
|
||||||
URL: string;
|
|
||||||
Name: string;
|
|
||||||
Description: string;
|
|
||||||
Created: number;
|
|
||||||
Updated: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchPageAction extends Action {
|
|
||||||
payload?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SearchPageReducer<S> = [S, Dispatch<SearchPageAction>];
|
|
@ -1,8 +1,20 @@
|
|||||||
import { DataFrame, getDisplayProcessor } from '@grafana/data';
|
import {
|
||||||
|
ArrayVector,
|
||||||
|
DataFrame,
|
||||||
|
DataFrameType,
|
||||||
|
DataFrameView,
|
||||||
|
Field,
|
||||||
|
FieldType,
|
||||||
|
getDisplayProcessor,
|
||||||
|
Vector,
|
||||||
|
} from '@grafana/data';
|
||||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
|
import { GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||||
|
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||||
|
import { QueryFilters } from './types';
|
||||||
|
import { QueryResult } from '.';
|
||||||
|
|
||||||
// The raw restuls from query server
|
// The raw restuls from query server
|
||||||
export interface RawIndexData {
|
export interface RawIndexData {
|
||||||
@ -27,8 +39,26 @@ export async function getRawIndexData(): Promise<RawIndexData> {
|
|||||||
for (const f of rsp.data) {
|
for (const f of rsp.data) {
|
||||||
const frame = f as DataFrame;
|
const frame = f as DataFrame;
|
||||||
for (const field of frame.fields) {
|
for (const field of frame.fields) {
|
||||||
|
// Parse tags/ds from JSON string
|
||||||
|
if (field.name === 'tags' || field.name === 'datasource') {
|
||||||
|
const values = field.values.toArray().map((v) => {
|
||||||
|
if (v?.length) {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(v);
|
||||||
|
return arr.length ? arr : undefined;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
field.type = FieldType.other; // []string
|
||||||
|
field.values = new ArrayVector(values);
|
||||||
|
}
|
||||||
|
|
||||||
field.display = getDisplayProcessor({ field, theme: config.theme2 });
|
field.display = getDisplayProcessor({ field, theme: config.theme2 });
|
||||||
}
|
}
|
||||||
|
frame.meta = {
|
||||||
|
type: DataFrameType.DirectoryListing,
|
||||||
|
};
|
||||||
|
|
||||||
switch (frame.name) {
|
switch (frame.name) {
|
||||||
case 'dashboards':
|
case 'dashboards':
|
||||||
@ -44,3 +74,120 @@ export async function getRawIndexData(): Promise<RawIndexData> {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildStatsTable(field?: Field): DataFrame {
|
||||||
|
if (!field) {
|
||||||
|
return { length: 0, fields: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = new Map<any, number>();
|
||||||
|
for (let i = 0; i < field.values.length; i++) {
|
||||||
|
const k = field.values.get(i);
|
||||||
|
const v = counts.get(k) ?? 0;
|
||||||
|
counts.set(k, v + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort largest first
|
||||||
|
counts[Symbol.iterator] = function* () {
|
||||||
|
yield* [...this.entries()].sort((a, b) => b[1] - a[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const keys: any[] = [];
|
||||||
|
const vals: number[] = [];
|
||||||
|
|
||||||
|
for (let [k, v] of counts) {
|
||||||
|
keys.push(k);
|
||||||
|
vals.push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields: [
|
||||||
|
{ ...field, values: new ArrayVector(keys) },
|
||||||
|
{ name: 'Count', type: FieldType.number, values: new ArrayVector(vals), config: {} },
|
||||||
|
],
|
||||||
|
length: keys.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTermCounts(field?: Field): TermCount[] {
|
||||||
|
if (!field) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = new Map<any, number>();
|
||||||
|
for (let i = 0; i < field.values.length; i++) {
|
||||||
|
const k = field.values.get(i);
|
||||||
|
if (k == null || !k.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(k)) {
|
||||||
|
for (const sub of k) {
|
||||||
|
const v = counts.get(sub) ?? 0;
|
||||||
|
counts.set(sub, v + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const v = counts.get(k) ?? 0;
|
||||||
|
counts.set(k, v + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort largest first
|
||||||
|
counts[Symbol.iterator] = function* () {
|
||||||
|
yield* [...this.entries()].sort((a, b) => b[1] - a[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const terms: TermCount[] = [];
|
||||||
|
for (let [term, count] of counts) {
|
||||||
|
terms.push({
|
||||||
|
term,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return terms;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterFrame(frame: DataFrame, filter?: QueryFilters): DataFrame {
|
||||||
|
if (!filter) {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
const view = new DataFrameView<QueryResult>(frame);
|
||||||
|
const keep: number[] = [];
|
||||||
|
|
||||||
|
let ok = true;
|
||||||
|
for (let i = 0; i < view.length; i++) {
|
||||||
|
ok = true;
|
||||||
|
const row = view.get(i);
|
||||||
|
if (filter.tags) {
|
||||||
|
const tags = row.tags;
|
||||||
|
if (!tags) {
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const t of filter.tags) {
|
||||||
|
if (!tags.includes(t)) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
keep.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta: frame.meta,
|
||||||
|
name: frame.name,
|
||||||
|
fields: frame.fields.map((f) => ({ ...f, values: filterValues(keep, f.values) })),
|
||||||
|
length: keep.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterValues(keep: number[], raw: Vector<any>): Vector<any> {
|
||||||
|
const values = new Array(keep.length);
|
||||||
|
for (let i = 0; i < keep.length; i++) {
|
||||||
|
values[i] = raw.get(keep[i]);
|
||||||
|
}
|
||||||
|
return new ArrayVector(values);
|
||||||
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import MiniSearch from 'minisearch';
|
import MiniSearch from 'minisearch';
|
||||||
import { ArrayVector, DataFrame, Field, FieldType, getDisplayProcessor, Vector } from '@grafana/data';
|
import { ArrayVector, DataFrame, DataSourceRef, Field, FieldType, getDisplayProcessor, Vector } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { GrafanaSearcher, QueryFilters, QueryResponse } from './types';
|
import { GrafanaSearcher, QueryFilters, QueryResponse } from './types';
|
||||||
import { getRawIndexData, RawIndexData, rawIndexSupplier } from './backend';
|
import { filterFrame, getRawIndexData, RawIndexData, rawIndexSupplier } from './backend';
|
||||||
|
import { LocationInfo } from '.';
|
||||||
|
import { isArray, isString } from 'lodash';
|
||||||
|
|
||||||
export type SearchResultKind = keyof RawIndexData;
|
export type SearchResultKind = keyof RawIndexData;
|
||||||
|
|
||||||
@ -16,10 +18,13 @@ interface InputDoc {
|
|||||||
url?: Vector<string>;
|
url?: Vector<string>;
|
||||||
uid?: Vector<string>;
|
uid?: Vector<string>;
|
||||||
name?: Vector<string>;
|
name?: Vector<string>;
|
||||||
|
folder?: Vector<number>;
|
||||||
description?: Vector<string>;
|
description?: Vector<string>;
|
||||||
dashboardID?: Vector<number>;
|
dashboardID?: Vector<number>;
|
||||||
|
location?: Vector<LocationInfo[]>;
|
||||||
|
datasource?: Vector<DataSourceRef[]>;
|
||||||
type?: Vector<string>;
|
type?: Vector<string>;
|
||||||
tags?: Vector<string>; // JSON strings?
|
tags?: Vector<string[]>; // JSON strings?
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompositeKey {
|
interface CompositeKey {
|
||||||
@ -42,7 +47,7 @@ export class MiniSearcher implements GrafanaSearcher {
|
|||||||
|
|
||||||
const searcher = new MiniSearch<InputDoc>({
|
const searcher = new MiniSearch<InputDoc>({
|
||||||
idField: '__id',
|
idField: '__id',
|
||||||
fields: ['name', 'description', 'tags'], // fields to index for full-text search
|
fields: ['name', 'description', 'tags', 'type', 'tags'], // fields to index for full-text search
|
||||||
searchOptions: {
|
searchOptions: {
|
||||||
boost: {
|
boost: {
|
||||||
name: 3,
|
name: 3,
|
||||||
@ -68,13 +73,20 @@ export class MiniSearcher implements GrafanaSearcher {
|
|||||||
return {
|
return {
|
||||||
kind: doc.kind,
|
kind: doc.kind,
|
||||||
index: doc.index,
|
index: doc.index,
|
||||||
};
|
} as any;
|
||||||
}
|
}
|
||||||
const values = (doc as any)[name] as Vector;
|
const values = (doc as any)[name] as Vector;
|
||||||
if (!values) {
|
if (!values) {
|
||||||
return undefined;
|
return '';
|
||||||
}
|
}
|
||||||
return values.get(doc.index);
|
const value = values.get(doc.index);
|
||||||
|
if (isString(value)) {
|
||||||
|
return value as string;
|
||||||
|
}
|
||||||
|
if (isArray(value)) {
|
||||||
|
return value.join(' ');
|
||||||
|
}
|
||||||
|
return JSON.stringify(value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -90,23 +102,53 @@ export class MiniSearcher implements GrafanaSearcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct the URL field for each panel
|
// Construct the URL field for each panel
|
||||||
|
const folderIDToIndex = new Map<number, number>();
|
||||||
|
const folder = lookup.get('folder');
|
||||||
const dashboard = lookup.get('dashboard');
|
const dashboard = lookup.get('dashboard');
|
||||||
const panel = lookup.get('panel');
|
const panel = lookup.get('panel');
|
||||||
|
if (folder?.id) {
|
||||||
|
for (let i = 0; i < folder.id?.length; i++) {
|
||||||
|
folderIDToIndex.set(folder.id.get(i), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (dashboard?.id && panel?.dashboardID && dashboard.url) {
|
if (dashboard?.id && panel?.dashboardID && dashboard.url) {
|
||||||
|
let location: LocationInfo[][] = new Array(dashboard.id.length);
|
||||||
const dashIDToIndex = new Map<number, number>();
|
const dashIDToIndex = new Map<number, number>();
|
||||||
for (let i = 0; i < dashboard.id?.length; i++) {
|
for (let i = 0; i < dashboard.id?.length; i++) {
|
||||||
dashIDToIndex.set(dashboard.id.get(i), i);
|
dashIDToIndex.set(dashboard.id.get(i), i);
|
||||||
|
const folderId = dashboard.folder?.get(i);
|
||||||
|
if (folderId != null) {
|
||||||
|
const index = folderIDToIndex.get(folderId);
|
||||||
|
const name = folder?.name?.get(index!);
|
||||||
|
if (name) {
|
||||||
|
location[i] = [
|
||||||
|
{
|
||||||
|
kind: 'folder',
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dashboard.location = new ArrayVector(location); // folder name
|
||||||
|
|
||||||
const urls: string[] = new Array(panel.dashboardID.length);
|
location = new Array(panel.dashboardID.length);
|
||||||
|
const urls: string[] = new Array(location.length);
|
||||||
for (let i = 0; i < panel.dashboardID.length; i++) {
|
for (let i = 0; i < panel.dashboardID.length; i++) {
|
||||||
const dashboardID = panel.dashboardID.get(i);
|
const dashboardID = panel.dashboardID.get(i);
|
||||||
const index = dashIDToIndex.get(dashboardID);
|
const index = dashIDToIndex.get(dashboardID);
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
urls[i] = dashboard.url.get(index) + '?viewPanel=' + panel.id?.get(i);
|
const idx = panel.id?.get(i);
|
||||||
|
urls[i] = dashboard.url.get(index) + '?viewPanel=' + idx;
|
||||||
|
|
||||||
|
const parent = dashboard.location.get(index) ?? [];
|
||||||
|
const name = dashboard.name?.get(index) ?? '?';
|
||||||
|
location[i] = [...parent, { kind: 'dashboard', name }];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panel.url = new ArrayVector(urls);
|
panel.url = new ArrayVector(urls);
|
||||||
|
panel.location = new ArrayVector(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.index = searcher;
|
this.index = searcher;
|
||||||
@ -122,7 +164,7 @@ export class MiniSearcher implements GrafanaSearcher {
|
|||||||
// empty query can return everything
|
// empty query can return everything
|
||||||
if (!query && this.data.dashboard) {
|
if (!query && this.data.dashboard) {
|
||||||
return {
|
return {
|
||||||
body: this.data.dashboard,
|
body: filterFrame(this.data.dashboard, filter),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +175,9 @@ export class MiniSearcher implements GrafanaSearcher {
|
|||||||
const kind: string[] = [];
|
const kind: string[] = [];
|
||||||
const type: string[] = [];
|
const type: string[] = [];
|
||||||
const name: string[] = [];
|
const name: string[] = [];
|
||||||
|
const tags: string[][] = [];
|
||||||
|
const location: LocationInfo[][] = [];
|
||||||
|
const datasource: DataSourceRef[][] = [];
|
||||||
const info: any[] = [];
|
const info: any[] = [];
|
||||||
const score: number[] = [];
|
const score: number[] = [];
|
||||||
|
|
||||||
@ -144,38 +189,34 @@ export class MiniSearcher implements GrafanaSearcher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter && !shouldKeep(filter, input, index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
url.push(input.url?.get(index) ?? '?');
|
url.push(input.url?.get(index) ?? '?');
|
||||||
|
location.push(input.location?.get(index) as any);
|
||||||
|
datasource.push(input.datasource?.get(index) as any);
|
||||||
|
tags.push(input.tags?.get(index) as any);
|
||||||
kind.push(key.kind);
|
kind.push(key.kind);
|
||||||
name.push(input.name?.get(index) ?? '?');
|
name.push(input.name?.get(index) ?? '?');
|
||||||
type.push(input.type?.get(index) as any);
|
type.push(input.type?.get(index)!);
|
||||||
info.push(res.match); // ???
|
info.push(res.match); // ???
|
||||||
score.push(res.score);
|
score.push(res.score);
|
||||||
}
|
}
|
||||||
const fields: Field[] = [
|
const fields: Field[] = [
|
||||||
{ name: 'Kind', config: {}, type: FieldType.string, values: new ArrayVector(kind) },
|
{ name: 'kind', config: {}, type: FieldType.string, values: new ArrayVector(kind) },
|
||||||
{ name: 'Name', config: {}, type: FieldType.string, values: new ArrayVector(name) },
|
{ name: 'name', config: {}, type: FieldType.string, values: new ArrayVector(name) },
|
||||||
{
|
{
|
||||||
name: 'URL',
|
name: 'url',
|
||||||
config: {
|
config: {},
|
||||||
links: [
|
|
||||||
{
|
|
||||||
title: 'view',
|
|
||||||
url: '?',
|
|
||||||
onClick: (evt) => {
|
|
||||||
const { field, rowIndex } = evt.origin;
|
|
||||||
if (field && rowIndex != null) {
|
|
||||||
const url = field.values.get(rowIndex) as string;
|
|
||||||
window.location.href = url; // HACK!
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
values: new ArrayVector(url),
|
values: new ArrayVector(url),
|
||||||
},
|
},
|
||||||
{ name: 'type', config: {}, type: FieldType.other, values: new ArrayVector(type) },
|
{ name: 'type', config: {}, type: FieldType.string, values: new ArrayVector(type) },
|
||||||
{ name: 'info', config: {}, type: FieldType.other, values: new ArrayVector(info) },
|
{ name: 'info', config: {}, type: FieldType.other, values: new ArrayVector(info) },
|
||||||
|
{ name: 'tags', config: {}, type: FieldType.other, values: new ArrayVector(tags) },
|
||||||
|
{ name: 'location', config: {}, type: FieldType.other, values: new ArrayVector(location) },
|
||||||
|
{ name: 'datasource', config: {}, type: FieldType.other, values: new ArrayVector(datasource) },
|
||||||
{ name: 'score', config: {}, type: FieldType.number, values: new ArrayVector(score) },
|
{ name: 'score', config: {}, type: FieldType.number, values: new ArrayVector(score) },
|
||||||
];
|
];
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
@ -190,6 +231,21 @@ export class MiniSearcher implements GrafanaSearcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldKeep(filter: QueryFilters, doc: InputDoc, index: number): boolean {
|
||||||
|
if (filter.tags) {
|
||||||
|
const tags = doc.tags?.get(index);
|
||||||
|
if (!tags?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const t of filter.tags) {
|
||||||
|
if (!tags.includes(t)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function getInputDoc(kind: SearchResultKind, frame: DataFrame): InputDoc {
|
function getInputDoc(kind: SearchResultKind, frame: DataFrame): InputDoc {
|
||||||
const input: InputDoc = {
|
const input: InputDoc = {
|
||||||
kind,
|
kind,
|
||||||
@ -217,6 +273,10 @@ function getInputDoc(kind: SearchResultKind, frame: DataFrame): InputDoc {
|
|||||||
case 'ID':
|
case 'ID':
|
||||||
input.id = field.values;
|
input.id = field.values;
|
||||||
break;
|
break;
|
||||||
|
case 'Tags':
|
||||||
|
case 'tags':
|
||||||
|
input.tags = field.values;
|
||||||
|
break;
|
||||||
case 'DashboardID':
|
case 'DashboardID':
|
||||||
case 'dashboardID':
|
case 'dashboardID':
|
||||||
input.dashboardID = field.values;
|
input.dashboardID = field.values;
|
||||||
@ -225,6 +285,15 @@ function getInputDoc(kind: SearchResultKind, frame: DataFrame): InputDoc {
|
|||||||
case 'type':
|
case 'type':
|
||||||
input.type = field.values;
|
input.type = field.values;
|
||||||
break;
|
break;
|
||||||
|
case 'folderID':
|
||||||
|
case 'FolderID':
|
||||||
|
input.folder = field.values;
|
||||||
|
break;
|
||||||
|
case 'datasource':
|
||||||
|
case 'dsList':
|
||||||
|
case 'DSList':
|
||||||
|
input.datasource = field.values;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrame, DataSourceRef } from '@grafana/data';
|
||||||
|
|
||||||
export interface QueryResult {
|
export interface QueryResult {
|
||||||
kind: string; // panel, dashboard, folder
|
kind: string; // panel, dashboard, folder
|
||||||
@ -6,13 +6,20 @@ export interface QueryResult {
|
|||||||
description?: string;
|
description?: string;
|
||||||
url: string; // link to value (unique)
|
url: string; // link to value (unique)
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
location?: string; // the folder name
|
location?: LocationInfo[]; // the folder name
|
||||||
|
datasource?: DataSourceRef[];
|
||||||
score?: number;
|
score?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LocationInfo {
|
||||||
|
kind: 'folder' | 'dashboard';
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueryFilters {
|
export interface QueryFilters {
|
||||||
kind?: string; // limit to a single type
|
kind?: string; // limit to a single type
|
||||||
tags?: string[]; // match all tags
|
tags?: string[]; // match all tags
|
||||||
|
datasource?: string; // limit to a single datasource
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryResponse {
|
export interface QueryResponse {
|
||||||
|
@ -10536,7 +10536,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-table@npm:7.7.10":
|
"@types/react-table@npm:7.7.10, @types/react-table@npm:^7":
|
||||||
version: 7.7.10
|
version: 7.7.10
|
||||||
resolution: "@types/react-table@npm:7.7.10"
|
resolution: "@types/react-table@npm:7.7.10"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -20502,6 +20502,7 @@ __metadata:
|
|||||||
"@types/react-loadable": 5.5.6
|
"@types/react-loadable": 5.5.6
|
||||||
"@types/react-redux": 7.1.23
|
"@types/react-redux": 7.1.23
|
||||||
"@types/react-router-dom": 5.3.3
|
"@types/react-router-dom": 5.3.3
|
||||||
|
"@types/react-table": ^7
|
||||||
"@types/react-test-renderer": 17.0.1
|
"@types/react-test-renderer": 17.0.1
|
||||||
"@types/react-transition-group": 4.4.4
|
"@types/react-transition-group": 4.4.4
|
||||||
"@types/react-virtualized-auto-sizer": 1.0.1
|
"@types/react-virtualized-auto-sizer": 1.0.1
|
||||||
@ -20650,6 +20651,7 @@ __metadata:
|
|||||||
react-select: 5.2.2
|
react-select: 5.2.2
|
||||||
react-select-event: ^5.1.0
|
react-select-event: ^5.1.0
|
||||||
react-split-pane: 0.1.92
|
react-split-pane: 0.1.92
|
||||||
|
react-table: ^7.7.0
|
||||||
react-test-renderer: 17.0.2
|
react-test-renderer: 17.0.2
|
||||||
react-transition-group: 4.4.2
|
react-transition-group: 4.4.2
|
||||||
react-use: 17.3.2
|
react-use: 17.3.2
|
||||||
@ -31187,7 +31189,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-table@npm:7.7.0":
|
"react-table@npm:7.7.0, react-table@npm:^7.7.0":
|
||||||
version: 7.7.0
|
version: 7.7.0
|
||||||
resolution: "react-table@npm:7.7.0"
|
resolution: "react-table@npm:7.7.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user