Skip to content

Commit 50d1101

Browse files
dsblankDouglas Blankclaude
authored
feat: add create_dashboard() and in-place panel update via upload_panel_zip() (#23)
* feat: add create_dashboard() and enhance upload_panel_zip() with in-place update - Add API.create_dashboard(workspace, project_name, template_name, panels, template_id) that looks up the project_id by name and POSTs to /write/dashboard-template/create - Add template_id param to API.upload_panel_zip() to overwrite an existing panel in place via the templateId query param on /write/template/upload - Fix API.upload_panel_code() to return the result from upload_panel_zip() - Add README-panel-and-dashboard-enhancements.md documenting the new API Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add update_dashboard() and template_id support to upload_panel_code() - upload_panel_code() now accepts optional template_id to overwrite an existing panel's code in place rather than creating a new one - add update_dashboard() to update an existing dashboard's name and/or panel associations via dashboard-templates/project/upsert endpoint - update README with new method signatures and examples Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: fix upload_panel_code return type and extract _get_project_id helper - Fix `-> None` annotation on `upload_panel_code` to `-> Dict[str, Any]` - Extract shared `_get_project_id()` helper used by both `create_dashboard` and `update_dashboard` to eliminate duplicated project lookup logic Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Douglas Blank <doug@comet.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6cc97cb commit 50d1101

2 files changed

Lines changed: 317 additions & 3 deletions

File tree

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Panel and Dashboard Enhancements
2+
3+
This document covers the new and updated methods in `cometx.api.API` for managing
4+
code panels and dashboards programmatically.
5+
6+
## Uploading a Panel
7+
8+
### `upload_panel_code(workspace, panel_name, code, template_id=None)`
9+
10+
Upload Python code as a panel in a workspace. If `template_id` is provided, the
11+
existing panel is **overwritten in place** rather than creating a new one.
12+
13+
```python
14+
from cometx import API
15+
16+
api = API()
17+
18+
# Create a new panel
19+
result = api.upload_panel_code("my-workspace", "My Panel", "print('hello world')")
20+
panel_id = result["templateId"]
21+
22+
# Update an existing panel's code in place
23+
result = api.upload_panel_code("my-workspace", "My Panel", "print('updated')", template_id=panel_id)
24+
```
25+
26+
Returns a dict with the panel's `templateId`.
27+
28+
---
29+
30+
### `upload_panel_zip(workspace, filename, template_id=None)`
31+
32+
Upload a panel zip file. If `template_id` is provided, the existing panel is
33+
**overwritten in place** rather than creating a new one. Dashboards that reference
34+
the panel will reflect the updated code after a browser refresh.
35+
36+
```python
37+
from cometx import API
38+
from cometx.panel_utils import create_panel_zip
39+
40+
api = API()
41+
42+
# Create a new panel
43+
result = api.upload_panel_zip("my-workspace", "panel.zip")
44+
45+
# Update an existing panel in place
46+
result = api.upload_panel_zip("my-workspace", "panel.zip", template_id="abc123")
47+
```
48+
49+
> **Note:** After updating a panel in place, you need to **refresh the browser page**
50+
> to see the updated panel code in the Comet UI.
51+
52+
---
53+
54+
## Creating a Dashboard
55+
56+
### `create_dashboard(workspace, project_name, template_name, template_id=None, panels=None)`
57+
58+
Create a new dashboard in a project.
59+
60+
```python
61+
from cometx import API
62+
63+
api = API()
64+
65+
# Create an empty dashboard
66+
dashboard = api.create_dashboard(
67+
workspace="my-workspace",
68+
project_name="my-project",
69+
template_name="My Dashboard",
70+
)
71+
72+
# Create a dashboard with panels
73+
dashboard = api.create_dashboard(
74+
workspace="my-workspace",
75+
project_name="my-project",
76+
template_name="My Dashboard",
77+
panels=["panel-template-id-1", "panel-template-id-2"],
78+
)
79+
80+
# Create a dashboard cloned from an existing one (copies panel associations)
81+
dashboard = api.create_dashboard(
82+
workspace="my-workspace",
83+
project_name="my-project",
84+
template_name="My Dashboard Copy",
85+
template_id="existing-dashboard-template-id",
86+
)
87+
```
88+
89+
**Parameters:**
90+
91+
| Parameter | Type | Description |
92+
|---|---|---|
93+
| `workspace` | str | The workspace name the project belongs to |
94+
| `project_name` | str | The project name to create the dashboard in |
95+
| `template_name` | str | Name for the new dashboard |
96+
| `template_id` | str (optional) | Source dashboard template ID to clone panel associations from |
97+
| `panels` | list (optional) | List of panel `templateId` values to include in the new dashboard |
98+
99+
Returns a dict representing the created `DashboardTemplate`, including its new `template_id`.
100+
101+
---
102+
103+
## Updating a Dashboard
104+
105+
### `update_dashboard(workspace, project_name, template_id, template_name=None, panels=None)`
106+
107+
Update an existing dashboard in a project. Optionally rename it, replace its panel
108+
associations, or both.
109+
110+
```python
111+
from cometx import API
112+
113+
api = API()
114+
115+
# Rename a dashboard
116+
api.update_dashboard(
117+
workspace="my-workspace",
118+
project_name="my-project",
119+
template_id="existing-dashboard-id",
120+
template_name="Renamed Dashboard",
121+
)
122+
123+
# Replace the panels on a dashboard
124+
api.update_dashboard(
125+
workspace="my-workspace",
126+
project_name="my-project",
127+
template_id="existing-dashboard-id",
128+
panels=["panel-template-id-1", "panel-template-id-2"],
129+
)
130+
131+
# Rename and update panels at once
132+
api.update_dashboard(
133+
workspace="my-workspace",
134+
project_name="my-project",
135+
template_id="existing-dashboard-id",
136+
template_name="Renamed Dashboard",
137+
panels=["panel-template-id-1", "panel-template-id-2"],
138+
)
139+
```
140+
141+
**Parameters:**
142+
143+
| Parameter | Type | Description |
144+
|---|---|---|
145+
| `workspace` | str | The workspace name the project belongs to |
146+
| `project_name` | str | The project name the dashboard belongs to |
147+
| `template_id` | str | The ID of the dashboard to update |
148+
| `template_name` | str (optional) | New name for the dashboard |
149+
| `panels` | list (optional) | List of panel `templateId` values to set on the dashboard |
150+
151+
Returns a dict representing the updated `DashboardTemplate`.
152+
153+
---
154+
155+
## Typical Workflow
156+
157+
```python
158+
from cometx import API
159+
160+
api = API()
161+
162+
# 1. Upload a panel
163+
result = api.upload_panel_code("my-workspace", "Hello World", "print('hello world')")
164+
panel_id = result["templateId"]
165+
166+
# 2. Create a dashboard with the panel
167+
dashboard = api.create_dashboard(
168+
workspace="my-workspace",
169+
project_name="my-project",
170+
template_name="My Dashboard",
171+
panels=[panel_id],
172+
)
173+
dashboard_id = dashboard["template_id"]
174+
175+
# 3. Later, update the panel code in place
176+
api.upload_panel_code("my-workspace", "Hello World", "print('updated')", template_id=panel_id)
177+
# Refresh your browser to see the updated panel in the Comet UI
178+
179+
# 4. Add more panels to the dashboard
180+
api.update_dashboard(
181+
workspace="my-workspace",
182+
project_name="my-project",
183+
template_id=dashboard_id,
184+
panels=[panel_id, "another-panel-id"],
185+
)
186+
```

cometx/api.py

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,22 @@ def upload_panel_url(self, workspace, item):
171171
else:
172172
raise Exception("I don't know what to do with %r" % parsed_url.path)
173173

174-
def upload_panel_code(self, workspace: str, panel_name: str, code: str) -> None:
174+
def upload_panel_code(
175+
self,
176+
workspace: str,
177+
panel_name: str,
178+
code: str,
179+
template_id: str = None,
180+
) -> Dict[str, Any]:
175181
"""
176182
Upload Python code as a panel in a workspace.
177183
178184
Args:
179185
workspace (str): the workspace to place the panel into
180186
panel_name (str): the name of the panel
181187
code (str): the code to turn into a panel
188+
template_id (str, optional): if provided, overwrites the existing
189+
panel with this ID instead of creating a new one
182190
183191
Example:
184192
```python linenums="1"
@@ -194,15 +202,19 @@ def upload_panel_code(self, workspace: str, panel_name: str, code: str) -> None:
194202
```
195203
"""
196204
filename = create_panel_zip(panel_name, code)
197-
self.upload_panel_zip(workspace, filename)
205+
return self.upload_panel_zip(workspace, filename, template_id=template_id)
198206

199-
def upload_panel_zip(self, workspace: str, filename: str) -> Dict[str, str]:
207+
def upload_panel_zip(
208+
self, workspace: str, filename: str, template_id: str = None
209+
) -> Dict[str, str]:
200210
"""
201211
Upload a panel zip file to a workspace.
202212
203213
Args:
204214
workspace (str): the workspace to place the panel into
205215
filename (str): the name of the panel zip to upload
216+
template_id (str, optional): if provided, overwrites the existing
217+
panel with this ID instead of creating a new one
206218
207219
Returns: dictionary of results
208220
@@ -215,6 +227,8 @@ def upload_panel_zip(self, workspace: str, filename: str) -> Dict[str, str]:
215227
```
216228
"""
217229
params = {"teamName": workspace}
230+
if template_id is not None:
231+
params["templateId"] = template_id
218232
payload = {}
219233
with open(filename, "rb") as fp:
220234
files = {"file": (filename, fp)}
@@ -226,6 +240,120 @@ def upload_panel_zip(self, workspace: str, filename: str) -> Dict[str, str]:
226240
)
227241
return results.json()
228242

