diff --git a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/(list)/page.tsx b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/(list)/page.tsx index 88dd8ab03..f9983a7f2 100644 --- a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/(list)/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/(list)/page.tsx @@ -1,8 +1,9 @@ "use client"; import { Button } from "@openstatus/ui/components/ui/button"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useParams } from "next/navigation"; +import { toast } from "sonner"; import { Link } from "@/components/common/link"; import { @@ -18,10 +19,12 @@ import { FormSheetMaintenance } from "@/components/forms/maintenance/sheet"; import { toCheckboxTreeItems } from "@/components/ui/checkbox-tree"; import { DataTable } from "@/components/ui/data-table/data-table"; import { useTRPC } from "@/lib/trpc/client"; +import { errorMessage } from "@/lib/trpc/error"; export default function Page() { const { id } = useParams<{ id: string }>(); const trpc = useTRPC(); + const queryClient = useQueryClient(); const { data: statusPage } = useQuery( trpc.page.get.queryOptions({ id: Number.parseInt(id) }), ); @@ -31,15 +34,22 @@ export default function Page() { }), ); const sendMaintenanceUpdateMutation = useMutation( - trpc.subscriberNotification.maintenance.mutationOptions(), + trpc.subscriberNotification.maintenance.mutationOptions({ + onError: (error) => { + toast.error(errorMessage(error, "Failed to notify subscribers")); + }, + }), ); const createMaintenanceMutation = useMutation( trpc.maintenance.new.mutationOptions({ onSuccess: (maintenance) => { refetch(); + queryClient.invalidateQueries({ + queryKey: trpc.page.list.queryKey(), + }); if (maintenance.notifySubscribers) { sendMaintenanceUpdateMutation.mutate({ - id: maintenance.id, + id: maintenance.initialUpdateId, }); } }, @@ -72,7 +82,7 @@ export default function Page() { await createMaintenanceMutation.mutateAsync({ pageId: Number.parseInt(id), title: values.title, - message: values.message, + message: values.message ?? "", startDate: values.startDate, endDate: values.endDate, pageComponents: values.pageComponents, diff --git a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/[maintenanceId]/client.tsx b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/[maintenanceId]/client.tsx index 126e75141..7a2e3243e 100644 --- a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/[maintenanceId]/client.tsx +++ b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/[maintenanceId]/client.tsx @@ -19,10 +19,12 @@ import { DetailTitleRow, } from "@/components/content/detail"; import { SectionGroup } from "@/components/content/section"; +import { Timeline } from "@/components/content/timeline"; import { MaintenanceActions } from "@/components/maintenances/maintenance-actions"; import { MaintenanceComponents } from "@/components/maintenances/maintenance-components"; -import { MaintenanceComposer } from "@/components/maintenances/maintenance-composer"; import { MaintenanceProperties } from "@/components/maintenances/maintenance-properties"; +import { MaintenanceUpdateComposer } from "@/components/maintenances/maintenance-update-composer"; +import { MaintenanceUpdateTimelineItem } from "@/components/maintenances/maintenance-update-timeline"; import { useMaintenanceStatus } from "@/components/maintenances/use-maintenance-status"; import { useUpdateMaintenance } from "@/components/maintenances/use-update-maintenance"; import { Notifications } from "@/components/status-pages/notifications"; @@ -35,6 +37,7 @@ export function Client({ id, pageId }: { id: number; pageId: number }) { trpc.maintenance.get.queryOptions({ id }), ); const { data: page } = useQuery(trpc.page.get.queryOptions({ id: pageId })); + const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); const rename = useUpdateMaintenance(id); const status = useMaintenanceStatus( maintenance ?? { from: new Date(0), to: new Date(0) }, @@ -43,6 +46,10 @@ export function Client({ id, pageId }: { id: number; pageId: number }) { if (!maintenance || !page) return null; const publicUrl = `${getPageUrl(page)}/events/maintenance/${maintenance.id}`; + const canNotify = workspace?.limits["status-subscribers"] === true; + // service returns updates newest first + const updates = maintenance.updates; + const latest = updates[0]; return ( @@ -76,14 +83,34 @@ export function Client({ id, pageId }: { id: number; pageId: number }) { {formatDistanceStrict(maintenance.from, maintenance.to)} + {updates.length ? ( + + {updates.length} {updates.length === 1 ? "update" : "updates"} + + ) : null} + {latest ? ( + + Last update + + ) : null} - - Message - - + + + {updates.map((update, i) => ( + + ))} + diff --git a/apps/dashboard/src/components/chat/tool-renderers/index.tsx b/apps/dashboard/src/components/chat/tool-renderers/index.tsx index 193f3872b..9989b2da5 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/index.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/index.tsx @@ -35,6 +35,11 @@ import { listPrivateLocationsTable } from "./list-private-locations"; import { listResponseLogsTable } from "./list-response-logs"; import { listStatusPagesTable } from "./list-status-pages"; import { listStatusReportsTable } from "./list-status-reports"; +import { + addMaintenanceUpdateChanges, + deleteMaintenanceUpdateChanges, + updateMaintenanceUpdateChanges, +} from "./maintenance-update"; import { resolveStatusReportChanges } from "./resolve-status-report"; import { ResultTable } from "./result-table"; import { searchContentTable } from "./search-content"; @@ -148,6 +153,32 @@ export const toolRenderers: ToolRendererRegistry = { ), summary: (o) => `ID ${o.id}`, }, + add_maintenance_update: { + renderDraft: (input) => addMaintenanceUpdateChanges(input), + renderResult: ({ input, output }) => ( + + ), + summary: (o) => `update #${o.id}`, + }, + update_maintenance_update: { + renderDraft: (input) => updateMaintenanceUpdateChanges(input), + renderResult: ({ input }) => ( + + ), + summary: (o) => `update #${o.id}`, + }, + delete_maintenance_update: { + renderDraft: (input) => deleteMaintenanceUpdateChanges(input), + renderResult: ({ input }) => ( + + ), + summary: (o) => `deleted #${o.id}`, + }, list_incidents: { renderResult: ({ output }) => ( diff --git a/apps/dashboard/src/components/chat/tool-renderers/maintenance-update.tsx b/apps/dashboard/src/components/chat/tool-renderers/maintenance-update.tsx new file mode 100644 index 000000000..c31fdbff0 --- /dev/null +++ b/apps/dashboard/src/components/chat/tool-renderers/maintenance-update.tsx @@ -0,0 +1,32 @@ +import type { AgentToolInput } from "@openstatus/services/agent-tools"; + +import type { ChangeRow } from "@/components/common/changes-table"; + +export function addMaintenanceUpdateChanges( + input: AgentToolInput<"add_maintenance_update">, + applied?: { id: number; notified: boolean }, +): ChangeRow[] { + return [ + ...(applied ? [{ field: "id", after: applied.id }] : []), + { field: "maintenanceId", after: input.maintenanceId }, + { field: "message", after: input.message }, + ...(input.date ? [{ field: "date", after: input.date }] : []), + { field: "notify", after: applied?.notified ?? input.notify }, + ]; +} + +export function updateMaintenanceUpdateChanges( + input: AgentToolInput<"update_maintenance_update">, +): ChangeRow[] { + return [ + { field: "id", after: input.id }, + ...(input.message ? [{ field: "message", after: input.message }] : []), + ...(input.date ? [{ field: "date", after: input.date }] : []), + ]; +} + +export function deleteMaintenanceUpdateChanges( + input: AgentToolInput<"delete_maintenance_update">, +): ChangeRow[] { + return [{ field: "id", before: input.id }]; +} diff --git a/apps/dashboard/src/components/data-table/maintenances/data-table-row-actions.tsx b/apps/dashboard/src/components/data-table/maintenances/data-table-row-actions.tsx index 051c54bf2..1637b344f 100644 --- a/apps/dashboard/src/components/data-table/maintenances/data-table-row-actions.tsx +++ b/apps/dashboard/src/components/data-table/maintenances/data-table-row-actions.tsx @@ -69,7 +69,6 @@ export function MaintenanceRowActions({ )} defaultValues={{ title: maintenance.title, - message: maintenance.message, startDate: maintenance.from, endDate: maintenance.to, pageComponents: maintenance.pageComponents?.map((c) => c.id) ?? [], @@ -78,7 +77,6 @@ export function MaintenanceRowActions({ await updateMaintenanceMutation.mutateAsync({ id: maintenance.id, title: values.title, - message: values.message, startDate: values.startDate, endDate: values.endDate, pageComponents: values.pageComponents, diff --git a/apps/dashboard/src/components/forms/form-alert-dialog.tsx b/apps/dashboard/src/components/forms/form-alert-dialog.tsx index 1138b47b1..8d92a5b0e 100644 --- a/apps/dashboard/src/components/forms/form-alert-dialog.tsx +++ b/apps/dashboard/src/components/forms/form-alert-dialog.tsx @@ -35,9 +35,14 @@ export function FormAlertDialog({ const { copy, isCopied } = useCopyToClipboard(); const [open, setOpen] = useState(false); - const handleDelete = async () => { - try { - startTransition(async () => { + const handleOpenChange = (next: boolean) => { + setOpen(next); + if (!next) setValue(""); + }; + + const handleDelete = () => { + startTransition(async () => { + try { const promise = submitAction(); toast.promise(promise, { loading: "Deleting...", @@ -50,15 +55,15 @@ export function FormAlertDialog({ }, }); await promise; - setOpen(false); - }); - } catch (error) { - console.error("Failed to revoke:", error); - } + handleOpenChange(false); + } catch (error) { + console.error("Failed to delete:", error); + } + }); }; return ( - + {children ?? (