11/**
22 * sentry dashboard view
33 *
4- * View details of a specific dashboard.
4+ * View a dashboard with rendered widget data (sparklines, tables, big numbers).
5+ * Supports --refresh for auto-refreshing live display.
56 */
67
78import type { SentryContext } from "../../context.js" ;
8- import { getDashboard } from "../../lib/api-client.js" ;
9+ import { getDashboard , queryAllWidgets } from "../../lib/api-client.js" ;
910import { parseOrgProjectArg } from "../../lib/arg-parsing.js" ;
1011import { openInBrowser } from "../../lib/browser.js" ;
1112import { buildCommand } from "../../lib/command.js" ;
12- import { formatDashboardView } from "../../lib/formatters/human.js" ;
13+ import type { DashboardViewData } from "../../lib/formatters/dashboard.js" ;
14+ import { createDashboardViewRenderer } from "../../lib/formatters/dashboard.js" ;
1315import { CommandOutput } from "../../lib/formatters/output.js" ;
16+ import { isPlainOutput } from "../../lib/formatters/plain-detect.js" ;
1417import {
1518 applyFreshFlag ,
1619 FRESH_ALIASES ,
1720 FRESH_FLAG ,
1821} from "../../lib/list-command.js" ;
22+ import { logger } from "../../lib/logger.js" ;
1923import { withProgress } from "../../lib/polling.js" ;
24+ import { resolveOrgRegion } from "../../lib/region.js" ;
2025import { buildDashboardUrl } from "../../lib/sentry-urls.js" ;
21- import type { DashboardDetail } from "../../types/dashboard.js" ;
26+ import type {
27+ DashboardWidget ,
28+ WidgetDataResult ,
29+ } from "../../types/dashboard.js" ;
2230import {
2331 parseDashboardPositionalArgs ,
2432 resolveDashboardId ,
2533 resolveOrgFromTarget ,
2634} from "./resolve.js" ;
2735
36+ /** Default auto-refresh interval in seconds */
37+ const DEFAULT_REFRESH_INTERVAL = 60 ;
38+
39+ /** Minimum auto-refresh interval in seconds (avoid rate limiting) */
40+ const MIN_REFRESH_INTERVAL = 10 ;
41+
2842type ViewFlags = {
2943 readonly web : boolean ;
3044 readonly fresh : boolean ;
45+ readonly refresh ?: number ;
46+ readonly period ?: string ;
3147 readonly json : boolean ;
3248 readonly fields ?: string [ ] ;
3349} ;
3450
35- type ViewResult = DashboardDetail & { url : string } ;
51+ /**
52+ * Parse --refresh flag value.
53+ * Supports: -r (empty string → 60s default), -r 30 (explicit interval in seconds)
54+ */
55+ function parseRefresh ( value : string ) : number {
56+ if ( value === "" ) {
57+ return DEFAULT_REFRESH_INTERVAL ;
58+ }
59+ const num = Number . parseInt ( value , 10 ) ;
60+ if ( Number . isNaN ( num ) || num < MIN_REFRESH_INTERVAL ) {
61+ throw new Error (
62+ `--refresh interval must be at least ${ MIN_REFRESH_INTERVAL } seconds`
63+ ) ;
64+ }
65+ return num ;
66+ }
67+
68+ /**
69+ * Sleep that resolves early when an AbortSignal fires.
70+ * Resolves (not rejects) on abort for clean generator shutdown.
71+ */
72+ function abortableSleep ( ms : number , signal : AbortSignal ) : Promise < void > {
73+ return new Promise < void > ( ( resolve ) => {
74+ if ( signal . aborted ) {
75+ resolve ( ) ;
76+ return ;
77+ }
78+ const onAbort = ( ) => {
79+ clearTimeout ( timer ) ;
80+ resolve ( ) ;
81+ } ;
82+ const timer = setTimeout ( ( ) => {
83+ signal . removeEventListener ( "abort" , onAbort ) ;
84+ resolve ( ) ;
85+ } , ms ) ;
86+ signal . addEventListener ( "abort" , onAbort , { once : true } ) ;
87+ } ) ;
88+ }
89+
90+ /**
91+ * Build the DashboardViewData from a dashboard and its widget query results.
92+ */
93+ function buildViewData (
94+ dashboard : {
95+ id : string ;
96+ title : string ;
97+ dateCreated ?: string ;
98+ environment ?: string [ ] ;
99+ } ,
100+ widgetResults : Map < number , WidgetDataResult > ,
101+ widgets : DashboardWidget [ ] ,
102+ opts : { period : string ; url : string }
103+ ) : DashboardViewData {
104+ return {
105+ id : dashboard . id ,
106+ title : dashboard . title ,
107+ period : opts . period ,
108+ fetchedAt : new Date ( ) . toISOString ( ) ,
109+ url : opts . url ,
110+ dateCreated : dashboard . dateCreated ,
111+ environment : dashboard . environment ,
112+ widgets : widgets . map ( ( w , i ) => ( {
113+ title : w . title ,
114+ displayType : w . displayType ,
115+ widgetType : w . widgetType ,
116+ layout : w . layout ,
117+ queries : w . queries ,
118+ data : widgetResults . get ( i ) ?? {
119+ type : "error" as const ,
120+ message : "No data returned" ,
121+ } ,
122+ } ) ) ,
123+ } ;
124+ }
36125
37126export const viewCommand = buildCommand ( {
38127 docs : {
39128 brief : "View a dashboard" ,
40129 fullDescription :
41- "View details of a specific Sentry dashboard.\n\n" +
130+ "View a Sentry dashboard with rendered widget data.\n\n" +
131+ "Fetches actual data for each widget and displays sparkline charts,\n" +
132+ "tables, and big numbers in the terminal.\n\n" +
42133 "The dashboard can be specified by numeric ID or title.\n\n" +
43134 "Examples:\n" +
44135 " sentry dashboard view 12345\n" +
45136 " sentry dashboard view 'My Dashboard'\n" +
46137 " sentry dashboard view my-org/ 12345\n" +
47138 " sentry dashboard view 12345 --json\n" +
139+ " sentry dashboard view 12345 --period 7d\n" +
140+ " sentry dashboard view 12345 -r\n" +
141+ " sentry dashboard view 12345 -r 30\n" +
48142 " sentry dashboard view 12345 --web" ,
49143 } ,
50144 output : {
51- human : formatDashboardView ,
145+ human : createDashboardViewRenderer ,
52146 } ,
53147 parameters : {
54148 positional : {
@@ -65,8 +159,21 @@ export const viewCommand = buildCommand({
65159 default : false ,
66160 } ,
67161 fresh : FRESH_FLAG ,
162+ refresh : {
163+ kind : "parsed" ,
164+ parse : parseRefresh ,
165+ brief : "Auto-refresh interval in seconds (default: 60, min: 10)" ,
166+ optional : true ,
167+ inferEmpty : true ,
168+ } ,
169+ period : {
170+ kind : "parsed" ,
171+ parse : String ,
172+ brief : 'Time period override (e.g., "24h", "7d", "14d")' ,
173+ optional : true ,
174+ } ,
68175 } ,
69- aliases : { ...FRESH_ALIASES , w : "web" } ,
176+ aliases : { ...FRESH_ALIASES , w : "web" , r : "refresh" , t : "period" } ,
70177 } ,
71178 async * func ( this : SentryContext , flags : ViewFlags , ...args : string [ ] ) {
72179 applyFreshFlag ( flags ) ;
@@ -80,20 +187,76 @@ export const viewCommand = buildCommand({
80187 "sentry dashboard view <org>/ <id>"
81188 ) ;
82189 const dashboardId = await resolveDashboardId ( orgSlug , dashboardRef ) ;
83-
84190 const url = buildDashboardUrl ( orgSlug , dashboardId ) ;
85191
86192 if ( flags . web ) {
87193 await openInBrowser ( url , "dashboard" ) ;
88194 return ;
89195 }
90196
197+ // Fetch the dashboard definition (widget structure)
91198 const dashboard = await withProgress (
92199 { message : "Fetching dashboard..." , json : flags . json } ,
93200 ( ) => getDashboard ( orgSlug , dashboardId )
94201 ) ;
95202
96- yield new CommandOutput ( { ...dashboard , url } as ViewResult ) ;
203+ const regionUrl = await resolveOrgRegion ( orgSlug ) ;
204+ const period = flags . period ?? dashboard . period ?? "24h" ;
205+ const widgets = dashboard . widgets ?? [ ] ;
206+
207+ if ( flags . refresh !== undefined ) {
208+ // ── Refresh mode: poll and re-render ──
209+ const interval = flags . refresh ;
210+ if ( ! flags . json ) {
211+ logger . info (
212+ `Auto-refreshing dashboard every ${ interval } s. Press Ctrl+C to stop.`
213+ ) ;
214+ }
215+
216+ const controller = new AbortController ( ) ;
217+ const stop = ( ) => controller . abort ( ) ;
218+ process . once ( "SIGINT" , stop ) ;
219+
220+ // Use in-place refresh on interactive terminals (ANSI clear)
221+ const canClear = ! ( isPlainOutput ( ) || flags . json ) ;
222+ let isFirstRender = true ;
223+
224+ try {
225+ while ( ! controller . signal . aborted ) {
226+ const widgetData = await queryAllWidgets (
227+ regionUrl ,
228+ orgSlug ,
229+ dashboard ,
230+ { period }
231+ ) ;
232+
233+ // Clear screen for in-place refresh (skip for first render and piped output)
234+ if ( ! isFirstRender && canClear ) {
235+ this . stdout . write ( "\x1b[H\x1b[J" ) ;
236+ }
237+ isFirstRender = false ;
238+
239+ yield new CommandOutput (
240+ buildViewData ( dashboard , widgetData , widgets , { period, url } )
241+ ) ;
242+
243+ await abortableSleep ( interval * 1000 , controller . signal ) ;
244+ }
245+ } finally {
246+ process . removeListener ( "SIGINT" , stop ) ;
247+ }
248+ return ;
249+ }
250+
251+ // ── Single fetch mode ──
252+ const widgetData = await withProgress (
253+ { message : "Querying widget data..." , json : flags . json } ,
254+ ( ) => queryAllWidgets ( regionUrl , orgSlug , dashboard , { period } )
255+ ) ;
256+
257+ yield new CommandOutput (
258+ buildViewData ( dashboard , widgetData , widgets , { period, url } )
259+ ) ;
97260 return { hint : `Dashboard: ${ url } ` } ;
98261 } ,
99262} ) ;
0 commit comments