Skip to content

Commit 55a68fd

Browse files
committed
fix(widget-delete): disable non-interactive guard for reversible operation
Widget deletion is reversible (re-add the widget), so the non-interactive guard from buildDeleteCommand is too aggressive. Pass noNonInteractiveGuard option and fix the JSDoc comment to accurately describe flag behavior.
1 parent 4f16fc7 commit 55a68fd

File tree

1 file changed

+107
-103
lines changed

1 file changed

+107
-103
lines changed

src/commands/dashboard/widget/delete.ts

Lines changed: 107 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
* Remove a widget from an existing dashboard.
55
*
66
* Uses `buildDeleteCommand` — auto-injects `--yes`/`--force`/`--dry-run`
7-
* flags and enforces the non-interactive guard before `func()` runs.
8-
* `--yes`/`--force` are no-ops for now (no confirmation prompt) but
9-
* available for scripting and forward-compatibility.
7+
* flags. Non-interactive guard is disabled (`noNonInteractiveGuard`) because
8+
* widget deletion is reversible (re-add the widget). `--yes`/`--force` are
9+
* accepted but have no effect today (no confirmation prompt); `--dry-run`
10+
* shows which widget would be removed without modifying the dashboard.
1011
*/
1112

1213
import type { SentryContext } from "../../../context.js";
@@ -47,128 +48,131 @@ type DeleteResult = {
4748
dryRun?: boolean;
4849
};
4950

