diff --git a/apps/dashboard/src/components/common/note.tsx b/apps/dashboard/src/components/common/note.tsx index 3f82faf5..55056018 100644 --- a/apps/dashboard/src/components/common/note.tsx +++ b/apps/dashboard/src/components/common/note.tsx @@ -3,7 +3,7 @@ import { cn } from "@/lib/utils"; import { type VariantProps, cva } from "class-variance-authority"; const noteVariants = cva( - "flex items-center gap-2 rounded-md border px-3 py-2 [&>svg]:size-4 [&>svg]:text-current [&>svg]:shrink-0", + "flex items-center gap-2 rounded-xl border px-3 py-2 [&>svg]:size-4 [&>svg]:text-current [&>svg]:shrink-0", { variants: { variant: { diff --git a/apps/dashboard/src/components/forms/form-alert-dialog.tsx b/apps/dashboard/src/components/forms/form-alert-dialog.tsx index d8cc5e3c..055f5110 100644 --- a/apps/dashboard/src/components/forms/form-alert-dialog.tsx +++ b/apps/dashboard/src/components/forms/form-alert-dialog.tsx @@ -61,18 +61,17 @@ export function FormAlertDialog({ {children ?? ( )} - Are you sure about revoking `{title}`? + Are you sure about delete `{title}`? - This action cannot be undone. This will permanently revoke the API - key. + This action cannot be undone. This will permanently delete the item.
diff --git a/apps/dashboard/src/components/forms/monitor/form-danger-zone.tsx b/apps/dashboard/src/components/forms/monitor/form-danger-zone.tsx index a990b065..24dd8a5f 100644 --- a/apps/dashboard/src/components/forms/monitor/form-danger-zone.tsx +++ b/apps/dashboard/src/components/forms/monitor/form-danger-zone.tsx @@ -11,8 +11,10 @@ import { export function FormDangerZone({ onSubmit, + title, }: { onSubmit: () => Promise; + title: string; }) { return ( @@ -22,7 +24,7 @@ export function FormDangerZone({ diff --git a/apps/dashboard/src/components/forms/monitor/update.tsx b/apps/dashboard/src/components/forms/monitor/update.tsx index 5789afce..d514e6b9 100644 --- a/apps/dashboard/src/components/forms/monitor/update.tsx +++ b/apps/dashboard/src/components/forms/monitor/update.tsx @@ -265,6 +265,7 @@ export function FormMonitorUpdate() { }} /> { await deleteMonitorMutation.mutateAsync({ id: Number.parseInt(id) }); }} diff --git a/apps/dashboard/src/components/forms/status-page/form-appearance.tsx b/apps/dashboard/src/components/forms/status-page/form-appearance.tsx index 74f552b6..ab22bdf2 100644 --- a/apps/dashboard/src/components/forms/status-page/form-appearance.tsx +++ b/apps/dashboard/src/components/forms/status-page/form-appearance.tsx @@ -1,6 +1,7 @@ import { useTransition } from "react"; import { z } from "zod"; +import { Link } from "@/components/common/link"; import { FormCard, FormCardContent, @@ -11,14 +12,28 @@ import { FormCardTitle, } from "@/components/forms/form-card"; import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Select, SelectContent, @@ -26,14 +41,22 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { THEME_KEYS } from "@openstatus/theme-store"; +import { THEMES } from "@openstatus/theme-store"; +import type { ThemeKey } from "@openstatus/theme-store"; import { isTRPCClientError } from "@trpc/client"; -import { Laptop, Moon, Sun } from "lucide-react"; +import { ArrowUpRight, Laptop, Moon, Sun } from "lucide-react"; +import { Check, ChevronsUpDown } from "lucide-react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; const schema = z.object({ forceTheme: z.enum(["light", "dark", "system"]), + configuration: z.object({ + theme: z.string(), + }), }); type FormValues = z.infer; @@ -92,7 +115,7 @@ export function FormAppearance({ name="forceTheme" render={({ field }) => ( - Theme + Mode + + Override the user's preference. + + + )} + /> + ( + + Style + + + + + + + + + + + No themes found. + + {THEME_KEYS.map((theme) => { + const { name, author } = THEMES[theme]; + return ( + field.onChange(v)} + > + {name} + + by {author.name} + + + + ); + })} + + + + + + + Choose a theme to apply. )} /> - Your user will still be able to change the theme via the theme + Your user will still be able to change the mode via the theme toggle. - +
+ + +
diff --git a/apps/dashboard/src/components/forms/status-page/form-configuration.tsx b/apps/dashboard/src/components/forms/status-page/form-configuration.tsx index 5b69a6b9..8ad2f8cc 100644 --- a/apps/dashboard/src/components/forms/status-page/form-configuration.tsx +++ b/apps/dashboard/src/components/forms/status-page/form-configuration.tsx @@ -14,14 +14,6 @@ import { FormCardTitle, } from "@/components/forms/form-card"; import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; import { Dialog, DialogClose, @@ -34,18 +26,11 @@ import { import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; import { Select, SelectContent, @@ -53,24 +38,19 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; -import { THEMES, THEME_KEYS, type ThemeKey } from "@openstatus/theme-store"; +import { THEMES, THEME_KEYS } from "@openstatus/theme-store"; import { isTRPCClientError } from "@trpc/client"; -import { ArrowUpRight, Check, ChevronsUpDown, Info } from "lucide-react"; +import { ArrowUpRight } from "lucide-react"; import { parseAsStringLiteral, useQueryStates } from "nuqs"; -import { useForm } from "react-hook-form"; +import { type UseFormReturn, useForm } from "react-hook-form"; import { toast } from "sonner"; const schema = z.object({ - new: z.boolean(), configuration: z.record( z.string(), z.string().or(z.boolean().nullish()).optional(), ), - homepageUrl: z.string().optional(), - contactUrl: z.string().optional(), }); const configurationSchema = z @@ -107,13 +87,9 @@ export function FormConfiguration({ const form = useForm({ resolver: zodResolver(schema), defaultValues: defaultValues ?? { - new: false, configuration: {}, - homepageUrl: "", - contactUrl: "", }, }); - const watchNew = form.watch("new"); const watchConfigurationType = form.watch("configuration.type") as | "manual" | "absolute"; @@ -165,320 +141,141 @@ export function FormConfiguration({
- Status Page Redesign (beta) + Tracker Configuration - Use the latest version of the status page and customize it. + Configure which data should be shown in the monitor tracker. - + + ( - -
- Enable New Version - - More controls, better UI. - -
- - - + + Bar Type + + )} /> -
- -
+ ( + + Card Value + + + + )} + /> + ( + + Show Uptime + + + + )} + /> + +
    +
  • + + Bar Type{" "} + + {watchConfigurationType} + + + : {message.type[watchConfigurationType]} +
  • +
  • + + Card Value{" "} + + {watchConfigurationValue} + + + :{" "} + {message.value[watchConfigurationValue] ?? + message.value.default} +
  • +
  • + + Show Uptime{" "} + + {watchConfigurationUptime} + + + : {message.uptime[watchConfigurationUptime]} +
  • +
+
- {watchNew && ( - <> - - - - Tracker Configuration - - Configure which data should be shown in the monitor - tracker. - - - ( - - Bar Type - - - - )} - /> - ( - - Card Value - - - - )} - /> - ( - - Show Uptime - - - - )} - /> - -
    -
  • - - Bar Type{" "} - - {watchConfigurationType} - - - : {message.type[watchConfigurationType]} -
  • -
  • - - Card Value{" "} - - {watchConfigurationValue} - - - :{" "} - {message.value[watchConfigurationValue] ?? - message.value.default} -
  • -
  • - - Show Uptime{" "} - - {watchConfigurationUptime} - - - : {message.uptime[watchConfigurationUptime]} -
  • -
-
-
- - - - Theme Explorer - - Configure the theme for the status page - or contribute - your own. Learn more about it at{" "} - - themes.openstatus.dev - - . - - - ( - - Theme - - - - - - - - - - - No themes found. - - {THEME_KEYS.map((theme) => { - const { name, author } = THEMES[theme]; - return ( - field.onChange(v)} - > - {name} - - by {author.name} - - - - ); - })} - - - - - - - )} - /> - - - - - Links - - Configure the links for the status page. - - - ( - - Homepage URL - - - - - - What URL should the logo link to? Leave empty to hide. - - - )} - /> - ( - - Contact URL - - - - - - Enter the URL for your contact page. Or start with{" "} - - mailto: - {" "} - to open the email client. Leave empty to hide. - - - )} - /> - - - )} Learn more about{" "} @@ -487,19 +284,33 @@ export function FormConfiguration({ rel="noreferrer" target="_blank" > - Status Page Redesign (beta) + Configuration . - +
+ + +
{ await onSubmit(e); // NOTE: make sure to sync the form with the new values @@ -545,6 +356,7 @@ function FormConfigurationDialog({ }: { defaultValues?: FormValues; onSubmit: (values: FormValues) => Promise; + form: UseFormReturn; }) { const [open, setOpen] = useState(false); const [isPending, startTransition] = useTransition(); @@ -578,7 +390,7 @@ function FormConfigurationDialog({ }, }); await promise; - setSearchParams({ + await setSearchParams({ type: null, value: null, uptime: null, @@ -587,6 +399,10 @@ function FormConfigurationDialog({ setOpen(false); } catch (error) { console.error(error); + } finally { + if (typeof window !== "undefined") { + window.location.reload(); + } } }); } @@ -595,22 +411,16 @@ function FormConfigurationDialog({ - Status Page Redesign (beta) + Status Page Configuration Do you want to update the status page based on the configured - settings? + settings? You can always change the settings later.
             {JSON.stringify({ type, value, uptime, theme }, null, 2)}
           
- {!defaultValues || !defaultValues.new ? ( - - -

You will activate the new version of the status page.

-
- ) : null}
@@ -621,7 +431,6 @@ function FormConfigurationDialog({ onClick={() => submitAction({ ...defaultValues, - new: true, configuration: { type: type ?? undefined, value: value ?? undefined, diff --git a/apps/dashboard/src/components/forms/status-page/form-danger-zone.tsx b/apps/dashboard/src/components/forms/status-page/form-danger-zone.tsx index b08ff603..44032ca6 100644 --- a/apps/dashboard/src/components/forms/status-page/form-danger-zone.tsx +++ b/apps/dashboard/src/components/forms/status-page/form-danger-zone.tsx @@ -11,8 +11,10 @@ import { export function FormDangerZone({ onSubmit, + title, }: { onSubmit: () => Promise; + title: string; }) { return ( @@ -22,7 +24,7 @@ export function FormDangerZone({ diff --git a/apps/dashboard/src/components/forms/status-page/form-links.tsx b/apps/dashboard/src/components/forms/status-page/form-links.tsx new file mode 100644 index 00000000..5a66c3b1 --- /dev/null +++ b/apps/dashboard/src/components/forms/status-page/form-links.tsx @@ -0,0 +1,144 @@ +import { useTransition } from "react"; +import { z } from "zod"; + +import { Link } from "@/components/common/link"; +import { + FormCard, + FormCardContent, + FormCardDescription, + FormCardFooter, + FormCardFooterInfo, + FormCardHeader, + FormCardTitle, +} from "@/components/forms/form-card"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { isTRPCClientError } from "@trpc/client"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; + +const schema = z.object({ + homepageUrl: z.string().optional(), + contactUrl: z.string().optional(), +}); + +type FormValues = z.infer; + +export function FormLinks({ + defaultValues, + onSubmit, +}: { + defaultValues?: FormValues; + onSubmit: (values: FormValues) => Promise; +}) { + const [isPending, startTransition] = useTransition(); + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaultValues ?? { + homepageUrl: "", + contactUrl: "", + }, + }); + + function submitAction(values: FormValues) { + if (isPending) return; + + startTransition(async () => { + try { + const promise = onSubmit(values); + toast.promise(promise, { + loading: "Saving...", + success: "Saved", + error: (error) => { + if (isTRPCClientError(error)) { + return error.message; + } + return "Failed to save"; + }, + }); + await promise; + } catch (error) { + console.error(error); + } + }); + } + + return ( +
+ + + + Links + + Configure the links for the status page. + + + + ( + + Homepage URL + + + + + + What URL should the logo link to? Leave empty to hide. + + + )} + /> + ( + + Contact URL + + + + + + Enter the URL for your contact page. Or start with{" "} + + mailto: + {" "} + to open the email client. Leave empty to hide. + + + )} + /> + + + + Learn more about{" "} + + links + + . + + + + +
+ + ); +} diff --git a/apps/dashboard/src/components/forms/status-page/update.tsx b/apps/dashboard/src/components/forms/status-page/update.tsx index b9094871..14231efb 100644 --- a/apps/dashboard/src/components/forms/status-page/update.tsx +++ b/apps/dashboard/src/components/forms/status-page/update.tsx @@ -10,6 +10,7 @@ import { FormConfiguration } from "./form-configuration"; import { FormCustomDomain } from "./form-custom-domain"; import { FormDangerZone } from "./form-danger-zone"; import { FormGeneral } from "./form-general"; +import { FormLinks } from "./form-links"; import { FormMonitors } from "./form-monitors"; import { FormPasswordProtection } from "./form-password-protection"; @@ -81,6 +82,12 @@ export function FormStatusPageUpdate() { }), ); + const updateLinksMutation = useMutation( + trpc.page.updateLinks.mutationOptions({ + onSuccess: () => refetch(), + }), + ); + if (!statusPage || !monitors || !workspace) return null; const configLink = `https://${ @@ -92,8 +99,15 @@ export function FormStatusPageUpdate() {

- We've released a new version of the status page.{" "} - Go to the section below to enable it. + We've enabled the new version of the status page. Read more about the{" "} + + configuration + + .

+ { + await updateLinksMutation.mutateAsync({ + id: Number.parseInt(id), + homepageUrl: values.homepageUrl ?? undefined, + contactUrl: values.contactUrl ?? undefined, + }); + }} + /> { await updatePageAppearanceMutation.mutateAsync({ id: Number.parseInt(id), forceTheme: values.forceTheme, + configuration: values.configuration, }); }} /> { await updatePageConfigurationMutation.mutateAsync({ id: Number.parseInt(id), - configuration: values.new - ? { - uptime: - typeof values.configuration.uptime === "boolean" - ? values.configuration.uptime - : values.configuration.uptime === "true", - value: values.configuration.value ?? "duration", - type: values.configuration.type ?? "absolute", - theme: values.configuration.theme ?? "default", - } - : undefined, - legacyPage: !values.new, - homepageUrl: values.homepageUrl ?? undefined, - contactUrl: values.contactUrl ?? undefined, + configuration: { + uptime: + typeof values.configuration.uptime === "boolean" + ? values.configuration.uptime + : values.configuration.uptime === "true", + value: values.configuration.value ?? "duration", + type: values.configuration.type ?? "absolute", + theme: values.configuration.theme ?? undefined, + }, }); }} configLink={configLink} @@ -216,6 +239,7 @@ export function FormStatusPageUpdate() { }} /> { await deleteStatusPageMutation.mutateAsync({ id: Number.parseInt(id), diff --git a/apps/dashboard/src/data/maintenances.client.ts b/apps/dashboard/src/data/maintenances.client.ts index 013ede96..d0a79e43 100644 --- a/apps/dashboard/src/data/maintenances.client.ts +++ b/apps/dashboard/src/data/maintenances.client.ts @@ -1,10 +1,10 @@ -import { Pencil, Trash2 } from "lucide-react"; +import { Cog, Trash2 } from "lucide-react"; export const actions = [ { id: "edit", - label: "Edit", - icon: Pencil, + label: "Settings", + icon: Cog, variant: "default" as const, }, { diff --git a/apps/dashboard/src/data/monitors.client.ts b/apps/dashboard/src/data/monitors.client.ts index 499cf523..49bb6d47 100644 --- a/apps/dashboard/src/data/monitors.client.ts +++ b/apps/dashboard/src/data/monitors.client.ts @@ -1,9 +1,9 @@ import { + Cog, Copy, CopyPlus, Globe, Network, - Pencil, Server, Trash2, } from "lucide-react"; @@ -29,8 +29,8 @@ export const monitorTypes = [ export const actions = [ { id: "edit", - label: "Edit", - icon: Pencil, + label: "Settings", + icon: Cog, variant: "default" as const, }, { diff --git a/apps/dashboard/src/data/notifications.client.ts b/apps/dashboard/src/data/notifications.client.ts index 5ca57b1c..9851920d 100644 --- a/apps/dashboard/src/data/notifications.client.ts +++ b/apps/dashboard/src/data/notifications.client.ts @@ -18,9 +18,9 @@ import { sendTestSlackMessage as sendTestSlack } from "@openstatus/notification- import { sendTest as sendTestWebhook } from "@openstatus/notification-webhook"; import { BellIcon, + Cog, Mail, MessageCircle, - Pencil, Trash2, Webhook, } from "lucide-react"; @@ -28,8 +28,8 @@ import { export const actions = [ { id: "edit", - label: "Edit", - icon: Pencil, + label: "Settings", + icon: Cog, variant: "default" as const, }, { diff --git a/apps/dashboard/src/data/status-pages.client.ts b/apps/dashboard/src/data/status-pages.client.ts index 59f4e9c7..8f2c87be 100644 --- a/apps/dashboard/src/data/status-pages.client.ts +++ b/apps/dashboard/src/data/status-pages.client.ts @@ -1,10 +1,10 @@ -import { Copy, Pencil, Trash2 } from "lucide-react"; +import { Cog, Copy, Trash2 } from "lucide-react"; export const actions = [ { id: "edit", - label: "Edit", - icon: Pencil, + label: "Settings", + icon: Cog, variant: "default" as const, }, { diff --git a/apps/dashboard/src/data/status-report-updates.client.ts b/apps/dashboard/src/data/status-report-updates.client.ts index d6da1b54..37bf3331 100644 --- a/apps/dashboard/src/data/status-report-updates.client.ts +++ b/apps/dashboard/src/data/status-report-updates.client.ts @@ -1,11 +1,11 @@ import type { StatusReportStatus } from "@openstatus/db/src/schema"; -import { Pencil, Trash2 } from "lucide-react"; +import { Cog, Trash2 } from "lucide-react"; export const actions = [ { id: "edit", - label: "Edit", - icon: Pencil, + label: "Settings", + icon: Cog, variant: "default" as const, }, { diff --git a/apps/dashboard/src/data/status-reports.client.ts b/apps/dashboard/src/data/status-reports.client.ts index 6aab7904..919b2b75 100644 --- a/apps/dashboard/src/data/status-reports.client.ts +++ b/apps/dashboard/src/data/status-reports.client.ts @@ -1,10 +1,10 @@ -import { Eye, Pencil, Plus, Trash2 } from "lucide-react"; +import { Cog, Eye, Plus, Trash2 } from "lucide-react"; export const actions = [ { id: "edit", - label: "Edit", - icon: Pencil, + label: "Settings", + icon: Cog, variant: "default" as const, }, { diff --git a/apps/docs/src/content/docs/tutorial/how-to-configure-status-page.mdx b/apps/docs/src/content/docs/tutorial/how-to-configure-status-page.mdx index 3be5dc0c..d86e1276 100644 --- a/apps/docs/src/content/docs/tutorial/how-to-configure-status-page.mdx +++ b/apps/docs/src/content/docs/tutorial/how-to-configure-status-page.mdx @@ -16,11 +16,7 @@ import StatusPageBeta4 from '../../../assets/tutorial/configure-status-page/stat We are releasing a new version of our status pages. - - -You can always have a look how your status page will look like via `https://[slug].stpg.dev`. - -Example: [https://themes.openstatus.dev/status](https://themes.openstatus.dev/status) +Explore the themes: [https://themes.openstatus.dev](https://themes.openstatus.dev) ## Get started diff --git a/packages/api/src/router/page.ts b/packages/api/src/router/page.ts index 46d0c029..e5943e1d 100644 --- a/packages/api/src/router/page.ts +++ b/packages/api/src/router/page.ts @@ -776,6 +776,56 @@ export const pageRouter = createTRPCRouter({ z.object({ id: z.number(), forceTheme: z.enum(["light", "dark", "system"]), + configuration: z.object({ + theme: z.string(), + }), + }), + ) + .mutation(async (opts) => { + const whereConditions: SQL[] = [ + eq(page.workspaceId, opts.ctx.workspace.id), + eq(page.id, opts.input.id), + ]; + + const _page = await opts.ctx.db.query.page.findFirst({ + where: and(...whereConditions), + }); + + if (!_page) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Page not found", + }); + } + + const currentConfiguration = + (typeof _page.configuration === "object" && + _page.configuration !== null && + _page.configuration) || + {}; + const updatedConfiguration = { + ...currentConfiguration, + theme: opts.input.configuration.theme, + }; + + await opts.ctx.db + .update(page) + .set({ + forceTheme: opts.input.forceTheme, + configuration: updatedConfiguration, + updatedAt: new Date(), + }) + .where(and(...whereConditions)) + .run(); + }), + + updateLinks: protectedProcedure + .meta({ track: Events.UpdatePage }) + .input( + z.object({ + id: z.number(), + homepageUrl: z.string().nullish(), + contactUrl: z.string().nullish(), }), ) .mutation(async (opts) => { @@ -786,7 +836,11 @@ export const pageRouter = createTRPCRouter({ await opts.ctx.db .update(page) - .set({ forceTheme: opts.input.forceTheme, updatedAt: new Date() }) + .set({ + homepageUrl: opts.input.homepageUrl, + contactUrl: opts.input.contactUrl, + updatedAt: new Date(), + }) .where(and(...whereConditions)) .run(); }), @@ -799,9 +853,6 @@ export const pageRouter = createTRPCRouter({ configuration: z .record(z.string(), z.string().or(z.boolean()).optional()) .nullish(), - legacyPage: z.boolean(), - homepageUrl: z.string().nullish(), - contactUrl: z.string().nullish(), }), ) .mutation(async (opts) => { @@ -821,29 +872,24 @@ export const pageRouter = createTRPCRouter({ }); } + const currentConfiguration = + (typeof _page.configuration === "object" && + _page.configuration !== null && + _page.configuration) || + {}; + const updatedConfiguration = { + ...currentConfiguration, + ...opts.input.configuration, + }; + await opts.ctx.db .update(page) .set({ - configuration: opts.input.configuration, - legacyPage: opts.input.legacyPage, - homepageUrl: opts.input.homepageUrl, - contactUrl: opts.input.contactUrl, + configuration: updatedConfiguration, updatedAt: new Date(), }) .where(and(...whereConditions)) .run(); - - if (opts.input.legacyPage) { - await redis.del(`page:${_page.slug}`); - if (_page.customDomain) { - await redis.del(`page:${_page.customDomain}`); - } - } else { - await redis.set(`page:${_page.slug}`, 1); - if (_page.customDomain) { - await redis.set(`page:${_page.customDomain}`, 1); - } - } }), updateMonitors: protectedProcedure