Openstatus www.openstatus.dev

๐ŸŒ private locations (#938)

* ๐ŸŒ private locations

* ๐ŸŒ private location

* ๐ŸŒ private location

* ๐ŸŒ private location

authored by

Thibault Le Ouay and committed by
GitHub
22c13a09 d002030c

+52 -20
+9 -1
apps/web/src/components/dashboard/limit.tsx
··· 1 + import Link from "next/link"; 2 + 1 3 export function Limit() { 2 4 return ( 3 5 <div className="col-span-full text-center"> 4 - <p className="text-muted-foreground text-xs">You reached the limits.</p> 6 + <p className="text-foreground text-sm"> 7 + You have reached the account limits. Please{" "} 8 + <Link href={"./settings/billing"} className="underline"> 9 + upgrade 10 + </Link>{" "} 11 + your account. 12 + </p> 5 13 </div> 6 14 ); 7 15 }
+3 -2
apps/web/src/components/marketing/pricing/pricing-table.tsx
··· 114 114 </TableRow> 115 115 {features.map(({ label, value, badge }, _i) => { 116 116 return ( 117 - <TableRow key={key}> 117 + <TableRow key={key + label}> 118 118 <TableCell className="gap-1"> 119 119 {label}{" "} 120 120 {badge ? ( ··· 156 156 157 157 return ( 158 158 <TableCell 159 - key={key} 159 + // biome-ignore lint/suspicious/noArrayIndexKey: <explanation> 160 + key={key + value + _i} 160 161 className={cn( 161 162 "p-3", 162 163 plan.key === "team" && "bg-muted/30"
+4 -2
apps/web/src/config/pricing-table.ts
··· 1 - import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 1 + import type { LimitsV2 } from "@openstatus/db/src/schema/plan/schema"; 2 2 3 3 export const pricingTableConfig: Record< 4 4 string, 5 5 { 6 6 label: string; 7 - features: { value: keyof Limits; label: string; badge?: string }[]; 7 + features: { value: keyof LimitsV2; label: string; badge?: string }[]; 8 8 } 9 9 > = { 10 10 monitors: { ··· 23 23 label: "Multi-region monitoring", 24 24 }, 25 25 { value: "max-regions", label: "Number of Regions" }, 26 + 26 27 { value: "data-retention", label: "Data retention" }, 27 28 ], 28 29 }, ··· 33 34 value: "synthetic-checks", 34 35 label: "Number of synthetic API checks", 35 36 }, 37 + { value: "private-locations", label: "Private Locations" }, 36 38 ], 37 39 }, 38 40 "status-pages": {
+8 -7
packages/api/src/router/workspace.ts
··· 3 3 import * as randomWordSlugs from "random-word-slugs"; 4 4 import { z } from "zod"; 5 5 6 - import { and, eq, isNotNull, sql } from "@openstatus/db"; 6 + import { and, eq, isNotNull, isNull, sql } from "@openstatus/db"; 7 7 import { 8 8 application, 9 9 monitor, ··· 124 124 await opts.ctx.db.query.usersToWorkspaces.findFirst({ 125 125 where: and( 126 126 eq(usersToWorkspaces.userId, opts.ctx.user.id), 127 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) 127 + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 128 128 ), 129 129 }); 130 130 ··· 141 141 .where( 142 142 and( 143 143 eq(usersToWorkspaces.userId, opts.input.id), 144 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) 145 - ) 144 + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 145 + ), 146 146 ) 147 147 .run(); 148 148 }), ··· 154 154 await opts.ctx.db.query.usersToWorkspaces.findFirst({ 155 155 where: and( 156 156 eq(usersToWorkspaces.userId, opts.ctx.user.id), 157 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) 157 + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 158 158 ), 159 159 }); 160 160 ··· 198 198 }), 199 199 200 200 getCurrentWorkspaceNumbers: protectedProcedure.query(async (opts) => { 201 + console.log(opts.ctx.workspace.id); 201 202 const currentNumbers = await opts.ctx.db.transaction(async (tx) => { 202 203 const notifications = await tx 203 204 .select({ count: sql<number>`count(*)` }) ··· 209 210 .where( 210 211 and( 211 212 eq(monitor.workspaceId, opts.ctx.workspace.id), 212 - isNotNull(monitor.deletedAt) 213 - ) 213 + isNull(monitor.deletedAt), 214 + ), 214 215 ); 215 216 const pages = await tx 216 217 .select({ count: sql<number>`count(*)` })
+6 -2
packages/db/src/schema/plan/config.ts
··· 1 1 import type { WorkspacePlan } from "../workspaces/validation"; 2 - import type { Limits } from "./schema"; 2 + import type { Limits, LimitsV1, LimitsV2 } from "./schema"; 3 3 4 4 // TODO: rename to `planConfig` 5 5 export const allPlans: Record< ··· 8 8 title: "Hobby" | "Starter" | "Growth" | "Pro"; 9 9 description: string; 10 10 price: number; 11 - limits: Limits; 11 + limits: Limits & { "private-locations": boolean }; 12 12 } 13 13 > = { 14 14 free: { ··· 35 35 members: 1, 36 36 "audit-log": false, 37 37 regions: ["ams", "gru", "iad", "jnb", "hkg", "syd"], 38 + "private-locations": false, 38 39 }, 39 40 }, 40 41 starter: { ··· 97 98 "yul", 98 99 "yyz", 99 100 ], 101 + "private-locations": false, 100 102 }, 101 103 }, 102 104 team: { ··· 159 161 "yul", 160 162 "yyz", 161 163 ], 164 + "private-locations": false, 162 165 }, 163 166 }, 164 167 pro: { ··· 221 224 "yul", 222 225 "yyz", 223 226 ], 227 + "private-locations": true, 224 228 }, 225 229 }, 226 230 };
+22 -6
packages/db/src/schema/plan/schema.ts
··· 29 29 regions: monitorFlyRegionSchema.array(), 30 30 }); 31 31 32 + export type LimitsV1 = z.infer<typeof limitsV1>; 33 + export const limitsV2 = limitsV1.extend({ 34 + version: z.literal("v2"), 35 + "private-locations": z.boolean(), 36 + }); 37 + 38 + export type LimitsV2 = z.infer<typeof limitsV2>; 39 + 32 40 const unknownLimit = z.discriminatedUnion("version", [limitsV1]); 33 41 34 - // export const limitSchema = unknownLimit.transform((val) => { 35 - // if (!val.version) { 36 - // return migrateFromV1ToV2(val); 37 - // } 38 - // return val; 39 - // }); 42 + export function migrateFromV1ToV2({ data }: { data: LimitsV1 }) { 43 + return { 44 + version: "v2", 45 + ...data, 46 + "private-locations": true, 47 + }; 48 + } 49 + 50 + export const limitSchema = unknownLimit.transform((val) => { 51 + if (!val.version) { 52 + return migrateFromV1ToV2({ data: val }); 53 + } 54 + return val; 55 + }); 40 56 41 57 export type Limits = z.infer<typeof unknownLimit>;