Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add notification channel limit (#564)

authored by

Maximilian Kaske and committed by
GitHub
384236ee 5ff3db02

+82 -20
+10 -4
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/page.tsx
··· 1 1 import * as React from "react"; 2 2 import Link from "next/link"; 3 3 4 - import { Button } from "@openstatus/ui"; 4 + import { ButtonWithDisableTooltip } from "@openstatus/ui"; 5 5 6 6 import { Header } from "@/components/dashboard/header"; 7 7 import { HelpCallout } from "@/components/dashboard/help-callout"; ··· 10 10 import { api } from "@/trpc/server"; 11 11 import { EmptyState } from "./_components/empty-state"; 12 12 13 - export default async function MonitorPage({ 13 + export default async function NotificationPage({ 14 14 params, 15 15 }: { 16 16 params: { workspaceSlug: string }; 17 17 }) { 18 18 const notifications = 19 19 await api.notification.getNotificationsByWorkspace.query(); 20 + const isLimitReached = 21 + await api.notification.isNotificationLimitReached.query(); 20 22 21 23 return ( 22 24 <div className="grid min-h-full grid-cols-1 grid-rows-[auto,1fr,auto] gap-6 md:grid-cols-2 md:gap-8"> ··· 24 26 title="Notifications" 25 27 description="Overview of all your notification channels." 26 28 actions={ 27 - <Button asChild> 29 + <ButtonWithDisableTooltip 30 + tooltip="You reached the limits" 31 + asChild={!isLimitReached} 32 + disabled={isLimitReached} 33 + > 28 34 <Link href="./notifications/edit">Create</Link> 29 - </Button> 35 + </ButtonWithDisableTooltip> 30 36 } 31 37 /> 32 38 {notifications && notifications.length > 0 ? (
+11 -4
apps/web/src/components/forms/monitor-form.tsx
··· 18 18 monitorMethodsSchema, 19 19 monitorPeriodicitySchema, 20 20 } from "@openstatus/db/src/schema"; 21 - import { allPlans } from "@openstatus/plans"; 21 + import { getLimit } from "@openstatus/plans"; 22 22 import { 23 23 Accordion, 24 24 AccordionContent, ··· 207 207 }); 208 208 }; 209 209 210 - const limit = allPlans[plan].limits.periodicity; 210 + const periodicityLimit = getLimit(plan, "periodicity"); 211 + const notificationLimit = getLimit(plan, "notification-channels"); 212 + const notificationLimitReached = notifications 213 + ? notifications.length >= notificationLimit 214 + : false; 211 215 212 216 return ( 213 217 <Dialog open={openDialog} onOpenChange={(val) => setOpenDialog(val)}> ··· 446 450 <SelectItem 447 451 key={value} 448 452 value={value} 449 - disabled={!limit.includes(value)} 453 + disabled={!periodicityLimit.includes(value)} 450 454 > 451 455 {label} 452 456 </SelectItem> ··· 662 666 <FormMessage /> 663 667 <div className="sm:col-span-2 sm:col-start-1"> 664 668 <DialogTrigger asChild> 665 - <Button variant="outline"> 669 + <Button 670 + variant="outline" 671 + disabled={notificationLimitReached} 672 + > 666 673 Add Notifications 667 674 </Button> 668 675 </DialogTrigger>
+22 -11
apps/web/src/components/forms/notification-form.tsx
··· 56 56 placeholder: "dev@documenso.com", 57 57 setupDocLink: null, 58 58 sendTest: null, 59 + plans: ["free", "starter", "pro", "team"], 59 60 }; 60 61 61 62 case "slack": ··· 65 66 setupDocLink: 66 67 "https://api.slack.com/messaging/webhooks#getting_started", 67 68 sendTest: sendTestSlackMessage, 69 + plans: ["free", "starter", "pro", "team"], 68 70 }; 69 71 70 72 case "discord": ··· 73 75 placeholder: "https://discord.com/api/webhooks/{channelId}/xxx...", 74 76 setupDocLink: "https://support.discord.com/hc/en-us/articles/228383668", 75 77 sendTest: sendTestDiscordMessage, 78 + plans: ["free", "starter", "pro", "team"], 76 79 }; 77 80 case "sms": 78 81 return { ··· 80 83 placeholder: "+123456789", 81 84 setupDocLink: null, 82 85 sendTest: null, 83 - isPro: true, 86 + plans: ["pro", "team"], 84 87 }; 85 88 86 89 default: ··· 89 92 placeholder: "xxxx", 90 93 setupDocLink: `https://docs.openstatus.dev/integrations/${provider}`, 91 94 send: null, 95 + plans: ["free", "starter", "pro", "team"], 92 96 }; 93 97 } 94 98 } ··· 194 198 </SelectTrigger> 195 199 </FormControl> 196 200 <SelectContent> 197 - {notificationProvider.map((provider) => ( 198 - <SelectItem 199 - key={provider} 200 - value={provider} 201 - className="capitalize" 202 - > 203 - {provider} 204 - </SelectItem> 205 - ))} 201 + {notificationProvider.map((provider) => { 202 + const isIncluded = 203 + getProviderMetaData(provider).plans?.includes( 204 + workspacePlan, 205 + ); 206 + return ( 207 + <SelectItem 208 + key={provider} 209 + value={provider} 210 + className="capitalize" 211 + disabled={!isIncluded} 212 + > 213 + {provider} 214 + </SelectItem> 215 + ); 216 + })} 206 217 </SelectContent> 207 218 </Select> 208 219 <FormDescription> ··· 244 255 placeholder={providerMetaData.placeholder} 245 256 {...field} 246 257 disabled={ 247 - providerMetaData.isPro && workspacePlan !== "pro" 258 + !providerMetaData.plans?.includes(workspacePlan) 248 259 } 249 260 /> 250 261 </FormControl>
+35
packages/api/src/router/notification.ts
··· 1 + import { TRPCError } from "@trpc/server"; 1 2 import { z } from "zod"; 2 3 3 4 import { and, eq } from "@openstatus/db"; ··· 6 7 notification, 7 8 selectNotificationSchema, 8 9 } from "@openstatus/db/src/schema"; 10 + import { getLimit } from "@openstatus/plans"; 9 11 10 12 import { trackNewNotification } from "../analytics"; 11 13 import { createTRPCRouter, protectedProcedure } from "../trpc"; ··· 15 17 .input(insertNotificationSchema) 16 18 .mutation(async (opts) => { 17 19 const { ...data } = opts.input; 20 + 21 + const notificationLimit = getLimit( 22 + opts.ctx.workspace.plan, 23 + "notification-channels", 24 + ); 25 + 26 + const notificationNumber = ( 27 + await opts.ctx.db.query.notification.findMany({ 28 + where: eq(notification.workspaceId, opts.ctx.workspace.id), 29 + }) 30 + ).length; 31 + 32 + // the user has reached the limits 33 + if (notificationNumber >= notificationLimit) { 34 + throw new TRPCError({ 35 + code: "FORBIDDEN", 36 + message: "You reached your notification limits.", 37 + }); 38 + } 18 39 19 40 const _notification = await opts.ctx.db 20 41 .insert(notification) ··· 87 108 .all(); 88 109 89 110 return z.array(selectNotificationSchema).parse(notifications); 111 + }), 112 + 113 + isNotificationLimitReached: protectedProcedure.query(async (opts) => { 114 + const notificationLimit = getLimit( 115 + opts.ctx.workspace.plan, 116 + "notification-channels", 117 + ); 118 + const notificationNumbers = ( 119 + await opts.ctx.db.query.notification.findMany({ 120 + where: eq(notification.workspaceId, opts.ctx.workspace.id), 121 + }) 122 + ).length; 123 + 124 + return notificationNumbers >= notificationLimit; 90 125 }), 91 126 });
+4 -1
packages/plans/src/utils.ts
··· 4 4 import type { Limits } from "./types"; 5 5 6 6 // TODO: use getLimit utils function 7 - export function getLimit(plan: WorkspacePlan, limit: keyof Limits) { 7 + export function getLimit<T extends keyof Limits>( 8 + plan: WorkspacePlan, 9 + limit: T, 10 + ) { 8 11 return allPlans[plan].limits[limit]; 9 12 }