Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions tools/jtk/api/dashboards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package api

import (
"encoding/json"
"fmt"
"net/url"
"strconv"
)

// Dashboard represents a Jira dashboard
type Dashboard struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Owner *User `json:"owner,omitempty"`
View string `json:"view,omitempty"`
IsFavourite bool `json:"isFavourite,omitempty"`
Popularity int `json:"popularity,omitempty"`
EditPerm []SharePerm `json:"editPermissions,omitempty"`
SharePerm []SharePerm `json:"sharePermissions,omitempty"`
}

// SharePerm represents a dashboard sharing permission
type SharePerm struct {
Type string `json:"type"` // "global", "project", "group", etc.
}

// DashboardGadget represents a gadget on a dashboard
type DashboardGadget struct {
ID int `json:"id"`
Title string `json:"title"`
ModuleID string `json:"moduleKey,omitempty"`
URI string `json:"uri,omitempty"`
Color string `json:"color,omitempty"`
Position DashboardGadgetPos `json:"position,omitempty"`
Props map[string]interface{} `json:"properties,omitempty"`
}

// DashboardGadgetPos represents the position of a gadget on a dashboard
type DashboardGadgetPos struct {
Row int `json:"row"`
Column int `json:"column"`
}

// DashboardsResponse represents a paginated list of dashboards
type DashboardsResponse struct {
StartAt int `json:"startAt"`
MaxResults int `json:"maxResults"`
Total int `json:"total"`
Dashboards []Dashboard `json:"dashboards"`
}

// DashboardGadgetsResponse represents a list of gadgets on a dashboard
type DashboardGadgetsResponse struct {
Gadgets []DashboardGadget `json:"gadgets"`
}

// CreateDashboardRequest represents a request to create a dashboard
type CreateDashboardRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
EditPermissions []SharePerm `json:"editPermissions"`
SharePermissions []SharePerm `json:"sharePermissions"`
}

// GetDashboards returns a paginated list of dashboards
func (c *Client) GetDashboards(startAt, maxResults int) (*DashboardsResponse, error) {
params := map[string]string{}
if startAt > 0 {
params["startAt"] = strconv.Itoa(startAt)
}
if maxResults > 0 {
params["maxResults"] = strconv.Itoa(maxResults)
}

urlStr := buildURL(fmt.Sprintf("%s/dashboard", c.BaseURL), params)

body, err := c.get(urlStr)
if err != nil {
return nil, err
}

var result DashboardsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse dashboards: %w", err)
}

return &result, nil
}

// SearchDashboards searches for dashboards by name
func (c *Client) SearchDashboards(name string, maxResults int) (*DashboardSearchResponse, error) {
params := map[string]string{}
if name != "" {
params["dashboardName"] = name
}
if maxResults > 0 {
params["maxResults"] = strconv.Itoa(maxResults)
}

urlStr := buildURL(fmt.Sprintf("%s/dashboard/search", c.BaseURL), params)

body, err := c.get(urlStr)
if err != nil {
return nil, err
}

var result DashboardSearchResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse dashboard search: %w", err)
}

return &result, nil
}

// DashboardSearchResponse represents the response from dashboard search
type DashboardSearchResponse struct {
StartAt int `json:"startAt"`
MaxResults int `json:"maxResults"`
Total int `json:"total"`
Values []Dashboard `json:"values"`
}

// GetDashboard returns a dashboard by ID
func (c *Client) GetDashboard(dashboardID string) (*Dashboard, error) {
if dashboardID == "" {
return nil, fmt.Errorf("dashboard ID is required")
}

urlStr := fmt.Sprintf("%s/dashboard/%s", c.BaseURL, url.PathEscape(dashboardID))

body, err := c.get(urlStr)
if err != nil {
return nil, err
}

var dash Dashboard
if err := json.Unmarshal(body, &dash); err != nil {
return nil, fmt.Errorf("failed to parse dashboard: %w", err)
}

return &dash, nil
}

// CreateDashboard creates a new dashboard
func (c *Client) CreateDashboard(req CreateDashboardRequest) (*Dashboard, error) {
urlStr := fmt.Sprintf("%s/dashboard", c.BaseURL)

body, err := c.post(urlStr, req)
if err != nil {
return nil, err
}

var dash Dashboard
if err := json.Unmarshal(body, &dash); err != nil {
return nil, fmt.Errorf("failed to parse dashboard: %w", err)
}

return &dash, nil
}

// DeleteDashboard deletes a dashboard by ID
func (c *Client) DeleteDashboard(dashboardID string) error {
if dashboardID == "" {
return fmt.Errorf("dashboard ID is required")
}

urlStr := fmt.Sprintf("%s/dashboard/%s", c.BaseURL, url.PathEscape(dashboardID))
_, err := c.delete(urlStr)
return err
}

// GetDashboardGadgets returns the gadgets on a dashboard
func (c *Client) GetDashboardGadgets(dashboardID string) (*DashboardGadgetsResponse, error) {
if dashboardID == "" {
return nil, fmt.Errorf("dashboard ID is required")
}

urlStr := fmt.Sprintf("%s/dashboard/%s/gadget", c.BaseURL, url.PathEscape(dashboardID))

body, err := c.get(urlStr)
if err != nil {
return nil, err
}

var result DashboardGadgetsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse gadgets: %w", err)
}

