Merge branch 'main' into v3/infra-updates

This commit is contained in:
nicktrn
2024-03-25 12:12:15 +00:00
15 changed files with 685 additions and 92 deletions
@@ -190,10 +190,10 @@ export function RunsFilters({ possibleEnvironments, possibleTasks }: RunFiltersP
</Paragraph>
</SelectItem>
{allTaskRunStatuses.map((status) => (
<TooltipProvider>
<TooltipProvider key={status}>
<Tooltip>
<TooltipTrigger className="group flex w-full flex-col py-0">
<SelectItem key={status} value={status} className="">
<SelectItem value={status} className="">
<TaskRunStatusCombo
status={status}
className="text-xs"
+205 -2
View File
@@ -1,9 +1,212 @@
import { prisma } from "~/db.server";
import { SearchParams } from "~/routes/admin._index";
export async function adminGetUsers() {
return await prisma.user.findMany({
const pageSize = 20;
export async function adminGetUsers(userId: string, { page, search }: SearchParams) {
page = page || 1;
search = search ? decodeURIComponent(search) : undefined;
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (user?.admin !== true) {
throw new Error("Unauthorized");
}
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
admin: true,
createdAt: true,
displayName: true,
orgMemberships: {
select: {
organization: {
select: {
title: true,
slug: true,
},
},
},
},
},
where: search
? {
OR: [
{
name: {
contains: search,
mode: "insensitive",
},
},
{
email: {
contains: search,
mode: "insensitive",
},
},
{
orgMemberships: {
some: {
organization: {
title: {
contains: search,
mode: "insensitive",
},
},
},
},
},
{
orgMemberships: {
some: {
organization: {
slug: {
contains: search,
mode: "insensitive",
},
},
},
},
},
],
}
: undefined,
orderBy: {
createdAt: "desc",
},
take: pageSize,
skip: (page - 1) * pageSize,
});
const totalUsers = await prisma.user.count();
return {
users,
page,
pageCount: Math.ceil(totalUsers / pageSize),
filters: {
search,
},
};
}
export async function adminGetOrganizations(userId: string, { page, search }: SearchParams) {
page = page || 1;
search = search ? decodeURIComponent(search) : undefined;
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (user?.admin !== true) {
throw new Error("Unauthorized");
}
const organizations = await prisma.organization.findMany({
select: {
id: true,
slug: true,
title: true,
v3Enabled: true,
members: {
select: {
user: {
select: {
email: true,
},
},
},
},
},
where: search
? {
OR: [
{
members: {
some: {
user: {
name: {
contains: search,
mode: "insensitive",
},
},
},
},
},
{
members: {
some: {
user: {
email: {
contains: search,
mode: "insensitive",
},
},
},
},
},
{
slug: {
contains: search,
mode: "insensitive",
},
},
{
title: {
contains: search,
mode: "insensitive",
},
},
],
}
: undefined,
orderBy: {
createdAt: "desc",
},
take: pageSize,
skip: (page - 1) * pageSize,
});
const totalOrgs = await prisma.organization.count();
return {
organizations,
page,
pageCount: Math.ceil(totalOrgs / pageSize),
filters: {
search,
},
};
}
export async function setV3Enabled(userId: string, id: string, v3Enabled: boolean) {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (user?.admin !== true) {
throw new Error("Unauthorized");
}
return prisma.organization.update({
where: {
id,
},
data: {
v3Enabled,
},
});
}
+11
View File
@@ -3,6 +3,7 @@ import slug from "slug";
import { prisma } from "~/db.server";
import type { Project } from "@trigger.dev/database";
import { Organization, createEnvironment } from "./organization.server";
import { env } from "~/env.server";
export type { Project } from "@trigger.dev/database";
const externalRefGenerator = customAlphabet("abcdefghijklmnopqrstuvwxyz", 20);
@@ -30,6 +31,16 @@ export async function createProject(
);
}
if (version === "v3") {
if (!organization.v3Enabled) {
throw new Error(`Organization can't create v3 projects.`);
}
if (!env.V3_ENABLED) {
throw new Error(`v3 is not available yet.`);
}
}
//ensure the slug is globally unique
const uniqueProjectSlug = `${slug(name)}-${nanoid(4)}`;
const projectWithSameSlug = await prisma.project.findFirst({
@@ -1,5 +1,5 @@
import { QueueListIcon, StopCircleIcon } from "@heroicons/react/20/solid";
import { useFetcher, useParams } from "@remix-run/react";
import { useParams } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { formatDurationNanoseconds, nanosecondsToMilliseconds } from "@trigger.dev/core/v3";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
@@ -8,17 +8,11 @@ import { CodeBlock } from "~/components/code/CodeBlock";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { DateTimeAccurate } from "~/components/primitives/DateTime";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "~/components/primitives/Dialog";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { Header2 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import { TextLink } from "~/components/primitives/TextLink";
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
import { RunIcon } from "~/components/runs/v3/RunIcon";
@@ -31,7 +25,8 @@ import { useProject } from "~/hooks/useProject";
import { SpanPresenter } from "~/presenters/v3/SpanPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { v3RunPath, v3RunSpanPath, v3SpanParamsSchema } from "~/utils/pathBuilder";
import { v3RunPath, v3RunSpanPath, v3SpanParamsSchema, v3TraceSpanPath } from "~/utils/pathBuilder";
import { SpanLink } from "~/v3/eventRepository.server";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -137,6 +132,17 @@ export default function Page() {
)}
</PropertyTable>
{event.links && event.links.length > 0 && (
<div>
<Header2 spacing>Links</Header2>
<div className="space-y-1">
{event.links.map((link, index) => (
<SpanLinkElement key={index} link={link} />
))}
</div>
</div>
)}
{event.events !== undefined && <SpanEvents spanEvents={event.events} />}
{event.payload !== undefined && (
<CodeBlock rowTitle="Payload" code={event.payload} maxLines={20} />
@@ -284,3 +290,41 @@ function classNameForState(state: TimelineState) {
}
}
}
function SpanLinkElement({ link }: { link: SpanLink }) {
const organization = useOrganization();
const project = useProject();
switch (link.type) {
case "run": {
return (
<LinkButton
to={v3RunPath(organization, project, { friendlyId: link.runId })}
variant="minimal/medium"
LeadingIcon={link.icon}
leadingIconClassName="text-text-dimmed"
fullWidth
textAlignLeft
>
{link.title}
</LinkButton>
);
}
case "span": {
return (
<LinkButton
to={v3TraceSpanPath(organization, project, link.traceId, link.spanId)}
variant="minimal/medium"
LeadingIcon={link.icon}
leadingIconClassName="text-text-dimmed"
fullWidth
textAlignLeft
>
{link.title}
</LinkButton>
);
}
}
return null;
}
@@ -0,0 +1,35 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
import { v3RunSpanPath } from "~/utils/pathBuilder";
import { eventRepository } from "~/v3/eventRepository.server";
const ParamsSchema = z.object({
organizationSlug: z.string(),
projectParam: z.string(),
traceId: z.string(),
spanId: z.string(),
});
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const validatedParams = ParamsSchema.parse(params);
const trace = await eventRepository.getTraceSummary(validatedParams.traceId);
if (!trace) {
return new Response("Not found", { status: 404 });
}
// Redirect to the project's runs page
return redirect(
v3RunSpanPath(
{ slug: validatedParams.organizationSlug },
{ slug: validatedParams.projectParam },
{ friendlyId: trace.rootSpan.runId },
{ spanId: validatedParams.spanId }
)
);
}
@@ -40,6 +40,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
select: {
id: true,
title: true,
v3Enabled: true,
_count: {
select: {
projects: {
@@ -62,6 +63,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
title: organization.title,
slug: organizationSlug,
projectsCount: organization._count.projects,
v3Enabled: organization.v3Enabled,
},
defaultVersion: url.searchParams.get("version") ?? "v2",
});
@@ -107,6 +109,8 @@ export default function NewOrganizationPage() {
const lastSubmission = useActionData();
const { v3Enabled } = useFeatures();
const canCreateV3Projects = organization.v3Enabled && v3Enabled;
const [form, { projectName, projectVersion }] = useForm({
id: "create-project",
// TODO: type this
@@ -141,7 +145,7 @@ export default function NewOrganizationPage() {
/>
<FormError id={projectName.errorId}>{projectName.error}</FormError>
</InputGroup>
{v3Enabled ? (
{canCreateV3Projects ? (
<InputGroup>
<Label htmlFor={projectVersion.id}>Project version</Label>
<SelectGroup>
@@ -95,9 +95,12 @@ export const action: ActionFunction = async ({ request }) => {
export default function NewOrganizationPage() {
const { hasOrganizations } = useTypedLoaderData<typeof loader>();
const lastSubmission = useActionData();
const { isManagedCloud, v3Enabled } = useFeatures();
const { isManagedCloud } = useFeatures();
const navigation = useNavigation();
//this is temporary whilst v3 is invite-only. Switch to the useFeatures value when v3 is generally available.
const v3Enabled = false;
const [form, { orgName, projectName, projectVersion }] = useForm({
id: "create-organization",
// TODO: type this
+118 -76
View File
@@ -1,18 +1,47 @@
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import { redirect } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Header1 } from "~/components/primitives/Headers";
import { Input } from "~/components/primitives/Input";
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { useUser } from "~/hooks/useUser";
import { adminGetUsers } from "~/models/admin.server";
import { commitImpersonationSession, setImpersonationId } from "~/services/impersonation.server";
import { requireUserId } from "~/services/session.server";
import { createSearchParams } from "~/utils/searchParams";
export async function loader() {
const users = await adminGetUsers();
export const SearchParams = z.object({
page: z.coerce.number().optional(),
search: z.string().optional(),
});
return typedjson({ users });
}
export type SearchParams = z.infer<typeof SearchParams>;
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const searchParams = createSearchParams(request.url, SearchParams);
if (!searchParams.success) {
throw new Error(searchParams.error);
}
const result = await adminGetUsers(userId, searchParams.params.getAll());
return typedjson(result);
};
const FormSchema = z.object({ id: z.string() });
@@ -30,87 +59,100 @@ export async function action({ request }: ActionFunctionArgs) {
headers: { "Set-Cookie": await commitImpersonationSession(session) },
});
}
const headerClassName =
"py-3 px-2 pr-3 text-xs font-semibold leading-tight text-text-bright text-left";
const cellClassName = "whitespace-nowrap px-2 py-2 text-xs text-text-bright";
export default function AdminDashboardRoute() {
const user = useUser();
const { users } = useTypedLoaderData<typeof loader>();
const { users, filters, page, pageCount } = useTypedLoaderData<typeof loader>();
return (
<main
aria-labelledby="primary-heading"
className="flex h-full min-w-0 flex-1 flex-col overflow-y-auto p-4 lg:order-last"
className="flex h-full min-w-0 flex-1 flex-col overflow-y-auto px-4 pb-4 lg:order-last"
>
<h1 className="mb-2 text-2xl">Accounts ({users.length})</h1>
<div className=" space-y-4">
<Form className="flex items-center gap-2">
<Input
placeholder="Search users or orgs"
variant="small"
icon={MagnifyingGlassIcon}
fullWidth={true}
name="search"
defaultValue={filters.search}
/>
<Button type="submit" variant="tertiary/small">
Search
</Button>
</Form>
<LinkButton to="/" variant="secondary/small" className="mb-4">
Back to me
</LinkButton>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Email</TableHeaderCell>
<TableHeaderCell>Orgs</TableHeaderCell>
<TableHeaderCell>GitHub</TableHeaderCell>
<TableHeaderCell>id</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Admin?</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{users.length === 0 ? (
<TableBlankRow colSpan={9}>
<Paragraph>No users found for search</Paragraph>
</TableBlankRow>
) : (
users.map((user) => {
return (
<TableRow key={user.id}>
<TableCell>{user.email}</TableCell>
<TableCell>
{user.orgMemberships.map((org) => (
<LinkButton
key={org.organization.slug}
variant="minimal/small"
to={`/admin/orgs?search=${encodeURIComponent(org.organization.slug)}`}
>
{org.organization.title} ({org.organization.slug})
</LinkButton>
))}
</TableCell>
<TableCell>
<a
href={`https://github.com/${user.displayName}`}
target="_blank"
className="text-indigo-500 underline"
rel="noreferrer"
>
{user.displayName}
</a>
</TableCell>
<TableCell>{user.id}</TableCell>
<TableCell>{user.createdAt.toISOString()}</TableCell>
<TableCell>{user.admin ? "✅" : ""}</TableCell>
<TableCell isSticky={true}>
<Form method="post" reloadDocument>
<input type="hidden" name="id" value={user.id} />
<table className="divide-border w-full divide-y">
<thead className="bg-midnight-800 sticky -top-4 text-left">
<tr>
<th scope="col" className={headerClassName}>
Email
</th>
<th scope="col" className={headerClassName}>
GitHub username
</th>
<th scope="col" className={headerClassName}>
id
</th>
<th scope="col" className={headerClassName}>
Created At
</th>
<th scope="col" className={headerClassName}>
Admin?
</th>
<th scope="col" className={headerClassName}>
Actions
</th>
</tr>
</thead>
<tbody className="divide-border divide-y">
{users.map((user) => {
return (
<tr key={user.id} className="w-full px-4 py-2 text-left hover:bg-charcoal-900">
<td className={cellClassName}>{user.email}</td>
<td className={cellClassName}>
<a
href={`https://github.com/${user.displayName}`}
target="_blank"
className="text-indigo-500 underline"
rel="noreferrer"
>
{user.displayName}
</a>
</td>
<td className={cellClassName}>{user.id}</td>
<td className={cellClassName}>{user.createdAt.toISOString()}</td>
<td className={cellClassName}>{user.admin ? "✅" : ""}</td>
<td className={cellClassName}>
<Form method="post" reloadDocument>
<input type="hidden" name="id" value={user.id} />
<Button
type="submit"
name="action"
value="impersonate"
className="mr-2"
variant="tertiary/small"
>
Impersonate
</Button>
</Form>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<Button
type="submit"
name="action"
value="impersonate"
className="mr-2"
variant="primary/small"
>
Impersonate
</Button>
</Form>
</td>
</tr>
);
})}
</tbody>
</table>
<PaginationControls currentPage={page} totalPages={pageCount} />
</div>
</main>
);
}
+160
View File
@@ -0,0 +1,160 @@
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import { redirect } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Header1 } from "~/components/primitives/Headers";
import { Input } from "~/components/primitives/Input";
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { useUser } from "~/hooks/useUser";
import { adminGetOrganizations, adminGetUsers, setV3Enabled } from "~/models/admin.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { commitImpersonationSession, setImpersonationId } from "~/services/impersonation.server";
import { requireUserId } from "~/services/session.server";
import { createSearchParams } from "~/utils/searchParams";
export const SearchParams = z.object({
page: z.coerce.number().optional(),
search: z.string().optional(),
});
export type SearchParams = z.infer<typeof SearchParams>;
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const searchParams = createSearchParams(request.url, SearchParams);
if (!searchParams.success) {
throw new Error(searchParams.error);
}
const result = await adminGetOrganizations(userId, searchParams.params.getAll());
return typedjson(result);
};
const FormSchema = z.object({ id: z.string(), v3: z.enum(["enable", "disable"]) });
export async function action({ request }: ActionFunctionArgs) {
if (request.method.toLowerCase() !== "post") {
return new Response("Method not allowed", { status: 405 });
}
const userId = await requireUserId(request);
const payload = Object.fromEntries(await request.formData());
const { id, v3 } = FormSchema.parse(payload);
const result = await setV3Enabled(userId, id, v3 === "enable");
return redirectWithSuccessMessage("/admin/orgs", request, `v3 ${v3}d for org ${id}`);
}
export default function AdminDashboardRoute() {
const { organizations, filters, page, pageCount } = useTypedLoaderData<typeof loader>();
return (
<main
aria-labelledby="primary-heading"
className="flex h-full min-w-0 flex-1 flex-col overflow-y-auto px-4 pb-4 lg:order-last"
>
<div className=" space-y-4">
<Form className="flex items-center gap-2">
<Input
placeholder="Search users or orgs"
variant="small"
icon={MagnifyingGlassIcon}
fullWidth={true}
name="search"
defaultValue={filters.search}
/>
<Button type="submit" variant="tertiary/small">
Search
</Button>
</Form>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Slug</TableHeaderCell>
<TableHeaderCell>Members</TableHeaderCell>
<TableHeaderCell>id</TableHeaderCell>
<TableHeaderCell>v3?</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{organizations.length === 0 ? (
<TableBlankRow colSpan={9}>
<Paragraph>No orgs found for search</Paragraph>
</TableBlankRow>
) : (
organizations.map((org) => {
return (
<TableRow key={org.id}>
<TableCell>{org.title}</TableCell>
<TableCell>{org.slug}</TableCell>
<TableCell>
{org.members.map((member) => (
<LinkButton
key={member.user.email}
variant="minimal/small"
to={`/admin?search=${encodeURIComponent(member.user.email)}`}
>
{member.user.email}
</LinkButton>
))}
</TableCell>
<TableCell>{org.id}</TableCell>
<TableCell>{org.v3Enabled ? "✅" : ""}</TableCell>
<TableCell isSticky={true}>
<Form method="post" reloadDocument>
<input type="hidden" name="id" value={org.id} />
{org.v3Enabled ? (
<Button
type="submit"
name="v3"
value="disable"
className="mr-2"
variant="tertiary/small"
>
Disable v3
</Button>
) : (
<Button
type="submit"
name="v3"
value="enable"
className="mr-2"
variant="tertiary/small"
>
Enable v3
</Button>
)}
</Form>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<PaginationControls currentPage={page} totalPages={pageCount} />
</div>
</main>
);
}
+20
View File
@@ -1,6 +1,8 @@
import { Outlet } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { redirect, typedjson } from "remix-typedjson";
import { LinkButton } from "~/components/primitives/Buttons";
import { Tabs } from "~/components/primitives/Tabs";
import { getUser, requireUserId } from "~/services/session.server";
export async function loader({ request }: LoaderFunctionArgs) {
@@ -20,6 +22,24 @@ export async function loader({ request }: LoaderFunctionArgs) {
export default function Page() {
return (
<div className="h-full w-full">
<div className="flex items-center justify-between p-4">
<Tabs
tabs={[
{
label: "Users",
to: "/admin",
},
{
label: "Organizations",
to: "/admin/orgs",
},
]}
layoutId={"admin"}
/>
<LinkButton to="/" variant="tertiary/small" className="mb-4">
Back to me
</LinkButton>
</div>
<Outlet />
</div>
);
+9
View File
@@ -365,6 +365,15 @@ export function v3RunSpanPath(
return `${v3RunPath(organization, project, run)}/spans/${span.spanId}`;
}
export function v3TraceSpanPath(
organization: OrgForPath,
project: ProjectForPath,
traceId: string,
spanId: string
) {
return `${v3ProjectPath(organization, project)}/traces/${traceId}/spans/${spanId}`;
}
export function v3RunStreamingPath(
organization: OrgForPath,
project: ProjectForPath,
@@ -7,6 +7,7 @@ import {
SemanticInternalAttributes,
SpanEvent,
SpanEvents,
SpanMessagingEvent,
TaskEventStyle,
correctErrorStackTrace,
flattenAttributes,
@@ -96,6 +97,21 @@ export type PreparedEvent = Omit<TaskEventRecord, "events" | "style" | "duration
style: TaskEventStyle;
};
export type SpanLink =
| {
type: "run";
icon?: string;
title: string;
runId: string;
}
| {
type: "span";
icon?: string;
title: string;
traceId: string;
spanId: string;
};
export type SpanSummary = {
recordId: string;
id: string;
@@ -364,6 +380,37 @@ export class EventRepository {
const properties = sanitizedAttributes(fullEvent.properties);
const messagingEvent = SpanMessagingEvent.optional().safeParse((properties as any)?.messaging);
const links: SpanLink[] = [];
if (messagingEvent.success && messagingEvent.data) {
if ("id" in messagingEvent.data.message) {
if (messagingEvent.data.message.id.startsWith("run_")) {
links.push({
type: "run",
icon: "runs",
title: `Run ${messagingEvent.data.message.id}`,
runId: messagingEvent.data.message.id,
});
}
}
}
const backLinks = fullEvent.links as any as Link[] | undefined;
if (backLinks && backLinks.length > 0) {
backLinks.forEach((l) => {
links.push({
type: "span",
icon: "trigger",
title: `Triggered by`,
traceId: l.context.traceId,
spanId: l.context.spanId,
});
});
}
const events = transformEvents(span.data.events, fullEvent.metadata as Attributes);
return {
@@ -374,6 +421,7 @@ export class EventRepository {
properties,
events,
show,
links,
};
}
@@ -51,3 +51,13 @@ export function isExceptionSpanEvent(event: SpanEvent): event is ExceptionSpanEv
export function isCancellationSpanEvent(event: SpanEvent): event is CancellationSpanEvent {
return event.name === "cancellation";
}
export const SpanMessagingEvent = z.object({
system: z.string().optional(),
client_id: z.string().optional(),
operation: z.enum(["publish", "create", "receive", "deliver"]),
message: z.any(),
destination: z.string().optional(),
});
export type SpanMessagingEvent = z.infer<typeof SpanMessagingEvent>;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Organization" ADD COLUMN "v3Enabled" BOOLEAN NOT NULL DEFAULT false;
+2
View File
@@ -119,6 +119,8 @@ model Organization {
runsEnabled Boolean @default(true)
v3Enabled Boolean @default(false)
environments RuntimeEnvironment[]
connections IntegrationConnection[]
endpoints Endpoint[]