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 { CommandOutput } from "../../lib/formatters/output.js" ;
13+ import type { DashboardViewData } from "../../lib/formatters/dashboard.js" ;
14+ import { createDashboardViewRenderer } from "../../lib/formatters/dashboard.js" ;
15+ import { ClearScreen , CommandOutput } from "../../lib/formatters/output.js" ;
1416import {
1517 applyFreshFlag ,
1618 FRESH_ALIASES ,
1719 FRESH_FLAG ,
1820} from "../../lib/list-command.js" ;
21+ import { logger } from "../../lib/logger.js" ;
1922import { withProgress } from "../../lib/polling.js" ;
23+ import { resolveOrgRegion } from "../../lib/region.js" ;
2024import { buildDashboardUrl } from "../../lib/sentry-urls.js" ;
21- import type { DashboardDetail } from "../../types/dashboard.js" ;
25+ import type {
26+ DashboardWidget ,
27+ WidgetDataResult ,
28+ } from "../../types/dashboard.js" ;
2229import {
2330 parseDashboardPositionalArgs ,
2431 resolveDashboardId ,
2532 resolveOrgFromTarget ,
2633} from "./resolve.js" ;
2734
35+ /** Default auto-refresh interval in seconds */
36+ const DEFAULT_REFRESH_INTERVAL = 60 ;
37+
38+ /** Minimum auto-refresh interval in seconds (avoid rate limiting) */
39+ const MIN_REFRESH_INTERVAL = 10 ;
40+
2841type ViewFlags = {
2942 readonly web : boolean ;
3043 readonly fresh : boolean ;
44+ readonly refresh ?: number ;
45+ readonly period ?: string ;
3146 readonly json : boolean ;
3247 readonly fields ?: string [ ] ;
3348} ;
3449
35- type ViewResult = DashboardDetail & { url : string } ;
50+ /**
51+ * Parse --refresh flag value.
52+ * Supports: -r (empty string → 60s default), -r 30 (explicit interval in seconds)
53+ */
54+ function parseRefresh ( value : string ) : number {
55+ if ( value === "" ) {
56+ return DEFAULT_REFRESH_INTERVAL ;
57+ }
58+ const num = Number . parseInt ( value , 10 ) ;
59+ if ( Number . isNaN ( num ) || num < MIN_REFRESH_INTERVAL ) {
60+ throw new Error (
61+ `--refresh interval must be at least ${ MIN_REFRESH_INTERVAL } seconds`
62+ ) ;
63+ }
64+ return num ;
65+ }
66+
67+ /**
68+ * Sleep that resolves early when an AbortSignal fires.
69+ * Resolves (not rejects) on abort for clean generator shutdown.
70+ */
71+ function abortableSleep ( ms : number , signal : AbortSignal ) : Promise < void > {
72+ return new Promise < void > ( ( resolve ) => {
73+ if ( signal . aborted ) {
74+ resolve ( ) ;
75+ return ;
76+ }
77+ const onAbort = ( ) => {
78+ clearTimeout ( timer ) ;
79+ resolve ( ) ;
80+ } ;
81+ const timer = setTimeout ( ( ) => {
82+ signal . removeEventListener ( "abort" , onAbort ) ;
83+ resolve ( ) ;
84+ } , ms ) ;
85+ signal . addEventListener ( "abort" , onAbort , { once : true } ) ;
86+ } ) ;
87+ }
88+
89+ /**
90+ * Build the DashboardViewData from a dashboard and its widget query results.
91+ */
92+ function buildViewData (
93+ dashboard : {
94+ id : string ;
95+ title : string ;
96+ dateCreated ?: string ;
97+ environment ?: string [ ] ;
98+ } ,
99+ widgetResults : Map < number , WidgetDataResult > ,
100+ widgets : DashboardWidget [ ] ,
101+ opts : { period : string ; url : string }
102+ ) : DashboardViewData {
103+ return {
104+ id : dashboard . id ,
105+ title : dashboard . title ,
106+ period : opts . period ,
107+ fetchedAt : new Date ( ) . toISOString ( ) ,
108+ url : opts . url ,
109+ dateCreated : dashboard . dateCreated ,
110+ environment : dashboard . environment ,
111+ widgets : widgets . map ( ( w , i ) => ( {
112+ title : w . title ,
113+ displayType : w . displayType ,
114+ widgetType : w . widgetType ,
115+ layout : w . layout ,
116+ queries : w . queries ,
117+ data : widgetResults . get ( i ) ?? {
118+ type : "error" as const ,
119+ message : "No data returned" ,
120+ } ,
121+ } ) ) ,
122+ } ;
123+ }
36124
37125export const viewCommand = buildCommand ( {
38126 docs : {
39127 brief : "View a dashboard" ,
40128 fullDescription :
41- "View details of a specific Sentry dashboard.\n\n" +
129+ "View a Sentry dashboard with rendered widget data.\n\n" +
130+ "Fetches actual data for each widget and displays sparkline charts,\n" +
131+ "tables, and big numbers in the terminal.\n\n" +
42132 "The dashboard can be specified by numeric ID or title.\n\n" +
43133 "Examples:\n" +
44134 " sentry dashboard view 12345\n" +
45135 " sentry dashboard view 'My Dashboard'\n" +
46136 " sentry dashboard view my-org/ 12345\n" +
47137 " sentry dashboard view 12345 --json\n" +
138+ " sentry dashboard view 12345 --period 7d\n" +
139+ " sentry dashboard view 12345 -r\n" +
140+ " sentry dashboard view 12345 -r 30\n" +
48141 " sentry dashboard view 12345 --web" ,
49142 } ,
50143 output : {
51- human : formatDashboardView ,
144+ human : createDashboardViewRenderer ,
52145 } ,
53146 parameters : {
54147 positional : {
@@ -65,8 +158,21 @@ export const viewCommand = buildCommand({
65158 default : false ,
66159 } ,
67160 fresh : FRESH_FLAG ,
161+ refresh : {
162+ kind : "parsed" ,
163+ parse : parseRefresh ,
164+ brief : "Auto-refresh interval in seconds (default: 60, min: 10)" ,
165+ optional : true ,
166+ inferEmpty : true ,
167+ } ,
168+ period : {
169+ kind : "parsed" ,
170+ parse : String ,
171+ brief : 'Time period override (e.g., "24h", "7d", "14d")' ,
172+ optional : true ,
173+ } ,
68174 } ,
69- aliases : { ...FRESH_ALIASES , w : "web" } ,
175+ aliases : { ...FRESH_ALIASES , w : "web" , r : "refresh" , t : "period" } ,
70176 } ,
71177 async * func ( this : SentryContext , flags : ViewFlags , ...args : string [ ] ) {
72178 applyFreshFlag ( flags ) ;
@@ -80,20 +186,77 @@ export const viewCommand = buildCommand({
80186 "sentry dashboard view <org>/ <id>"
81187 ) ;
82188 const dashboardId = await resolveDashboardId ( orgSlug , dashboardRef ) ;
83-
84189 const url = buildDashboardUrl ( orgSlug , dashboardId ) ;
85190
86191 if ( flags . web ) {
87192 await openInBrowser ( url , "dashboard" ) ;
88193 return ;
89194 }
90195
196+ // Fetch the dashboard definition (widget structure)
91197 const dashboard = await withProgress (
92198 { message : "Fetching dashboard..." , json : flags . json } ,
93199 ( ) => getDashboard ( orgSlug , dashboardId )
94200 ) ;
95201
96- yield new CommandOutput ( { ...dashboard , url } as ViewResult ) ;
202+ const regionUrl = await resolveOrgRegion ( orgSlug ) ;
203+ const period = flags . period ?? dashboard . period ?? "24h" ;
204+ const widgets = dashboard . widgets ?? [ ] ;
205+
206+ if ( flags . refresh !== undefined ) {
207+ // ── Refresh mode: poll and re-render ──
208+ const interval = flags . refresh ;
209+ if ( ! flags . json ) {
210+ logger . info (
211+ `Auto-refreshing dashboard every ${ interval } s. Press Ctrl+C to stop.`
212+ ) ;
213+ }
214+
215+ const controller = new AbortController ( ) ;
216+ const stop = ( ) => controller . abort ( ) ;
217+ process . once ( "SIGINT" , stop ) ;
218+
219+ let isFirstRender = true ;
220+
221+ try {
222+ while ( ! controller . signal . aborted ) {
223+ const widgetData = await queryAllWidgets (
224+ regionUrl ,
225+ orgSlug ,
226+ dashboard ,
227+ { period }
228+ ) ;
229+
230+ // Build output data before clearing so clear→render is instantaneous
231+ const viewData = buildViewData ( dashboard , widgetData , widgets , {
232+ period,
233+ url,
234+ } ) ;
235+
236+ if ( ! isFirstRender ) {
237+ yield new ClearScreen ( ) ;
238+ }
239+ isFirstRender = false ;
240+
241+ yield new CommandOutput ( viewData ) ;
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