diff --git a/.github/workflows/build_frontends.yml b/.github/workflows/build_frontends.yml index 2a2501429..f10cebcf5 100644 --- a/.github/workflows/build_frontends.yml +++ b/.github/workflows/build_frontends.yml @@ -4,8 +4,16 @@ name: Build node packages on: pull_request: branches: ["*"] + paths: + - "api/**" + - "packages/client-library-otel/**" + - "studio/**" push: branches: ["main", "release-*"] + paths: + - "api/**" + - "packages/client-library-otel/**" + - "studio/**" env: CARGO_TERM_COLOR: always diff --git a/examples/goose-quotes/drizzle/0001_last_captain_america.sql b/examples/goose-quotes/drizzle/0001_last_captain_america.sql new file mode 100644 index 000000000..af0a66cd4 --- /dev/null +++ b/examples/goose-quotes/drizzle/0001_last_captain_america.sql @@ -0,0 +1 @@ +ALTER TABLE "geese" ADD COLUMN "honks" integer DEFAULT 0; \ No newline at end of file diff --git a/examples/goose-quotes/drizzle/meta/0001_snapshot.json b/examples/goose-quotes/drizzle/meta/0001_snapshot.json new file mode 100644 index 000000000..26eb4b43a --- /dev/null +++ b/examples/goose-quotes/drizzle/meta/0001_snapshot.json @@ -0,0 +1,101 @@ +{ + "id": "d13ec396-5ca3-4ae5-8451-3442765c5abb", + "prevId": "301ac579-5843-4fa3-8b30-6012ab5546da", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.geese": { + "name": "geese", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_leader": { + "name": "is_leader", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "programming_language": { + "name": "programming_language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "motivations": { + "name": "motivations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "honks": { + "name": "honks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/examples/goose-quotes/drizzle/meta/_journal.json b/examples/goose-quotes/drizzle/meta/_journal.json index 23d1b2be6..1a74fcb56 100644 --- a/examples/goose-quotes/drizzle/meta/_journal.json +++ b/examples/goose-quotes/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1722995764012, "tag": "0000_talented_the_watchers", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1726591286939, + "tag": "0001_last_captain_america", + "breakpoints": true } ] } \ No newline at end of file diff --git a/examples/goose-quotes/src/db/client.ts b/examples/goose-quotes/src/db/client.ts index 305e0b63d..ee5a9b202 100644 --- a/examples/goose-quotes/src/db/client.ts +++ b/examples/goose-quotes/src/db/client.ts @@ -127,7 +127,24 @@ export const updateGoose = async ( updateData: Partial, ) => { console.log({ action: "updateGoose", id, updateData }); - return ( - await db.update(geese).set(updateData).where(eq(geese.id, id)).returning() - )[0]; + + // Simulate a race condition by splitting the update into two parts + const updatePromises = Object.entries(updateData).map( + async ([key, value]) => { + // Introduce a random delay to increase the chance of interleaved updates + await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); + + return db + .update(geese) + .set({ [key]: value }) + .where(eq(geese.id, id)) + .returning(); + }, + ); + + // Wait for all updates to complete + const results = await Promise.all(updatePromises); + + // Return the last result, which may not contain all updates + return results[results.length - 1][0]; }; diff --git a/examples/goose-quotes/src/db/schema.ts b/examples/goose-quotes/src/db/schema.ts index b85784f21..c364dd17f 100644 --- a/examples/goose-quotes/src/db/schema.ts +++ b/examples/goose-quotes/src/db/schema.ts @@ -1,5 +1,6 @@ import { boolean, + integer, jsonb, pgTable, serial, @@ -17,6 +18,7 @@ export const geese = pgTable("geese", { location: text("location"), bio: text("bio"), avatar: text("avatar"), + honks: integer("honks").default(0), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); diff --git a/examples/goose-quotes/src/index.ts b/examples/goose-quotes/src/index.ts index c558db5ea..e5384f859 100644 --- a/examples/goose-quotes/src/index.ts +++ b/examples/goose-quotes/src/index.ts @@ -290,8 +290,19 @@ app.post("/api/geese/:id/honk", async (c) => { return c.json({ message: "Goose not found" }, 404); } - console.log(`Honk received for goose: ${goose.name}`); - return c.json({ message: `Honk honk! ${goose.name} honks back at you!` }); + const currentHonks = goose.honks || 0; + + const updatedGoose = await measure("updateGoose", () => + updateGoose(db, +id, { honks: currentHonks + 1 }), + )(); + + console.log( + `Honk received for goose: ${goose.name}. New honk count: ${updatedGoose.honks}`, + ); + return c.json({ + message: `Honk honk! ${goose.name} honks back at you!`, + honks: updatedGoose.honks, + }); }); /** @@ -302,21 +313,35 @@ app.patch("/api/geese/:id", async (c) => { const db = drizzle(sql); const id = c.req.param("id"); - const { name } = await c.req.json(); + const updateData = await c.req.json(); - console.log(`Updating goose ${id} with new name: ${name}`); + console.log(`Updating goose ${id} with data:`, updateData); - const goose = await measure("updateGoose", () => - updateGoose(db, +id, { name }), - )(); + const goose = await measure("getGooseById", () => getGooseById(db, +id))(); if (!goose) { console.warn(`Goose not found: ${id}`); return c.json({ message: "Goose not found" }, 404); } + // Simulate a race condition by splitting the update into multiple parts + const updatePromises = Object.entries(updateData).map( + async ([key, value]) => { + await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); + return measure("updateGoose", () => + updateGoose(db, +id, { [key]: value }), + )(); + }, + ); + + await Promise.all(updatePromises); + + const updatedGoose = await measure("getGooseById", () => + getGooseById(db, +id), + )(); + console.log(`Goose ${id} updated successfully`); - return c.json(goose); + return c.json(updatedGoose); }); /** diff --git a/studio/src/Layout/BottomBar.tsx b/studio/src/Layout/BottomBar.tsx new file mode 100644 index 000000000..e1b907eb7 --- /dev/null +++ b/studio/src/Layout/BottomBar.tsx @@ -0,0 +1,172 @@ +import IconWithNotification from "@/components/IconWithNotification"; +import { KeyboardShortcutKey } from "@/components/KeyboardShortcut"; +import { WebhoncBadge } from "@/components/WebhoncBadge"; +import { Button } from "@/components/ui/button"; +import { useProxyRequestsEnabled } from "@/hooks/useProxyRequestsEnabled"; +import { useOrphanLogs } from "@/pages/RequestDetailsPage/RequestDetailsPageV2/useOrphanLogs"; +import { useRequestorStore } from "@/pages/RequestorPage/store"; +import { useOtelTrace } from "@/queries"; +import { cn } from "@/utils"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@radix-ui/react-tooltip"; +import { useEffect, useState } from "react"; +import { Branding } from "./Branding"; +import { SettingsMenu, SettingsScreen } from "./Settings"; +import { FloatingSidePanel } from "./SidePanel"; +import { SidePanelTrigger } from "./SidePanel"; + +export function BottomBar() { + const shouldShowProxyRequests = useProxyRequestsEnabled(); + + const [settingsOpen, setSettingsOpen] = useState(false); + + const { + logsPanel, + timelinePanel, + aiPanel, + togglePanel, + activeHistoryResponseTraceId, + } = useRequestorStore( + "togglePanel", + "logsPanel", + "timelinePanel", + "aiPanel", + "activeHistoryResponseTraceId", + ); + + const traceId = activeHistoryResponseTraceId ?? ""; + const { data: spans } = useOtelTrace(traceId); + const logs = useOrphanLogs(traceId, spans ?? []); + + const hasErrorLogs = logs.some((log) => log.level === "error"); + + useEffect(() => { + if (hasErrorLogs && logsPanel !== "open") { + togglePanel("logsPanel"); + } + }, [hasErrorLogs, logsPanel, togglePanel]); + + return ( + + ); +} diff --git a/studio/src/Layout/Branding.tsx b/studio/src/Layout/Branding.tsx new file mode 100644 index 000000000..170113079 --- /dev/null +++ b/studio/src/Layout/Branding.tsx @@ -0,0 +1,10 @@ +import FpLogo from "@/assets/fp-logo.svg"; + +export function Branding() { + return ( +
+ + Fiberplane +
+ ); +} diff --git a/studio/src/Layout/Layout.tsx b/studio/src/Layout/Layout.tsx index 96ee54e08..d2b64dc70 100644 --- a/studio/src/Layout/Layout.tsx +++ b/studio/src/Layout/Layout.tsx @@ -1,44 +1,13 @@ -import FpLogo from "@/assets/fp-logo.svg"; -import { KeyboardShortcutKey } from "@/components/KeyboardShortcut"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { useRequestorStore } from "@/pages/RequestorPage/store"; -import { Icon } from "@iconify/react"; -import { - DialogClose, - DialogContent, - DialogDescription, - Root, -} from "@radix-ui/react-dialog"; -import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; -import { - Menubar, - MenubarContent, - MenubarItem, - MenubarMenu, - MenubarSeparator, - MenubarTrigger, -} from "@radix-ui/react-menubar"; import type React from "react"; -import { useRef, useState } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; -import { WebhoncBadge } from "../components/WebhoncBadge"; -import { Button } from "../components/ui/button"; -import { DialogPortal, DialogTitle } from "../components/ui/dialog"; import { useWebsocketQueryInvalidation } from "../hooks"; -import { useProxyRequestsEnabled } from "../hooks/useProxyRequestsEnabled"; -import { SettingsPage } from "../pages/SettingsPage/SettingsPage"; import { cn } from "../utils"; -import { FloatingSidePanel } from "./FloatingSidePanel"; +import { BottomBar } from "./BottomBar"; export function Layout({ children }: { children?: React.ReactNode }) { useWebsocketQueryInvalidation(); return ( -
+
@@ -49,284 +18,4 @@ export function Layout({ children }: { children?: React.ReactNode }) { ); } -function Branding() { - return ( -
- - Fiberplane -
- ); -} - -function BottomBar() { - const shouldShowProxyRequests = useProxyRequestsEnabled(); - - const [settingsOpen, setSettingsOpen] = useState(false); - - const { logsPanel, timelinePanel, aiPanel, togglePanel } = useRequestorStore( - "togglePanel", - "logsPanel", - "timelinePanel", - "aiPanel", - ); - - return ( - - ); -} - -function SidePanelTrigger() { - const { sidePanel, togglePanel } = useRequestorStore( - "sidePanel", - "togglePanel", - ); - - useHotkeys("mod+b", () => { - togglePanel("sidePanel"); - }); - - return ( - - ); -} - -function MenuItemLink({ - href, - icon, - children, -}: { href: string; icon: React.ReactNode; children: React.ReactNode }) { - return ( - - - {icon} - {children} - - - ); -} - -function SettingsMenu({ - setSettingsOpen, -}: { setSettingsOpen: (open: boolean) => void }) { - const menuBarTriggerRef = useRef(null); - const [menuOpen, setMenuOpen] = useState(undefined); - - useHotkeys("shift+?", () => { - setMenuOpen(true); - if (menuBarTriggerRef.current) { - menuBarTriggerRef.current.click(); - } - }); - - return ( - - - - - - setMenuOpen(undefined)} - onInteractOutside={() => setMenuOpen(undefined)} - forceMount={menuOpen} - className="z-50 min-w-[200px] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md grid gap-1 data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2" - > - } - > - Docs - - } - > - GitHub - - } - > - Discord - - - setSettingsOpen(true)} - > -
- - Settings -
-
-
-
-
- ); -} - -function SettingsScreen({ - settingsOpen, - setSettingsOpen, -}: { settingsOpen: boolean; setSettingsOpen: (open: boolean) => void }) { - return ( - - - -
-
-
- Settings - - - -
-
- - Manage your settings and preferences. - - - -
-
-
-
-
-
- ); -} - export default Layout; diff --git a/studio/src/Layout/Settings/SettingsMenu.tsx b/studio/src/Layout/Settings/SettingsMenu.tsx new file mode 100644 index 000000000..919887fd5 --- /dev/null +++ b/studio/src/Layout/Settings/SettingsMenu.tsx @@ -0,0 +1,94 @@ +import { Icon } from "@iconify/react/dist/iconify.js"; +import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; +import { + Menubar, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarSeparator, + MenubarTrigger, +} from "@radix-ui/react-menubar"; +import { useRef, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +export function SettingsMenu({ + setSettingsOpen, +}: { setSettingsOpen: (open: boolean) => void }) { + const menuBarTriggerRef = useRef(null); + const [menuOpen, setMenuOpen] = useState(undefined); + + useHotkeys("shift+?", () => { + setMenuOpen(true); + if (menuBarTriggerRef.current) { + menuBarTriggerRef.current.click(); + } + }); + + return ( + + + + + + setMenuOpen(undefined)} + onInteractOutside={() => setMenuOpen(undefined)} + forceMount={menuOpen} + className="z-50 min-w-[200px] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md grid gap-1 data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2" + > + } + > + Docs + + } + > + GitHub + + } + > + Discord + + + setSettingsOpen(true)} + > +
+ + Settings +
+
+
+
+
+ ); +} + +function MenuItemLink({ + href, + icon, + children, +}: { href: string; icon: React.ReactNode; children: React.ReactNode }) { + return ( + + + {icon} + {children} + + + ); +} diff --git a/studio/src/Layout/Settings/SettingsScreen.tsx b/studio/src/Layout/Settings/SettingsScreen.tsx new file mode 100644 index 000000000..2bfb4e147 --- /dev/null +++ b/studio/src/Layout/Settings/SettingsScreen.tsx @@ -0,0 +1,44 @@ +import { Button } from "@/components/ui/button"; +import { SettingsPage } from "@/pages/SettingsPage"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogPortal, + DialogTitle, +} from "@radix-ui/react-dialog"; + +export function SettingsScreen({ + settingsOpen, + setSettingsOpen, +}: { settingsOpen: boolean; setSettingsOpen: (open: boolean) => void }) { + return ( + + + +
+
+
+ Settings + + + +
+
+ + Manage your settings and preferences. + + + +
+
+
+
+
+
+ ); +} diff --git a/studio/src/Layout/Settings/index.tsx b/studio/src/Layout/Settings/index.tsx new file mode 100644 index 000000000..fd7252c22 --- /dev/null +++ b/studio/src/Layout/Settings/index.tsx @@ -0,0 +1,2 @@ +export { SettingsMenu } from "./SettingsMenu"; +export { SettingsScreen } from "./SettingsScreen"; diff --git a/studio/src/Layout/FloatingSidePanel.tsx b/studio/src/Layout/SidePanel/FloatingSidePanel.tsx similarity index 91% rename from studio/src/Layout/FloatingSidePanel.tsx rename to studio/src/Layout/SidePanel/FloatingSidePanel.tsx index 7d3c252b9..95b92b873 100644 --- a/studio/src/Layout/FloatingSidePanel.tsx +++ b/studio/src/Layout/SidePanel/FloatingSidePanel.tsx @@ -1,4 +1,11 @@ +import { Button } from "@/components/ui/button"; import { useIsLgScreen } from "@/hooks"; +import { + NavigationFrame, + NavigationPanel, +} from "@/pages/RequestorPage/NavigationPanel"; +import { useRequestorStore } from "@/pages/RequestorPage/store"; +import { cn } from "@/utils"; import { Icon } from "@iconify/react"; import { DialogClose, @@ -9,13 +16,6 @@ import { DialogTitle, Root, } from "@radix-ui/react-dialog"; -import { Button } from "../components/ui/button"; -import { - NavigationFrame, - NavigationPanel, -} from "../pages/RequestorPage/NavigationPanel"; -import { useRequestorStore } from "../pages/RequestorPage/store"; -import { cn } from "../utils"; export function FloatingSidePanel() { const { sidePanel, togglePanel } = useRequestorStore( diff --git a/studio/src/Layout/SidePanel/SidePanelTrigger.tsx b/studio/src/Layout/SidePanel/SidePanelTrigger.tsx new file mode 100644 index 000000000..7df5126bc --- /dev/null +++ b/studio/src/Layout/SidePanel/SidePanelTrigger.tsx @@ -0,0 +1,28 @@ +import { Button } from "@/components/ui/button"; +import { useRequestorStore } from "@/pages/RequestorPage/store"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { useHotkeys } from "react-hotkeys-hook"; + +export function SidePanelTrigger() { + const { sidePanel, togglePanel } = useRequestorStore( + "sidePanel", + "togglePanel", + ); + + useHotkeys("mod+b", () => { + togglePanel("sidePanel"); + }); + + return ( + + ); +} diff --git a/studio/src/Layout/SidePanel/index.tsx b/studio/src/Layout/SidePanel/index.tsx new file mode 100644 index 000000000..8b022676b --- /dev/null +++ b/studio/src/Layout/SidePanel/index.tsx @@ -0,0 +1,2 @@ +export { SidePanelTrigger } from "./SidePanelTrigger"; +export { FloatingSidePanel } from "./FloatingSidePanel"; diff --git a/studio/src/components/IconWithNotification.tsx b/studio/src/components/IconWithNotification.tsx new file mode 100644 index 000000000..21d2dc571 --- /dev/null +++ b/studio/src/components/IconWithNotification.tsx @@ -0,0 +1,51 @@ +import { cn } from "@/utils"; +import { Icon, type IconProps } from "@iconify/react"; +import type React from "react"; + +interface IconWithNotificationProps extends IconProps { + notificationColor?: string; + notificationSize?: number; + notificationPosition?: + | "top-right" + | "top-left" + | "bottom-right" + | "bottom-left"; + notificationContent?: string | number; + showNotification?: boolean; +} + +const IconWithNotification: React.FC = ({ + notificationColor = "bg-red-500", + notificationSize = 10, + notificationPosition = "top-right", + notificationContent, + showNotification = true, + ...iconProps +}) => { + return ( +
+ + {showNotification && ( + + )} +
+ ); +}; + +export default IconWithNotification; diff --git a/studio/src/pages/RequestorPage/LogsTable/Empty.tsx b/studio/src/pages/RequestorPage/LogsTable/Empty.tsx new file mode 100644 index 000000000..844eaf3ec --- /dev/null +++ b/studio/src/pages/RequestorPage/LogsTable/Empty.tsx @@ -0,0 +1,22 @@ +import { Icon } from "@iconify/react"; + +export function LogsEmptyState() { + return ( +
+
+
+ +
+

No logs found

+

+ There are currently no logs to display. This could be because no + events have been logged yet, or the traces are not available. +

+
+
+ ); +} diff --git a/studio/src/pages/RequestorPage/LogsTable/LogsTable.tsx b/studio/src/pages/RequestorPage/LogsTable/LogsTable.tsx index 1cf1a3c26..40a71132e 100644 --- a/studio/src/pages/RequestorPage/LogsTable/LogsTable.tsx +++ b/studio/src/pages/RequestorPage/LogsTable/LogsTable.tsx @@ -4,104 +4,31 @@ import { } from "@/components/Timeline/utils"; import { Button } from "@/components/ui/button"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useCopyToClipboard } from "@/hooks"; +import type { MizuOrphanLog } from "@/queries"; import { useOtelTrace } from "@/queries"; -import { cn } from "@/utils"; -import { Cross1Icon } from "@radix-ui/react-icons"; +import { cn, safeParseJson } from "@/utils"; +import { CopyIcon, Cross1Icon } from "@radix-ui/react-icons"; import { Tabs } from "@radix-ui/react-tabs"; -import { - type ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table"; import { useState } from "react"; import { useOrphanLogs } from "../../RequestDetailsPage/RequestDetailsPageV2/useOrphanLogs"; import { CustomTabTrigger, CustomTabsContent, CustomTabsList } from "../Tabs"; import { useRequestorStore } from "../store"; - -type OrphanLog = { - traceId: string; - id: number; - timestamp: string; - level: "error" | "warn" | "info" | "debug"; - message: string | null; - args: unknown[]; - createdAt: string; - updatedAt: string; - callerLocation?: { file: string; line: string; column: string } | null; - ignored?: boolean | null; - service?: string | null; - relatedSpanId?: string | null; -}; +import { LogsEmptyState } from "./Empty"; type Props = { traceId?: string; }; -//TODO: add a better empty state export function LogsTable({ traceId = "" }: Props) { const { data: spans } = useOtelTrace(traceId); const { togglePanel } = useRequestorStore("togglePanel"); const logs = useOrphanLogs(traceId, spans ?? []); - const [expandedRowId, setExpandedRowId] = useState(null); - - const columns: ColumnDef[] = [ - { - accessorKey: "timestamp", - header: "Timestamp", - cell: ({ row }) => { - const isExpanded = expandedRowId === row.original.id; - return ( -
-
- {new Date(row.original.timestamp) - .toISOString() - .replace("T", " ") - .replace("Z", "")} -
- {isExpanded && ( -

- level:{" "} - - {row.original.level} - -

- )} -
- ); - }, - }, - { - accessorKey: "message", - header: "Message", - cell: ({ row }) => { - const isExpanded = expandedRowId === row.original.id; - return ( -
- {row.original.message} -
- ); - }, - }, - ]; return ( @@ -118,105 +45,133 @@ export function LogsTable({ traceId = "" }: Props) {
- - + + {logs.length === 0 ? ( + + ) : ( +
+ {logs.map((log) => ( + + ))} +
+ )}
); } -type TableProps = { - columns: ColumnDef[]; - data: OrphanLog[]; - expandedRowId: number | null; - setExpandedRowId: (id: number | null) => void; +type LogRowProps = { + log: MizuOrphanLog; }; -function TableContent({ - columns, - data, - expandedRowId, - setExpandedRowId, -}: TableProps) { - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - }); +function LogRow({ log }: LogRowProps) { + const bgColor = getBgColorForLevel(log.level); + const textColor = getTextColorForLevel(log.level); + const [isExpanded, setIsExpanded] = useState(false); + // we don't want the focus ring to be visible when the user is selecting the row with the mouse + const [isMouseSelected, setIsMouseSelected] = useState(false); + const { isCopied, copyToClipboard } = useCopyToClipboard(); + return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => { - const bgColor = getBgColorForLevel(row.original.level); - const isExpanded = expandedRowId === row.original.id; - return ( - - setExpandedRowId(isExpanded ? null : row.original.id) - } - > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ); - }) - ) : ( - - - No logs found. - - +
setIsExpanded(e.currentTarget.open)} + onMouseDown={() => setIsMouseSelected(true)} + onBlur={() => setIsMouseSelected(false)} + > + -
+ > +
+
+ {log.message} +
+
+ {formatTimestamp(log.timestamp)} +
+ +
+ {/* +
+ */} +
+

