Skip to content

Commit 9c1b756

Browse files
betegonclaude
andcommitted
refactor(dashboard): cleanup widget commands to match codebase patterns
- Extract shared buildWidgetFromFlags() into resolve.ts (DRY create + add) - Add return { hint } with dashboard URL to all dashboard commands - Move enum validation before API calls in widget add (fail fast) - Use proper DashboardDetail/DashboardWidget types in widget formatters - Wrap getDashboard in withProgress for loading spinner in view command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 10c3c4e commit 9c1b756

File tree

7 files changed

+88
-58
lines changed

7 files changed

+88
-58
lines changed

src/commands/dashboard/create.ts

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@ import {
2727
type DashboardDetail,
2828
type DashboardWidget,
2929
DISPLAY_TYPES,
30-
parseAggregate,
31-
parseSortExpression,
32-
parseWidgetInput,
33-
prepareWidgetQueries,
3430
} from "../../types/dashboard.js";
31+
import { buildWidgetFromFlags } from "./resolve.js";
3532

3633
type CreateFlags = {
3734
readonly "widget-title"?: string;
@@ -158,30 +155,16 @@ function buildInlineWidget(flags: CreateFlags): DashboardWidget {
158155
);
159156
}
160157

161-
const aggregates = (flags["widget-query"] ?? ["count"]).map(parseAggregate);
162-
const columns = flags["widget-group-by"] ?? [];
163-
const orderby = flags["widget-sort"]
164-
? parseSortExpression(flags["widget-sort"])
165-
: undefined;
166-
167-
const rawWidget = {
158+
return buildWidgetFromFlags({
168159
title: flags["widget-title"],
169-
displayType: flags["widget-display"] as string,
170-
...(flags["widget-dataset"] && { widgetType: flags["widget-dataset"] }),
171-
queries: [
172-
{
173-
aggregates,
174-
columns,
175-
conditions: flags["widget-where"] ?? "",
176-
...(orderby && { orderby }),
177-
name: "",
178-
},
179-
],
180-
...(flags["widget-limit"] !== undefined && {
181-
limit: flags["widget-limit"],
182-
}),
183-
};
184-
return prepareWidgetQueries(parseWidgetInput(rawWidget));
160+
display: flags["widget-display"] as string,
161+
dataset: flags["widget-dataset"],
162+
query: flags["widget-query"],
163+
where: flags["widget-where"],
164+
groupBy: flags["widget-group-by"],
165+
sort: flags["widget-sort"],
166+
limit: flags["widget-limit"],
167+
});
185168
}
186169

