Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ cli/
│ ├── commands/ # CLI commands
│ │ ├── auth/ # login, logout, refresh, status, token, whoami
│ │ ├── cli/ # defaults, feedback, fix, setup, upgrade
│ │ ├── dashboard/ # list, view, create, add, edit, delete
│ │ ├── dashboard/ # list, view, create, add, edit, delete, revisions, restore
│ │ ├── event/ # view, list
│ │ ├── issue/ # list, events, explain, plan, view, resolve, unresolve, archive, merge
│ │ ├── log/ # list, view
Expand Down
2 changes: 2 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ Manage Sentry dashboards
- `sentry dashboard widget add <org/project/dashboard/title...>` — Add a widget to a dashboard
- `sentry dashboard widget edit <org/project/dashboard...>` — Edit a widget in a dashboard
- `sentry dashboard widget delete <org/project/dashboard...>` — Delete a widget from a dashboard
- `sentry dashboard revisions <org/dashboard...>` — List dashboard revisions
- `sentry dashboard restore <org/dashboard...>` — Restore a dashboard revision

→ Full flags and examples: `references/dashboard.md`

Expand Down
15 changes: 15 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,19 @@ sentry dashboard widget delete 'My Dashboard' --title 'Error Count'
sentry dashboard widget delete 12345 --index 2
```

### `sentry dashboard revisions <org/dashboard...>`

List dashboard revisions

**Flags:**
- `-n, --limit <value> - Maximum number of revisions to list - (default: "25")`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

### `sentry dashboard restore <org/dashboard...>`

Restore a dashboard revision

**Flags:**
- `-r, --revision <value> - Revision ID to restore`

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
15 changes: 11 additions & 4 deletions src/commands/dashboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { buildRouteMap } from "../../lib/route-map.js";
import { createCommand } from "./create.js";
import { listCommand } from "./list.js";
import { restoreCommand } from "./restore.js";
import { revisionsCommand } from "./revisions.js";
import { viewCommand } from "./view.js";
import { widgetRoute } from "./widget/index.js";

Expand All @@ -10,17 +12,22 @@ export const dashboardRoute = buildRouteMap({
view: viewCommand,
create: createCommand,
widget: widgetRoute,
revisions: revisionsCommand,
restore: restoreCommand,
},
defaultCommand: "view",
aliases: { history: "revisions" },
docs: {
brief: "Manage Sentry dashboards",
fullDescription:
"View and manage dashboards in your Sentry organization.\n\n" +
"Commands:\n" +
" list List dashboards\n" +
" view View a dashboard\n" +
" create Create a dashboard\n" +
" widget Manage dashboard widgets (add, edit, delete)",
" list List dashboards\n" +
" view View a dashboard\n" +
" create Create a dashboard\n" +
" widget Manage dashboard widgets (add, edit, delete)\n" +
" revisions List dashboard revision history\n" +
" restore Restore a dashboard to a previous revision",
hideRoute: {},
},
});
132 changes: 132 additions & 0 deletions src/commands/dashboard/restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* sentry dashboard restore
*
* Restore a dashboard to a previous revision.
*/

import type { SentryContext } from "../../context.js";
import { restoreDashboardRevision } from "../../lib/api-client.js";
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
import { buildCommand } from "../../lib/command.js";
import { ValidationError } from "../../lib/errors.js";
import { colorTag, escapeMarkdownCell } from "../../lib/formatters/markdown.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { formatRelativeTime } from "../../lib/formatters/time-utils.js";
import { withProgress } from "../../lib/polling.js";
import { buildDashboardUrl } from "../../lib/sentry-urls.js";
import type { DashboardDetail } from "../../types/dashboard.js";
import {
enrichDashboardError,
parseDashboardPositionalArgs,
resolveDashboardId,
resolveOrgFromTarget,
} from "./resolve.js";

type RestoreFlags = {
readonly revision: number;
readonly json: boolean;
readonly fields?: string[];
};

type RestoreResult = {
dashboard: DashboardDetail;
orgSlug: string;
revisionId: number;
};

function formatRestoreHuman(result: RestoreResult): string {
const d = result.dashboard;
const url = buildDashboardUrl(result.orgSlug, d.id);
const widgetCount = d.widgets?.length ?? 0;
const created = formatRelativeTime(d.dateCreated);

return (
`Restored dashboard **${escapeMarkdownCell(d.title)}** to revision ${result.revisionId}.\n\n` +
"| Field | Value |\n" +
"|-------|-------|\n" +
`| ID | ${d.id} |\n` +
`| Title | ${escapeMarkdownCell(d.title)} |\n` +
`| Widgets | ${widgetCount} |\n` +
`| Created | ${created} |\n` +
`| URL | ${colorTag("muted", url)} |`
);
}

export const restoreCommand = buildCommand({
docs: {
brief: "Restore a dashboard revision",
fullDescription:
"Restore a Sentry dashboard to a previous revision.\n\n" +
"Use `sentry dashboard revisions` to list available revisions first.\n\n" +
"Examples:\n" +
" sentry dashboard restore 12345 --revision 42\n" +
" sentry dashboard restore my-org 12345 --revision 42\n" +
" sentry dashboard restore 'My Dashboard' --revision 42\n" +
" sentry dashboard restore 12345 --revision 42 --json",
},
output: {
human: formatRestoreHuman,
},
parameters: {
positional: {
kind: "array",
parameter: {
placeholder: "org/dashboard",
brief: "[<org/project>] <dashboard-id-or-title>",
parse: String,
},
},
flags: {
revision: {
kind: "parsed",
parse: (value: string) => {
const num = Number.parseInt(value, 10);
if (Number.isNaN(num) || num < 1) {
throw new ValidationError(
"--revision must be a positive integer.",
"revision"
);
}
return num;
},
brief: "Revision ID to restore",
},
},
aliases: { r: "revision" },
},
async *func(this: SentryContext, flags: RestoreFlags, ...args: string[]) {
const { cwd } = this;

const { dashboardRef, targetArg } = parseDashboardPositionalArgs(args);
const parsed = parseOrgProjectArg(targetArg);
const orgSlug = await resolveOrgFromTarget(
parsed,
cwd,
"sentry dashboard restore <org>/ <id> --revision <rev>"
);
const dashboardId = await resolveDashboardId(orgSlug, dashboardRef);

const dashboard = await withProgress(
{ message: `Restoring revision ${flags.revision}...`, json: flags.json },
() => restoreDashboardRevision(orgSlug, dashboardId, flags.revision)
).catch(async (error: unknown) =>
enrichDashboardError(error, {
orgSlug,
dashboardId,
operation: "update",
})
);

const outputData: RestoreResult = {
dashboard,
orgSlug,
revisionId: flags.revision,
};
yield new CommandOutput(outputData);

const url = buildDashboardUrl(orgSlug, dashboardId);
return {
hint: `Dashboard restored. View: ${url}`,
};
},
});
Loading
Loading