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-redux": "7.1.23",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-table": "^7",
|
||||
"@types/react-test-renderer": "17.0.1",
|
||||
"@types/react-transition-group": "4.4.4",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.1",
|
||||
@ -353,6 +354,7 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "5.2.2",
|
||||
"react-split-pane": "0.1.92",
|
||||
"react-table": "^7.7.0",
|
||||
"react-transition-group": "4.4.2",
|
||||
"react-use": "17.3.2",
|
||||
"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 { FunctionalVector } from '../vector/FunctionalVector';
|
||||
|
||||
@ -16,13 +16,22 @@ import { FunctionalVector } from '../vector/FunctionalVector';
|
||||
export class DataFrameView<T = any> extends FunctionalVector<T> {
|
||||
private index = 0;
|
||||
private obj: T;
|
||||
readonly fields: {
|
||||
readonly [Property in keyof T]: Field<T[Property]>;
|
||||
};
|
||||
|
||||
constructor(private data: DataFrame) {
|
||||
super();
|
||||
const obj = {} as unknown as T;
|
||||
const fields = {} as any;
|
||||
|
||||
for (let i = 0; i < data.fields.length; i++) {
|
||||
const field = data.fields[i];
|
||||
if (!field.name) {
|
||||
continue; // unsupported
|
||||
}
|
||||
|
||||
fields[field.name] = field;
|
||||
const getter = () => field.values.get(this.index);
|
||||
|
||||
if (!(obj as any).hasOwnProperty(field.name)) {
|
||||
@ -41,6 +50,7 @@ export class DataFrameView<T = any> extends FunctionalVector<T> {
|
||||
}
|
||||
|
||||
this.obj = obj;
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
get dataFrame() {
|
||||
|
@ -21,4 +21,7 @@ export enum DataFrameType {
|
||||
* All values in the grid exist and have regular spacing
|
||||
*/
|
||||
HeatmapScanlines = 'heatmap-scanlines',
|
||||
|
||||
/** Directory listing */
|
||||
DirectoryListing = 'directory-listing',
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ export function getColumns(
|
||||
return columns;
|
||||
}
|
||||
|
||||
function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent {
|
||||
export function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent {
|
||||
switch (displayMode) {
|
||||
case TableCellDisplayMode.ColorText:
|
||||
case TableCellDisplayMode.ColorBackground:
|
||||
|
@ -12,7 +12,7 @@ func logf(format string, a ...interface{}) {
|
||||
|
||||
// nolint:gocyclo
|
||||
// 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)
|
||||
dash := &DashboardInfo{}
|
||||
|
||||
@ -73,7 +73,7 @@ func ReadDashboard(stream io.Reader, datasource DatasourceLookup) *DashboardInfo
|
||||
|
||||
case "panels":
|
||||
for iter.ReadArray() {
|
||||
dash.Panels = append(dash.Panels, readPanelInfo(iter))
|
||||
dash.Panels = append(dash.Panels, readPanelInfo(iter, lookup))
|
||||
}
|
||||
|
||||
case "rows":
|
||||
@ -129,16 +129,29 @@ func ReadDashboard(stream io.Reader, datasource DatasourceLookup) *DashboardInfo
|
||||
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
|
||||
}
|
||||
|
||||
// will always return strings for now
|
||||
func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
|
||||
func readPanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelInfo {
|
||||
panel := PanelInfo{}
|
||||
|
||||
targets := newTargetInfo(lookup)
|
||||
|
||||
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 l1Field == "datasource" {
|
||||
targets.addDatasource(iter)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip null values so we don't need special int handling
|
||||
iter.Skip()
|
||||
continue
|
||||
}
|
||||
@ -160,13 +173,11 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
|
||||
panel.PluginVersion = iter.ReadString() // since 7x (the saved version for the plugin model)
|
||||
|
||||
case "datasource":
|
||||
v := iter.Read()
|
||||
logf(">>Panel.datasource = %v\n", v) // string or object!!!
|
||||
targets.addDatasource(iter)
|
||||
|
||||
case "targets":
|
||||
for iter.ReadArray() {
|
||||
v := iter.Read()
|
||||
logf("[Panel.TARGET] %v\n", v)
|
||||
targets.addTarget(iter)
|
||||
}
|
||||
|
||||
case "transformations":
|
||||
@ -183,7 +194,7 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
|
||||
// Rows have nested panels
|
||||
case "panels":
|
||||
for iter.ReadArray() {
|
||||
panel.Collapsed = append(panel.Collapsed, readPanelInfo(iter))
|
||||
panel.Collapsed = append(panel.Collapsed, readPanelInfo(iter, lookup))
|
||||
}
|
||||
|
||||
case "options":
|
||||
@ -201,5 +212,7 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
|
||||
}
|
||||
}
|
||||
|
||||
panel.Datasource = targets.GetDatasourceInfo()
|
||||
|
||||
return panel
|
||||
}
|
||||
|
@ -12,19 +12,36 @@ import (
|
||||
|
||||
func TestReadDashboard(t *testing.T) {
|
||||
inputs := []string{
|
||||
"all-panels.json",
|
||||
"panel-graph/graph-shared-tooltips.json",
|
||||
"check-string-datasource-id",
|
||||
"all-panels",
|
||||
"panel-graph/graph-shared-tooltips",
|
||||
}
|
||||
|
||||
// key will allow name or uid
|
||||
ds := func(key string) *DatasourceInfo {
|
||||
return nil // TODO!
|
||||
ds := func(ref *DataSourceRef) *DataSourceRef {
|
||||
if ref == nil || ref.UID == "" {
|
||||
return &DataSourceRef{
|
||||
UID: "default.uid",
|
||||
Type: "default.type",
|
||||
}
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
devdash := "../../../../devenv/dev-dashboards/"
|
||||
|
||||
for _, input := range inputs {
|
||||
// nolint:gosec
|
||||
// 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)
|
||||
|
||||
dash := ReadDashboard(f, ds)
|
||||
@ -32,7 +49,7 @@ func TestReadDashboard(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
update := false
|
||||
savedPath := "testdata/" + filepath.Base(input)
|
||||
savedPath := "testdata/" + input + "-info.json"
|
||||
saved, err := os.ReadFile(savedPath)
|
||||
if err != nil {
|
||||
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,45 +1,40 @@
|
||||
package extract
|
||||
|
||||
type DatasourceLookup = func(key string) *DatasourceInfo
|
||||
// empty everything will return the default
|
||||
type DatasourceLookup = func(ref *DataSourceRef) *DataSourceRef
|
||||
|
||||
type DatasourceInfo struct {
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // plugin name
|
||||
Version string `json:"version"`
|
||||
Access string `json:"access,omitempty"` // proxy, direct, or empty
|
||||
type DataSourceRef struct {
|
||||
UID string `json:"uid,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type PanelInfo struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Type string `json:"type,omitempty"` // PluginID
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
Datasource []string `json:"datasource,omitempty"` // UIDs
|
||||
DatasourceType []string `json:"datasourceType,omitempty"` // PluginIDs
|
||||
Transformations []string `json:"transformations,omitempty"` // ids of the transformation steps
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Type string `json:"type,omitempty"` // PluginID
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||
Transformations []string `json:"transformations,omitempty"` // ids of the transformation steps
|
||||
|
||||
// Rows define panels as sub objects
|
||||
Collapsed []PanelInfo `json:"collapsed,omitempty"`
|
||||
}
|
||||
|
||||
type DashboardInfo struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags []string `json:"tags"` // UIDs
|
||||
Datasource []string `json:"datasource,omitempty"` // UIDs
|
||||
DatasourceType []string `json:"datasourceType,omitempty"` // PluginIDs
|
||||
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
|
||||
Panels []PanelInfo `json:"panels"` // nesed documents
|
||||
SchemaVersion int64 `json:"schemaVersion"`
|
||||
LinkCount int64 `json:"linkCount"`
|
||||
TimeFrom string `json:"timeFrom"`
|
||||
TimeTo string `json:"timeTo"`
|
||||
TimeZone string `json:"timezone"`
|
||||
Refresh string `json:"refresh,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
|
||||
ID int64 `json:"id,omitempty"` // internal ID
|
||||
UID string `json:"uid,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
|
||||
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||
Panels []PanelInfo `json:"panels"` // nesed documents
|
||||
SchemaVersion int64 `json:"schemaVersion"`
|
||||
LinkCount int64 `json:"linkCount"`
|
||||
TimeFrom string `json:"timeFrom"`
|
||||
TimeTo string `json:"timeTo"`
|
||||
TimeZone string `json:"timezone"`
|
||||
Refresh string `json:"refresh,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ func (s *StandardSearchService) applyAuthFilter(user *models.SignedInUser, dash
|
||||
// create a list of all viewable dashboards for this user
|
||||
res := make([]dashMeta, 0, len(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)
|
||||
}
|
||||
}
|
||||
@ -106,15 +106,38 @@ type dashDataQueryResult struct {
|
||||
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) {
|
||||
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
|
||||
lookup := func(key string) *extract.DatasourceInfo {
|
||||
return nil // TODO!
|
||||
lookup, err := loadDatasoureLookup(ctx, orgID, sql)
|
||||
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)
|
||||
|
||||
sess.Table("dashboard").
|
||||
@ -146,6 +169,64 @@ func loadDashboards(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) ([
|
||||
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 {
|
||||
values map[string]int64
|
||||
}
|
||||
@ -173,10 +254,12 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
||||
folderID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||
folderUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||
folderName := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||
folderDashCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||
|
||||
folderID.Name = "ID"
|
||||
folderUID.Name = "UID"
|
||||
folderName.Name = "Name"
|
||||
folderID.Name = "id"
|
||||
folderUID.Name = "uid"
|
||||
folderName.Name = "name"
|
||||
folderDashCount.Name = "DashCount"
|
||||
|
||||
dashID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
||||
dashUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||
@ -188,22 +271,28 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
||||
dashUpdated := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
|
||||
dashSchemaVersion := data.NewFieldFromFieldType(data.FieldTypeInt64, 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"
|
||||
dashUID.Name = "UID"
|
||||
dashFolderID.Name = "FolderID"
|
||||
dashName.Name = "Name"
|
||||
dashDescr.Name = "Description"
|
||||
dashTags.Name = "Tags"
|
||||
dashID.Name = "id"
|
||||
dashUID.Name = "uid"
|
||||
dashFolderID.Name = "folderID"
|
||||
dashName.Name = "name"
|
||||
dashDescr.Name = "description"
|
||||
dashTags.Name = "tags"
|
||||
dashSchemaVersion.Name = "SchemaVersion"
|
||||
dashCreated.Name = "Created"
|
||||
dashUpdated.Name = "Updated"
|
||||
dashURL.Name = "URL"
|
||||
dashURL.Name = "url"
|
||||
dashURL.Config = &data.FieldConfig{
|
||||
Links: []data.DataLink{
|
||||
{Title: "link", URL: "${__value.text}"},
|
||||
},
|
||||
}
|
||||
dashPanelCount.Name = "panelCount"
|
||||
dashVarCount.Name = "varCount"
|
||||
dashDSList.Name = "datasource"
|
||||
|
||||
dashTags.Config = &data.FieldConfig{
|
||||
Custom: map[string]interface{}{
|
||||
@ -218,11 +307,11 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
||||
panelDescr := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||
panelType := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
||||
|
||||
panelDashID.Name = "DashboardID"
|
||||
panelID.Name = "ID"
|
||||
panelName.Name = "Name"
|
||||
panelDescr.Name = "Description"
|
||||
panelType.Name = "Type"
|
||||
panelDashID.Name = "dashboardID"
|
||||
panelID.Name = "id"
|
||||
panelName.Name = "name"
|
||||
panelDescr.Name = "description"
|
||||
panelType.Name = "type"
|
||||
|
||||
panelTypeCounter := simpleCounter{
|
||||
values: make(map[string]int64, 30),
|
||||
@ -232,12 +321,14 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
||||
values: make(map[string]int64, 30),
|
||||
}
|
||||
|
||||
var tags *string
|
||||
folderCounter := make(map[int64]int64, 20)
|
||||
|
||||
for _, row := range meta {
|
||||
if row.is_folder {
|
||||
folderID.Append(row.id)
|
||||
folderUID.Append(row.dash.UID)
|
||||
folderName.Append(row.dash.Title)
|
||||
folderDashCount.Append(int64(0)) // filled in later
|
||||
continue
|
||||
}
|
||||
|
||||
@ -250,22 +341,23 @@ func metaToFrame(meta []dashMeta) data.Frames {
|
||||
dashCreated.Append(row.created)
|
||||
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)
|
||||
dashURL.Append(url)
|
||||
|
||||
// stats
|
||||
schemaVersionCounter.add(strconv.FormatInt(row.dash.SchemaVersion, 10))
|
||||
|
||||
// Send tags as JSON array
|
||||
tags = nil
|
||||
if len(row.dash.Tags) > 0 {
|
||||
b, err := json.Marshal(row.dash.Tags)
|
||||
if err == nil {
|
||||
s := string(b)
|
||||
tags = &s
|
||||
}
|
||||
}
|
||||
dashTags.Append(tags)
|
||||
dashTags.Append(toJSONString(row.dash.Tags))
|
||||
dashPanelCount.Append(int64(len(row.dash.Panels)))
|
||||
dashVarCount.Append(int64(len(row.dash.TemplateVars)))
|
||||
dashDSList.Append(dsAsJSONString(row.dash.Datasource))
|
||||
|
||||
// Row for each panel
|
||||
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{
|
||||
data.NewFrame("folders", folderID, folderUID, folderName),
|
||||
data.NewFrame("dashboards", dashID, dashUID, dashURL, dashFolderID, dashName, dashDescr, dashTags, dashSchemaVersion, dashCreated, dashUpdated),
|
||||
data.NewFrame("folders", folderID, folderUID, folderName, folderDashCount),
|
||||
data.NewFrame("dashboards", dashID, dashUID, dashURL, dashFolderID,
|
||||
dashName, dashDescr, dashTags,
|
||||
dashSchemaVersion,
|
||||
dashPanelCount, dashVarCount, dashDSList,
|
||||
dashCreated, dashUpdated),
|
||||
data.NewFrame("panels", panelDashID, panelID, panelName, panelDescr, panelType),
|
||||
panelTypeCounter.toFrame("panel-type-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 Page from 'app/core/components/Page/Page';
|
||||
import { SearchPageDashboards } from './SearchPageDashboards';
|
||||
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 = {
|
||||
id: 'search',
|
||||
@ -20,29 +22,43 @@ const node: NavModelItem = {
|
||||
export default function SearchPage() {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [query, setQuery] = useState('');
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
|
||||
const results = useAsync(() => {
|
||||
return getGrafanaSearcher().search(query);
|
||||
}, [query]);
|
||||
const filters: QueryFilters = {
|
||||
tags,
|
||||
};
|
||||
return getGrafanaSearcher().search(query, tags.length ? filters : undefined);
|
||||
}, [query, tags]);
|
||||
|
||||
if (!config.featureToggles.panelTitleSearch) {
|
||||
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 (
|
||||
<Page navModel={{ node: node, main: node }}>
|
||||
<Page.Contents>
|
||||
<Input value={query} onChange={(e) => setQuery(e.currentTarget.value)} autoFocus spellCheck={false} />
|
||||
<br /> <br />
|
||||
<br />
|
||||
{results.loading && <Spinner />}
|
||||
{results.value?.body && (
|
||||
<div>
|
||||
<AutoSizer style={{ width: '100%', height: '1000px' }}>
|
||||
<TagFilter isClearable tags={tags} tagOptions={getTagOptions} onChange={setTags} /> <br />
|
||||
<AutoSizer style={{ width: '100%', height: '2000px' }}>
|
||||
{({ width }) => {
|
||||
return (
|
||||
<div>
|
||||
<SearchPageDashboards dashboards={results.value!.body} width={width} />
|
||||
</div>
|
||||
<>
|
||||
<Table data={results.value!.body} width={width} />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</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 { GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
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
|
||||
export interface RawIndexData {
|
||||
@ -27,8 +39,26 @@ export async function getRawIndexData(): Promise<RawIndexData> {
|
||||
for (const f of rsp.data) {
|
||||
const frame = f as DataFrame;
|
||||
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 });
|
||||
}
|
||||
frame.meta = {
|
||||
type: DataFrameType.DirectoryListing,
|
||||
};
|
||||
|
||||
switch (frame.name) {
|
||||
case 'dashboards':
|
||||
@ -44,3 +74,120 @@ export async function getRawIndexData(): Promise<RawIndexData> {
|
||||
}
|
||||
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 { 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 { 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;
|
||||
|
||||
@ -16,10 +18,13 @@ interface InputDoc {
|
||||
url?: Vector<string>;
|
||||
uid?: Vector<string>;
|
||||
name?: Vector<string>;
|
||||
folder?: Vector<number>;
|
||||
description?: Vector<string>;
|
||||
dashboardID?: Vector<number>;
|
||||
location?: Vector<LocationInfo[]>;
|
||||
datasource?: Vector<DataSourceRef[]>;
|
||||
type?: Vector<string>;
|
||||
tags?: Vector<string>; // JSON strings?
|
||||
tags?: Vector<string[]>; // JSON strings?
|
||||
}
|
||||
|
||||
interface CompositeKey {
|
||||
@ -42,7 +47,7 @@ export class MiniSearcher implements GrafanaSearcher {
|
||||
|
||||
const searcher = new MiniSearch<InputDoc>({
|
||||
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: {
|
||||
boost: {
|
||||
name: 3,
|
||||
@ -68,13 +73,20 @@ export class MiniSearcher implements GrafanaSearcher {
|
||||
return {
|
||||
kind: doc.kind,
|
||||
index: doc.index,
|
||||
};
|
||||
} as any;
|
||||
}
|
||||
const values = (doc as any)[name] as Vector;
|
||||
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
|
||||
const folderIDToIndex = new Map<number, number>();
|
||||
const folder = lookup.get('folder');
|
||||
const dashboard = lookup.get('dashboard');
|
||||
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) {
|
||||
let location: LocationInfo[][] = new Array(dashboard.id.length);
|
||||
const dashIDToIndex = new Map<number, number>();
|
||||
for (let i = 0; i < dashboard.id?.length; 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++) {
|
||||
const dashboardID = panel.dashboardID.get(i);
|
||||
const index = dashIDToIndex.get(dashboardID);
|
||||
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.location = new ArrayVector(location);
|
||||
}
|
||||
|
||||
this.index = searcher;
|
||||
@ -122,7 +164,7 @@ export class MiniSearcher implements GrafanaSearcher {
|
||||
// empty query can return everything
|
||||
if (!query && this.data.dashboard) {
|
||||
return {
|
||||
body: this.data.dashboard,
|
||||
body: filterFrame(this.data.dashboard, filter),
|
||||
};
|
||||
}
|
||||
|
||||
@ -133,6 +175,9 @@ export class MiniSearcher implements GrafanaSearcher {
|
||||
const kind: string[] = [];
|
||||
const type: string[] = [];
|
||||
const name: string[] = [];
|
||||
const tags: string[][] = [];
|
||||
const location: LocationInfo[][] = [];
|
||||
const datasource: DataSourceRef[][] = [];
|
||||
const info: any[] = [];
|
||||
const score: number[] = [];
|
||||
|
||||
@ -144,38 +189,34 @@ export class MiniSearcher implements GrafanaSearcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter && !shouldKeep(filter, input, index)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
name.push(input.name?.get(index) ?? '?');
|
||||
type.push(input.type?.get(index) as any);
|
||||
type.push(input.type?.get(index)!);
|
||||
info.push(res.match); // ???
|
||||
score.push(res.score);
|
||||
}
|
||||
const fields: Field[] = [
|
||||
{ name: 'Kind', config: {}, type: FieldType.string, values: new ArrayVector(kind) },
|
||||
{ name: 'Name', config: {}, type: FieldType.string, values: new ArrayVector(name) },
|
||||
{ name: 'kind', config: {}, type: FieldType.string, values: new ArrayVector(kind) },
|
||||
{ name: 'name', config: {}, type: FieldType.string, values: new ArrayVector(name) },
|
||||
{
|
||||
name: 'URL',
|
||||
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!
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
name: 'url',
|
||||
config: {},
|
||||
type: FieldType.string,
|
||||
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: '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) },
|
||||
];
|
||||
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 {
|
||||
const input: InputDoc = {
|
||||
kind,
|
||||
@ -217,6 +273,10 @@ function getInputDoc(kind: SearchResultKind, frame: DataFrame): InputDoc {
|
||||
case 'ID':
|
||||
input.id = field.values;
|
||||
break;
|
||||
case 'Tags':
|
||||
case 'tags':
|
||||
input.tags = field.values;
|
||||
break;
|
||||
case 'DashboardID':
|
||||
case 'dashboardID':
|
||||
input.dashboardID = field.values;
|
||||
@ -225,6 +285,15 @@ function getInputDoc(kind: SearchResultKind, frame: DataFrame): InputDoc {
|
||||
case 'type':
|
||||
input.type = field.values;
|
||||
break;
|
||||
case 'folderID':
|
||||
case 'FolderID':
|
||||
input.folder = field.values;
|
||||
break;
|
||||
case 'datasource':
|
||||
case 'dsList':
|
||||
case 'DSList':
|
||||
input.datasource = field.values;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return input;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { DataFrame, DataSourceRef } from '@grafana/data';
|
||||
|
||||
export interface QueryResult {
|
||||
kind: string; // panel, dashboard, folder
|
||||
@ -6,13 +6,20 @@ export interface QueryResult {
|
||||
description?: string;
|
||||
url: string; // link to value (unique)
|
||||
tags?: string[];
|
||||
location?: string; // the folder name
|
||||
location?: LocationInfo[]; // the folder name
|
||||
datasource?: DataSourceRef[];
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface LocationInfo {
|
||||
kind: 'folder' | 'dashboard';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface QueryFilters {
|
||||
kind?: string; // limit to a single type
|
||||
tags?: string[]; // match all tags
|
||||
datasource?: string; // limit to a single datasource
|
||||
}
|
||||
|
||||
export interface QueryResponse {
|
||||
|
@ -10536,7 +10536,7 @@ __metadata:
|
||||
languageName: node
|
||||
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
|
||||
resolution: "@types/react-table@npm:7.7.10"
|
||||
dependencies:
|
||||
@ -20502,6 +20502,7 @@ __metadata:
|
||||
"@types/react-loadable": 5.5.6
|
||||
"@types/react-redux": 7.1.23
|
||||
"@types/react-router-dom": 5.3.3
|
||||
"@types/react-table": ^7
|
||||
"@types/react-test-renderer": 17.0.1
|
||||
"@types/react-transition-group": 4.4.4
|
||||
"@types/react-virtualized-auto-sizer": 1.0.1
|
||||
@ -20650,6 +20651,7 @@ __metadata:
|
||||
react-select: 5.2.2
|
||||
react-select-event: ^5.1.0
|
||||
react-split-pane: 0.1.92
|
||||
react-table: ^7.7.0
|
||||
react-test-renderer: 17.0.2
|
||||
react-transition-group: 4.4.2
|
||||
react-use: 17.3.2
|
||||
@ -31187,7 +31189,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-table@npm:7.7.0":
|
||||
"react-table@npm:7.7.0, react-table@npm:^7.7.0":
|
||||
version: 7.7.0
|
||||
resolution: "react-table@npm:7.7.0"
|
||||
peerDependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user