Openstatus www.openstatus.dev

feat: monitor tags (#721)

* feat: monitor tags

* fix: style

* fix: facets select

* feat: add changelog

* fix: trpc

authored by

Maximilian Kaske and committed by
GitHub
877a1709 ea28cc76

+2735 -117
apps/web/public/assets/changelog/monitor-tags.png

This is a binary file and will not be displayed.

+7 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/(overview)/loading.tsx
··· 1 1 import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; 2 + import { DataTableToolbarSkeleton } from "@/components/data-table/data-table-toolbar-skeleton"; 2 3 3 4 export default function Loading() { 4 - return <DataTableSkeleton />; 5 + return ( 6 + <div className="space-y-4"> 7 + <DataTableToolbarSkeleton /> 8 + <DataTableSkeleton /> 9 + </div> 10 + ); 5 11 }
+7 -2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/(overview)/page.tsx
··· 34 34 ); 35 35 36 36 const _incidents = await api.incident.getIncidentsByWorkspace.query(); 37 + const tags = await api.monitorTag.getMonitorTagsByWorkspace.query(); 37 38 38 39 // maybe not very efficient? 39 40 // use Suspense and Client call instead? ··· 57 58 (incident) => incident.monitorId === monitor.id, 58 59 ); 59 60 60 - return { monitor, metrics: current, data, incidents }; 61 + const tags = monitor.monitorTagsToMonitors.map( 62 + ({ monitorTag }) => monitorTag, 63 + ); 64 + 65 + return { monitor, metrics: current, data, incidents, tags }; 61 66 }), 62 67 ); 63 68 ··· 69 74 70 75 return ( 71 76 <> 72 - <DataTable columns={columns} data={monitorsWithData} /> 77 + <DataTable columns={columns} data={monitorsWithData} tags={tags} /> 73 78 {isLimitReached ? <Limit /> : null} 74 79 {/* <RefreshWidget defaultValue={lastCronTimestamp} /> */} 75 80 </>
+8
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/edit/page.tsx
··· 26 26 27 27 const pages = await api.page.getPagesByWorkspace.query(); 28 28 29 + const tags = await api.monitorTag.getMonitorTagsByWorkspace.query(); 30 + 29 31 // default is request 30 32 const search = searchParamsSchema.safeParse(searchParams); 31 33 ··· 40 42 ) 41 43 .map(({ id }) => id), 42 44 notifications: monitorNotifications?.map(({ id }) => id), 45 + tags: tags 46 + .filter((tag) => 47 + tag.monitor.map(({ monitorId }) => monitorId).includes(id), 48 + ) 49 + .map(({ id }) => id), 43 50 }} 44 51 plan={workspace?.plan} 45 52 notifications={notifications} 53 + tags={tags} 46 54 pages={pages} 47 55 /> 48 56 );
+2 -2
apps/web/src/components/data-table/data-table-faceted-filter.tsx
··· 118 118 <option.icon className="text-muted-foreground mr-2 h-4 w-4" /> 119 119 )} 120 120 <span>{option.label}</span> 121 - {facets?.get(option.value) && ( 121 + {facets?.get(option.value) ? ( 122 122 <span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs"> 123 123 {facets.get(option.value)} 124 124 </span> 125 - )} 125 + ) : null} 126 126 </CommandItem> 127 127 ); 128 128 })}
+11
apps/web/src/components/data-table/data-table-toolbar-skeleton.tsx
··· 1 + import { Skeleton } from "@openstatus/ui"; 2 + 3 + export function DataTableToolbarSkeleton() { 4 + return ( 5 + <div className="flex flex-wrap items-center justify-between gap-3"> 6 + <div className="flex flex-1 items-center gap-2"> 7 + <Skeleton className="h-8 w-[150px] lg:w-[250px]" /> 8 + </div> 9 + </div> 10 + ); 11 + }
+45 -1
apps/web/src/components/data-table/monitor/columns.tsx
··· 4 4 import type { ColumnDef } from "@tanstack/react-table"; 5 5 import { formatDistanceToNowStrict } from "date-fns"; 6 6 7 - import type { Incident, Monitor } from "@openstatus/db/src/schema"; 7 + import type { Incident, Monitor, MonitorTag } from "@openstatus/db/src/schema"; 8 8 import type { 9 9 Monitor as MonitorTracker, 10 10 ResponseTimeMetrics, 11 11 } from "@openstatus/tinybird"; 12 12 import { Tracker } from "@openstatus/tracker"; 13 13 import { 14 + Badge, 14 15 Tooltip, 15 16 TooltipContent, 16 17 TooltipProvider, ··· 18 19 } from "@openstatus/ui"; 19 20 20 21 import { StatusDotWithTooltip } from "@/components/monitor/status-dot-with-tooltip"; 22 + import { TagBadge } from "@/components/monitor/tag-badge"; 21 23 import { Bar } from "@/components/tracker/tracker"; 22 24 import { DataTableRowActions } from "./data-table-row-actions"; 23 25 ··· 26 28 metrics?: ResponseTimeMetrics; 27 29 data?: MonitorTracker[]; 28 30 incidents?: Incident[]; 31 + tags?: MonitorTag[]; 29 32 }>[] = [ 30 33 { 31 34 accessorKey: "name", 35 + accessorFn: (row) => row.monitor.name, // used for filtering as name is nested within the monitor object 32 36 header: "Name", 33 37 cell: ({ row }) => { 34 38 const { active, status, name } = row.original.monitor; ··· 40 44 <StatusDotWithTooltip active={active} status={status} /> 41 45 <span className="truncate group-hover:underline">{name}</span> 42 46 </Link> 47 + ); 48 + }, 49 + }, 50 + { 51 + accessorKey: "tags", 52 + header: "Tags", 53 + cell: ({ row }) => { 54 + const { tags } = row.original; 55 + const [first, second, ...rest] = tags || []; 56 + return ( 57 + <div className="flex gap-2"> 58 + {first ? <TagBadge {...first} /> : null} 59 + {second ? <TagBadge {...second} /> : null} 60 + {rest.length > 0 ? <TagsTooltip tags={rest || []} /> : null} 61 + </div> 62 + ); 63 + }, 64 + filterFn: (row, id, value) => { 65 + if (!Array.isArray(value)) return true; 66 + // REMINDER: if one value is found, return true 67 + // we could consider restricting it to all the values have to be found 68 + return value.some( 69 + (item) => row.original.tags?.some((tag) => tag.name === item), 43 70 ); 44 71 }, 45 72 }, ··· 126 153 }, 127 154 }, 128 155 ]; 156 + 157 + function TagsTooltip({ tags }: { tags: MonitorTag[] }) { 158 + return ( 159 + <TooltipProvider> 160 + <Tooltip delayDuration={200}> 161 + <TooltipTrigger> 162 + <Badge variant="secondary">+{tags.length}</Badge> 163 + </TooltipTrigger> 164 + <TooltipContent side="top" className="flex gap-2"> 165 + {tags.map((tag) => ( 166 + <TagBadge key={tag.id} {...tag} /> 167 + ))} 168 + </TooltipContent> 169 + </Tooltip> 170 + </TooltipProvider> 171 + ); 172 + } 129 173 130 174 function HeaderTooltip({ label, content }: { label: string; content: string }) { 131 175 return (
+2 -3
apps/web/src/components/data-table/monitor/data-table-row-actions.tsx
··· 64 64 try { 65 65 const { jobType, ...rest } = monitor; 66 66 if (!monitor.id) return; 67 - await api.monitor.update.mutate({ 68 - ...rest, 69 - active: !monitor.active, 67 + await api.monitor.toggleMonitorActive.mutate({ 68 + id: monitor.id, 70 69 }); 71 70 toastAction("success"); 72 71 router.refresh();
+56
apps/web/src/components/data-table/monitor/data-table-toolbar.tsx
··· 1 + "use client"; 2 + 3 + import type { Table } from "@tanstack/react-table"; 4 + import { X } from "lucide-react"; 5 + 6 + import type { MonitorTag } from "@openstatus/db/src/schema"; 7 + import { Button, Input } from "@openstatus/ui"; 8 + 9 + import { DataTableFacetedFilter } from "../data-table-faceted-filter"; 10 + 11 + interface DataTableToolbarProps<TData> { 12 + table: Table<TData>; 13 + tags?: MonitorTag[]; 14 + } 15 + 16 + export function DataTableToolbar<TData>({ 17 + table, 18 + tags, 19 + }: DataTableToolbarProps<TData>) { 20 + const isFiltered = table.getState().columnFilters.length > 0; 21 + 22 + return ( 23 + <div className="flex flex-wrap items-center justify-between gap-3"> 24 + <div className="flex flex-1 items-center gap-2"> 25 + <Input 26 + placeholder="Filter names..." 27 + value={(table.getColumn("name")?.getFilterValue() as string) ?? ""} 28 + onChange={(event) => 29 + table.getColumn("name")?.setFilterValue(event.target.value) 30 + } 31 + className="h-8 w-[150px] lg:w-[250px]" 32 + /> 33 + {table.getColumn("tags") && tags && ( 34 + <DataTableFacetedFilter 35 + column={table.getColumn("tags")} 36 + title="Tags" 37 + options={tags?.map((tag) => ({ 38 + label: tag.name, 39 + value: tag.name, 40 + }))} 41 + /> 42 + )} 43 + {isFiltered && ( 44 + <Button 45 + variant="ghost" 46 + onClick={() => table.resetColumnFilters()} 47 + className="h-8 px-2 lg:px-3" 48 + > 49 + Reset 50 + <X className="ml-2 h-4 w-4" /> 51 + </Button> 52 + )} 53 + </div> 54 + </div> 55 + ); 56 + }
+94 -46
apps/web/src/components/data-table/monitor/data-table.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 - import type { ColumnDef } from "@tanstack/react-table"; 4 + import type { 5 + ColumnDef, 6 + ColumnFiltersState, 7 + Table as TTable, 8 + } from "@tanstack/react-table"; 5 9 import { 6 10 flexRender, 7 11 getCoreRowModel, 12 + getFacetedRowModel, 13 + getFilteredRowModel, 8 14 useReactTable, 9 15 } from "@tanstack/react-table"; 16 + import { z } from "zod"; 10 17 18 + import { selectMonitorTagSchema } from "@openstatus/db/src/schema"; 19 + import type { MonitorTag } from "@openstatus/db/src/schema"; 11 20 import { 12 21 Table, 13 22 TableBody, ··· 17 26 TableRow, 18 27 } from "@openstatus/ui"; 19 28 29 + import { DataTableToolbar } from "./data-table-toolbar"; 30 + 20 31 interface DataTableProps<TData, TValue> { 21 32 columns: ColumnDef<TData, TValue>[]; 22 33 data: TData[]; 34 + tags?: MonitorTag[]; 23 35 } 24 36 25 - // FIXME: right now, the mobile layout is messed up 26 - // https://github.com/TanStack/table/discussions/3192#discussioncomment-6458134 27 - 28 37 export function DataTable<TData, TValue>({ 29 38 columns, 30 39 data, 40 + tags, 31 41 }: DataTableProps<TData, TValue>) { 42 + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( 43 + [], 44 + ); 32 45 const table = useReactTable({ 33 46 data, 34 47 columns, 48 + state: { 49 + columnFilters, 50 + }, 51 + onColumnFiltersChange: setColumnFilters, 52 + getFilteredRowModel: getFilteredRowModel(), 35 53 getCoreRowModel: getCoreRowModel(), 54 + getFacetedRowModel: getFacetedRowModel(), 55 + // getFacetedUniqueValues: getFacetedUniqueValues(), 56 + // REMINDER: We cannot use the default getFacetedUniqueValues as it doesnt support Array of Objects 57 + getFacetedUniqueValues: (table: TTable<TData>, columnId: string) => () => { 58 + const map = new Map(); 59 + if (columnId === "tags") { 60 + tags?.forEach((tag) => { 61 + const tagsNumber = data.reduce((prev, curr) => { 62 + const values = z 63 + .object({ tags: z.array(selectMonitorTagSchema) }) 64 + .safeParse(curr); 65 + if (!values.success) return prev; 66 + if (values.data.tags?.find((t) => t.name === tag.name)) 67 + return prev + 1; 68 + return prev; 69 + }, 0); 70 + map.set(tag.name, tagsNumber); 71 + }); 72 + } 73 + return map; 74 + }, 36 75 }); 37 76 38 77 return ( 39 - <div className="rounded-md border"> 40 - <Table> 41 - <TableHeader className="bg-muted/50"> 42 - {table.getHeaderGroups().map((headerGroup) => ( 43 - <TableRow key={headerGroup.id} className="hover:bg-transparent"> 44 - {headerGroup.headers.map((header) => { 45 - return ( 46 - <TableHead key={header.id}> 47 - {header.isPlaceholder 48 - ? null 49 - : flexRender( 50 - header.column.columnDef.header, 51 - header.getContext(), 52 - )} 53 - </TableHead> 54 - ); 55 - })} 56 - </TableRow> 57 - ))} 58 - </TableHeader> 59 - <TableBody> 60 - {table.getRowModel().rows?.length ? ( 61 - table.getRowModel().rows.map((row) => ( 62 - <TableRow 63 - key={row.id} 64 - data-state={row.getIsSelected() && "selected"} 65 - > 66 - {row.getVisibleCells().map((cell) => ( 67 - <TableCell key={cell.id}> 68 - {flexRender(cell.column.columnDef.cell, cell.getContext())} 69 - </TableCell> 70 - ))} 78 + <div className="space-y-4"> 79 + <DataTableToolbar table={table} tags={tags} /> 80 + <div className="rounded-md border"> 81 + <Table> 82 + <TableHeader className="bg-muted/50"> 83 + {table.getHeaderGroups().map((headerGroup) => ( 84 + <TableRow key={headerGroup.id} className="hover:bg-transparent"> 85 + {headerGroup.headers.map((header) => { 86 + return ( 87 + <TableHead key={header.id}> 88 + {header.isPlaceholder 89 + ? null 90 + : flexRender( 91 + header.column.columnDef.header, 92 + header.getContext(), 93 + )} 94 + </TableHead> 95 + ); 96 + })} 71 97 </TableRow> 72 - )) 73 - ) : ( 74 - <TableRow> 75 - <TableCell colSpan={columns.length} className="h-24 text-center"> 76 - No results. 77 - </TableCell> 78 - </TableRow> 79 - )} 80 - </TableBody> 81 - </Table> 98 + ))} 99 + </TableHeader> 100 + <TableBody> 101 + {table.getRowModel().rows?.length ? ( 102 + table.getRowModel().rows.map((row) => ( 103 + <TableRow 104 + key={row.id} 105 + data-state={row.getIsSelected() && "selected"} 106 + > 107 + {row.getVisibleCells().map((cell) => ( 108 + <TableCell key={cell.id}> 109 + {flexRender( 110 + cell.column.columnDef.cell, 111 + cell.getContext(), 112 + )} 113 + </TableCell> 114 + ))} 115 + </TableRow> 116 + )) 117 + ) : ( 118 + <TableRow> 119 + <TableCell 120 + colSpan={columns.length} 121 + className="h-24 text-center" 122 + > 123 + No results. 124 + </TableCell> 125 + </TableRow> 126 + )} 127 + </TableBody> 128 + </Table> 129 + </div> 82 130 </div> 83 131 ); 84 132 }
+97
apps/web/src/components/forms/monitor-tag/form.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { useRouter } from "next/navigation"; 5 + import { zodResolver } from "@hookform/resolvers/zod"; 6 + import { useForm } from "react-hook-form"; 7 + 8 + import { insertMonitorTagSchema } from "@openstatus/db/src/schema"; 9 + import type { InsertMonitorTag, MonitorTag } from "@openstatus/db/src/schema"; 10 + import { 11 + Form, 12 + FormControl, 13 + FormField, 14 + FormItem, 15 + FormLabel, 16 + FormMessage, 17 + Input, 18 + } from "@openstatus/ui"; 19 + 20 + import { toastAction } from "@/lib/toast"; 21 + import { api } from "@/trpc/client"; 22 + import { SaveButton } from "../shared/save-button"; 23 + 24 + interface Props { 25 + defaultValues?: MonitorTag; 26 + } 27 + 28 + export function MonitorTagForm({ defaultValues }: Props) { 29 + const form = useForm<InsertMonitorTag>({ 30 + resolver: zodResolver(insertMonitorTagSchema), 31 + defaultValues, 32 + }); 33 + const router = useRouter(); 34 + const [isPending, startTransition] = React.useTransition(); 35 + 36 + const onSubmit = ({ ...props }: InsertMonitorTag) => { 37 + startTransition(async () => { 38 + try { 39 + if (defaultValues) { 40 + await api.monitorTag.update.mutate({ ...props }); 41 + } else { 42 + await api.monitorTag.create.mutate({ ...props }); 43 + } 44 + router.refresh(); 45 + toastAction("saved"); 46 + } catch { 47 + toastAction("error"); 48 + } 49 + }); 50 + }; 51 + 52 + return ( 53 + <Form {...form}> 54 + <form 55 + onSubmit={async (e) => { 56 + e.preventDefault(); 57 + form.handleSubmit(onSubmit)(e); 58 + }} 59 + className="grid w-full gap-6" 60 + > 61 + <div className="flex gap-6"> 62 + <FormField 63 + control={form.control} 64 + name="name" 65 + render={({ field }) => ( 66 + <FormItem> 67 + <FormLabel>Name</FormLabel> 68 + <FormControl> 69 + <Input placeholder="API" {...field} /> 70 + </FormControl> 71 + <FormMessage /> 72 + </FormItem> 73 + )} 74 + /> 75 + <FormField 76 + control={form.control} 77 + name="color" 78 + render={({ field }) => ( 79 + <FormItem className="w-12"> 80 + <FormLabel>Color</FormLabel> 81 + <FormControl> 82 + <Input type="color" {...field} /> 83 + </FormControl> 84 + <FormMessage /> 85 + </FormItem> 86 + )} 87 + /> 88 + </div> 89 + <SaveButton 90 + isPending={isPending} 91 + isDirty={form.formState.isDirty} 92 + onSubmit={form.handleSubmit(onSubmit)} 93 + /> 94 + </form> 95 + </Form> 96 + ); 97 + }
+11 -7
apps/web/src/components/forms/monitor/form.tsx
··· 8 8 import type { 9 9 InsertMonitor, 10 10 MonitorFlyRegion, 11 + MonitorTag, 11 12 Notification, 12 13 Page, 13 14 WorkspacePlan, ··· 39 40 defaultValues?: InsertMonitor; 40 41 plan?: WorkspacePlan; 41 42 notifications?: Notification[]; 43 + tags?: MonitorTag[]; 42 44 pages?: Page[]; 43 45 nextUrl?: string; 44 46 } ··· 49 51 plan = "free", 50 52 notifications, 51 53 pages, 54 + tags, 52 55 nextUrl, 53 56 }: Props) { 54 57 const form = useForm<InsertMonitor>({ ··· 69 72 method: defaultValues?.method ?? "GET", 70 73 notifications: defaultValues?.notifications ?? [], 71 74 pages: defaultValues?.pages ?? [], 75 + tags: defaultValues?.tags ?? [], 72 76 }, 73 77 }); 74 78 const router = useRouter(); ··· 98 102 const onSubmit = ({ ...props }: InsertMonitor) => { 99 103 startTransition(async () => { 100 104 try { 101 - const pingResult = await pingEndpoint(); 102 - const isOk = pingResult?.status >= 200 && pingResult?.status < 300; 103 - if (!isOk) { 104 - setPingFailed(true); 105 - return; 106 - } 105 + // const pingResult = await pingEndpoint(); 106 + // const isOk = pingResult?.status >= 200 && pingResult?.status < 300; 107 + // if (!isOk) { 108 + // setPingFailed(true); 109 + // return; 110 + // } 107 111 await handleDataUpdateOrInsertion(props); 108 112 } catch { 109 113 toastAction("error"); ··· 140 144 onKeyDown={(e) => e.key === "Enter" && e.preventDefault()} 141 145 className="flex w-full flex-col gap-6" 142 146 > 143 - <General {...{ form, plan }} /> 147 + <General {...{ form, plan, tags }} /> 144 148 <Tabs 145 149 defaultValue={defaultSection} 146 150 className="w-full"
+66 -34
apps/web/src/components/forms/monitor/general.tsx
··· 3 3 import * as React from "react"; 4 4 import type { UseFormReturn } from "react-hook-form"; 5 5 6 - import type { InsertMonitor, WorkspacePlan } from "@openstatus/db/src/schema"; 6 + import type { 7 + InsertMonitor, 8 + MonitorTag, 9 + WorkspacePlan, 10 + } from "@openstatus/db/src/schema"; 7 11 import { 8 12 FormControl, 9 13 FormDescription, ··· 15 19 Switch, 16 20 } from "@openstatus/ui"; 17 21 22 + import { TagBadge } from "@/components/monitor/tag-badge"; 18 23 import { SectionHeader } from "../shared/section-header"; 24 + import { TagsMultiBox } from "./tags-multi-box"; 19 25 20 26 interface Props { 21 27 form: UseFormReturn<InsertMonitor>; 22 28 plan: WorkspacePlan; 29 + tags?: MonitorTag[]; 23 30 } 24 31 25 - export function General({ form }: Props) { 32 + export function General({ form, tags }: Props) { 33 + const watchTags = form.watch("tags"); 26 34 return ( 27 35 <div className="grid gap-4 sm:grid-cols-3 sm:gap-6"> 28 36 <SectionHeader 29 37 title="Basic Information" 30 38 description="Be able to find your monitor easily." 31 39 /> 32 - <div className="flex w-full gap-4 sm:col-span-2"> 33 - <FormField 34 - control={form.control} 35 - name="name" 36 - render={({ field }) => ( 37 - <FormItem className="w-full"> 38 - <FormLabel>Name</FormLabel> 39 - <FormControl> 40 - <Input placeholder="Documenso" {...field} /> 41 - </FormControl> 42 - <FormDescription>Displayed on the status page.</FormDescription> 43 - <FormMessage /> 44 - </FormItem> 45 - )} 46 - /> 47 - <FormField 48 - control={form.control} 49 - name="active" 50 - render={({ field }) => ( 51 - <FormItem className="flex flex-row items-center justify-between space-x-1 space-y-0"> 52 - <FormLabel>Active</FormLabel> 53 - <FormControl> 54 - <Switch 55 - checked={field.value || false} 56 - onCheckedChange={(value) => field.onChange(value)} 57 - /> 58 - </FormControl> 59 - <FormMessage /> 60 - </FormItem> 61 - )} 62 - /> 40 + <div className="flex w-full flex-col gap-4 sm:col-span-2"> 41 + <div className="flex w-full gap-4"> 42 + <FormField 43 + control={form.control} 44 + name="name" 45 + render={({ field }) => ( 46 + <FormItem className="w-full"> 47 + <FormLabel>Name</FormLabel> 48 + <FormControl> 49 + <Input placeholder="Documenso" {...field} /> 50 + </FormControl> 51 + <FormDescription>Displayed on the status page.</FormDescription> 52 + <FormMessage /> 53 + </FormItem> 54 + )} 55 + /> 56 + <FormField 57 + control={form.control} 58 + name="active" 59 + render={({ field }) => ( 60 + <FormItem className="flex flex-row items-center justify-between space-x-1 space-y-0"> 61 + <FormLabel>Active</FormLabel> 62 + <FormControl> 63 + <Switch 64 + checked={field.value || false} 65 + onCheckedChange={(value) => field.onChange(value)} 66 + /> 67 + </FormControl> 68 + <FormMessage /> 69 + </FormItem> 70 + )} 71 + /> 72 + </div> 73 + <div className="grid gap-4 sm:grid-cols-3"> 74 + <FormField 75 + control={form.control} 76 + name="tags" 77 + render={({ field }) => ( 78 + <FormItem className="sm:col-span-2"> 79 + <FormLabel>Tags</FormLabel> 80 + <FormControl> 81 + <TagsMultiBox 82 + tags={tags} 83 + onChange={field.onChange} 84 + values={field.value} 85 + // {...field} 86 + /> 87 + </FormControl> 88 + <FormDescription> 89 + Easily categorize your monitors. 90 + </FormDescription> 91 + <FormMessage /> 92 + </FormItem> 93 + )} 94 + /> 95 + </div> 63 96 </div> 64 - {/* TODO: add FancyBox with "Create Tag option" once `monitor_tabs` db table is set up */} 65 97 </div> 66 98 ); 67 99 }
+391
apps/web/src/components/forms/monitor/tags-multi-box.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { Check, ChevronsUpDown, Edit2 } from "lucide-react"; 5 + 6 + import type { MonitorTag } from "@openstatus/db/src/schema"; 7 + import { 8 + Accordion, 9 + AccordionContent, 10 + AccordionItem, 11 + AccordionTrigger, 12 + AlertDialog, 13 + AlertDialogAction, 14 + AlertDialogCancel, 15 + AlertDialogContent, 16 + AlertDialogDescription, 17 + AlertDialogFooter, 18 + AlertDialogHeader, 19 + AlertDialogTitle, 20 + AlertDialogTrigger, 21 + Button, 22 + Command, 23 + CommandGroup, 24 + CommandInput, 25 + CommandItem, 26 + CommandSeparator, 27 + Dialog, 28 + DialogContent, 29 + DialogDescription, 30 + DialogHeader, 31 + DialogTitle, 32 + Input, 33 + Label, 34 + Popover, 35 + PopoverContent, 36 + PopoverTrigger, 37 + } from "@openstatus/ui"; 38 + 39 + import { Icons } from "@/components/icons"; 40 + import { LoadingAnimation } from "@/components/loading-animation"; 41 + import { TagBadge } from "@/components/monitor/tag-badge"; 42 + import { toastAction } from "@/lib/toast"; 43 + import { cn } from "@/lib/utils"; 44 + import { api } from "@/trpc/client"; 45 + 46 + const colors = [ 47 + "#ff5c5c", // Red 48 + "#6fcf97", // Green 49 + "#70a1ff", // Blue 50 + "#ffb74d", // Orange 51 + "#b19cd9", // Violet 52 + "#7986cb", // Indigo 53 + "#64b5f6", // Turquoise 54 + "#ffee58", // Yellow 55 + "#f06292", // Pink 56 + "#ff77ff", // Fuchsia 57 + "#808080", // Gray 58 + ]; 59 + 60 + interface TagsMultiBoxProps { 61 + tags?: MonitorTag[]; 62 + values: number[]; // values from the form 63 + onChange: (values: number[]) => void; 64 + } 65 + 66 + export function TagsMultiBox({ 67 + tags: defaultTags, 68 + values, 69 + onChange, 70 + }: TagsMultiBoxProps) { 71 + const inputRef = React.useRef<HTMLInputElement>(null); 72 + const [tags, setTags] = React.useState<MonitorTag[]>(defaultTags || []); 73 + const [openCombobox, setOpenCombobox] = React.useState(false); 74 + const [openDialog, setOpenDialog] = React.useState(false); 75 + const [inputValue, setInputValue] = React.useState<string>(""); 76 + 77 + const create = async (name: string) => { 78 + try { 79 + const randomIndex = Math.floor(Math.random() * colors.length); 80 + const newTag = await api.monitorTag.create.mutate({ 81 + name: name.trim(), 82 + color: colors[randomIndex] || "#808080", // gray as default 83 + }); 84 + toastAction("saved"); 85 + setTags((prev) => [...prev, newTag]); 86 + // TODO: seems like the new value is not taken into account.... 87 + // That's mainly because we only update the id, and not the name! Same is for the update() function 88 + onChange([...values, newTag.id]); 89 + } catch { 90 + toastAction("error"); 91 + } 92 + }; 93 + 94 + const toggle = (item: MonitorTag) => { 95 + onChange( 96 + values?.includes(item.id) 97 + ? values.filter((v) => v !== item.id) 98 + : [...values, item.id], 99 + ); 100 + inputRef?.current?.focus(); 101 + }; 102 + 103 + const update = async (tag: MonitorTag) => { 104 + try { 105 + const updateTag = await api.monitorTag.update.mutate(tag); 106 + if (!updateTag) return; 107 + setTags((prev) => prev.map((f) => (f.id === tag.id ? updateTag : f))); 108 + toastAction("saved"); 109 + } catch { 110 + toastAction("error"); 111 + } 112 + }; 113 + 114 + const _delete = async (item: MonitorTag) => { 115 + try { 116 + await api.monitorTag.delete.mutate({ id: item.id }); 117 + setTags((prev) => prev.filter((f) => f.id !== item.id)); 118 + onChange(values?.filter((v) => v !== item.id)); 119 + toastAction("deleted"); 120 + } catch { 121 + toastAction("error"); 122 + } 123 + }; 124 + 125 + const onComboboxOpenChange = (value: boolean) => { 126 + inputRef.current?.blur(); // HACK: otherwise, would scroll automatically to the bottom of page 127 + setOpenCombobox(value); 128 + }; 129 + 130 + return ( 131 + <div className="w-full"> 132 + <Popover open={openCombobox} onOpenChange={onComboboxOpenChange}> 133 + <PopoverTrigger asChild> 134 + <Button 135 + variant="outline" 136 + role="combobox" 137 + aria-expanded={openCombobox} 138 + className="text-foreground h-auto w-full justify-between" 139 + > 140 + <span className="flex flex-wrap gap-2 truncate"> 141 + {values.length > 0 142 + ? values.map((id) => { 143 + const tag = tags.find((tag) => tag.id === id); 144 + return tag ? <TagBadge key={tag.id} {...tag} /> : null; 145 + }) 146 + : "Select tags"} 147 + </span> 148 + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> 149 + </Button> 150 + </PopoverTrigger> 151 + <PopoverContent className="w-full p-0"> 152 + <Command className="w-[var(--radix-popover-trigger-width)]" loop> 153 + <CommandInput 154 + ref={inputRef} 155 + placeholder="Search tag..." 156 + value={inputValue} 157 + onValueChange={setInputValue} 158 + /> 159 + <CommandGroup className="max-h-[145px] overflow-auto"> 160 + {tags.map((item) => { 161 + const isActive = values?.includes(item.id); 162 + return ( 163 + <CommandItem 164 + key={item.id} 165 + value={item.name} 166 + onSelect={() => toggle(item)} 167 + > 168 + <Check 169 + className={cn( 170 + "mr-2 h-4 w-4", 171 + isActive ? "opacity-100" : "opacity-0", 172 + )} 173 + /> 174 + <div className="flex-1">{item.name}</div> 175 + <div 176 + className="h-4 w-4 rounded-full" 177 + style={{ backgroundColor: item.color }} 178 + /> 179 + </CommandItem> 180 + ); 181 + })} 182 + <CommandItemCreate 183 + onSelect={() => create(inputValue)} 184 + {...{ inputValue, tags }} 185 + /> 186 + </CommandGroup> 187 + <CommandSeparator alwaysRender /> 188 + <CommandGroup> 189 + <CommandItem 190 + value={`:${inputValue}:`} // HACK: that way, the edit button will always be shown 191 + className="text-muted-foreground text-xs" 192 + onSelect={() => setOpenDialog(true)} 193 + > 194 + <div className={cn("mr-2 h-4 w-4")} /> 195 + <Edit2 className="mr-2 h-2.5 w-2.5" /> 196 + Edit tags 197 + </CommandItem> 198 + </CommandGroup> 199 + </Command> 200 + </PopoverContent> 201 + </Popover> 202 + <Dialog 203 + open={openDialog} 204 + onOpenChange={(open) => { 205 + if (!open) setOpenCombobox(true); 206 + setOpenDialog(open); 207 + }} 208 + > 209 + <DialogContent className="flex max-h-[90vh] flex-col"> 210 + <DialogHeader> 211 + <DialogTitle>Edit Tags</DialogTitle> 212 + <DialogDescription> 213 + Update or delete tags. Create a tag through the combobox. 214 + </DialogDescription> 215 + </DialogHeader> 216 + <div className="flex-1 overflow-scroll"> 217 + {tags.map((item) => { 218 + return ( 219 + <DialogListItem 220 + key={item.id} 221 + onDelete={() => _delete(item)} 222 + onSubmit={async (values) => { 223 + await update({ 224 + ...item, 225 + ...values, 226 + }); 227 + }} 228 + {...item} 229 + /> 230 + ); 231 + })} 232 + </div> 233 + </DialogContent> 234 + </Dialog> 235 + </div> 236 + ); 237 + } 238 + 239 + const CommandItemCreate = ({ 240 + inputValue, 241 + tags, 242 + onSelect, 243 + }: { 244 + inputValue: string; 245 + tags: MonitorTag[]; 246 + onSelect: () => Promise<void>; 247 + }) => { 248 + const [isPending, startTransition] = React.useTransition(); 249 + const hasNoTag = !tags 250 + .map(({ name }) => name.toLowerCase()) 251 + .includes(`${inputValue.toLowerCase()}`); 252 + 253 + const render = inputValue !== "" && hasNoTag; 254 + 255 + if (!render) return null; 256 + 257 + // BUG: whenever a space is appended, the Create-Button will not be shown. 258 + return ( 259 + <CommandItem 260 + key={`${inputValue}`} 261 + value={`${inputValue}`} 262 + className="text-muted-foreground text-xs" 263 + onSelect={() => { 264 + startTransition(async () => { 265 + await onSelect(); 266 + }); 267 + }} 268 + disabled={isPending} 269 + > 270 + <div className={cn("mr-2 h-4 w-4")} /> 271 + {isPending ? "Creating" : "Create"} new label &quot;{inputValue}&quot; 272 + </CommandItem> 273 + ); 274 + }; 275 + 276 + const DialogListItem = ({ 277 + id, 278 + name, 279 + color, 280 + onSubmit, 281 + onDelete, 282 + }: MonitorTag & { 283 + onSubmit: (values: { name: string; color: string }) => Promise<void>; 284 + onDelete: () => Promise<void>; 285 + }) => { 286 + const inputRef = React.useRef<HTMLInputElement>(null); 287 + const [accordionValue, setAccordionValue] = React.useState<string>(""); 288 + const [inputValue, setInputValue] = React.useState<string>(name); 289 + const [colorValue, setColorValue] = React.useState<string>(color); 290 + const [isPending, startTransition] = React.useTransition(); 291 + const disabled = name === inputValue && color === colorValue; 292 + 293 + React.useEffect(() => { 294 + if (accordionValue !== "") { 295 + inputRef.current?.focus(); 296 + } 297 + }, [accordionValue]); 298 + 299 + // const handleSubmit = () => {} 300 + // const handleDelete = () => {} 301 + 302 + return ( 303 + <Accordion 304 + key={id} 305 + type="single" // will never work as we have only one accordion for each tag 306 + collapsible 307 + value={accordionValue} 308 + onValueChange={setAccordionValue} 309 + > 310 + <AccordionItem value={`item-${id}`}> 311 + <AccordionTrigger className="w-full hover:no-underline"> 312 + <TagBadge color={color} name={name} /> 313 + </AccordionTrigger> 314 + <AccordionContent> 315 + {/* REMINDER: cannot nest form within form! HOTFIX: no form */} 316 + <div className="flex items-end gap-4 px-1"> 317 + <div className="grid w-full gap-3"> 318 + <Label htmlFor="name">Label name</Label> 319 + <Input 320 + ref={inputRef} 321 + id="name" 322 + value={inputValue} 323 + onChange={(e) => setInputValue(e.target.value)} 324 + className="h-8" 325 + /> 326 + </div> 327 + <div className="grid gap-3"> 328 + <Label htmlFor="color">Color</Label> 329 + <Input 330 + id="color" 331 + type="color" 332 + value={colorValue} 333 + onChange={(e) => setColorValue(e.target.value)} 334 + className="h-8 px-2 py-1" 335 + /> 336 + </div> 337 + {/* FIXME: shouldnt saves the monitor form */} 338 + <Button 339 + onClick={() => { 340 + startTransition(() => { 341 + onSubmit({ 342 + name: inputValue.trim(), 343 + color: colorValue, 344 + }); 345 + setAccordionValue(""); 346 + }); 347 + }} 348 + disabled={disabled || isPending} 349 + > 350 + {isPending ? <LoadingAnimation /> : "Save"} 351 + </Button> 352 + <div className="flex items-center gap-4"> 353 + <AlertDialog> 354 + <AlertDialogTrigger asChild> 355 + <Button 356 + size="icon" 357 + variant="destructive" 358 + disabled={isPending} 359 + > 360 + <Icons.trash className="h-4 w-4" /> 361 + </Button> 362 + </AlertDialogTrigger> 363 + <AlertDialogContent> 364 + <AlertDialogHeader> 365 + <AlertDialogTitle>Are you sure sure?</AlertDialogTitle> 366 + <AlertDialogDescription> 367 + You are about to delete the tag{" "} 368 + <TagBadge color={color} name={name} /> . 369 + </AlertDialogDescription> 370 + </AlertDialogHeader> 371 + <AlertDialogFooter> 372 + <AlertDialogCancel>Cancel</AlertDialogCancel> 373 + <AlertDialogAction 374 + onClick={() => { 375 + startTransition(async () => { 376 + await onDelete(); 377 + }); 378 + }} 379 + > 380 + {isPending ? <LoadingAnimation /> : "Delete"} 381 + </AlertDialogAction> 382 + </AlertDialogFooter> 383 + </AlertDialogContent> 384 + </AlertDialog> 385 + </div> 386 + </div> 387 + </AccordionContent> 388 + </AccordionItem> 389 + </Accordion> 390 + ); 391 + };
+18
apps/web/src/components/monitor/tag-badge.tsx
··· 1 + import { Badge } from "@openstatus/ui"; 2 + 3 + function getStyle(color: string) { 4 + return { 5 + borderColor: `${color}20`, 6 + backgroundColor: `${color}30`, 7 + color, 8 + }; 9 + } 10 + 11 + interface TagBadgeProps { 12 + name: string; 13 + color: string; 14 + } 15 + 16 + export function TagBadge({ color, name }: TagBadgeProps) { 17 + return <Badge style={getStyle(color)}>{name}</Badge>; 18 + }
+11
apps/web/src/content/changelog/monitor-tags.mdx
··· 1 + --- 2 + title: Monitor Tags 3 + description: Easily categorize your monitors with tags. 4 + image: /assets/changelog/monitor-tags.png 5 + publishedAt: 2024-03-20 6 + --- 7 + 8 + You can now create tags for your monitors to easily categorize them. Tags can be 9 + used to filter monitors on the dashboard and in the monitor list. 10 + 11 + Create, update or delete a tag via your monitor settings.
+2
packages/api/src/edge.ts
··· 3 3 import { integrationRouter } from "./router/integration"; 4 4 import { invitationRouter } from "./router/invitation"; 5 5 import { monitorRouter } from "./router/monitor"; 6 + import { monitorTagRouter } from "./router/monitorTag"; 6 7 import { notificationRouter } from "./router/notification"; 7 8 import { pageRouter } from "./router/page"; 8 9 import { pageSubscriberRouter } from "./router/pageSubscriber"; ··· 26 27 incident: incidentRouter, 27 28 pageSubscriber: pageSubscriberRouter, 28 29 tinybird: tinybirdRouter, 30 + monitorTag: monitorTagRouter, 29 31 });
+121 -14
packages/api/src/router/monitor.ts
··· 9 9 monitorPeriodicitySchema, 10 10 monitorStatusTable, 11 11 monitorsToPages, 12 + monitorTag, 13 + monitorTagsToMonitors, 12 14 notification, 13 15 notificationsToMonitors, 14 16 page, 15 17 selectMonitorSchema, 16 18 selectMonitorStatusSchema, 19 + selectMonitorTagSchema, 17 20 selectNotificationSchema, 18 21 } from "@openstatus/db/src/schema"; 19 22 import { allPlans } from "@openstatus/plans"; ··· 56 59 } 57 60 58 61 // FIXME: this is a hotfix 59 - const { regions, headers, notifications, id, pages, ...data } = 62 + const { regions, headers, notifications, id, pages, tags, ...data } = 60 63 opts.input; 61 64 62 65 const newMonitor = await opts.ctx.db ··· 73 76 .get(); 74 77 75 78 if (notifications.length > 0) { 76 - // We should make sure the user has access to the notifications 77 79 const allNotifications = await opts.ctx.db.query.notification.findMany({ 78 - where: inArray(notification.id, notifications), 80 + where: and( 81 + eq(notification.workspaceId, opts.ctx.workspace.id), 82 + inArray(notification.id, notifications), 83 + ), 79 84 }); 80 85 81 86 const values = allNotifications.map((notification) => ({ ··· 86 91 await opts.ctx.db.insert(notificationsToMonitors).values(values).run(); 87 92 } 88 93 94 + if (tags.length > 0) { 95 + const allTags = await opts.ctx.db.query.monitorTag.findMany({ 96 + where: and( 97 + eq(monitorTag.workspaceId, opts.ctx.workspace.id), 98 + inArray(monitorTag.id, tags), 99 + ), 100 + }); 101 + 102 + const values = allTags.map((monitorTag) => ({ 103 + monitorId: newMonitor.id, 104 + monitorTagId: monitorTag.id, 105 + })); 106 + 107 + await opts.ctx.db.insert(monitorTagsToMonitors).values(values).run(); 108 + } 109 + 89 110 if (pages.length > 0) { 90 - // We should make sure the user has access to the notifications 91 111 const allPages = await opts.ctx.db.query.page.findMany({ 92 - where: inArray(page.id, pages), 112 + where: and( 113 + eq(page.workspaceId, opts.ctx.workspace.id), 114 + inArray(page.id, pages), 115 + ), 93 116 }); 94 117 95 118 const values = allPages.map((page) => ({ ··· 126 149 const parsedMonitor = selectMonitorSchema.safeParse(currentMonitor); 127 150 128 151 if (!parsedMonitor.success) { 152 + console.log(parsedMonitor.error); 129 153 throw new TRPCError({ 130 154 code: "UNAUTHORIZED", 131 155 message: "You are not allowed to access the monitor.", 132 156 }); 133 157 } 134 - return selectMonitorSchema.parse(currentMonitor); 158 + return parsedMonitor.data; 135 159 }), 136 160 137 161 update: protectedProcedure ··· 153 177 }); 154 178 } 155 179 156 - const { regions, headers, notifications, pages, ...data } = opts.input; 180 + console.log(opts.input); 181 + 182 + const { regions, headers, notifications, pages, tags, ...data } = 183 + opts.input; 157 184 158 185 const currentMonitor = await opts.ctx.db 159 186 .update(monitor) ··· 213 240 .run(); 214 241 } 215 242 243 + const currentMonitorTags = await opts.ctx.db 244 + .select() 245 + .from(monitorTagsToMonitors) 246 + .where(eq(monitorTagsToMonitors.monitorId, currentMonitor.id)) 247 + .all(); 248 + 249 + const addedTags = tags.filter( 250 + (x) => 251 + !currentMonitorTags 252 + .map(({ monitorTagId }) => monitorTagId) 253 + ?.includes(x), 254 + ); 255 + 256 + if (addedTags.length > 0) { 257 + const values = addedTags.map((monitorTagId) => ({ 258 + monitorId: currentMonitor.id, 259 + monitorTagId, 260 + })); 261 + 262 + await opts.ctx.db.insert(monitorTagsToMonitors).values(values).run(); 263 + } 264 + 265 + const removedTags = currentMonitorTags 266 + .map(({ monitorTagId }) => monitorTagId) 267 + .filter((x) => !tags?.includes(x)); 268 + 269 + if (removedTags.length > 0) { 270 + await opts.ctx.db 271 + .delete(monitorTagsToMonitors) 272 + .where( 273 + and( 274 + eq(monitorTagsToMonitors.monitorId, currentMonitor.id), 275 + inArray(monitorTagsToMonitors.monitorTagId, removedTags), 276 + ), 277 + ) 278 + .run(); 279 + } 280 + 216 281 const currentMonitorPages = await opts.ctx.db 217 282 .select() 218 283 .from(monitorsToPages) ··· 270 335 .run(); 271 336 }), 272 337 273 - getMonitorsByWorkspace: protectedProcedure 274 - .output(z.array(selectMonitorSchema)) 275 - .query(async (opts) => { 276 - const monitors = await opts.ctx.db 338 + getMonitorsByWorkspace: protectedProcedure.query(async (opts) => { 339 + const monitors = await opts.ctx.db.query.monitor.findMany({ 340 + where: eq(monitor.workspaceId, opts.ctx.workspace.id), 341 + with: { 342 + monitorTagsToMonitors: { with: { monitorTag: true } }, 343 + }, 344 + }); 345 + 346 + return z 347 + .array( 348 + selectMonitorSchema.extend({ 349 + monitorTagsToMonitors: z 350 + .array(z.object({ monitorTag: selectMonitorTagSchema })) 351 + .default([]), 352 + }), 353 + ) 354 + .parse(monitors); 355 + }), 356 + 357 + toggleMonitorActive: protectedProcedure 358 + .input(z.object({ id: z.number() })) 359 + .mutation(async (opts) => { 360 + const monitorToUpdate = await opts.ctx.db 277 361 .select() 278 362 .from(monitor) 279 - .where(eq(monitor.workspaceId, opts.ctx.workspace.id)) 280 - .all(); 363 + .where( 364 + and( 365 + eq(monitor.id, opts.input.id), 366 + eq(monitor.workspaceId, opts.ctx.workspace.id), 367 + ), 368 + ) 369 + .get(); 370 + 371 + if (!monitorToUpdate) { 372 + throw new TRPCError({ 373 + code: "NOT_FOUND", 374 + message: "Monitor not found.", 375 + }); 376 + } 281 377 282 - return z.array(selectMonitorSchema).parse(monitors); 378 + await opts.ctx.db 379 + .update(monitor) 380 + .set({ 381 + active: !monitorToUpdate.active, 382 + }) 383 + .where( 384 + and( 385 + eq(monitor.id, opts.input.id), 386 + eq(monitor.workspaceId, opts.ctx.workspace.id), 387 + ), 388 + ) 389 + .run(); 283 390 }), 284 391 285 392 // rename to getActiveMonitorsCount
+61
packages/api/src/router/monitorTag.ts
··· 1 + import { TRPCError } from "@trpc/server"; 2 + import { z } from "zod"; 3 + 4 + import { and, eq } from "@openstatus/db"; 5 + import { insertMonitorTagSchema, monitorTag } from "@openstatus/db/src/schema"; 6 + 7 + import { createTRPCRouter, protectedProcedure } from "../trpc"; 8 + 9 + export const monitorTagRouter = createTRPCRouter({ 10 + getMonitorTagsByWorkspace: protectedProcedure.query(async (opts) => { 11 + return opts.ctx.db.query.monitorTag.findMany({ 12 + where: eq(monitorTag.workspaceId, opts.ctx.workspace.id), 13 + with: { monitor: true }, 14 + }); 15 + }), 16 + 17 + update: protectedProcedure 18 + .input(insertMonitorTagSchema) 19 + .mutation(async (opts) => { 20 + if (!opts.input.id) return; 21 + return await opts.ctx.db 22 + .update(monitorTag) 23 + .set({ name: opts.input.name, color: opts.input.color }) 24 + .where( 25 + and( 26 + eq(monitorTag.workspaceId, opts.ctx.workspace.id), 27 + eq(monitorTag.id, opts.input.id), 28 + ), 29 + ) 30 + .returning() 31 + .get(); 32 + }), 33 + 34 + delete: protectedProcedure 35 + .input(z.object({ id: z.number() })) 36 + .mutation(async (opts) => { 37 + await opts.ctx.db 38 + .delete(monitorTag) 39 + .where( 40 + and( 41 + eq(monitorTag.id, opts.input.id), 42 + eq(monitorTag.workspaceId, opts.ctx.workspace.id), 43 + ), 44 + ) 45 + .run(); 46 + }), 47 + 48 + create: protectedProcedure 49 + .input(z.object({ name: z.string(), color: z.string() })) 50 + .mutation(async (opts) => { 51 + return opts.ctx.db 52 + .insert(monitorTag) 53 + .values({ 54 + name: opts.input.name, 55 + color: opts.input.color, 56 + workspaceId: opts.ctx.workspace.id, 57 + }) 58 + .returning() 59 + .get(); 60 + }), 61 + });
+18
packages/db/drizzle/0021_reflective_nico_minoru.sql
··· 1 + CREATE TABLE `monitor_tag` ( 2 + `id` integer PRIMARY KEY NOT NULL, 3 + `workspace_id` integer NOT NULL, 4 + `name` text NOT NULL, 5 + `color` text NOT NULL, 6 + `created_at` integer DEFAULT (strftime('%s', 'now')), 7 + `updated_at` integer DEFAULT (strftime('%s', 'now')), 8 + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade 9 + ); 10 + --> statement-breakpoint 11 + CREATE TABLE `monitor_tag_to_monitor` ( 12 + `monitor_id` integer NOT NULL, 13 + `monitor_tag_id` integer NOT NULL, 14 + `created_at` integer DEFAULT (strftime('%s', 'now')), 15 + PRIMARY KEY(`monitor_id`, `monitor_tag_id`), 16 + FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade, 17 + FOREIGN KEY (`monitor_tag_id`) REFERENCES `monitor_tag`(`id`) ON UPDATE no action ON DELETE cascade 18 + );
+1605
packages/db/drizzle/meta/0021_snapshot.json
··· 1 + { 2 + "version": "5", 3 + "dialect": "sqlite", 4 + "id": "44c2bbb5-69d5-40e8-b301-f2569bc2e499", 5 + "prevId": "47000fd1-f273-4406-8380-803a0a63bc59", 6 + "tables": { 7 + "status_report_to_monitors": { 8 + "name": "status_report_to_monitors", 9 + "columns": { 10 + "monitor_id": { 11 + "name": "monitor_id", 12 + "type": "integer", 13 + "primaryKey": false, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "status_report_id": { 18 + "name": "status_report_id", 19 + "type": "integer", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "created_at": { 25 + "name": "created_at", 26 + "type": "integer", 27 + "primaryKey": false, 28 + "notNull": false, 29 + "autoincrement": false, 30 + "default": "(strftime('%s', 'now'))" 31 + } 32 + }, 33 + "indexes": {}, 34 + "foreignKeys": { 35 + "status_report_to_monitors_monitor_id_monitor_id_fk": { 36 + "name": "status_report_to_monitors_monitor_id_monitor_id_fk", 37 + "tableFrom": "status_report_to_monitors", 38 + "tableTo": "monitor", 39 + "columnsFrom": [ 40 + "monitor_id" 41 + ], 42 + "columnsTo": [ 43 + "id" 44 + ], 45 + "onDelete": "cascade", 46 + "onUpdate": "no action" 47 + }, 48 + "status_report_to_monitors_status_report_id_status_report_id_fk": { 49 + "name": "status_report_to_monitors_status_report_id_status_report_id_fk", 50 + "tableFrom": "status_report_to_monitors", 51 + "tableTo": "status_report", 52 + "columnsFrom": [ 53 + "status_report_id" 54 + ], 55 + "columnsTo": [ 56 + "id" 57 + ], 58 + "onDelete": "cascade", 59 + "onUpdate": "no action" 60 + } 61 + }, 62 + "compositePrimaryKeys": { 63 + "status_report_to_monitors_monitor_id_status_report_id_pk": { 64 + "columns": [ 65 + "monitor_id", 66 + "status_report_id" 67 + ], 68 + "name": "status_report_to_monitors_monitor_id_status_report_id_pk" 69 + } 70 + }, 71 + "uniqueConstraints": {} 72 + }, 73 + "status_reports_to_pages": { 74 + "name": "status_reports_to_pages", 75 + "columns": { 76 + "page_id": { 77 + "name": "page_id", 78 + "type": "integer", 79 + "primaryKey": false, 80 + "notNull": true, 81 + "autoincrement": false 82 + }, 83 + "status_report_id": { 84 + "name": "status_report_id", 85 + "type": "integer", 86 + "primaryKey": false, 87 + "notNull": true, 88 + "autoincrement": false 89 + }, 90 + "created_at": { 91 + "name": "created_at", 92 + "type": "integer", 93 + "primaryKey": false, 94 + "notNull": false, 95 + "autoincrement": false, 96 + "default": "(strftime('%s', 'now'))" 97 + } 98 + }, 99 + "indexes": {}, 100 + "foreignKeys": { 101 + "status_reports_to_pages_page_id_page_id_fk": { 102 + "name": "status_reports_to_pages_page_id_page_id_fk", 103 + "tableFrom": "status_reports_to_pages", 104 + "tableTo": "page", 105 + "columnsFrom": [ 106 + "page_id" 107 + ], 108 + "columnsTo": [ 109 + "id" 110 + ], 111 + "onDelete": "cascade", 112 + "onUpdate": "no action" 113 + }, 114 + "status_reports_to_pages_status_report_id_status_report_id_fk": { 115 + "name": "status_reports_to_pages_status_report_id_status_report_id_fk", 116 + "tableFrom": "status_reports_to_pages", 117 + "tableTo": "status_report", 118 + "columnsFrom": [ 119 + "status_report_id" 120 + ], 121 + "columnsTo": [ 122 + "id" 123 + ], 124 + "onDelete": "cascade", 125 + "onUpdate": "no action" 126 + } 127 + }, 128 + "compositePrimaryKeys": { 129 + "status_reports_to_pages_page_id_status_report_id_pk": { 130 + "columns": [ 131 + "page_id", 132 + "status_report_id" 133 + ], 134 + "name": "status_reports_to_pages_page_id_status_report_id_pk" 135 + } 136 + }, 137 + "uniqueConstraints": {} 138 + }, 139 + "status_report": { 140 + "name": "status_report", 141 + "columns": { 142 + "id": { 143 + "name": "id", 144 + "type": "integer", 145 + "primaryKey": true, 146 + "notNull": true, 147 + "autoincrement": false 148 + }, 149 + "status": { 150 + "name": "status", 151 + "type": "text", 152 + "primaryKey": false, 153 + "notNull": true, 154 + "autoincrement": false 155 + }, 156 + "title": { 157 + "name": "title", 158 + "type": "text(256)", 159 + "primaryKey": false, 160 + "notNull": true, 161 + "autoincrement": false 162 + }, 163 + "workspace_id": { 164 + "name": "workspace_id", 165 + "type": "integer", 166 + "primaryKey": false, 167 + "notNull": false, 168 + "autoincrement": false 169 + }, 170 + "created_at": { 171 + "name": "created_at", 172 + "type": "integer", 173 + "primaryKey": false, 174 + "notNull": false, 175 + "autoincrement": false, 176 + "default": "(strftime('%s', 'now'))" 177 + }, 178 + "updated_at": { 179 + "name": "updated_at", 180 + "type": "integer", 181 + "primaryKey": false, 182 + "notNull": false, 183 + "autoincrement": false, 184 + "default": "(strftime('%s', 'now'))" 185 + } 186 + }, 187 + "indexes": {}, 188 + "foreignKeys": { 189 + "status_report_workspace_id_workspace_id_fk": { 190 + "name": "status_report_workspace_id_workspace_id_fk", 191 + "tableFrom": "status_report", 192 + "tableTo": "workspace", 193 + "columnsFrom": [ 194 + "workspace_id" 195 + ], 196 + "columnsTo": [ 197 + "id" 198 + ], 199 + "onDelete": "no action", 200 + "onUpdate": "no action" 201 + } 202 + }, 203 + "compositePrimaryKeys": {}, 204 + "uniqueConstraints": {} 205 + }, 206 + "status_report_update": { 207 + "name": "status_report_update", 208 + "columns": { 209 + "id": { 210 + "name": "id", 211 + "type": "integer", 212 + "primaryKey": true, 213 + "notNull": true, 214 + "autoincrement": false 215 + }, 216 + "status": { 217 + "name": "status", 218 + "type": "text(4)", 219 + "primaryKey": false, 220 + "notNull": true, 221 + "autoincrement": false 222 + }, 223 + "date": { 224 + "name": "date", 225 + "type": "integer", 226 + "primaryKey": false, 227 + "notNull": true, 228 + "autoincrement": false 229 + }, 230 + "message": { 231 + "name": "message", 232 + "type": "text", 233 + "primaryKey": false, 234 + "notNull": true, 235 + "autoincrement": false 236 + }, 237 + "status_report_id": { 238 + "name": "status_report_id", 239 + "type": "integer", 240 + "primaryKey": false, 241 + "notNull": true, 242 + "autoincrement": false 243 + }, 244 + "created_at": { 245 + "name": "created_at", 246 + "type": "integer", 247 + "primaryKey": false, 248 + "notNull": false, 249 + "autoincrement": false, 250 + "default": "(strftime('%s', 'now'))" 251 + }, 252 + "updated_at": { 253 + "name": "updated_at", 254 + "type": "integer", 255 + "primaryKey": false, 256 + "notNull": false, 257 + "autoincrement": false, 258 + "default": "(strftime('%s', 'now'))" 259 + } 260 + }, 261 + "indexes": {}, 262 + "foreignKeys": { 263 + "status_report_update_status_report_id_status_report_id_fk": { 264 + "name": "status_report_update_status_report_id_status_report_id_fk", 265 + "tableFrom": "status_report_update", 266 + "tableTo": "status_report", 267 + "columnsFrom": [ 268 + "status_report_id" 269 + ], 270 + "columnsTo": [ 271 + "id" 272 + ], 273 + "onDelete": "cascade", 274 + "onUpdate": "no action" 275 + } 276 + }, 277 + "compositePrimaryKeys": {}, 278 + "uniqueConstraints": {} 279 + }, 280 + "integration": { 281 + "name": "integration", 282 + "columns": { 283 + "id": { 284 + "name": "id", 285 + "type": "integer", 286 + "primaryKey": true, 287 + "notNull": true, 288 + "autoincrement": false 289 + }, 290 + "name": { 291 + "name": "name", 292 + "type": "text(256)", 293 + "primaryKey": false, 294 + "notNull": true, 295 + "autoincrement": false 296 + }, 297 + "workspace_id": { 298 + "name": "workspace_id", 299 + "type": "integer", 300 + "primaryKey": false, 301 + "notNull": false, 302 + "autoincrement": false 303 + }, 304 + "credential": { 305 + "name": "credential", 306 + "type": "text", 307 + "primaryKey": false, 308 + "notNull": false, 309 + "autoincrement": false 310 + }, 311 + "external_id": { 312 + "name": "external_id", 313 + "type": "text", 314 + "primaryKey": false, 315 + "notNull": true, 316 + "autoincrement": false 317 + }, 318 + "created_at": { 319 + "name": "created_at", 320 + "type": "integer", 321 + "primaryKey": false, 322 + "notNull": false, 323 + "autoincrement": false, 324 + "default": "(strftime('%s', 'now'))" 325 + }, 326 + "updated_at": { 327 + "name": "updated_at", 328 + "type": "integer", 329 + "primaryKey": false, 330 + "notNull": false, 331 + "autoincrement": false, 332 + "default": "(strftime('%s', 'now'))" 333 + }, 334 + "data": { 335 + "name": "data", 336 + "type": "text", 337 + "primaryKey": false, 338 + "notNull": true, 339 + "autoincrement": false 340 + } 341 + }, 342 + "indexes": {}, 343 + "foreignKeys": { 344 + "integration_workspace_id_workspace_id_fk": { 345 + "name": "integration_workspace_id_workspace_id_fk", 346 + "tableFrom": "integration", 347 + "tableTo": "workspace", 348 + "columnsFrom": [ 349 + "workspace_id" 350 + ], 351 + "columnsTo": [ 352 + "id" 353 + ], 354 + "onDelete": "no action", 355 + "onUpdate": "no action" 356 + } 357 + }, 358 + "compositePrimaryKeys": {}, 359 + "uniqueConstraints": {} 360 + }, 361 + "page": { 362 + "name": "page", 363 + "columns": { 364 + "id": { 365 + "name": "id", 366 + "type": "integer", 367 + "primaryKey": true, 368 + "notNull": true, 369 + "autoincrement": false 370 + }, 371 + "workspace_id": { 372 + "name": "workspace_id", 373 + "type": "integer", 374 + "primaryKey": false, 375 + "notNull": true, 376 + "autoincrement": false 377 + }, 378 + "title": { 379 + "name": "title", 380 + "type": "text", 381 + "primaryKey": false, 382 + "notNull": true, 383 + "autoincrement": false 384 + }, 385 + "description": { 386 + "name": "description", 387 + "type": "text", 388 + "primaryKey": false, 389 + "notNull": true, 390 + "autoincrement": false 391 + }, 392 + "icon": { 393 + "name": "icon", 394 + "type": "text(256)", 395 + "primaryKey": false, 396 + "notNull": false, 397 + "autoincrement": false, 398 + "default": "''" 399 + }, 400 + "slug": { 401 + "name": "slug", 402 + "type": "text(256)", 403 + "primaryKey": false, 404 + "notNull": true, 405 + "autoincrement": false 406 + }, 407 + "custom_domain": { 408 + "name": "custom_domain", 409 + "type": "text(256)", 410 + "primaryKey": false, 411 + "notNull": true, 412 + "autoincrement": false 413 + }, 414 + "published": { 415 + "name": "published", 416 + "type": "integer", 417 + "primaryKey": false, 418 + "notNull": false, 419 + "autoincrement": false, 420 + "default": false 421 + }, 422 + "created_at": { 423 + "name": "created_at", 424 + "type": "integer", 425 + "primaryKey": false, 426 + "notNull": false, 427 + "autoincrement": false, 428 + "default": "(strftime('%s', 'now'))" 429 + }, 430 + "updated_at": { 431 + "name": "updated_at", 432 + "type": "integer", 433 + "primaryKey": false, 434 + "notNull": false, 435 + "autoincrement": false, 436 + "default": "(strftime('%s', 'now'))" 437 + } 438 + }, 439 + "indexes": { 440 + "page_slug_unique": { 441 + "name": "page_slug_unique", 442 + "columns": [ 443 + "slug" 444 + ], 445 + "isUnique": true 446 + } 447 + }, 448 + "foreignKeys": { 449 + "page_workspace_id_workspace_id_fk": { 450 + "name": "page_workspace_id_workspace_id_fk", 451 + "tableFrom": "page", 452 + "tableTo": "workspace", 453 + "columnsFrom": [ 454 + "workspace_id" 455 + ], 456 + "columnsTo": [ 457 + "id" 458 + ], 459 + "onDelete": "cascade", 460 + "onUpdate": "no action" 461 + } 462 + }, 463 + "compositePrimaryKeys": {}, 464 + "uniqueConstraints": {} 465 + }, 466 + "monitor": { 467 + "name": "monitor", 468 + "columns": { 469 + "id": { 470 + "name": "id", 471 + "type": "integer", 472 + "primaryKey": true, 473 + "notNull": true, 474 + "autoincrement": false 475 + }, 476 + "job_type": { 477 + "name": "job_type", 478 + "type": "text", 479 + "primaryKey": false, 480 + "notNull": true, 481 + "autoincrement": false, 482 + "default": "'other'" 483 + }, 484 + "periodicity": { 485 + "name": "periodicity", 486 + "type": "text", 487 + "primaryKey": false, 488 + "notNull": true, 489 + "autoincrement": false, 490 + "default": "'other'" 491 + }, 492 + "status": { 493 + "name": "status", 494 + "type": "text", 495 + "primaryKey": false, 496 + "notNull": true, 497 + "autoincrement": false, 498 + "default": "'active'" 499 + }, 500 + "active": { 501 + "name": "active", 502 + "type": "integer", 503 + "primaryKey": false, 504 + "notNull": false, 505 + "autoincrement": false, 506 + "default": false 507 + }, 508 + "regions": { 509 + "name": "regions", 510 + "type": "text", 511 + "primaryKey": false, 512 + "notNull": true, 513 + "autoincrement": false, 514 + "default": "''" 515 + }, 516 + "url": { 517 + "name": "url", 518 + "type": "text(2048)", 519 + "primaryKey": false, 520 + "notNull": true, 521 + "autoincrement": false 522 + }, 523 + "name": { 524 + "name": "name", 525 + "type": "text(256)", 526 + "primaryKey": false, 527 + "notNull": true, 528 + "autoincrement": false, 529 + "default": "''" 530 + }, 531 + "description": { 532 + "name": "description", 533 + "type": "text", 534 + "primaryKey": false, 535 + "notNull": true, 536 + "autoincrement": false, 537 + "default": "''" 538 + }, 539 + "headers": { 540 + "name": "headers", 541 + "type": "text", 542 + "primaryKey": false, 543 + "notNull": false, 544 + "autoincrement": false, 545 + "default": "''" 546 + }, 547 + "body": { 548 + "name": "body", 549 + "type": "text", 550 + "primaryKey": false, 551 + "notNull": false, 552 + "autoincrement": false, 553 + "default": "''" 554 + }, 555 + "method": { 556 + "name": "method", 557 + "type": "text", 558 + "primaryKey": false, 559 + "notNull": false, 560 + "autoincrement": false, 561 + "default": "'GET'" 562 + }, 563 + "workspace_id": { 564 + "name": "workspace_id", 565 + "type": "integer", 566 + "primaryKey": false, 567 + "notNull": false, 568 + "autoincrement": false 569 + }, 570 + "created_at": { 571 + "name": "created_at", 572 + "type": "integer", 573 + "primaryKey": false, 574 + "notNull": false, 575 + "autoincrement": false, 576 + "default": "(strftime('%s', 'now'))" 577 + }, 578 + "updated_at": { 579 + "name": "updated_at", 580 + "type": "integer", 581 + "primaryKey": false, 582 + "notNull": false, 583 + "autoincrement": false, 584 + "default": "(strftime('%s', 'now'))" 585 + } 586 + }, 587 + "indexes": {}, 588 + "foreignKeys": { 589 + "monitor_workspace_id_workspace_id_fk": { 590 + "name": "monitor_workspace_id_workspace_id_fk", 591 + "tableFrom": "monitor", 592 + "tableTo": "workspace", 593 + "columnsFrom": [ 594 + "workspace_id" 595 + ], 596 + "columnsTo": [ 597 + "id" 598 + ], 599 + "onDelete": "no action", 600 + "onUpdate": "no action" 601 + } 602 + }, 603 + "compositePrimaryKeys": {}, 604 + "uniqueConstraints": {} 605 + }, 606 + "monitors_to_pages": { 607 + "name": "monitors_to_pages", 608 + "columns": { 609 + "monitor_id": { 610 + "name": "monitor_id", 611 + "type": "integer", 612 + "primaryKey": false, 613 + "notNull": true, 614 + "autoincrement": false 615 + }, 616 + "page_id": { 617 + "name": "page_id", 618 + "type": "integer", 619 + "primaryKey": false, 620 + "notNull": true, 621 + "autoincrement": false 622 + }, 623 + "created_at": { 624 + "name": "created_at", 625 + "type": "integer", 626 + "primaryKey": false, 627 + "notNull": false, 628 + "autoincrement": false, 629 + "default": "(strftime('%s', 'now'))" 630 + } 631 + }, 632 + "indexes": {}, 633 + "foreignKeys": { 634 + "monitors_to_pages_monitor_id_monitor_id_fk": { 635 + "name": "monitors_to_pages_monitor_id_monitor_id_fk", 636 + "tableFrom": "monitors_to_pages", 637 + "tableTo": "monitor", 638 + "columnsFrom": [ 639 + "monitor_id" 640 + ], 641 + "columnsTo": [ 642 + "id" 643 + ], 644 + "onDelete": "cascade", 645 + "onUpdate": "no action" 646 + }, 647 + "monitors_to_pages_page_id_page_id_fk": { 648 + "name": "monitors_to_pages_page_id_page_id_fk", 649 + "tableFrom": "monitors_to_pages", 650 + "tableTo": "page", 651 + "columnsFrom": [ 652 + "page_id" 653 + ], 654 + "columnsTo": [ 655 + "id" 656 + ], 657 + "onDelete": "cascade", 658 + "onUpdate": "no action" 659 + } 660 + }, 661 + "compositePrimaryKeys": { 662 + "monitors_to_pages_monitor_id_page_id_pk": { 663 + "columns": [ 664 + "monitor_id", 665 + "page_id" 666 + ], 667 + "name": "monitors_to_pages_monitor_id_page_id_pk" 668 + } 669 + }, 670 + "uniqueConstraints": {} 671 + }, 672 + "user": { 673 + "name": "user", 674 + "columns": { 675 + "id": { 676 + "name": "id", 677 + "type": "integer", 678 + "primaryKey": true, 679 + "notNull": true, 680 + "autoincrement": false 681 + }, 682 + "tenant_id": { 683 + "name": "tenant_id", 684 + "type": "text(256)", 685 + "primaryKey": false, 686 + "notNull": false, 687 + "autoincrement": false 688 + }, 689 + "first_name": { 690 + "name": "first_name", 691 + "type": "text", 692 + "primaryKey": false, 693 + "notNull": false, 694 + "autoincrement": false, 695 + "default": "''" 696 + }, 697 + "last_name": { 698 + "name": "last_name", 699 + "type": "text", 700 + "primaryKey": false, 701 + "notNull": false, 702 + "autoincrement": false, 703 + "default": "''" 704 + }, 705 + "email": { 706 + "name": "email", 707 + "type": "text", 708 + "primaryKey": false, 709 + "notNull": false, 710 + "autoincrement": false, 711 + "default": "''" 712 + }, 713 + "photo_url": { 714 + "name": "photo_url", 715 + "type": "text", 716 + "primaryKey": false, 717 + "notNull": false, 718 + "autoincrement": false, 719 + "default": "''" 720 + }, 721 + "created_at": { 722 + "name": "created_at", 723 + "type": "integer", 724 + "primaryKey": false, 725 + "notNull": false, 726 + "autoincrement": false, 727 + "default": "(strftime('%s', 'now'))" 728 + }, 729 + "updated_at": { 730 + "name": "updated_at", 731 + "type": "integer", 732 + "primaryKey": false, 733 + "notNull": false, 734 + "autoincrement": false, 735 + "default": "(strftime('%s', 'now'))" 736 + } 737 + }, 738 + "indexes": { 739 + "user_tenant_id_unique": { 740 + "name": "user_tenant_id_unique", 741 + "columns": [ 742 + "tenant_id" 743 + ], 744 + "isUnique": true 745 + } 746 + }, 747 + "foreignKeys": {}, 748 + "compositePrimaryKeys": {}, 749 + "uniqueConstraints": {} 750 + }, 751 + "users_to_workspaces": { 752 + "name": "users_to_workspaces", 753 + "columns": { 754 + "user_id": { 755 + "name": "user_id", 756 + "type": "integer", 757 + "primaryKey": false, 758 + "notNull": true, 759 + "autoincrement": false 760 + }, 761 + "workspace_id": { 762 + "name": "workspace_id", 763 + "type": "integer", 764 + "primaryKey": false, 765 + "notNull": true, 766 + "autoincrement": false 767 + }, 768 + "role": { 769 + "name": "role", 770 + "type": "text", 771 + "primaryKey": false, 772 + "notNull": true, 773 + "autoincrement": false, 774 + "default": "'member'" 775 + }, 776 + "created_at": { 777 + "name": "created_at", 778 + "type": "integer", 779 + "primaryKey": false, 780 + "notNull": false, 781 + "autoincrement": false, 782 + "default": "(strftime('%s', 'now'))" 783 + } 784 + }, 785 + "indexes": {}, 786 + "foreignKeys": { 787 + "users_to_workspaces_user_id_user_id_fk": { 788 + "name": "users_to_workspaces_user_id_user_id_fk", 789 + "tableFrom": "users_to_workspaces", 790 + "tableTo": "user", 791 + "columnsFrom": [ 792 + "user_id" 793 + ], 794 + "columnsTo": [ 795 + "id" 796 + ], 797 + "onDelete": "no action", 798 + "onUpdate": "no action" 799 + }, 800 + "users_to_workspaces_workspace_id_workspace_id_fk": { 801 + "name": "users_to_workspaces_workspace_id_workspace_id_fk", 802 + "tableFrom": "users_to_workspaces", 803 + "tableTo": "workspace", 804 + "columnsFrom": [ 805 + "workspace_id" 806 + ], 807 + "columnsTo": [ 808 + "id" 809 + ], 810 + "onDelete": "no action", 811 + "onUpdate": "no action" 812 + } 813 + }, 814 + "compositePrimaryKeys": { 815 + "users_to_workspaces_user_id_workspace_id_pk": { 816 + "columns": [ 817 + "user_id", 818 + "workspace_id" 819 + ], 820 + "name": "users_to_workspaces_user_id_workspace_id_pk" 821 + } 822 + }, 823 + "uniqueConstraints": {} 824 + }, 825 + "page_subscriber": { 826 + "name": "page_subscriber", 827 + "columns": { 828 + "id": { 829 + "name": "id", 830 + "type": "integer", 831 + "primaryKey": true, 832 + "notNull": true, 833 + "autoincrement": false 834 + }, 835 + "email": { 836 + "name": "email", 837 + "type": "text", 838 + "primaryKey": false, 839 + "notNull": true, 840 + "autoincrement": false 841 + }, 842 + "page_id": { 843 + "name": "page_id", 844 + "type": "integer", 845 + "primaryKey": false, 846 + "notNull": true, 847 + "autoincrement": false 848 + }, 849 + "token": { 850 + "name": "token", 851 + "type": "text", 852 + "primaryKey": false, 853 + "notNull": false, 854 + "autoincrement": false 855 + }, 856 + "accepted_at": { 857 + "name": "accepted_at", 858 + "type": "integer", 859 + "primaryKey": false, 860 + "notNull": false, 861 + "autoincrement": false 862 + }, 863 + "expires_at": { 864 + "name": "expires_at", 865 + "type": "integer", 866 + "primaryKey": false, 867 + "notNull": false, 868 + "autoincrement": false 869 + }, 870 + "created_at": { 871 + "name": "created_at", 872 + "type": "integer", 873 + "primaryKey": false, 874 + "notNull": false, 875 + "autoincrement": false, 876 + "default": "(strftime('%s', 'now'))" 877 + }, 878 + "updated_at": { 879 + "name": "updated_at", 880 + "type": "integer", 881 + "primaryKey": false, 882 + "notNull": false, 883 + "autoincrement": false, 884 + "default": "(strftime('%s', 'now'))" 885 + } 886 + }, 887 + "indexes": {}, 888 + "foreignKeys": { 889 + "page_subscriber_page_id_page_id_fk": { 890 + "name": "page_subscriber_page_id_page_id_fk", 891 + "tableFrom": "page_subscriber", 892 + "tableTo": "page", 893 + "columnsFrom": [ 894 + "page_id" 895 + ], 896 + "columnsTo": [ 897 + "id" 898 + ], 899 + "onDelete": "no action", 900 + "onUpdate": "no action" 901 + } 902 + }, 903 + "compositePrimaryKeys": {}, 904 + "uniqueConstraints": {} 905 + }, 906 + "workspace": { 907 + "name": "workspace", 908 + "columns": { 909 + "id": { 910 + "name": "id", 911 + "type": "integer", 912 + "primaryKey": true, 913 + "notNull": true, 914 + "autoincrement": false 915 + }, 916 + "slug": { 917 + "name": "slug", 918 + "type": "text", 919 + "primaryKey": false, 920 + "notNull": true, 921 + "autoincrement": false 922 + }, 923 + "name": { 924 + "name": "name", 925 + "type": "text", 926 + "primaryKey": false, 927 + "notNull": false, 928 + "autoincrement": false 929 + }, 930 + "stripe_id": { 931 + "name": "stripe_id", 932 + "type": "text(256)", 933 + "primaryKey": false, 934 + "notNull": false, 935 + "autoincrement": false 936 + }, 937 + "subscription_id": { 938 + "name": "subscription_id", 939 + "type": "text", 940 + "primaryKey": false, 941 + "notNull": false, 942 + "autoincrement": false 943 + }, 944 + "plan": { 945 + "name": "plan", 946 + "type": "text", 947 + "primaryKey": false, 948 + "notNull": false, 949 + "autoincrement": false 950 + }, 951 + "ends_at": { 952 + "name": "ends_at", 953 + "type": "integer", 954 + "primaryKey": false, 955 + "notNull": false, 956 + "autoincrement": false 957 + }, 958 + "paid_until": { 959 + "name": "paid_until", 960 + "type": "integer", 961 + "primaryKey": false, 962 + "notNull": false, 963 + "autoincrement": false 964 + }, 965 + "created_at": { 966 + "name": "created_at", 967 + "type": "integer", 968 + "primaryKey": false, 969 + "notNull": false, 970 + "autoincrement": false, 971 + "default": "(strftime('%s', 'now'))" 972 + }, 973 + "updated_at": { 974 + "name": "updated_at", 975 + "type": "integer", 976 + "primaryKey": false, 977 + "notNull": false, 978 + "autoincrement": false, 979 + "default": "(strftime('%s', 'now'))" 980 + } 981 + }, 982 + "indexes": { 983 + "workspace_slug_unique": { 984 + "name": "workspace_slug_unique", 985 + "columns": [ 986 + "slug" 987 + ], 988 + "isUnique": true 989 + }, 990 + "workspace_stripe_id_unique": { 991 + "name": "workspace_stripe_id_unique", 992 + "columns": [ 993 + "stripe_id" 994 + ], 995 + "isUnique": true 996 + } 997 + }, 998 + "foreignKeys": {}, 999 + "compositePrimaryKeys": {}, 1000 + "uniqueConstraints": {} 1001 + }, 1002 + "notification": { 1003 + "name": "notification", 1004 + "columns": { 1005 + "id": { 1006 + "name": "id", 1007 + "type": "integer", 1008 + "primaryKey": true, 1009 + "notNull": true, 1010 + "autoincrement": false 1011 + }, 1012 + "name": { 1013 + "name": "name", 1014 + "type": "text", 1015 + "primaryKey": false, 1016 + "notNull": true, 1017 + "autoincrement": false 1018 + }, 1019 + "provider": { 1020 + "name": "provider", 1021 + "type": "text", 1022 + "primaryKey": false, 1023 + "notNull": true, 1024 + "autoincrement": false 1025 + }, 1026 + "data": { 1027 + "name": "data", 1028 + "type": "text", 1029 + "primaryKey": false, 1030 + "notNull": false, 1031 + "autoincrement": false, 1032 + "default": "'{}'" 1033 + }, 1034 + "workspace_id": { 1035 + "name": "workspace_id", 1036 + "type": "integer", 1037 + "primaryKey": false, 1038 + "notNull": false, 1039 + "autoincrement": false 1040 + }, 1041 + "created_at": { 1042 + "name": "created_at", 1043 + "type": "integer", 1044 + "primaryKey": false, 1045 + "notNull": false, 1046 + "autoincrement": false, 1047 + "default": "(strftime('%s', 'now'))" 1048 + }, 1049 + "updated_at": { 1050 + "name": "updated_at", 1051 + "type": "integer", 1052 + "primaryKey": false, 1053 + "notNull": false, 1054 + "autoincrement": false, 1055 + "default": "(strftime('%s', 'now'))" 1056 + } 1057 + }, 1058 + "indexes": {}, 1059 + "foreignKeys": { 1060 + "notification_workspace_id_workspace_id_fk": { 1061 + "name": "notification_workspace_id_workspace_id_fk", 1062 + "tableFrom": "notification", 1063 + "tableTo": "workspace", 1064 + "columnsFrom": [ 1065 + "workspace_id" 1066 + ], 1067 + "columnsTo": [ 1068 + "id" 1069 + ], 1070 + "onDelete": "no action", 1071 + "onUpdate": "no action" 1072 + } 1073 + }, 1074 + "compositePrimaryKeys": {}, 1075 + "uniqueConstraints": {} 1076 + }, 1077 + "notifications_to_monitors": { 1078 + "name": "notifications_to_monitors", 1079 + "columns": { 1080 + "monitor_id": { 1081 + "name": "monitor_id", 1082 + "type": "integer", 1083 + "primaryKey": false, 1084 + "notNull": true, 1085 + "autoincrement": false 1086 + }, 1087 + "notification_id": { 1088 + "name": "notification_id", 1089 + "type": "integer", 1090 + "primaryKey": false, 1091 + "notNull": true, 1092 + "autoincrement": false 1093 + }, 1094 + "created_at": { 1095 + "name": "created_at", 1096 + "type": "integer", 1097 + "primaryKey": false, 1098 + "notNull": false, 1099 + "autoincrement": false, 1100 + "default": "(strftime('%s', 'now'))" 1101 + } 1102 + }, 1103 + "indexes": {}, 1104 + "foreignKeys": { 1105 + "notifications_to_monitors_monitor_id_monitor_id_fk": { 1106 + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", 1107 + "tableFrom": "notifications_to_monitors", 1108 + "tableTo": "monitor", 1109 + "columnsFrom": [ 1110 + "monitor_id" 1111 + ], 1112 + "columnsTo": [ 1113 + "id" 1114 + ], 1115 + "onDelete": "cascade", 1116 + "onUpdate": "no action" 1117 + }, 1118 + "notifications_to_monitors_notification_id_notification_id_fk": { 1119 + "name": "notifications_to_monitors_notification_id_notification_id_fk", 1120 + "tableFrom": "notifications_to_monitors", 1121 + "tableTo": "notification", 1122 + "columnsFrom": [ 1123 + "notification_id" 1124 + ], 1125 + "columnsTo": [ 1126 + "id" 1127 + ], 1128 + "onDelete": "cascade", 1129 + "onUpdate": "no action" 1130 + } 1131 + }, 1132 + "compositePrimaryKeys": { 1133 + "notifications_to_monitors_monitor_id_notification_id_pk": { 1134 + "columns": [ 1135 + "monitor_id", 1136 + "notification_id" 1137 + ], 1138 + "name": "notifications_to_monitors_monitor_id_notification_id_pk" 1139 + } 1140 + }, 1141 + "uniqueConstraints": {} 1142 + }, 1143 + "monitor_status": { 1144 + "name": "monitor_status", 1145 + "columns": { 1146 + "monitor_id": { 1147 + "name": "monitor_id", 1148 + "type": "integer", 1149 + "primaryKey": false, 1150 + "notNull": true, 1151 + "autoincrement": false 1152 + }, 1153 + "region": { 1154 + "name": "region", 1155 + "type": "text", 1156 + "primaryKey": false, 1157 + "notNull": true, 1158 + "autoincrement": false, 1159 + "default": "''" 1160 + }, 1161 + "status": { 1162 + "name": "status", 1163 + "type": "text", 1164 + "primaryKey": false, 1165 + "notNull": true, 1166 + "autoincrement": false, 1167 + "default": "'active'" 1168 + }, 1169 + "created_at": { 1170 + "name": "created_at", 1171 + "type": "integer", 1172 + "primaryKey": false, 1173 + "notNull": false, 1174 + "autoincrement": false, 1175 + "default": "(strftime('%s', 'now'))" 1176 + }, 1177 + "updated_at": { 1178 + "name": "updated_at", 1179 + "type": "integer", 1180 + "primaryKey": false, 1181 + "notNull": false, 1182 + "autoincrement": false, 1183 + "default": "(strftime('%s', 'now'))" 1184 + } 1185 + }, 1186 + "indexes": { 1187 + "monitor_status_idx": { 1188 + "name": "monitor_status_idx", 1189 + "columns": [ 1190 + "monitor_id", 1191 + "region" 1192 + ], 1193 + "isUnique": false 1194 + } 1195 + }, 1196 + "foreignKeys": { 1197 + "monitor_status_monitor_id_monitor_id_fk": { 1198 + "name": "monitor_status_monitor_id_monitor_id_fk", 1199 + "tableFrom": "monitor_status", 1200 + "tableTo": "monitor", 1201 + "columnsFrom": [ 1202 + "monitor_id" 1203 + ], 1204 + "columnsTo": [ 1205 + "id" 1206 + ], 1207 + "onDelete": "cascade", 1208 + "onUpdate": "no action" 1209 + } 1210 + }, 1211 + "compositePrimaryKeys": { 1212 + "monitor_status_monitor_id_region_pk": { 1213 + "columns": [ 1214 + "monitor_id", 1215 + "region" 1216 + ], 1217 + "name": "monitor_status_monitor_id_region_pk" 1218 + } 1219 + }, 1220 + "uniqueConstraints": {} 1221 + }, 1222 + "invitation": { 1223 + "name": "invitation", 1224 + "columns": { 1225 + "id": { 1226 + "name": "id", 1227 + "type": "integer", 1228 + "primaryKey": true, 1229 + "notNull": true, 1230 + "autoincrement": false 1231 + }, 1232 + "email": { 1233 + "name": "email", 1234 + "type": "text", 1235 + "primaryKey": false, 1236 + "notNull": true, 1237 + "autoincrement": false 1238 + }, 1239 + "role": { 1240 + "name": "role", 1241 + "type": "text", 1242 + "primaryKey": false, 1243 + "notNull": true, 1244 + "autoincrement": false, 1245 + "default": "'member'" 1246 + }, 1247 + "workspace_id": { 1248 + "name": "workspace_id", 1249 + "type": "integer", 1250 + "primaryKey": false, 1251 + "notNull": true, 1252 + "autoincrement": false 1253 + }, 1254 + "token": { 1255 + "name": "token", 1256 + "type": "text", 1257 + "primaryKey": false, 1258 + "notNull": true, 1259 + "autoincrement": false 1260 + }, 1261 + "expires_at": { 1262 + "name": "expires_at", 1263 + "type": "integer", 1264 + "primaryKey": false, 1265 + "notNull": true, 1266 + "autoincrement": false 1267 + }, 1268 + "created_at": { 1269 + "name": "created_at", 1270 + "type": "integer", 1271 + "primaryKey": false, 1272 + "notNull": false, 1273 + "autoincrement": false, 1274 + "default": "(strftime('%s', 'now'))" 1275 + }, 1276 + "accepted_at": { 1277 + "name": "accepted_at", 1278 + "type": "integer", 1279 + "primaryKey": false, 1280 + "notNull": false, 1281 + "autoincrement": false 1282 + } 1283 + }, 1284 + "indexes": {}, 1285 + "foreignKeys": {}, 1286 + "compositePrimaryKeys": {}, 1287 + "uniqueConstraints": {} 1288 + }, 1289 + "incident": { 1290 + "name": "incident", 1291 + "columns": { 1292 + "id": { 1293 + "name": "id", 1294 + "type": "integer", 1295 + "primaryKey": true, 1296 + "notNull": true, 1297 + "autoincrement": false 1298 + }, 1299 + "title": { 1300 + "name": "title", 1301 + "type": "text", 1302 + "primaryKey": false, 1303 + "notNull": true, 1304 + "autoincrement": false, 1305 + "default": "''" 1306 + }, 1307 + "summary": { 1308 + "name": "summary", 1309 + "type": "text", 1310 + "primaryKey": false, 1311 + "notNull": true, 1312 + "autoincrement": false, 1313 + "default": "''" 1314 + }, 1315 + "status": { 1316 + "name": "status", 1317 + "type": "text", 1318 + "primaryKey": false, 1319 + "notNull": true, 1320 + "autoincrement": false, 1321 + "default": "'triage'" 1322 + }, 1323 + "monitor_id": { 1324 + "name": "monitor_id", 1325 + "type": "integer", 1326 + "primaryKey": false, 1327 + "notNull": false, 1328 + "autoincrement": false 1329 + }, 1330 + "workspace_id": { 1331 + "name": "workspace_id", 1332 + "type": "integer", 1333 + "primaryKey": false, 1334 + "notNull": false, 1335 + "autoincrement": false 1336 + }, 1337 + "started_at": { 1338 + "name": "started_at", 1339 + "type": "integer", 1340 + "primaryKey": false, 1341 + "notNull": true, 1342 + "autoincrement": false, 1343 + "default": "(strftime('%s', 'now'))" 1344 + }, 1345 + "acknowledged_at": { 1346 + "name": "acknowledged_at", 1347 + "type": "integer", 1348 + "primaryKey": false, 1349 + "notNull": false, 1350 + "autoincrement": false 1351 + }, 1352 + "acknowledged_by": { 1353 + "name": "acknowledged_by", 1354 + "type": "integer", 1355 + "primaryKey": false, 1356 + "notNull": false, 1357 + "autoincrement": false 1358 + }, 1359 + "resolved_at": { 1360 + "name": "resolved_at", 1361 + "type": "integer", 1362 + "primaryKey": false, 1363 + "notNull": false, 1364 + "autoincrement": false 1365 + }, 1366 + "resolved_by": { 1367 + "name": "resolved_by", 1368 + "type": "integer", 1369 + "primaryKey": false, 1370 + "notNull": false, 1371 + "autoincrement": false 1372 + }, 1373 + "auto_resolved": { 1374 + "name": "auto_resolved", 1375 + "type": "integer", 1376 + "primaryKey": false, 1377 + "notNull": false, 1378 + "autoincrement": false, 1379 + "default": false 1380 + }, 1381 + "created_at": { 1382 + "name": "created_at", 1383 + "type": "integer", 1384 + "primaryKey": false, 1385 + "notNull": false, 1386 + "autoincrement": false, 1387 + "default": "(strftime('%s', 'now'))" 1388 + }, 1389 + "updated_at": { 1390 + "name": "updated_at", 1391 + "type": "integer", 1392 + "primaryKey": false, 1393 + "notNull": false, 1394 + "autoincrement": false, 1395 + "default": "(strftime('%s', 'now'))" 1396 + } 1397 + }, 1398 + "indexes": { 1399 + "incident_monitor_id_started_at_unique": { 1400 + "name": "incident_monitor_id_started_at_unique", 1401 + "columns": [ 1402 + "monitor_id", 1403 + "started_at" 1404 + ], 1405 + "isUnique": true 1406 + } 1407 + }, 1408 + "foreignKeys": { 1409 + "incident_monitor_id_monitor_id_fk": { 1410 + "name": "incident_monitor_id_monitor_id_fk", 1411 + "tableFrom": "incident", 1412 + "tableTo": "monitor", 1413 + "columnsFrom": [ 1414 + "monitor_id" 1415 + ], 1416 + "columnsTo": [ 1417 + "id" 1418 + ], 1419 + "onDelete": "set default", 1420 + "onUpdate": "no action" 1421 + }, 1422 + "incident_workspace_id_workspace_id_fk": { 1423 + "name": "incident_workspace_id_workspace_id_fk", 1424 + "tableFrom": "incident", 1425 + "tableTo": "workspace", 1426 + "columnsFrom": [ 1427 + "workspace_id" 1428 + ], 1429 + "columnsTo": [ 1430 + "id" 1431 + ], 1432 + "onDelete": "no action", 1433 + "onUpdate": "no action" 1434 + }, 1435 + "incident_acknowledged_by_user_id_fk": { 1436 + "name": "incident_acknowledged_by_user_id_fk", 1437 + "tableFrom": "incident", 1438 + "tableTo": "user", 1439 + "columnsFrom": [ 1440 + "acknowledged_by" 1441 + ], 1442 + "columnsTo": [ 1443 + "id" 1444 + ], 1445 + "onDelete": "no action", 1446 + "onUpdate": "no action" 1447 + }, 1448 + "incident_resolved_by_user_id_fk": { 1449 + "name": "incident_resolved_by_user_id_fk", 1450 + "tableFrom": "incident", 1451 + "tableTo": "user", 1452 + "columnsFrom": [ 1453 + "resolved_by" 1454 + ], 1455 + "columnsTo": [ 1456 + "id" 1457 + ], 1458 + "onDelete": "no action", 1459 + "onUpdate": "no action" 1460 + } 1461 + }, 1462 + "compositePrimaryKeys": {}, 1463 + "uniqueConstraints": {} 1464 + }, 1465 + "monitor_tag": { 1466 + "name": "monitor_tag", 1467 + "columns": { 1468 + "id": { 1469 + "name": "id", 1470 + "type": "integer", 1471 + "primaryKey": true, 1472 + "notNull": true, 1473 + "autoincrement": false 1474 + }, 1475 + "workspace_id": { 1476 + "name": "workspace_id", 1477 + "type": "integer", 1478 + "primaryKey": false, 1479 + "notNull": true, 1480 + "autoincrement": false 1481 + }, 1482 + "name": { 1483 + "name": "name", 1484 + "type": "text", 1485 + "primaryKey": false, 1486 + "notNull": true, 1487 + "autoincrement": false 1488 + }, 1489 + "color": { 1490 + "name": "color", 1491 + "type": "text", 1492 + "primaryKey": false, 1493 + "notNull": true, 1494 + "autoincrement": false 1495 + }, 1496 + "created_at": { 1497 + "name": "created_at", 1498 + "type": "integer", 1499 + "primaryKey": false, 1500 + "notNull": false, 1501 + "autoincrement": false, 1502 + "default": "(strftime('%s', 'now'))" 1503 + }, 1504 + "updated_at": { 1505 + "name": "updated_at", 1506 + "type": "integer", 1507 + "primaryKey": false, 1508 + "notNull": false, 1509 + "autoincrement": false, 1510 + "default": "(strftime('%s', 'now'))" 1511 + } 1512 + }, 1513 + "indexes": {}, 1514 + "foreignKeys": { 1515 + "monitor_tag_workspace_id_workspace_id_fk": { 1516 + "name": "monitor_tag_workspace_id_workspace_id_fk", 1517 + "tableFrom": "monitor_tag", 1518 + "tableTo": "workspace", 1519 + "columnsFrom": [ 1520 + "workspace_id" 1521 + ], 1522 + "columnsTo": [ 1523 + "id" 1524 + ], 1525 + "onDelete": "cascade", 1526 + "onUpdate": "no action" 1527 + } 1528 + }, 1529 + "compositePrimaryKeys": {}, 1530 + "uniqueConstraints": {} 1531 + }, 1532 + "monitor_tag_to_monitor": { 1533 + "name": "monitor_tag_to_monitor", 1534 + "columns": { 1535 + "monitor_id": { 1536 + "name": "monitor_id", 1537 + "type": "integer", 1538 + "primaryKey": false, 1539 + "notNull": true, 1540 + "autoincrement": false 1541 + }, 1542 + "monitor_tag_id": { 1543 + "name": "monitor_tag_id", 1544 + "type": "integer", 1545 + "primaryKey": false, 1546 + "notNull": true, 1547 + "autoincrement": false 1548 + }, 1549 + "created_at": { 1550 + "name": "created_at", 1551 + "type": "integer", 1552 + "primaryKey": false, 1553 + "notNull": false, 1554 + "autoincrement": false, 1555 + "default": "(strftime('%s', 'now'))" 1556 + } 1557 + }, 1558 + "indexes": {}, 1559 + "foreignKeys": { 1560 + "monitor_tag_to_monitor_monitor_id_monitor_id_fk": { 1561 + "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk", 1562 + "tableFrom": "monitor_tag_to_monitor", 1563 + "tableTo": "monitor", 1564 + "columnsFrom": [ 1565 + "monitor_id" 1566 + ], 1567 + "columnsTo": [ 1568 + "id" 1569 + ], 1570 + "onDelete": "cascade", 1571 + "onUpdate": "no action" 1572 + }, 1573 + "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": { 1574 + "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk", 1575 + "tableFrom": "monitor_tag_to_monitor", 1576 + "tableTo": "monitor_tag", 1577 + "columnsFrom": [ 1578 + "monitor_tag_id" 1579 + ], 1580 + "columnsTo": [ 1581 + "id" 1582 + ], 1583 + "onDelete": "cascade", 1584 + "onUpdate": "no action" 1585 + } 1586 + }, 1587 + "compositePrimaryKeys": { 1588 + "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": { 1589 + "columns": [ 1590 + "monitor_id", 1591 + "monitor_tag_id" 1592 + ], 1593 + "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk" 1594 + } 1595 + }, 1596 + "uniqueConstraints": {} 1597 + } 1598 + }, 1599 + "enums": {}, 1600 + "_meta": { 1601 + "schemas": {}, 1602 + "tables": {}, 1603 + "columns": {} 1604 + } 1605 + }
+7
packages/db/drizzle/meta/_journal.json
··· 148 148 "when": 1707905605592, 149 149 "tag": "0020_flat_bedlam", 150 150 "breakpoints": true 151 + }, 152 + { 153 + "idx": 21, 154 + "version": "5", 155 + "when": 1710677383007, 156 + "tag": "0021_reflective_nico_minoru", 157 + "breakpoints": true 151 158 } 152 159 ] 153 160 }
+1
packages/db/src/schema/index.ts
··· 10 10 export * from "./monitor_status"; 11 11 export * from "./invitations"; 12 12 export * from "./incidents"; 13 + export * from "./monitor_tags";
+2
packages/db/src/schema/monitor_tags/index.ts
··· 1 + export * from "./monitor_tag"; 2 + export * from "./validation";
+67
packages/db/src/schema/monitor_tags/monitor_tag.ts
··· 1 + import { relations, sql } from "drizzle-orm"; 2 + import { 3 + integer, 4 + primaryKey, 5 + sqliteTable, 6 + text, 7 + } from "drizzle-orm/sqlite-core"; 8 + 9 + import { monitor } from "../monitors"; 10 + import { workspace } from "../workspaces"; 11 + 12 + export const monitorTag = sqliteTable("monitor_tag", { 13 + id: integer("id").primaryKey(), 14 + workspaceId: integer("workspace_id") 15 + .references(() => workspace.id, { onDelete: "cascade" }) 16 + .notNull(), 17 + 18 + name: text("name").notNull(), 19 + color: text("color").notNull(), 20 + 21 + createdAt: integer("created_at", { mode: "timestamp" }).default( 22 + sql`(strftime('%s', 'now'))`, 23 + ), 24 + updatedAt: integer("updated_at", { mode: "timestamp" }).default( 25 + sql`(strftime('%s', 'now'))`, 26 + ), 27 + }); 28 + 29 + export const monitorTagsToMonitors = sqliteTable( 30 + "monitor_tag_to_monitor", 31 + { 32 + monitorId: integer("monitor_id") 33 + .notNull() 34 + .references(() => monitor.id, { onDelete: "cascade" }), 35 + monitorTagId: integer("monitor_tag_id") 36 + .notNull() 37 + .references(() => monitorTag.id, { onDelete: "cascade" }), 38 + createdAt: integer("created_at", { mode: "timestamp" }).default( 39 + sql`(strftime('%s', 'now'))`, 40 + ), 41 + }, 42 + (t) => ({ 43 + pk: primaryKey(t.monitorId, t.monitorTagId), 44 + }), 45 + ); 46 + 47 + export const monitorTagsToMonitorsRelation = relations( 48 + monitorTagsToMonitors, 49 + ({ one }) => ({ 50 + monitor: one(monitor, { 51 + fields: [monitorTagsToMonitors.monitorId], 52 + references: [monitor.id], 53 + }), 54 + monitorTag: one(monitorTag, { 55 + fields: [monitorTagsToMonitors.monitorTagId], 56 + references: [monitorTag.id], 57 + }), 58 + }), 59 + ); 60 + 61 + export const monitorTagRelations = relations(monitorTag, ({ one, many }) => ({ 62 + monitor: many(monitorTagsToMonitors), 63 + workspace: one(workspace, { 64 + fields: [monitorTag.workspaceId], 65 + references: [workspace.id], 66 + }), 67 + }));
+11
packages/db/src/schema/monitor_tags/validation.ts
··· 1 + import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 2 + import type { z } from "zod"; 3 + 4 + import { monitorTag } from "./monitor_tag"; 5 + 6 + export const selectMonitorTagSchema = createSelectSchema(monitorTag); 7 + 8 + export const insertMonitorTagSchema = createInsertSchema(monitorTag); 9 + 10 + export type InsertMonitorTag = z.infer<typeof insertMonitorTagSchema>; 11 + export type MonitorTag = z.infer<typeof selectMonitorTagSchema>;
+2
packages/db/src/schema/monitors/monitor.ts
··· 6 6 text, 7 7 } from "drizzle-orm/sqlite-core"; 8 8 9 + import { monitorTagsToMonitors } from "../monitor_tags"; 9 10 import { notificationsToMonitors } from "../notifications"; 10 11 import { page } from "../pages"; 11 12 import { monitorsToStatusReport } from "../status_reports"; ··· 51 52 export const monitorRelation = relations(monitor, ({ one, many }) => ({ 52 53 monitorsToPages: many(monitorsToPages), 53 54 monitorsToStatusReports: many(monitorsToStatusReport), 55 + monitorTagsToMonitors: many(monitorTagsToMonitors), 54 56 workspace: one(workspace, { 55 57 fields: [monitor.workspaceId], 56 58 references: [workspace.id],
+1
packages/db/src/schema/monitors/validation.ts
··· 72 72 notifications: z.array(z.number()).optional().default([]), 73 73 pages: z.array(z.number()).optional().default([]), 74 74 body: z.string().default("").optional(), 75 + tags: z.array(z.number()).optional().default([]), 75 76 }); 76 77 77 78 export type InsertMonitor = z.infer<typeof insertMonitorSchema>;
+7 -3
packages/tinybird/src/os-client.ts
··· 3 3 4 4 import { flyRegions } from "@openstatus/utils"; 5 5 6 - const MIN_CACHE = 60; // 60s 7 - const DEFAULT_CACHE = 120; // 2min 8 - const MAX_CACHE = 86400; // 1d 6 + const isProd = process.env.NODE_ENV === "production"; 7 + 8 + const DEV_CACHE = 3_600; // 1h 9 + 10 + const MIN_CACHE = isProd ? 60 : DEV_CACHE; // 60s 11 + const DEFAULT_CACHE = isProd ? 120 : DEV_CACHE; // 2min 12 + const MAX_CACHE = 86_400; // 1d 9 13 10 14 export const latencySchema = z.object({ 11 15 p50Latency: z.number().int().nullable(),
+2 -2
packages/ui/package.json
··· 23 23 "@radix-ui/react-alert-dialog": "1.0.5", 24 24 "@radix-ui/react-avatar": "1.0.4", 25 25 "@radix-ui/react-checkbox": "1.0.4", 26 - "@radix-ui/react-collapsible": "^1.0.3", 26 + "@radix-ui/react-collapsible": "1.0.3", 27 27 "@radix-ui/react-context-menu": "2.1.5", 28 28 "@radix-ui/react-dialog": "1.0.4", 29 29 "@radix-ui/react-dropdown-menu": "2.0.6", ··· 37 37 "@radix-ui/react-slot": "1.0.2", 38 38 "@radix-ui/react-switch": "1.0.3", 39 39 "@radix-ui/react-tabs": "1.0.4", 40 - "@radix-ui/react-toggle": "^1.0.3", 40 + "@radix-ui/react-toggle": "1.0.3", 41 41 "@radix-ui/react-tooltip": "1.0.7", 42 42 "class-variance-authority": "0.7.0", 43 43 "clsx": "2.0.0",
+2 -2
pnpm-lock.yaml
··· 805 805 specifier: 1.0.4 806 806 version: 1.0.4(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) 807 807 '@radix-ui/react-collapsible': 808 - specifier: ^1.0.3 808 + specifier: 1.0.3 809 809 version: 1.0.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) 810 810 '@radix-ui/react-context-menu': 811 811 specifier: 2.1.5 ··· 847 847 specifier: 1.0.4 848 848 version: 1.0.4(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) 849 849 '@radix-ui/react-toggle': 850 - specifier: ^1.0.3 850 + specifier: 1.0.3 851 851 version: 1.0.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) 852 852 '@radix-ui/react-tooltip': 853 853 specifier: 1.0.7