return &result, nil
}

// RemoveDashboardGadget removes a gadget from a dashboard
func (c *Client) RemoveDashboardGadget(dashboardID string, gadgetID int) error {
if dashboardID == "" {
return fmt.Errorf("dashboard ID is required")
}

urlStr := fmt.Sprintf("%s/dashboard/%s/gadget/%d", c.BaseURL, url.PathEscape(dashboardID), gadgetID)
_, err := c.delete(urlStr)
return err
}
161 changes: 161 additions & 0 deletions tools/jtk/api/dashboards_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package api

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetDashboards(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/rest/api/3/dashboard", r.URL.Path)
json.NewEncoder(w).Encode(DashboardsResponse{
Total: 1,
Dashboards: []Dashboard{
{ID: "10001", Name: "My Dashboard"},
},
})
}))
defer server.Close()

client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
require.NoError(t, err)

result, err := client.GetDashboards(0, 50)
require.NoError(t, err)
require.Len(t, result.Dashboards, 1)
assert.Equal(t, "My Dashboard", result.Dashboards[0].Name)
}

func TestSearchDashboards(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/rest/api/3/dashboard/search", r.URL.Path)
assert.Equal(t, "Sprint", r.URL.Query().Get("dashboardName"))
json.NewEncoder(w).Encode(DashboardSearchResponse{
Total: 1,
Values: []Dashboard{
{ID: "10002", Name: "Sprint Board"},
},
})
}))
defer server.Close()

client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
require.NoError(t, err)

result, err := client.SearchDashboards("Sprint", 50)
require.NoError(t, err)
require.Len(t, result.Values, 1)
assert.Equal(t, "Sprint Board", result.Values[0].Name)
}

func TestGetDashboard(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/rest/api/3/dashboard/10001", r.URL.Path)
json.NewEncoder(w).Encode(Dashboard{
ID: "10001",
Name: "My Dashboard",
})
}))
defer server.Close()

client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
require.NoError(t, err)

dash, err := client.GetDashboard("10001")
require.NoError(t, err)
assert.Equal(t, "My Dashboard", dash.Name)
}

func TestGetDashboard_EmptyID(t *testing.T) {
_, err := (&Client{}).GetDashboard("")
assert.Error(t, err)
}

func TestCreateDashboard(t *testing.T) {
var capturedBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
capturedBody, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Dashboard{ID: "10099", Name: "New Board"})
}))
defer server.Close()

client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
require.NoError(t, err)

dash, err := client.CreateDashboard(CreateDashboardRequest{
Name: "New Board",
EditPermissions: []SharePerm{{Type: "global"}},
SharePermissions: []SharePerm{{Type: "global"}},
})
require.NoError(t, err)
assert.Equal(t, "10099", dash.ID)
assert.Equal(t, "New Board", dash.Name)

var req CreateDashboardRequest
err = json.Unmarshal(capturedBody, &req)
require.NoError(t, err)
assert.Equal(t, "New Board", req.Name)
}

func TestDeleteDashboard(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/rest/api/3/dashboard/10001", r.URL.Path)
assert.Equal(t, "DELETE", r.Method)
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()

client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
require.NoError(t, err)

err = client.DeleteDashboard("10001")
require.NoError(t, err)
}

func TestDeleteDashboard_EmptyID(t *testing.T) {
assert.Error(t, (&Client{}).DeleteDashboard(""))
}

func TestGetDashboardGadgets(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/rest/api/3/dashboard/10001/gadget", r.URL.Path)
json.NewEncoder(w).Encode(DashboardGadgetsResponse{
Gadgets: []DashboardGadget{
{ID: 1, Title: "Filter Results"},
{ID: 2, Title: "Pie Chart"},
},
})
}))
defer server.Close()

client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
require.NoError(t, err)

result, err := client.GetDashboardGadgets("10001")
require.NoError(t, err)
require.Len(t, result.Gadgets, 2)
assert.Equal(t, "Filter Results", result.Gadgets[0].Title)
}

func TestRemoveDashboardGadget(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/rest/api/3/dashboard/10001/gadget/42", r.URL.Path)
assert.Equal(t, "DELETE", r.Method)
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()

client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
require.NoError(t, err)

err = client.RemoveDashboardGadget("10001", 42)
require.NoError(t, err)
}
2 changes: 2 additions & 0 deletions tools/jtk/cmd/jtk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/comments"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/completion"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/configcmd"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/dashboards"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/fields"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/initcmd"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/issues"
Expand Down Expand Up @@ -46,6 +47,7 @@ func run() error {
attachments.Register(rootCmd, opts)
automation.Register(rootCmd, opts)
boards.Register(rootCmd, opts)
dashboards.Register(rootCmd, opts)
projects.Register(rootCmd, opts)
sprints.Register(rootCmd, opts)
users.Register(rootCmd, opts)
Expand Down
Loading
Loading