+ Level: {log.level.toUpperCase()} +

+ {log.service &&

Service: {log.service}

} + {log.callerLocation && ( +

+ Location: {log.callerLocation.file}:{log.callerLocation.line}: + {log.callerLocation.column} +

+ )} + {log.message && ( +
+

Message:

+

+ {safeParseJson(log.message) ? ( +

+                    {JSON.stringify(JSON.parse(log.message), null, 2)}
+                  
+ ) : ( + log.message + )} +

+
+ )} +
+
+ + + + + + +

Message copied

+
+
+
+
+
+ ); } + +function getIconColor(level: MizuOrphanLog["level"]) { + switch (level) { + case "error": + return "bg-red-500"; + case "warn": + return "bg-yellow-500"; + case "info": + return "bg-blue-500"; + case "debug": + return "bg-green-500"; + default: + return "bg-gray-500"; + } +} + +function formatTimestamp(timestamp: Date) { + return timestamp.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} diff --git a/studio/src/pages/RequestorPage/RequestPanel/RequestPanel.tsx b/studio/src/pages/RequestorPage/RequestPanel/RequestPanel.tsx index 1a834e7f0..dce0feb91 100644 --- a/studio/src/pages/RequestorPage/RequestPanel/RequestPanel.tsx +++ b/studio/src/pages/RequestorPage/RequestPanel/RequestPanel.tsx @@ -337,15 +337,17 @@ export function PanelSectionHeader({ {children} {handleClearData && ( - + )}
); diff --git a/studio/src/pages/RequestorPage/RequestorPage.tsx b/studio/src/pages/RequestorPage/RequestorPage.tsx index b27bb0b4b..fb63fe0d5 100644 --- a/studio/src/pages/RequestorPage/RequestorPage.tsx +++ b/studio/src/pages/RequestorPage/RequestorPage.tsx @@ -81,8 +81,7 @@ export const RequestorPage = () => { "flex", "flex-col", "gap-2", - "py-4 px-2", - "sm:px-4 sm:py-3", + "p-2", "lg:gap-4", )} > diff --git a/www/src/content/changelog/!canary.mdx b/www/src/content/changelog/!canary.mdx new file mode 100644 index 000000000..c908d70b5 --- /dev/null +++ b/www/src/content/changelog/!canary.mdx @@ -0,0 +1,9 @@ +--- +date: 2024-09-20 +version: canary +draft: true +--- + +### Features + +- **Improved logs panel.** The logs panel now shows up automatically when there are error-level logs recorded (the logs icon in the bottom-right corner also shows a red dot). The panel itself is now more responsive, readable, and features a button for quickly copying the message to clipboard. diff --git a/www/src/content/config.ts b/www/src/content/config.ts index 46eadc547..d9b12857b 100644 --- a/www/src/content/config.ts +++ b/www/src/content/config.ts @@ -27,7 +27,8 @@ const changelog = defineCollection({ type: "content", schema: z.object({ date: z.coerce.date(), - version: z.string() + version: z.string(), + draft: z.boolean().optional() }) }); diff --git a/www/src/pages/changelog.astro b/www/src/pages/changelog.astro index 789854986..753bdca52 100644 --- a/www/src/pages/changelog.astro +++ b/www/src/pages/changelog.astro @@ -9,6 +9,7 @@ const changelogEntries = await Promise.all( (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime() ) + .filter((entry) => !entry.data.draft) .map(async (entry) => ({ ...entry, content: await entry.render()