Skip to content

Commit 00eb935

Browse files
authored
feat(jtk): add dashboard management commands (#165)
Add commands for managing Jira dashboards and their gadgets: - dashboards list (with --search and --max flags) - dashboards get <id> (shows details + gadgets) - dashboards create --name <name> [--description <desc>] - dashboards delete <id> - dashboards gadgets list <dashboard-id> - dashboards gadgets remove <dashboard-id> <gadget-id> Closes #147
1 parent 2ad774d commit 00eb935

5 files changed

Lines changed: 945 additions & 0 deletions

File tree

tools/jtk/api/dashboards.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/url"
7+
"strconv"
8+
)
9+
10+
// Dashboard represents a Jira dashboard
11+
type Dashboard struct {
12+
ID string `json:"id"`
13+
Name string `json:"name"`
14+
Description string `json:"description,omitempty"`
15+
Owner *User `json:"owner,omitempty"`
16+
View string `json:"view,omitempty"`
17+
IsFavourite bool `json:"isFavourite,omitempty"`
18+
Popularity int `json:"popularity,omitempty"`
19+
EditPerm []SharePerm `json:"editPermissions,omitempty"`
20+
SharePerm []SharePerm `json:"sharePermissions,omitempty"`
21+
}
22+
23+
// SharePerm represents a dashboard sharing permission
24+
type SharePerm struct {
25+
Type string `json:"type"` // "global", "project", "group", etc.
26+
}
27+
28+
// DashboardGadget represents a gadget on a dashboard
29+
type DashboardGadget struct {
30+
ID int `json:"id"`
31+
Title string `json:"title"`
32+
ModuleID string `json:"moduleKey,omitempty"`
33+
URI string `json:"uri,omitempty"`
34+
Color string `json:"color,omitempty"`
35+
Position DashboardGadgetPos `json:"position,omitempty"`
36+
Props map[string]interface{} `json:"properties,omitempty"`
37+
}
38+
39+
// DashboardGadgetPos represents the position of a gadget on a dashboard
40+
type DashboardGadgetPos struct {
41+
Row int `json:"row"`
42+
Column int `json:"column"`
43+
}
44+
45+
// DashboardsResponse represents a paginated list of dashboards
46+
type DashboardsResponse struct {
47+
StartAt int `json:"startAt"`
48+
MaxResults int `json:"maxResults"`
49+
Total int `json:"total"`
50+
Dashboards []Dashboard `json:"dashboards"`
51+
}
52+
53+
// DashboardGadgetsResponse represents a list of gadgets on a dashboard
54+
type DashboardGadgetsResponse struct {
55+
Gadgets []DashboardGadget `json:"gadgets"`
56+
}
57+
58+
// CreateDashboardRequest represents a request to create a dashboard
59+
type CreateDashboardRequest struct {
60+
Name string `json:"name"`
61+
Description string `json:"description,omitempty"`
62+
EditPermissions []SharePerm `json:"editPermissions"`
63+
SharePermissions []SharePerm `json:"sharePermissions"`
64+
}
65+
66+
// GetDashboards returns a paginated list of dashboards
67+
func (c *Client) GetDashboards(startAt, maxResults int) (*DashboardsResponse, error) {
68+
params := map[string]string{}
69+
if startAt > 0 {
70+
params["startAt"] = strconv.Itoa(startAt)
71+
}
72+
if maxResults > 0 {
73+
params["maxResults"] = strconv.Itoa(maxResults)
74+
}
75+
76+
urlStr := buildURL(fmt.Sprintf("%s/dashboard", c.BaseURL), params)
77+
78+
body, err := c.get(urlStr)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
var result DashboardsResponse
84+
if err := json.Unmarshal(body, &result); err != nil {
85+
return nil, fmt.Errorf("failed to parse dashboards: %w", err)
86+
}
87+
88+
return &result, nil
89+
}
90+
91+
// SearchDashboards searches for dashboards by name
92+
func (c *Client) SearchDashboards(name string, maxResults int) (*DashboardSearchResponse, error) {
93+
params := map[string]string{}
94+
if name != "" {
95+
params["dashboardName"] = name
96+
}
97+
if maxResults > 0 {
98+
params["maxResults"] = strconv.Itoa(maxResults)
99+
}
100+
101+
urlStr := buildURL(fmt.Sprintf("%s/dashboard/search", c.BaseURL), params)
102+
103+
body, err := c.get(urlStr)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
var result DashboardSearchResponse
109+
if err := json.Unmarshal(body, &result); err != nil {
110+
return nil, fmt.Errorf("failed to parse dashboard search: %w", err)
111+
}
112+
113+
return &result, nil
114+
}
115+
116+
// DashboardSearchResponse represents the response from dashboard search
117+
type DashboardSearchResponse struct {
118+
StartAt int `json:"startAt"`
119+
MaxResults int `json:"maxResults"`
120+
Total int `json:"total"`
121+
Values []Dashboard `json:"values"`
122+
}
123+
124+
// GetDashboard returns a dashboard by ID
125+
func (c *Client) GetDashboard(dashboardID string) (*Dashboard, error) {
126+
if dashboardID == "" {
127+
return nil, fmt.Errorf("dashboard ID is required")
128+
}
129+
130+
urlStr := fmt.Sprintf("%s/dashboard/%s", c.BaseURL, url.PathEscape(dashboardID))
131+
132+
body, err := c.get(urlStr)
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
var dash Dashboard
138+
if err := json.Unmarshal(body, &dash); err != nil {
139+
return nil, fmt.Errorf("failed to parse dashboard: %w", err)
140+
}
141+
142+
return &dash, nil
143+
}
144+
145+
// CreateDashboard creates a new dashboard
146+
func (c *Client) CreateDashboard(req CreateDashboardRequest) (*Dashboard, error) {
147+
urlStr := fmt.Sprintf("%s/dashboard", c.BaseURL)
148+
149+
body, err := c.post(urlStr, req)
150+
if err != nil {
151+
return nil, err
152+
}
153+
154+
var dash Dashboard
155+
if err := json.Unmarshal(body, &dash); err != nil {
156+
return nil, fmt.Errorf("failed to parse dashboard: %w", err)
157+
}
158+
159+
return &dash, nil
160+
}
161+
162+
// DeleteDashboard deletes a dashboard by ID
163+
func (c *Client) DeleteDashboard(dashboardID string) error {
164+
if dashboardID == "" {
165+
return fmt.Errorf("dashboard ID is required")
166+
}
167+
168+
urlStr := fmt.Sprintf("%s/dashboard/%s", c.BaseURL, url.PathEscape(dashboardID))
169+
_, err := c.delete(urlStr)
170+
return err
171+
}
172+
173+
// GetDashboardGadgets returns the gadgets on a dashboard
174+
func (c *Client) GetDashboardGadgets(dashboardID string) (*DashboardGadgetsResponse, error) {
175+
if dashboardID == "" {
176+
return nil, fmt.Errorf("dashboard ID is required")
177+
}
178+
179+
urlStr := fmt.Sprintf("%s/dashboard/%s/gadget", c.BaseURL, url.PathEscape(dashboardID))
180+
181+
body, err := c.get(urlStr)
182+
if err != nil {
183+
return nil, err
184+
}
185+
186+
var result DashboardGadgetsResponse
187+
if err := json.Unmarshal(body, &result); err != nil {
188+
return nil, fmt.Errorf("failed to parse gadgets: %w", err)
189+
}
190+
191+
return &result, nil
192+
}
193+
194+
// RemoveDashboardGadget removes a gadget from a dashboard
195+
func (c *Client) RemoveDashboardGadget(dashboardID string, gadgetID int) error {
196+
if dashboardID == "" {
197+
return fmt.Errorf("dashboard ID is required")
198+
}
199+
200+
urlStr := fmt.Sprintf("%s/dashboard/%s/gadget/%d", c.BaseURL, url.PathEscape(dashboardID), gadgetID)
201+
_, err := c.delete(urlStr)
202+
return err
203+
}