187170
export const createCommand = buildCommand({
@@ -290,5 +273,6 @@ export const createCommand = buildCommand({
290273
const url = buildDashboardUrl(orgSlug, dashboard.id);
291274

292275
yield new CommandOutput({ ...dashboard, url } as CreateResult);
276+
return { hint: `Dashboard: ${url}` };
293277
},
294278
});

src/commands/dashboard/resolve.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { isAllDigits } from "../../lib/utils.js";
1313
import {
1414
type DashboardWidget,
1515
DISPLAY_TYPES,
16+
parseAggregate,
17+
parseSortExpression,
18+
parseWidgetInput,
19+
prepareWidgetQueries,
1620
WIDGET_TYPES,
1721
} from "../../types/dashboard.js";
1822

@@ -157,6 +161,47 @@ export function resolveWidgetIndex(
157161
return matchIndex;
158162
}
159163

164+
/**
165+
* Build a widget from user-provided flag values.
166+
*
167+
* Shared between `dashboard create --widget-*` and `dashboard widget add`.
168+
* Parses aggregate shorthand, sort expressions, and validates via Zod schema.
169+
*
170+
* @param opts - Widget configuration from parsed flags
171+
* @returns Validated widget with computed query fields
172+
*/
173+
export function buildWidgetFromFlags(opts: {
174+
title: string;
175+
display: string;
176+
dataset?: string;
177+
query?: string[];
178+
where?: string;
179+
groupBy?: string[];
180+
sort?: string;
181+
limit?: number;
182+
}): DashboardWidget {
183+
const aggregates = (opts.query ?? ["count"]).map(parseAggregate);
184+
const columns = opts.groupBy ?? [];
185+
const orderby = opts.sort ? parseSortExpression(opts.sort) : undefined;
186+
187+
const raw = {
188+
title: opts.title,
189+
displayType: opts.display,
190+
...(opts.dataset && { widgetType: opts.dataset }),
191+
queries: [
192+
{
193+
aggregates,
194+
columns,
195+
conditions: opts.where ?? "",
196+
...(orderby && { orderby }),
197+
name: "",
198+
},
199+
],
200+
...(opts.limit !== undefined && { limit: opts.limit }),
201+
};
202+
return prepareWidgetQueries(parseWidgetInput(raw));
203+
}
204+
160205
/**
161206
* Validate --display and --dataset flag values against known enums.
162207
*

src/commands/dashboard/view.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
FRESH_ALIASES,
1717
FRESH_FLAG,
1818
} from "../../lib/list-command.js";
19+
import { withProgress } from "../../lib/polling.js";
1920
import { buildDashboardUrl } from "../../lib/sentry-urls.js";
2021
import type { DashboardDetail } from "../../types/dashboard.js";
2122
import {
@@ -87,8 +88,12 @@ export const viewCommand = buildCommand({
8788
return;
8889
}
8990

90-
const dashboard = await getDashboard(orgSlug, dashboardId);
91+
const dashboard = await withProgress(
92+
{ message: "Fetching dashboard..." },
93+
() => getDashboard(orgSlug, dashboardId)
94+
);
9195

9296
yield new CommandOutput({ ...dashboard, url } as ViewResult);
97+
return { hint: `Dashboard: ${url}` };
9398
},
9499
});

src/commands/dashboard/widget/add.ts

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@ import {
1616
assignDefaultLayout,
1717
type DashboardDetail,
1818
type DashboardWidget,
19-
parseAggregate,
20-
parseSortExpression,
21-
parseWidgetInput,
2219
prepareDashboardForUpdate,
23-
prepareWidgetQueries,
2420
} from "../../../types/dashboard.js";
2521
import {
22+
buildWidgetFromFlags,
2623
parseDashboardPositionalArgs,
2724
resolveDashboardId,
2825
resolveOrgFromTarget,
@@ -160,6 +157,10 @@ export const addCommand = buildCommand({
160157
const { cwd } = this;
161158

162159
const { dashboardArgs, title } = parseAddPositionalArgs(args);
160+
161+
// Validate enums before any network calls (fail fast)
162+
validateWidgetEnums(flags.display, flags.dataset);
163+
163164
const { dashboardRef, targetArg } =
164165
parseDashboardPositionalArgs(dashboardArgs);
165166
const parsed = parseOrgProjectArg(targetArg);
@@ -170,28 +171,16 @@ export const addCommand = buildCommand({
170171
);
171172
const dashboardId = await resolveDashboardId(orgSlug, dashboardRef);
172173

173-
validateWidgetEnums(flags.display, flags.dataset);
174-
175-
const aggregates = (flags.query ?? ["count"]).map(parseAggregate);
176-
const columns = flags["group-by"] ?? [];
177-
const orderby = flags.sort ? parseSortExpression(flags.sort) : undefined;
178-
179-
const raw = {
174+
let newWidget = buildWidgetFromFlags({
180175
title,
181-
displayType: flags.display,
182-
...(flags.dataset && { widgetType: flags.dataset }),
183-
queries: [
184-
{
185-
aggregates,
186-
columns,
187-
conditions: flags.where ?? "",
188-
...(orderby && { orderby }),
189-
name: "",
190-
},
191-
],
192-
...(flags.limit !== undefined && { limit: flags.limit }),
193-
};
194-
let newWidget = prepareWidgetQueries(parseWidgetInput(raw));
176+
display: flags.display,
177+
dataset: flags.dataset,
178+
query: flags.query,
179+
where: flags.where,
180+
groupBy: flags["group-by"],
181+
sort: flags.sort,
182+
limit: flags.limit,
183+
});
195184

196185
// GET current dashboard → append widget with auto-layout → PUT
197186
const current = await getDashboard(orgSlug, dashboardId);
@@ -207,5 +196,6 @@ export const addCommand = buildCommand({
207196
widget: newWidget,
208197
url,
209198
} as AddResult);
199+
return { hint: `Dashboard: ${url}` };
210200
},
211201
});

src/commands/dashboard/widget/delete.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,6 @@ export const deleteCommand = buildCommand({
111111
widgetTitle,
112112
url,
113113
} as DeleteResult);
114+
return { hint: `Dashboard: ${url}` };
114115
},
115116
});

src/commands/dashboard/widget/edit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,5 +240,6 @@ export const editCommand = buildCommand({
240240
widget: replacement,
241241
url,
242242
} as EditResult);
243+
return { hint: `Dashboard: ${url}` };
243244
},
244245
});

src/lib/formatters/human.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
1111
import * as Sentry from "@sentry/bun";
1212
import prettyMs from "pretty-ms";
13+
import type {
14+
DashboardDetail,
15+
DashboardWidget,
16+
} from "../../types/dashboard.js";
1317
import type {
1418
BreadcrumbsEntry,
1519
ExceptionEntry,
@@ -2168,8 +2172,8 @@ export function formatDashboardView(result: {
21682172
* Format a widget add result for human-readable output.
21692173
*/
21702174
export function formatWidgetAdded(result: {
2171-
dashboard: { id: string; widgets?: unknown[] };
2172-
widget: { title: string };
2175+
dashboard: DashboardDetail;
2176+
widget: DashboardWidget;
21732177
url: string;
21742178
}): string {
21752179
const widgetCount = result.dashboard.widgets?.length ?? 0;
@@ -2185,7 +2189,7 @@ export function formatWidgetAdded(result: {
21852189
* Format a widget deletion result for human-readable output.
21862190
*/
21872191
export function formatWidgetDeleted(result: {
2188-
dashboard: { id: string; widgets?: unknown[] };
2192+
dashboard: DashboardDetail;
21892193
widgetTitle: string;
21902194
url: string;
21912195
}): string {
@@ -2202,8 +2206,8 @@ export function formatWidgetDeleted(result: {
22022206
* Format a widget edit result for human-readable output.
22032207
*/
22042208
export function formatWidgetEdited(result: {
2205-
dashboard: { id: string };
2206-
widget: { title: string };
2209+
dashboard: DashboardDetail;
2210+
widget: DashboardWidget;
22072211
url: string;
22082212
}): string {
22092213
const lines: string[] = [

0 commit comments

Comments
 (0)