50-
export const deleteCommand = buildDeleteCommand({
51-
docs: {
52-
brief: "Delete a widget from a dashboard",
53-
fullDescription:
54-
"Remove a widget from an existing Sentry dashboard.\n\n" +
55-
"The dashboard can be specified by numeric ID or title.\n" +
56-
"Identify the widget by --index (0-based) or --title.\n\n" +
57-
"Examples:\n" +
58-
" sentry dashboard widget delete 12345 --index 0\n" +
59-
" sentry dashboard widget delete 'My Dashboard' --title 'Error Rate'\n" +
60-
" sentry dashboard widget delete 12345 --index 0 --dry-run",
61-
},
62-
output: {
63-
human: formatWidgetDeleted,
64-
jsonTransform: (result: DeleteResult) => {
65-
if (result.dryRun) {
51+
export const deleteCommand = buildDeleteCommand(
52+
{
53+
docs: {
54+
brief: "Delete a widget from a dashboard",
55+
fullDescription:
56+
"Remove a widget from an existing Sentry dashboard.\n\n" +
57+
"The dashboard can be specified by numeric ID or title.\n" +
58+
"Identify the widget by --index (0-based) or --title.\n\n" +
59+
"Examples:\n" +
60+
" sentry dashboard widget delete 12345 --index 0\n" +
61+
" sentry dashboard widget delete 'My Dashboard' --title 'Error Rate'\n" +
62+
" sentry dashboard widget delete 12345 --index 0 --dry-run",
63+
},
64+
output: {
65+
human: formatWidgetDeleted,
66+
jsonTransform: (result: DeleteResult) => {
67+
if (result.dryRun) {
68+
return {
69+
dryRun: true,
70+
widgetTitle: result.widgetTitle,
71+
widgetCount: result.dashboard.widgets?.length ?? 0,
72+
url: result.url,
73+
};
74+
}
6675
return {
67-
dryRun: true,
76+
deleted: true,
6877
widgetTitle: result.widgetTitle,
6978
widgetCount: result.dashboard.widgets?.length ?? 0,
7079
url: result.url,
7180
};
72-
}
73-
return {
74-
deleted: true,
75-
widgetTitle: result.widgetTitle,
76-
widgetCount: result.dashboard.widgets?.length ?? 0,
77-
url: result.url,
78-
};
79-
},
80-
},
81-
parameters: {
82-
positional: {
83-
kind: "array",
84-
parameter: {
85-
placeholder: "org/project/dashboard",
86-
brief: "[<org/project>] <dashboard-id-or-title>",
87-
parse: String,
8881
},
8982
},
90-
flags: {
91-
index: {
92-
kind: "parsed",
93-
parse: numberParser,
94-
brief: "Widget index (0-based)",
95-
optional: true,
83+
parameters: {
84+
positional: {
85+
kind: "array",
86+
parameter: {
87+
placeholder: "org/project/dashboard",
88+
brief: "[<org/project>] <dashboard-id-or-title>",
89+
parse: String,
90+
},
9691
},
97-
title: {
98-
kind: "parsed",
99-
parse: String,
100-
brief: "Widget title to match",
101-
optional: true,
92+
flags: {
93+
index: {
94+
kind: "parsed",
95+
parse: numberParser,
96+
brief: "Widget index (0-based)",
97+
optional: true,
98+
},
99+
title: {
100+
kind: "parsed",
101+
parse: String,
102+
brief: "Widget title to match",
103+
optional: true,
104+
},
102105
},
106+
aliases: { i: "index", t: "title" },
103107
},
104-
aliases: { i: "index", t: "title" },
105-
},
106-
async *func(this: SentryContext, flags: DeleteFlags, ...args: string[]) {
107-
const { cwd } = this;
108+
async *func(this: SentryContext, flags: DeleteFlags, ...args: string[]) {
109+
const { cwd } = this;
108110

109-
if (flags.index === undefined && !flags.title) {
110-
throw new ValidationError(
111-
"Specify --index or --title to identify the widget to delete.",
112-
"index"
111+
if (flags.index === undefined && !flags.title) {
112+
throw new ValidationError(
113+
"Specify --index or --title to identify the widget to delete.",
114+
"index"
115+
);
116+
}
117+
118+
const { dashboardRef, targetArg } = parseDashboardPositionalArgs(args);
119+
const parsed = parseOrgProjectArg(targetArg);
120+
const orgSlug = await resolveOrgFromTarget(
121+
parsed,
122+
cwd,
123+
"sentry dashboard widget delete <org>/ <id> (--index <n> | --title <name>)"
124+
);
125+
const dashboardId = await resolveDashboardId(orgSlug, dashboardRef);
126+
127+
// GET current dashboard → find widget
128+
const current = await getDashboard(orgSlug, dashboardId).catch(
129+
(error: unknown) =>
130+
enrichDashboardError(error, {
131+
orgSlug,
132+
dashboardId,
133+
operation: "view",
134+
})
113135
);
114-
}
136+
const widgets = current.widgets ?? [];
115137

116-
const { dashboardRef, targetArg } = parseDashboardPositionalArgs(args);
117-
const parsed = parseOrgProjectArg(targetArg);
118-
const orgSlug = await resolveOrgFromTarget(
119-
parsed,
120-
cwd,
121-
"sentry dashboard widget delete <org>/ <id> (--index <n> | --title <name>)"
122-
);
123-
const dashboardId = await resolveDashboardId(orgSlug, dashboardRef);
138+
const widgetIndex = resolveWidgetIndex(widgets, flags.index, flags.title);
139+
const widgetTitle = widgets[widgetIndex]?.title;
140+
const url = buildDashboardUrl(orgSlug, dashboardId);
124141

125-
// GET current dashboard → find widget
126-
const current = await getDashboard(orgSlug, dashboardId).catch(
127-
(error: unknown) =>
142+
// Dry-run mode: show what would be removed without removing it
143+
if (flags["dry-run"]) {
144+
yield new CommandOutput({
145+
dashboard: current,
146+
widgetTitle,
147+
url,
148+
dryRun: true,
149+
} as DeleteResult);
150+
return { hint: `Dashboard: ${url}` };
151+
}
152+
153+
// Splice the widget and PUT the updated dashboard
154+
const updateBody = prepareDashboardForUpdate(current);
155+
updateBody.widgets.splice(widgetIndex, 1);
156+
157+
const updated = await updateDashboard(
158+
orgSlug,
159+
dashboardId,
160+
updateBody
161+
).catch((error: unknown) =>
128162
enrichDashboardError(error, {
129163
orgSlug,
130164
dashboardId,
131-
operation: "view",
165+
operation: "update",
132166
})
133-
);
134-
const widgets = current.widgets ?? [];
135-
136-
const widgetIndex = resolveWidgetIndex(widgets, flags.index, flags.title);
137-
const widgetTitle = widgets[widgetIndex]?.title;
138-
const url = buildDashboardUrl(orgSlug, dashboardId);
167+
);
139168

140-
// Dry-run mode: show what would be removed without removing it
141-
if (flags["dry-run"]) {
142169
yield new CommandOutput({
143-
dashboard: current,
170+
dashboard: updated,
144171
widgetTitle,
145172
url,
146-
dryRun: true,
147173
} as DeleteResult);
148174
return { hint: `Dashboard: ${url}` };
149-
}
150-
151-
// Splice the widget and PUT the updated dashboard
152-
const updateBody = prepareDashboardForUpdate(current);
153-
updateBody.widgets.splice(widgetIndex, 1);
154-
155-
const updated = await updateDashboard(
156-
orgSlug,
157-
dashboardId,
158-
updateBody
159-
).catch((error: unknown) =>
160-
enrichDashboardError(error, {
161-
orgSlug,
162-
dashboardId,
163-
operation: "update",
164-
})
165-
);
166-
167-
yield new CommandOutput({
168-
dashboard: updated,
169-
widgetTitle,
170-
url,
171-
} as DeleteResult);
172-
return { hint: `Dashboard: ${url}` };
175+
},
173176
},
174-
});
177+
{ noNonInteractiveGuard: true }
178+
);

0 commit comments

Comments
 (0)