tools/jtk/api/dashboards_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestGetDashboards(t *testing.T) {
15+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16+
assert.Equal(t, "/rest/api/3/dashboard", r.URL.Path)
17+
json.NewEncoder(w).Encode(DashboardsResponse{
18+
Total: 1,
19+
Dashboards: []Dashboard{
20+
{ID: "10001", Name: "My Dashboard"},
21+
},
22+
})
23+
}))
24+
defer server.Close()
25+
26+
client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
27+
require.NoError(t, err)
28+
29+
result, err := client.GetDashboards(0, 50)
30+
require.NoError(t, err)
31+
require.Len(t, result.Dashboards, 1)
32+
assert.Equal(t, "My Dashboard", result.Dashboards[0].Name)
33+
}
34+
35+
func TestSearchDashboards(t *testing.T) {
36+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37+
assert.Equal(t, "/rest/api/3/dashboard/search", r.URL.Path)
38+
assert.Equal(t, "Sprint", r.URL.Query().Get("dashboardName"))
39+
json.NewEncoder(w).Encode(DashboardSearchResponse{
40+
Total: 1,
41+
Values: []Dashboard{
42+
{ID: "10002", Name: "Sprint Board"},
43+
},
44+
})
45+
}))
46+
defer server.Close()
47+
48+
client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
49+
require.NoError(t, err)
50+
51+
result, err := client.SearchDashboards("Sprint", 50)
52+
require.NoError(t, err)
53+
require.Len(t, result.Values, 1)
54+
assert.Equal(t, "Sprint Board", result.Values[0].Name)
55+
}
56+
57+
func TestGetDashboard(t *testing.T) {
58+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59+
assert.Equal(t, "/rest/api/3/dashboard/10001", r.URL.Path)
60+
json.NewEncoder(w).Encode(Dashboard{
61+
ID: "10001",
62+
Name: "My Dashboard",
63+
})
64+
}))
65+
defer server.Close()
66+
67+
client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
68+
require.NoError(t, err)
69+
70+
dash, err := client.GetDashboard("10001")
71+
require.NoError(t, err)
72+
assert.Equal(t, "My Dashboard", dash.Name)
73+
}
74+
75+
func TestGetDashboard_EmptyID(t *testing.T) {
76+
_, err := (&Client{}).GetDashboard("")
77+
assert.Error(t, err)
78+
}
79+
80+
func TestCreateDashboard(t *testing.T) {
81+
var capturedBody []byte
82+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
83+
assert.Equal(t, "POST", r.Method)
84+
capturedBody, _ = io.ReadAll(r.Body)
85+
w.WriteHeader(http.StatusOK)
86+
json.NewEncoder(w).Encode(Dashboard{ID: "10099", Name: "New Board"})
87+
}))
88+
defer server.Close()
89+
90+
client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
91+
require.NoError(t, err)
92+
93+
dash, err := client.CreateDashboard(CreateDashboardRequest{
94+
Name: "New Board",
95+
EditPermissions: []SharePerm{{Type: "global"}},
96+
SharePermissions: []SharePerm{{Type: "global"}},
97+
})
98+
require.NoError(t, err)
99+
assert.Equal(t, "10099", dash.ID)
100+
assert.Equal(t, "New Board", dash.Name)
101+
102+
var req CreateDashboardRequest
103+
err = json.Unmarshal(capturedBody, &req)
104+
require.NoError(t, err)
105+
assert.Equal(t, "New Board", req.Name)
106+
}
107+
108+
func TestDeleteDashboard(t *testing.T) {
109+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
110+
assert.Equal(t, "/rest/api/3/dashboard/10001", r.URL.Path)
111+
assert.Equal(t, "DELETE", r.Method)
112+
w.WriteHeader(http.StatusNoContent)
113+
}))
114+
defer server.Close()
115+
116+
client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
117+
require.NoError(t, err)
118+
119+
err = client.DeleteDashboard("10001")
120+
require.NoError(t, err)
121+
}
122+
123+
func TestDeleteDashboard_EmptyID(t *testing.T) {
124+
assert.Error(t, (&Client{}).DeleteDashboard(""))
125+
}
126+
127+
func TestGetDashboardGadgets(t *testing.T) {
128+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129+
assert.Equal(t, "/rest/api/3/dashboard/10001/gadget", r.URL.Path)
130+
json.NewEncoder(w).Encode(DashboardGadgetsResponse{
131+
Gadgets: []DashboardGadget{
132+
{ID: 1, Title: "Filter Results"},
133+
{ID: 2, Title: "Pie Chart"},
134+
},
135+
})
136+
}))
137+
defer server.Close()
138+
139+
client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
140+
require.NoError(t, err)
141+
142+
result, err := client.GetDashboardGadgets("10001")
143+
require.NoError(t, err)
144+
require.Len(t, result.Gadgets, 2)
145+
assert.Equal(t, "Filter Results", result.Gadgets[0].Title)
146+
}
147+
148+
func TestRemoveDashboardGadget(t *testing.T) {
149+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
150+
assert.Equal(t, "/rest/api/3/dashboard/10001/gadget/42", r.URL.Path)
151+
assert.Equal(t, "DELETE", r.Method)
152+
w.WriteHeader(http.StatusNoContent)
153+
}))
154+
defer server.Close()
155+
156+
client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
157+
require.NoError(t, err)
158+
159+
err = client.RemoveDashboardGadget("10001", 42)
160+
require.NoError(t, err)
161+
}

tools/jtk/cmd/jtk/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/comments"
1414
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/completion"
1515
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/configcmd"
16+
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/dashboards"
1617
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/fields"
1718
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/initcmd"
1819
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/issues"
@@ -46,6 +47,7 @@ func run() error {
4647
attachments.Register(rootCmd, opts)
4748
automation.Register(rootCmd, opts)
4849
boards.Register(rootCmd, opts)
50+
dashboards.Register(rootCmd, opts)
4951
projects.Register(rootCmd, opts)
5052
sprints.Register(rootCmd, opts)
5153
users.Register(rootCmd, opts)

0 commit comments

Comments
 (0)