feat(settings): data-safe editors, deep-linked search, Data/About/Cost pages
- EditorModal loads once per path (a parent re-render no longer reloads the file over the user's typing), refuses Save after a failed load, opens a truncated (>256 KB) file read-only, and asks before overwriting a file that changed on disk or could not be re-read. - Settings/Hooks editors unset only keys from the snapshot taken at Edit, warn when the file changed underneath, keep a renamed key's value, and refuse a new key named like a redacted one (env) or a read-only hook event, which used to replace the secret/hooks. - Project scope shows its resolved folder and is disabled inside the plugin cache. Account-profile, pricing-row and pet deletes confirm; two-click actions disarm after 3 s; unsaved drafts ask before closing. - Search indexes every Claude Code / Codex sub-tab, opens it via ?tab= and scrolls to + highlights the row; the active config tab is in the URL. - General splits into General / Data / About; Pricing moves to a Cost group with one shared load; config tabs group as Content / Extensions / Config with arrow-key roving; Promise.allSettled per tab. - Data: browser-preferences export/import (allowlisted keys), per-section reset, housekeeping dry-run first, clear-all behind a typed phrase. Server: purge days must be a whole number >= 1 (INVALID_PURGE_DAYS) and dry_run returns exactly what the real run would delete. - Alerts can be edited; webhooks/alerts/remote-source failures toast; copies go through copyText; modals get dialog roles and useDialog. - Sagi: Ctrl/Cmd+B only when shown and not typing, default spot clear of row controls, arrow keys move it, reduced motion honoured; the dead /run launch link goes to the workspace.
This commit is contained in:
@@ -86,6 +86,7 @@ import { Checkbox } from "./Checkbox";
|
||||
import { ContextMenu, MenuItem, MenuSeparator, useContextMenu } from "./ContextMenu";
|
||||
import { WEBHOOK_DOCS } from "./webhookGuides";
|
||||
import { timeAgo } from "../lib/format";
|
||||
import { toast } from "../lib/toast";
|
||||
import type {
|
||||
AlertRule,
|
||||
WebhookProvider,
|
||||
@@ -94,20 +95,21 @@ import type {
|
||||
WebhookTestResult,
|
||||
} from "../lib/types";
|
||||
|
||||
// Brand-ish accent per provider type; anything unmapped falls back to neutral.
|
||||
// Brand-ish accent per provider type, on the BORDER only: brand hues fail
|
||||
// contrast as text on several themes, so the label stays text-fg-primary.
|
||||
const TYPE_STYLES: Partial<Record<WebhookType, string>> = {
|
||||
slack: "text-[#E01E5A] bg-[#E01E5A]/10 border-[#E01E5A]/20",
|
||||
discord: "text-[#5865F2] bg-[#5865F2]/10 border-[#5865F2]/20",
|
||||
teams: "text-[#6264A7] bg-[#6264A7]/10 border-[#6264A7]/20",
|
||||
google_chat: "text-[#1A73E8] bg-[#1A73E8]/10 border-[#1A73E8]/20",
|
||||
mattermost: "text-[#0058CC] bg-[#0058CC]/10 border-[#0058CC]/20",
|
||||
rocketchat: "text-[#F5455C] bg-[#F5455C]/10 border-[#F5455C]/20",
|
||||
telegram: "text-[#26A5E4] bg-[#26A5E4]/10 border-[#26A5E4]/20",
|
||||
pagerduty: "text-[#06AC38] bg-[#06AC38]/10 border-[#06AC38]/20",
|
||||
opsgenie: "text-[#2684FF] bg-[#2684FF]/10 border-[#2684FF]/20",
|
||||
splunk_oncall: "text-[#F99D1C] bg-[#F99D1C]/10 border-[#F99D1C]/20",
|
||||
slack: "border-[#E01E5A]/60",
|
||||
discord: "border-[#5865F2]/60",
|
||||
teams: "border-[#6264A7]/60",
|
||||
google_chat: "border-[#1A73E8]/60",
|
||||
mattermost: "border-[#0058CC]/60",
|
||||
rocketchat: "border-[#F5455C]/60",
|
||||
telegram: "border-[#26A5E4]/60",
|
||||
pagerduty: "border-[#06AC38]/60",
|
||||
opsgenie: "border-[#2684FF]/60",
|
||||
splunk_oncall: "border-[#F99D1C]/60",
|
||||
};
|
||||
const NEUTRAL_STYLE = "text-fg-secondary bg-surface-2 border-border";
|
||||
const NEUTRAL_STYLE = "border-border";
|
||||
|
||||
interface HeaderRow {
|
||||
key: string;
|
||||
@@ -168,11 +170,13 @@ export function WebhookSettings() {
|
||||
const res = await api.webhooks.list();
|
||||
setTargets(res.targets);
|
||||
} catch (err) {
|
||||
console.error("Failed to load webhook targets:", err);
|
||||
toast.error(
|
||||
t("webhooks.loadFailed", { error: err instanceof Error ? err.message : String(err) })
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -252,6 +256,9 @@ export function WebhookSettings() {
|
||||
const canSubmit = useMemo(() => {
|
||||
if (!form || !provider) return false;
|
||||
if (!form.name.trim()) return false;
|
||||
// An empty rule list means ALL rules server-side — never save that from
|
||||
// "only selected rules".
|
||||
if (!form.scopeAll && form.ruleIds.length === 0) return false;
|
||||
if (isEdit) return true; // server merge - existing values fill the gaps
|
||||
if (provider.url_required && !form.url.trim()) return false;
|
||||
for (const f of provider.fields) {
|
||||
@@ -326,7 +333,9 @@ export function WebhookSettings() {
|
||||
await api.webhooks.update(target.id, { enabled: !target.enabled });
|
||||
load();
|
||||
} catch (err) {
|
||||
console.error("Failed to toggle webhook:", err);
|
||||
toast.error(
|
||||
t("webhooks.toggleFailed", { error: err instanceof Error ? err.message : String(err) })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -336,7 +345,10 @@ export function WebhookSettings() {
|
||||
setConfirmDelete(null);
|
||||
load();
|
||||
} catch (err) {
|
||||
console.error("Failed to delete webhook:", err);
|
||||
setConfirmDelete(null);
|
||||
toast.error(
|
||||
t("webhooks.deleteFailed", { error: err instanceof Error ? err.message : String(err) })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -402,7 +414,7 @@ export function WebhookSettings() {
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border ${
|
||||
className={`inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border bg-surface-2 text-fg-primary ${
|
||||
TYPE_STYLES[target.type] || NEUTRAL_STYLE
|
||||
}`}
|
||||
>
|
||||
@@ -765,6 +777,9 @@ export function WebhookSettings() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!form.scopeAll && form.ruleIds.length === 0 && (
|
||||
<p className="mt-1.5 text-[11px] text-status-warning">{t("webhooks.pickRule")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user