Openstatus www.openstatus.dev

chore: clean up monitor visibility (#1062)

* wip:

* wip:

* chore: update changelog

* ci: apply automated fixes

* chore: reorder pricing

* wip:

* chore: add example to features page

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Maximilian Kaske
autofix-ci[bot]
and committed by
GitHub
f38efd2d aac7a641

+222 -88
+9
apps/server/src/v1/pages/schema.ts
··· 68 68 }) 69 69 .optional() 70 70 .nullish(), 71 + showMonitorValues: z 72 + .boolean() 73 + .openapi({ 74 + description: 75 + "Displays the total and failed request numbers for each monitor", 76 + example: true, 77 + }) 78 + .optional() 79 + .nullish(), 71 80 monitors: z 72 81 .array(z.number()) 73 82 .openapi({
apps/web/public/assets/changelog/status-page-hide-request-numbers.png

This is a binary file and will not be displayed.

apps/web/public/assets/changelog/status-page-monitor-values-visibility.png

This is a binary file and will not be displayed.

+38
apps/web/src/app/(content)/features/_components/tracker-example.tsx
··· 1 + "use client"; 2 + 3 + import { Tracker } from "@/components/tracker/tracker"; 4 + import { Checkbox } from "@openstatus/ui"; 5 + import { useState } from "react"; 6 + import { mockTrackerData } from "../mock"; 7 + 8 + export function TrackerWithVisibilityToggle() { 9 + const [visible, setVisible] = useState(true); 10 + return ( 11 + <div className="flex flex-col gap-8 my-auto"> 12 + <div className="items-top flex space-x-2"> 13 + <Checkbox 14 + id="visibility" 15 + onCheckedChange={(e) => setVisible(!!e)} 16 + checked={visible} 17 + /> 18 + <div className="grid gap-1.5 leading-none"> 19 + <label 20 + htmlFor="visibility" 21 + className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 22 + > 23 + Show values 24 + </label> 25 + <p className="text-sm text-muted-foreground"> 26 + Share the uptime and number of requests. 27 + </p> 28 + </div> 29 + </div> 30 + <Tracker 31 + data={mockTrackerData} 32 + name="OpenStatus" 33 + description="Website Health" 34 + showValues={visible} 35 + /> 36 + </div> 37 + ); 38 + }
+3 -11
apps/web/src/app/(content)/features/status-page/page.tsx
··· 8 8 import { MaintenanceContainer } from "@/components/status-page/maintenance"; 9 9 import { StatusCheck } from "@/components/status-page/status-check"; 10 10 import { StatusReport } from "@/components/status-page/status-report"; 11 - import { Tracker } from "@/components/tracker/tracker"; 12 11 import { marketingProductPagesConfig } from "@/config/pages"; 13 12 import { Button, InputWithAddons } from "@openstatus/ui"; 14 13 import type { Metadata } from "next"; ··· 16 15 import { Banner } from "../_components/banner"; 17 16 import { Hero } from "../_components/hero"; 18 17 import { InteractiveFeature } from "../_components/interactive-feature"; 19 - import { maintenanceData, mockTrackerData, statusReportData } from "../mock"; 18 + import { TrackerWithVisibilityToggle } from "../_components/tracker-example"; 19 + import { maintenanceData, statusReportData } from "../mock"; 20 20 21 21 const { description, subtitle } = marketingProductPagesConfig[1]; 22 22 ··· 61 61 iconText="Simple by default" 62 62 title="Status page." 63 63 subTitle="Connect your monitors and inform your users about the uptime." 64 - component={ 65 - <div className="my-auto"> 66 - <Tracker 67 - data={mockTrackerData} 68 - name="OpenStatus" 69 - description="Website Health" 70 - /> 71 - </div> 72 - } 64 + component={<TrackerWithVisibilityToggle />} 73 65 col={2} 74 66 position={"left"} 75 67 />
+17
apps/web/src/components/billing/pro-feature-badge.tsx
··· 1 + import type { WorkspacePlan } from "@openstatus/db/src/schema"; 2 + import { Badge } from "@openstatus/ui"; 3 + import { upgradePlan } from "./utils"; 4 + 5 + export function ProFeatureBadge({ 6 + plan, 7 + minRequiredPlan, 8 + }: { 9 + plan: WorkspacePlan; 10 + minRequiredPlan: WorkspacePlan; 11 + }) { 12 + const shouldUpgrade = upgradePlan(plan, minRequiredPlan); 13 + 14 + if (!shouldUpgrade) return null; 15 + 16 + return <Badge>{minRequiredPlan}</Badge>; 17 + }
+10 -8
apps/web/src/components/billing/pro-feature-hover-card.tsx
··· 2 2 3 3 import { useState } from "react"; 4 4 5 - import { workspacePlanHierarchy } from "@openstatus/db/src/schema"; 6 5 import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 7 - import { HoverCard, HoverCardContent, HoverCardTrigger } from "@openstatus/ui"; 6 + import { 7 + Badge, 8 + HoverCard, 9 + HoverCardContent, 10 + HoverCardTrigger, 11 + } from "@openstatus/ui"; 8 12 import { ArrowUpRight } from "lucide-react"; 9 13 import Link from "next/link"; 10 - 11 - function upgradePlan(current: WorkspacePlan, required: WorkspacePlan) { 12 - return workspacePlanHierarchy[current] < workspacePlanHierarchy[required]; 13 - } 14 + import { upgradePlan } from "./utils"; 14 15 15 16 // TBD: we could useParams() to access workspaceSlug 16 17 ··· 25 26 minRequiredPlan: WorkspacePlan; 26 27 workspaceSlug: string; 27 28 }) { 28 - console.log({ workspaceSlug, plan, minRequiredPlan }); 29 29 const [open, setOpen] = useState(false); 30 30 const shouldUpgrade = upgradePlan(plan, minRequiredPlan); 31 31 32 32 if (!shouldUpgrade) return children; 33 + 34 + // TODO: add a <Badge /> component to display the plan name 33 35 34 36 return ( 35 37 <HoverCard openDelay={0} open={open} onOpenChange={setOpen}> 36 38 <HoverCardTrigger 37 39 onClick={() => setOpen(true)} 38 - className="opacity-70 cursor-not-allowed" 40 + className="opacity-70 cursor-not-allowed relative" 39 41 asChild 40 42 > 41 43 {children}
+6
apps/web/src/components/billing/utils.ts
··· 1 + import { workspacePlanHierarchy } from "@openstatus/db/src/schema"; 2 + import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 3 + 4 + export function upgradePlan(current: WorkspacePlan, required: WorkspacePlan) { 5 + return workspacePlanHierarchy[current] < workspacePlanHierarchy[required]; 6 + }
+1 -1
apps/web/src/components/forms/status-page/form.tsx
··· 188 188 <SectionMonitor form={form} monitors={allMonitors} /> 189 189 </TabsContent> 190 190 <TabsContent value="advanced"> 191 - <SectionAdvanced form={form} /> 191 + <SectionAdvanced {...{ form }} /> 192 192 </TabsContent> 193 193 <TabsContent value="visibility"> 194 194 <SectionVisibility {...{ form, plan, workspaceSlug }} />
+14 -5
apps/web/src/components/forms/status-page/section-advanced.tsx
··· 163 163 /> 164 164 <div className="grid w-full gap-4 md:grid-rows-2 md:grid-cols-3 md:col-span-full"> 165 165 <SectionHeader 166 - title="Bar Settings" 167 - description="You can display or hide the amount of scheduled request an entpoint gets per day." 166 + title="Monitor Values Visibility" 167 + description={ 168 + <> 169 + Toggle the visibility of the values on the status page. Share your{" "} 170 + <span className="font-medium text-foreground">uptime</span> and 171 + the{" "} 172 + <span className="font-medium text-foreground"> 173 + number of request 174 + </span>{" "} 175 + to your endpoint. 176 + </> 177 + } 168 178 className="md:col-span-2" 169 179 /> 170 180 <div className="group md:row-span-2 flex flex-col justify-center gap-1 border border-dashed rounded-md p-3"> ··· 197 207 /> 198 208 </FormControl> 199 209 <div className="space-y-1 leading-none"> 200 - <FormLabel>Show number of request</FormLabel> 210 + <FormLabel>Show values</FormLabel> 201 211 <FormDescription> 202 - Share the total and failed amount of scheduled request to your 203 - endpoint. 212 + Share the numbers to your users. 204 213 </FormDescription> 205 214 </div> 206 215 </FormItem>
+74 -52
apps/web/src/components/marketing/pricing/pricing-table.tsx
··· 1 1 "use client"; 2 2 3 - import { Check } from "lucide-react"; 3 + import { Check, Info } from "lucide-react"; 4 4 import { useRouter } from "next/navigation"; 5 5 import { Fragment } from "react"; 6 6 ··· 22 22 import { LoadingAnimation } from "@/components/loading-animation"; 23 23 import { cn } from "@/lib/utils"; 24 24 import { allPlans } from "@openstatus/db/src/schema/plan/config"; 25 + import { 26 + Tooltip, 27 + TooltipContent, 28 + TooltipProvider, 29 + TooltipTrigger, 30 + } from "@openstatus/ui"; 25 31 26 32 export function PricingTable({ 27 33 plans = workspacePlans, ··· 112 118 {label} 113 119 </TableCell> 114 120 </TableRow> 115 - {features.map(({ label, value, badge, monthly }, _i) => { 116 - return ( 117 - <TableRow key={key + label}> 118 - <TableCell className="gap-1"> 119 - {label}{" "} 120 - {badge ? ( 121 - <Badge variant="secondary">{badge}</Badge> 122 - ) : null} 123 - </TableCell> 124 - {selectedPlans.map((plan, _i) => { 125 - const limitValue = 126 - plan.limits[value as keyof typeof plan.limits]; 127 - function renderContent() { 128 - if (typeof limitValue === "boolean") { 129 - if (limitValue) { 121 + {features.map( 122 + ({ label, value, badge, monthly, description }, _i) => { 123 + return ( 124 + <TableRow key={key + label}> 125 + <TableCell> 126 + <div className="flex gap-2 items-center"> 127 + {label} 128 + {badge ? ( 129 + <Badge variant="secondary">{badge}</Badge> 130 + ) : null} 131 + {description ? ( 132 + <TooltipProvider delayDuration={200}> 133 + <Tooltip> 134 + <TooltipTrigger className="ml-auto data-[state=closed]:text-muted-foreground"> 135 + <Info className="w-4 h-4" /> 136 + </TooltipTrigger> 137 + <TooltipContent className="w-64"> 138 + {description} 139 + </TooltipContent> 140 + </Tooltip> 141 + </TooltipProvider> 142 + ) : null} 143 + </div> 144 + </TableCell> 145 + {selectedPlans.map((plan, _i) => { 146 + const limitValue = 147 + plan.limits[value as keyof typeof plan.limits]; 148 + function renderContent() { 149 + if (typeof limitValue === "boolean") { 150 + if (limitValue) { 151 + return ( 152 + <Check className="h-4 w-4 text-foreground" /> 153 + ); 154 + } 130 155 return ( 131 - <Check className="h-4 w-4 text-foreground" /> 156 + <span className="text-muted-foreground/50"> 157 + &#8208; 158 + </span> 132 159 ); 133 160 } 134 - return ( 135 - <span className="text-muted-foreground/50"> 136 - &#8208; 137 - </span> 138 - ); 139 - } 140 - if (typeof limitValue === "number") { 141 - return new Intl.NumberFormat("us") 142 - .format(limitValue) 143 - .toString(); 144 - } 145 - if ( 146 - Array.isArray(limitValue) && 147 - limitValue.length > 0 148 - ) { 149 - return limitValue[0]; 161 + if (typeof limitValue === "number") { 162 + return new Intl.NumberFormat("us") 163 + .format(limitValue) 164 + .toString(); 165 + } 166 + if ( 167 + Array.isArray(limitValue) && 168 + limitValue.length > 0 169 + ) { 170 + return limitValue[0]; 171 + } 172 + return limitValue; 150 173 } 151 - return limitValue; 152 - } 153 174 154 - return ( 155 - <TableCell 156 - // biome-ignore lint/suspicious/noArrayIndexKey: <explanation> 157 - key={key + value + _i} 158 - className={cn( 159 - "p-3 font-mono", 160 - plan.key === "team" && "bg-muted/30", 161 - )} 162 - > 163 - {renderContent()} 164 - {monthly ? "/mo" : ""} 165 - </TableCell> 166 - ); 167 - })} 168 - </TableRow> 169 - ); 170 - })} 175 + return ( 176 + <TableCell 177 + // biome-ignore lint/suspicious/noArrayIndexKey: <explanation> 178 + key={key + value + _i} 179 + className={cn( 180 + "p-3 font-mono", 181 + plan.key === "team" && "bg-muted/30", 182 + )} 183 + > 184 + {renderContent()} 185 + {monthly ? "/mo" : ""} 186 + </TableCell> 187 + ); 188 + })} 189 + </TableRow> 190 + ); 191 + }, 192 + )} 171 193 </Fragment> 172 194 ); 173 195 },
+1 -1
apps/web/src/components/tracker/tracker.tsx
··· 102 102 </TooltipProvider> 103 103 ) : null} 104 104 </div> 105 - {!isMissing ? ( 105 + {!isMissing && showValues ? ( 106 106 <p className="shrink-0 font-light text-muted-foreground">{uptime}%</p> 107 107 ) : null} 108 108 </div>
+32
apps/web/src/config/pricing-table.ts apps/web/src/config/pricing-table.tsx
··· 1 1 import type { LimitsV1, LimitsV2 } from "@openstatus/db/src/schema/plan/schema"; 2 + import Link from "next/link"; 3 + import type React from "react"; 4 + 5 + import { type Changelog, allChangelogs } from "contentlayer/generated"; 6 + 7 + function renderChangelogDescription(slug: Changelog["slug"]) { 8 + const changelog = allChangelogs.find((c) => c.slug === slug); 9 + if (!changelog) return null; 10 + return ( 11 + <span> 12 + {changelog?.description}{" "} 13 + <Link 14 + href={`/changelog/${changelog.slug}`} 15 + className="underline underline-offset-4 text-nowrap" 16 + > 17 + Learn more 18 + </Link> 19 + . 20 + </span> 21 + ); 22 + } 2 23 3 24 export const pricingTableConfig: Record< 4 25 string, ··· 7 28 features: { 8 29 value: keyof LimitsV1 | keyof LimitsV2; 9 30 label: string; 31 + description?: React.ReactNode; // tooltip informations 10 32 badge?: string; 11 33 monthly?: boolean; 12 34 }[]; ··· 55 77 label: "Maintenance status", 56 78 }, 57 79 { 80 + value: "monitor-values-visibility", 81 + label: "Toggle numbers visibility", 82 + description: renderChangelogDescription( 83 + "status-page-monitor-values-visibility", 84 + ), 85 + }, 86 + { 58 87 value: "status-subscribers", 59 88 label: "Subscribers", 60 89 }, ··· 65 94 { 66 95 value: "password-protection", 67 96 label: "Password-protected", 97 + description: renderChangelogDescription( 98 + "password-protected-status-page", 99 + ), 68 100 }, 69 101 { 70 102 value: "white-label",
-10
apps/web/src/content/changelog/status-page-hide-request-numbers.mdx
··· 1 - --- 2 - title: Toggle monitor request numbers 3 - description: Show or hide the total and failed requests on a status page. 4 - publishedAt: 2024-10-20 5 - image: /assets/changelog/status-page-hide-request-numbers.png 6 - --- 7 - 8 - You can now hide your request numbers. 9 - 10 - We have added a checkbox within your Status Page settings to hide/show the number of total and failed requests for each monitor connected to the page.
+11
apps/web/src/content/changelog/status-page-monitor-values-visibility.mdx
··· 1 + --- 2 + title: Monitor values visibility 3 + description: Hide the uptime and number of request on your status pages. 4 + publishedAt: 2024-10-20 5 + image: /assets/changelog/status-page-monitor-values-visibility.png 6 + --- 7 + 8 + 9 + You can now hide your request values. 10 + 11 + We have added a checkbox within your Status Page settings to hide/show the number of total and failed requests incl. the average percentage for each monitor connected to the page.
+4
packages/db/src/schema/plan/config.ts
··· 24 24 "data-retention": "14 days", 25 25 "status-pages": 1, 26 26 maintenance: true, 27 + "monitor-values-visibility": true, 27 28 "status-subscribers": false, 28 29 "custom-domain": false, 29 30 "password-protection": false, ··· 51 52 "data-retention": "3 months", 52 53 "status-pages": 1, 53 54 maintenance: true, 55 + "monitor-values-visibility": true, 54 56 "status-subscribers": true, 55 57 "custom-domain": true, 56 58 "password-protection": true, ··· 114 116 "data-retention": "12 months", 115 117 "status-pages": 5, 116 118 maintenance: true, 119 + "monitor-values-visibility": true, 117 120 "status-subscribers": true, 118 121 "custom-domain": true, 119 122 "password-protection": true, ··· 177 180 "data-retention": "24 months", 178 181 "status-pages": 20, 179 182 maintenance: true, 183 + "monitor-values-visibility": true, 180 184 "status-subscribers": true, 181 185 "custom-domain": true, 182 186 "password-protection": true,
+2
packages/db/src/schema/plan/schema.ts
··· 33 33 export const limitsV2 = limitsV1.extend({ 34 34 version: z.literal("v2"), 35 35 "private-locations": z.boolean(), 36 + "monitor-values-visibility": z.boolean(), 36 37 }); 37 38 38 39 export type LimitsV2 = z.infer<typeof limitsV2>; ··· 44 45 version: "v2", 45 46 ...data, 46 47 "private-locations": true, 48 + "monitor-values-visibility": true, 47 49 }; 48 50 } 49 51