Skip to content

Commit e4b9c34

Browse files
authored
Merge pull request #15 from posit-dev/otel-dash-updates
fix(champion-dashboard): incorporate feedback
2 parents 5415639 + 7e6665c commit e4b9c34

File tree

3 files changed

+161
-24
lines changed

3 files changed

+161
-24
lines changed

extensions/champion-dashboard/.python-version

Lines changed: 0 additions & 1 deletion
This file was deleted.

extensions/champion-dashboard/app.py

Lines changed: 160 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from shiny import App, ui, render, reactive
44
from posit.connect import Client
55
from io import BytesIO, TextIOWrapper
6+
import os
67
import requests
78
from prometheus_client import parser
89
from typing import Dict, List, Tuple, Optional
@@ -89,6 +90,24 @@
8990
padding-bottom: 10px;
9091
border-bottom: 1px solid rgba(55, 53, 47, 0.09);
9192
}
93+
.card-title a {
94+
color: #37352f;
95+
text-decoration: none;
96+
display: flex;
97+
align-items: center;
98+
gap: 6px;
99+
}
100+
.card-title a:hover {
101+
color: #2383e2;
102+
}
103+
.external-link-icon {
104+
width: 14px;
105+
height: 14px;
106+
opacity: 0.5;
107+
}
108+
.card-title a:hover .external-link-icon {
109+
opacity: 1;
110+
}
92111
.section-grid-4 {
93112
display: grid;
94113
grid-template-columns: repeat(4, 1fr);
@@ -180,35 +199,35 @@
180199
.section-content::-webkit-scrollbar-thumb:hover {
181200
background: rgba(55, 53, 47, 0.3);
182201
}
183-
.integration-table {
202+
.oauth-association-table {
184203
width: 100%;
185204
border-collapse: collapse;
186205
font-size: 14px;
187206
}
188-
.integration-table th {
207+
.oauth-association-table th {
189208
background-color: #f7f6f3;
190209
color: #37352f;
191210
font-weight: 600;
192211
padding: 12px;
193212
text-align: left;
194213
border-bottom: 2px solid rgba(55, 53, 47, 0.09);
195214
}
196-
.integration-table td {
215+
.oauth-association-table td {
197216
padding: 12px;
198217
color: #37352f;
199218
border-bottom: 1px solid rgba(55, 53, 47, 0.09);
200219
}
201-
.integration-table th:first-child {
220+
.oauth-associatio-table th:first-child {
202221
border-right: 2px solid rgba(55, 53, 47, 0.09);
203222
}
204-
.integration-table td:first-child {
223+
.oauth-association-table td:first-child {
205224
font-weight: 500;
206225
border-right: 1px solid rgba(55, 53, 47, 0.09);
207226
}
208-
.integration-table td:not(:first-child) {
227+
.oauth-association-table td:not(:first-child) {
209228
text-align: center;
210229
}
211-
.integration-table th:not(:first-child) {
230+
.oauth-association-table th:not(:first-child) {
212231
text-align: center;
213232
}
214233
.content-table {
@@ -247,6 +266,42 @@
247266
.content-table tbody tr:hover {
248267
background-color: #f7f6f3;
249268
}
269+
.template-header {
270+
position: relative;
271+
cursor: help;
272+
}
273+
.template-header .tooltip-text {
274+
visibility: hidden;
275+
background-color: #37352f;
276+
color: #fff;
277+
text-align: center;
278+
border-radius: 6px;
279+
padding: 8px 12px;
280+
position: absolute;
281+
z-index: 1;
282+
bottom: 125%;
283+
left: 50%;
284+
transform: translateX(-50%);
285+
white-space: nowrap;
286+
font-size: 12px;
287+
font-weight: 400;
288+
opacity: 0;
289+
transition: opacity 0.2s;
290+
}
291+
.template-header .tooltip-text::after {
292+
content: "";
293+
position: absolute;
294+
top: 100%;
295+
left: 50%;
296+
margin-left: -5px;
297+
border-width: 5px;
298+
border-style: solid;
299+
border-color: #37352f transparent transparent transparent;
300+
}
301+
.template-header:hover .tooltip-text {
302+
visibility: visible;
303+
opacity: 1;
304+
}
250305
"""
251306

252307
def fetch_all_prometheus_metrics(url: str) -> Dict[str, List[Tuple[Dict[str, str], float]]]:
@@ -307,17 +362,21 @@ def get_content_stats(metrics: Dict) -> Dict:
307362

308363
return stats
309364

310-
def get_integration_metrics(metrics: Dict) -> Dict:
311-
integrations_count = metrics.get('integrations_count', [])
365+
def get_oauth_association_metrics(metrics: Dict) -> Dict:
366+
associations_count = metrics.get('associations_count', [])
312367

313368
matrix = {}
314369
templates = set()
315370
auth_types = set()
316371

317-
for labels, value in integrations_count:
372+
for labels, value in associations_count:
318373
template = labels.get('integration_template')
319374
auth_type = labels.get('integration_auth_type')
320375

376+
# Collapse "Visitor API Key" into "Viewer" for this dashboard
377+
if auth_type == "Visitor API Key":
378+
auth_type = "Viewer"
379+
321380
if template and auth_type:
322381
templates.add(template)
323382
auth_types.add(auth_type)
@@ -332,6 +391,20 @@ def get_integration_metrics(metrics: Dict) -> Dict:
332391
'auth_types': sorted(auth_types)
333392
}
334393

394+
def get_integration_count_by_template(metrics: Dict) -> Dict[str, int]:
395+
"""Sum integration_count by template."""
396+
integration_count = metrics.get('integrations_count', [])
397+
result = {}
398+
399+
for labels, value in integration_count:
400+
template = labels.get('integration_template')
401+
if template:
402+
if template not in result:
403+
result[template] = 0
404+
result[template] += int(value)
405+
406+
return result
407+
335408
def get_system_info(client):
336409
response = client.get("server_settings")
337410
settings = response.json()
@@ -461,6 +534,36 @@ def create_key_value_list(items_dict):
461534
for label, value in items_dict.items()
462535
]
463536

537+
def external_link_icon():
538+
"""Create an SVG external link icon."""
539+
return ui.HTML(
540+
'<svg class="external-link-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
541+
'<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>'
542+
'<polyline points="15 3 21 3 21 9"/>'
543+
'<line x1="10" y1="14" x2="21" y2="3"/>'
544+
'</svg>'
545+
)
546+
547+
def create_linked_card_title(title: str, url: str):
548+
"""Create a card title with an external link."""
549+
return ui.div(
550+
ui.tags.a(
551+
title,
552+
external_link_icon(),
553+
href=url,
554+
target="_blank"
555+
),
556+
class_="card-title"
557+
)
558+
559+
def get_server_base_url() -> str:
560+
"""Get the base URL of the Connect server from environment."""
561+
url = os.environ.get('CONNECT_SERVER', '')
562+
# Remove trailing slash if present
563+
if url.endswith('/'):
564+
url = url[:-1]
565+
return url
566+
464567
app_ui = ui.page_fluid(
465568
ui.tags.style(DASHBOARD_CSS),
466569
ui.div(
@@ -481,8 +584,8 @@ def create_key_value_list(items_dict):
481584
class_="section-grid-4"
482585
),
483586
ui.div(
484-
ui.div("OAuth Integration Stats", class_="card-title"),
485-
ui.output_ui("integration_metrics_table"),
587+
ui.div("Content items with OAuth integrations", class_="card-title"),
588+
ui.output_ui("oauth_association_metrics_table"),
486589
class_="content-card"
487590
),
488591
ui.div(
@@ -501,6 +604,7 @@ def create_key_value_list(items_dict):
501604
def server(input, output, session):
502605
metrics = fetch_all_prometheus_metrics("http://localhost:3232/metrics")
503606
client = Client()
607+
server_base_url = get_server_base_url()
504608

505609
@output
506610
@render.ui
@@ -641,15 +745,26 @@ def sort_key(item):
641745

642746
@output
643747
@render.ui
644-
def integration_metrics_table():
645-
integration_data = get_integration_metrics(metrics)
646-
matrix = integration_data['matrix']
647-
templates = integration_data['templates']
648-
auth_types = integration_data['auth_types']
748+
def oauth_association_metrics_table():
749+
associations_data = get_oauth_association_metrics(metrics)
750+
matrix = associations_data['matrix']
751+
templates = associations_data['templates']
752+
auth_types = associations_data['auth_types']
753+
integration_counts = get_integration_count_by_template(metrics)
754+
755+
def create_template_header(template):
756+
count = integration_counts.get(template, 0)
757+
return ui.tags.th(
758+
ui.span(
759+
template,
760+
ui.span(f"Unique Integrations: {count}", class_="tooltip-text"),
761+
class_="template-header"
762+
)
763+
)
649764

650765
header_row = ui.tags.tr(
651766
ui.tags.th(""),
652-
*[ui.tags.th(template) for template in templates],
767+
*[create_template_header(template) for template in templates],
653768
ui.tags.th("Total")
654769
)
655770

@@ -668,7 +783,7 @@ def integration_metrics_table():
668783
return ui.tags.table(
669784
ui.tags.thead(header_row),
670785
ui.tags.tbody(*table_rows),
671-
class_="integration-table"
786+
class_="oauth-association-table"
672787
)
673788

674789
@output
@@ -693,9 +808,10 @@ def running_schedule_grid():
693808
)
694809
]
695810

696-
col1 = create_content_card(
697-
"Currently Running",
698-
ui.div(*running_items, class_="running-items-container")
811+
col1 = ui.div(
812+
ui.div("Currently Running", class_="card-title"),
813+
ui.div(*running_items, class_="running-items-container"),
814+
class_="content-card"
699815
)
700816

701817
col2 = ui.div(
@@ -741,11 +857,22 @@ def show_process_breakdown():
741857
process_count = get_process_count_by_tag(metrics)
742858
sorted_process_count = dict(sorted(process_count['by_tag'].items(), key=lambda x: x[1], reverse=True))
743859
breakdown_items = create_key_value_list(sorted_process_count)
860+
processes_url = f"{server_base_url}/connect/#/system/processes"
744861

745862
m = ui.modal(
746863
ui.div(
747864
ui.div("Process Breakdown by Tag", style="font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #37352f;"),
748865
ui.div(*breakdown_items, class_="section-content"),
866+
ui.div(
867+
ui.tags.a(
868+
"View all processes ",
869+
external_link_icon(),
870+
href=processes_url,
871+
target="_blank",
872+
style="color: #2383e2; text-decoration: none; font-size: 14px; display: inline-flex; align-items: center; gap: 4px;"
873+
),
874+
style="margin-top: 16px; padding-top: 12px; border-top: 1px solid rgba(55, 53, 47, 0.09);"
875+
),
749876
style="padding: 10px;"
750877
),
751878
title=None,
@@ -760,11 +887,22 @@ def show_schedule_breakdown():
760887
schedule_by_status = get_schedule_count_by_status(metrics)
761888
sorted_schedule = dict(sorted(schedule_by_status.items(), key=lambda x: x[1], reverse=True))
762889
breakdown_items = create_key_value_list(sorted_schedule)
890+
schedules_url = f"{server_base_url}/connect/#/system/schedules"
763891

764892
m = ui.modal(
765893
ui.div(
766894
ui.div("Schedule by Status", style="font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #37352f;"),
767895
ui.div(*breakdown_items, class_="section-content"),
896+
ui.div(
897+
ui.tags.a(
898+
"View all schedules ",
899+
external_link_icon(),
900+
href=schedules_url,
901+
target="_blank",
902+
style="color: #2383e2; text-decoration: none; font-size: 14px; display: inline-flex; align-items: center; gap: 4px;"
903+
),
904+
style="margin-top: 16px; padding-top: 12px; border-top: 1px solid rgba(55, 53, 47, 0.09);"
905+
),
768906
style="padding: 10px;"
769907
),
770908
title=None,

extensions/champion-dashboard/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"category": "example",
2727
"tags": ["python"],
2828
"minimumConnectVersion": "2025.11.0",
29-
"version": "1.0.5"
29+
"version": "1.0.6"
3030
},
3131
"files": {
3232
"requirements.txt": {

0 commit comments

Comments
 (0)