243+
def _get_project_id(self, workspace: str, project_name: str) -> str:
244+
projects = self._client.get_from_endpoint(
245+
"projects", params={"workspaceName": workspace}
246+
)
247+
project = next(
248+
(p for p in projects["projects"] if p["projectName"] == project_name),
249+
None,
250+
)
251+
if project is None:
252+
raise Exception(
253+
f"Project {project_name!r} not found in workspace {workspace!r}"
254+
)
255+
return project["projectId"]
256+
257+
def create_dashboard(
258+
self,
259+
workspace: str,
260+
project_name: str,
261+
template_name: str,
262+
template_id: str = None,
263+
panels: List[str] = None,
264+
**kwargs,
265+
) -> Dict[str, Any]:
266+
"""
267+
Create a new dashboard in a project.
268+
269+
Args:
270+
workspace (str): the workspace name the project belongs to
271+
project_name (str): the project name to create the dashboard in
272+
template_name (str): name for the new dashboard
273+
template_id (str, optional): source template ID to clone code
274+
panel associations from an existing dashboard
275+
panels (list, optional): list of panel template IDs to include
276+
in the new dashboard
277+
**kwargs: additional DashboardTemplate fields
278+
279+
Returns: dictionary representing the created DashboardTemplate
280+
281+
Example:
282+
```python linenums="1"
283+
from cometx import API
284+
285+
api = API()
286+
dashboard = api.create_dashboard(
287+
workspace="my-workspace",
288+
project_name="my-project",
289+
template_name="My Dashboard",
290+
panels=["panel-template-id-1", "panel-template-id-2"],
291+
)
292+
```
293+
"""
294+
project_id = self._get_project_id(workspace, project_name)
295+
296+
body = {"project_id": project_id, "template_name": template_name, **kwargs}
297+
if template_id is not None:
298+
body["template_id"] = template_id
299+
if panels is not None:
300+
body["codePanelTemplateIds"] = panels
301+
results = self._client.post_from_endpoint(
302+
"write/dashboard-template/create",
303+
payload=body,
304+
)
305+
return results.json()
306+
307+
def update_dashboard(
308+
self,
309+
workspace: str,
310+
project_name: str,
311+
template_id: str,
312+
template_name: str = None,
313+
panels: List[str] = None,
314+
**kwargs,
315+
) -> Dict[str, Any]:
316+
"""
317+
Update an existing dashboard in a project.
318+
319+
Args:
320+
workspace (str): the workspace name the project belongs to
321+
project_name (str): the project name the dashboard belongs to
322+
template_id (str): the ID of the dashboard to update
323+
template_name (str, optional): new name for the dashboard
324+
panels (list, optional): list of panel template IDs to set on
325+
the dashboard
326+
**kwargs: additional DashboardTemplate fields
327+
328+
Returns: dictionary representing the updated DashboardTemplate
329+
330+
Example:
331+
```python linenums="1"
332+
from cometx import API
333+
334+
api = API()
335+
dashboard = api.update_dashboard(
336+
workspace="my-workspace",
337+
project_name="my-project",
338+
template_id="existing-dashboard-id",
339+
template_name="Renamed Dashboard",
340+
panels=["panel-template-id-1", "panel-template-id-2"],
341+
)
342+
```
343+
"""
344+
project_id = self._get_project_id(workspace, project_name)
345+
346+
body = {"project_id": project_id, "template_id": template_id, **kwargs}
347+
if template_name is not None:
348+
body["template_name"] = template_name
349+
if panels is not None:
350+
body["codePanelTemplateIds"] = panels
351+
results = self._client.post_from_endpoint(
352+
"dashboard-templates/project/upsert",
353+
payload=body,
354+
)
355+
return results.json()
356+
229357
def log_pr_curves(
230358
self, experiment, y_true, y_predicted, labels=None, overwrite=False, step=None
231359
):

0 commit comments

Comments
 (0)