diff --git a/dashboard.go b/dashboard.go index b5914432..5eded2d7 100644 --- a/dashboard.go +++ b/dashboard.go @@ -89,6 +89,21 @@ func (c *Client) DashboardByUID(uid string) (*Dashboard, error) { return c.dashboard(fmt.Sprintf("/api/dashboards/uid/%s", uid)) } +// DashboardsByIDs uses the folder and dashboard search endpoint to find +// dashboards by list of dashboard IDs. +func (c *Client) DashboardsByIDs(ids []int64) ([]FolderDashboardSearchResponse, error) { + dashboardIdsJSON, err := json.Marshal(ids) + if err != nil { + return nil, err + } + + params := map[string]string{ + "type": "dash-db", + "dashboardIds": string(dashboardIdsJSON), + } + return c.FolderDashboardSearch(params) +} + func (c *Client) dashboard(path string) (*Dashboard, error) { result := &Dashboard{} err := c.request("GET", path, nil, nil, &result) diff --git a/go.mod b/go.mod index 89711271..661f7c5e 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/grafana/grafana-api-golang-client +module github.com/justinTM/grafana-api-golang-client go 1.14 diff --git a/library_panel.go b/library_panel.go new file mode 100644 index 00000000..9aedf48e --- /dev/null +++ b/library_panel.go @@ -0,0 +1,159 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "fmt" + "time" +) + +// LibraryPanelMetaUser represents the Grafana library panel createdBy and updatedBy fields +type LibraryPanelMetaUser struct { + ID int64 `json:"id"` + Name string `json:"name"` + AvatarURL string `json:"folderId"` +} + +// LibraryPanelMeta represents Grafana library panel metadata. +type LibraryPanelMeta struct { + FolderName string `json:"folderName,,omitempty"` + FolderUID string `json:"folderUid,omitempty"` + ConnectedDashboards int64 `json:"connectedDashboards,omitempty"` + Created time.Time `json:"created,omitempty"` + Updated time.Time `json:"updated,omitempty"` + CreatedBy LibraryPanelMetaUser `json:"createdBy,omitempty"` + UpdatedBy LibraryPanelMetaUser `json:"updatedBy,omitempty"` +} + +// LibraryPanel represents a Grafana library panel. +type LibraryPanel struct { + Folder int64 `json:"folderId"` + Name string `json:"name"` + Model map[string]interface{} `json:"model"` + Description string `json:"description,omitempty"` + ID int64 `json:"id,omitempty"` + Kind int64 `json:"kind,omitempty"` + OrgID int64 `json:"orgId,omitempty"` + UID string `json:"uid,omitempty"` + Version int64 `json:"version,omitempty"` + Meta LibraryPanelMeta `json:"meta,omitempty"` +} + +// LibraryPanelCreateResponse represents the Grafana API response to creating or saving a library panel. +type LibraryPanelCreateResponse struct { + Result LibraryPanel `json:"result"` +} + +// LibraryPanelDeleteResponse represents the Grafana API response to deleting a library panel. +type LibraryPanelDeleteResponse struct { + Message string `json:"message"` + ID int64 `json:"id,omitempty"` +} + +// LibraryPanelConnection represents a Grafana connection between a library panel and a dashboard. +type LibraryPanelConnection struct { + ID int64 `json:"id"` + Kind int64 `json:"kind"` + PanelID int64 `json:"elementId"` + DashboardID int64 `json:"connectionId"` + Created time.Time `json:"created"` + CreatedBy LibraryPanelMetaUser `json:"createdBy"` +} + +// NewLibraryPanel creates a new Grafana library panel. +func (c *Client) NewLibraryPanel(panel LibraryPanel) (*LibraryPanel, error) { + panel.Kind = int64(1) + data, err := json.Marshal(panel) + if err != nil { + return nil, err + } + + resp := &LibraryPanelCreateResponse{} + err = c.request("POST", "/api/library-elements", nil, bytes.NewBuffer(data), &resp) + if err != nil { + return nil, err + } + + return &resp.Result, err +} + +// LibraryPanelByUID gets a library panel by UID. +func (c *Client) LibraryPanelByUID(uid string) (*LibraryPanel, error) { + return c.panel(fmt.Sprintf("/api/library-elements/%s", uid)) +} + +// LibraryPanelByName gets a library panel by name. +func (c *Client) LibraryPanelByName(name string) (*LibraryPanel, error) { + return c.panel(fmt.Sprintf("/api/library-elements/name/%s", name)) +} + +func (c *Client) panel(path string) (*LibraryPanel, error) { + resp := &LibraryPanelCreateResponse{} + err := c.request("GET", path, nil, nil, &resp) + if err != nil { + return nil, err + } + + return &resp.Result, err +} + +// PatchLibraryPanel updates one or more properties of an existing panel that matches the specified UID. +func (c *Client) PatchLibraryPanel(uid string, panel *LibraryPanel) (*LibraryPanel, error) { + path := fmt.Sprintf("/api/library-elements/%s", uid) + data, err := json.Marshal(panel) + if err != nil { + return nil, err + } + + resp := &LibraryPanelCreateResponse{} + err = c.request("PATCH", path, nil, bytes.NewBuffer(data), &resp) + if err != nil { + return nil, err + } + + return &resp.Result, err +} + +// DeleteLibraryPanel deletes a panel by UID. +func (c *Client) DeleteLibraryPanel(uid string) (*LibraryPanelDeleteResponse, error) { + path := fmt.Sprintf("/api/library-elements/%s", uid) + + resp := &LibraryPanelDeleteResponse{} + err := c.request("DELETE", path, nil, bytes.NewBuffer(nil), &resp) + if err != nil { + return nil, err + } + + return resp, err +} + +// LibraryPanelConnections gets library panel connections by UID. +func (c *Client) LibraryPanelConnections(uid string) (*[]LibraryPanelConnection, error) { + path := fmt.Sprintf("/api/library-elements/%s/connections", uid) + + resp := struct { + Result []LibraryPanelConnection `json:"result"` + }{} + + err := c.request("POST", path, nil, bytes.NewBuffer(nil), &resp) + if err != nil { + return nil, err + } + + return &resp.Result, err +} + +// LibraryPanelConnectedDashboards gets Dashboards using this Library Panel. +func (c *Client) LibraryPanelConnectedDashboards(uid string) ([]FolderDashboardSearchResponse, error) { + connections, err := c.LibraryPanelConnections(uid) + if err != nil { + return nil, err + } + + var dashboardIds []int64 + for _, connection := range *connections { + dashboardIds = append(dashboardIds, connection.DashboardID) + } + + return c.DashboardsByIDs(dashboardIds) +} diff --git a/library_panel_test.go b/library_panel_test.go new file mode 100644 index 00000000..e39023d4 --- /dev/null +++ b/library_panel_test.go @@ -0,0 +1,268 @@ +package gapi + +import ( + "testing" + + "github.com/gobs/pretty" +) + +const ( + getLibraryPanelResponse = `{ + "result": { + "id": 25, + "orgId": 1, + "folderId": 0, + "uid": "V--OrYHnz", + "name": "API docs Example", + "kind": 1, + "model": { + "description": "", + "type": "" + }, + "version": 1, + "meta": { + "folderName": "General", + "folderUid": "", + "connectedDashboards": 1, + "created": "2021-09-27T09:56:17+02:00", + "updated": "2021-09-27T09:56:17+02:00", + "createdBy": { + "id": 1, + "name": "admin", + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56" + }, + "updatedBy": { + "id": 1, + "name": "admin", + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56" + } + } + } + }` + + patchLibraryPanelResponse = `{ + "result": { + "id": 25, + "orgId": 1, + "folderId": 0, + "uid": "V--OrYHnz", + "name": "Updated library panel name", + "kind": 1, + "model": { + "description": "new description", + "type": "" + }, + "version": 1, + "meta": { + "folderName": "General", + "folderUid": "", + "connectedDashboards": 1, + "created": "2021-09-27T09:56:17+02:00", + "updated": "2021-09-27T09:56:17+02:00", + "createdBy": { + "id": 1, + "name": "admin", + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56" + }, + "updatedBy": { + "id": 1, + "name": "admin", + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56" + } + } + } + }` + + deleteLibraryPanelResponse = `{ + "message": "Library element deleted", + "id": 28 + }` + + getLibraryPanelConnectionsResponse = `{ + "result": [ + { + "id": 148, + "kind": 1, + "elementId": 25, + "connectionId": 527, + "created": "2021-09-27T10:00:07+02:00", + "createdBy": { + "id": 1, + "name": "admin", + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56" + } + } + ] + }` + + getLibraryPanelConnectedDashboardsResponse = `[ + { + "id":1, + "uid": "cIBgcSjkk", + "title":"Production Overview", + "url": "/d/cIBgcSjkk/production-overview", + "type":"dash-db", + "tags":["prod"], + "isStarred":true, + "uri":"db/production-overview" + }, + { + "id":2, + "uid": "SjkkcIBgc", + "title":"Production Overview 2", + "url": "/d/SjkkcIBgc/production-overview-2", + "type":"dash-db", + "tags":["prod"], + "isStarred":true, + "uri":"db/production-overview" + } + ]` +) + +func TestLibraryPanelCreate(t *testing.T) { + server, client := gapiTestTools(t, 200, getLibraryPanelResponse) + defer server.Close() + + panel := LibraryPanel{ + Folder: 0, + Name: "API docs Example", + Model: map[string]interface{}{"description": "", "type": ""}, + } + + resp, err := client.NewLibraryPanel(panel) + if err != nil { + t.Fatal(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp.UID != "V--OrYHnz" { + t.Errorf("Invalid uid - %s, Expected %s", resp.UID, "V--OrYHnz") + } + + for _, code := range []int{400, 401, 403} { + server.code = code + _, err = client.NewLibraryPanel(panel) + if err == nil { + t.Errorf("%d not detected", code) + } + } +} + +func TestLibraryPanelGet(t *testing.T) { + server, client := gapiTestTools(t, 200, getLibraryPanelResponse) + defer server.Close() + + resp, err := client.LibraryPanelByName("API docs Example") + if err != nil { + t.Error(err) + } + if resp.UID != "V--OrYHnz" { + t.Errorf("Invalid uid - %s, Expected %s", resp.UID, "V--OrYHnz") + } + + resp, err = client.LibraryPanelByUID("V--OrYHnz") + if err != nil { + t.Fatal(err) + } + if resp.Name != "API docs Example" { + t.Fatalf("Invalid Name - %s, Expected %s", resp.Name, "API docs Example") + } + + for _, code := range []int{401, 403, 404} { + server.code = code + _, err = client.LibraryPanelByName("test") + if err == nil { + t.Errorf("%d not detected", code) + } + + _, err = client.LibraryPanelByUID("V--OrYHnz") + if err == nil { + t.Errorf("%d not detected", code) + } + } +} + +func TestPatchLibraryPanel(t *testing.T) { + server, client := gapiTestTools(t, 200, patchLibraryPanelResponse) + defer server.Close() + + panel := &LibraryPanel{ + Folder: 1, + Name: "Updated library panel name", + Model: map[string]interface{}{"description": "new description", "type": ""}, + } + resp, err := client.PatchLibraryPanel("V--OrYHnz", panel) + if err != nil { + t.Fatal(err) + } + + if resp.Name != "Updated library panel name" { + t.Fatalf("Invalid Name - %s, Expected %s", resp.Name, "Updated library panel name") + } + if resp.Model["description"] != "new description" { + t.Fatalf("Invalid panel JSON description - %s, Expected %s", resp.Name, "Updated library panel name") + } + + for _, code := range []int{401, 403, 404} { + server.code = code + + _, err := client.LibraryPanelByUID("V--OrYHnz") + if err == nil { + t.Errorf("%d not detected", code) + } + } +} + +func TestLibraryPanelGetConnections(t *testing.T) { + server, client := gapiTestTools(t, 200, getLibraryPanelConnectionsResponse) + defer server.Close() + + resp, err := client.LibraryPanelConnections("V--OrYHnz") + if err != nil { + t.Fatal(err) + } + + if (*resp)[0].ID != int64(148) { + t.Fatalf("Invalid connection id - %d, Expected %d", (*resp)[0].ID, 148) + } +} + +func TestLibraryPanelConnectedDashboards(t *testing.T) { + server, client := gapiTestTools(t, 200, getLibraryPanelConnectionsResponse) + defer server.Close() + + connections, err := client.LibraryPanelConnections("V--OrYHnz") + if err != nil { + t.Fatal(err) + } + + var dashboardIds []int64 + for _, connection := range *connections { + dashboardIds = append(dashboardIds, connection.DashboardID) + } + + _, client = gapiTestTools(t, 200, getLibraryPanelConnectedDashboardsResponse) + dashboards, err := client.DashboardsByIDs(dashboardIds) + if err != nil { + t.Fatal(err) + } + + if dashboards[0].Title != "Production Overview" { + t.Fatalf("Invalid title from connected dashboard 0 - %s, Expected %s", dashboards[0].Title, "Production Overview") + } +} + +func TestLibraryPanelDelete(t *testing.T) { + server, client := gapiTestTools(t, 200, deleteLibraryPanelResponse) + defer server.Close() + + resp, err := client.DeleteLibraryPanel("V--OrYHnz") + if err != nil { + t.Fatal(err) + } + + if resp.Message != "Library element deleted" { + t.Error("Failed to delete. Response should contain the correct response message") + } +}