From 39de6061e252bd90effdd50436daf5174019657e Mon Sep 17 00:00:00 2001 From: kemal Date: Mon, 13 Jul 2026 16:41:18 +0100 Subject: [PATCH] feat: dummy journey timeline --- .../interfaces/Auth/Users/UserJourney.tsx | 263 ++++++++++++++++++ .../interfaces/Auth/Users/UserPanel.tsx | 14 +- 2 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 apps/studio/components/interfaces/Auth/Users/UserJourney.tsx diff --git a/apps/studio/components/interfaces/Auth/Users/UserJourney.tsx b/apps/studio/components/interfaces/Auth/Users/UserJourney.tsx new file mode 100644 index 00000000000..166869fa75b --- /dev/null +++ b/apps/studio/components/interfaces/Auth/Users/UserJourney.tsx @@ -0,0 +1,263 @@ +/** + * UserJourney — PROTOTYPE + * + * This is a demo-quality prototype of the "User Journey" tab. It renders a + * chronological timeline reconstructed from Auth and PostgREST logs using + * HARDCODED MOCK DATA — there are no real log queries wired up yet. + * + * See PRFAQ: https://www.notion.so/supabase/User-Journey-PRFAQ (placeholder link) + */ +import { useParams } from 'common' +import { AlertTriangle, Database, ExternalLink, LogIn, RefreshCw, UserPlus } from 'lucide-react' +import Link from 'next/link' +import { useState } from 'react' +import { Button, cn, Separator } from 'ui' +import { Admonition } from 'ui-patterns/admonition' +import { TimestampInfo } from 'ui-patterns/TimestampInfo' + +import { UserHeader } from './UserHeader' +import { PANEL_PADDING } from './Users.constants' +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { User } from '@/data/auth/users-infinite-query' + +type JourneyEventStatus = 'success' | 'neutral' | 'error' +type JourneyEventSource = 'auth' | 'postgrest' + +interface JourneyEvent { + id: string + timestamp: string // ISO, ms precision + source: JourneyEventSource + status: JourneyEventStatus + title: string + description: string + request?: { method: string; path: string; statusCode: number } + error?: { message: string; policy?: string; table?: string } +} + +const MOCK_JOURNEY_EVENTS: JourneyEvent[] = [ + { + id: '1', + timestamp: '2026-07-13T09:41:02.118Z', + source: 'auth', + status: 'success', + title: 'Signed up', + description: 'Auth · new user created via email signup', + }, + { + id: '2', + timestamp: '2026-07-13T09:41:02.421Z', + source: 'auth', + status: 'success', + title: 'Authenticated', + description: 'Auth · session issued, JWT minted', + }, + { + id: '3', + timestamp: '2026-07-13T09:41:03.905Z', + source: 'postgrest', + status: 'neutral', + title: 'Read profile', + description: 'PostgREST · fetched the current user profile', + request: { method: 'GET', path: '/rest/v1/profiles', statusCode: 200 }, + }, + { + id: '4', + timestamp: '2026-07-13T09:41:05.332Z', + source: 'postgrest', + status: 'neutral', + title: 'Created order', + description: 'PostgREST · inserted a new order row', + request: { method: 'POST', path: '/rest/v1/orders', statusCode: 201 }, + }, + { + id: '5', + timestamp: '2026-07-13T09:41:06.744Z', + source: 'postgrest', + status: 'error', + title: 'Write blocked', + description: 'PostgREST · insert rejected before reaching the table', + request: { method: 'POST', path: '/rest/v1/payments', statusCode: 403 }, + error: { + message: 'Denied by RLS policy', + policy: 'payments_insert_owner', + table: 'payments', + }, + }, +] + +const getEventIcon = (event: JourneyEvent) => { + if (event.status === 'error') return AlertTriangle + if (event.source === 'postgrest') return Database + return event.title === 'Signed up' ? UserPlus : LogIn +} + +const RequestLine = ({ request }: { request: NonNullable }) => ( + + {request.method} {request.path} · {request.statusCode} + +) + +interface UserJourneyProps { + user: User +} + +export const UserJourney = ({ user }: UserJourneyProps) => { + const { ref } = useParams() + const [errorsOnly, setErrorsOnly] = useState(false) + + const events = errorsOnly + ? MOCK_JOURNEY_EVENTS.filter((event) => event.status === 'error') + : MOCK_JOURNEY_EVENTS + + return ( +
+ + + + +
+
+
+

User journey

+
+ + Last 24 hours + +
+ +
+
+ +
+ +
+
+ + + + + + } + onClick={() => setErrorsOnly(false)} + tooltip={{ content: { text: 'Refresh' } }} + /> +
+
+ + {events.length === 0 ? ( + + ) : ( +
    + {events.map((event, index) => { + const Icon = getEventIcon(event) + const isLast = index === events.length - 1 + const isError = event.status === 'error' + + return ( +
  1. +
    +
    + +
    + {!isLast &&
    } +
    + +
    + {isError ? ( +
    +
    +
    +

    {event.title}

    + +
    +

    + {event.description} +

    + {event.request && ( +

    + +

    + )} +
    + {event.error && ( + <> + +
    +

    {event.error.message}

    +

    + policy + + {event.error.policy} + + on table + + {event.error.table} + +

    +
    + + )} +
    + ) : ( + <> +
    +

    {event.title}

    + +
    +

    {event.description}

    + {event.request && ( +

    + +

    + )} + + )} +
    +
  2. + ) + })} +
+ )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Auth/Users/UserPanel.tsx b/apps/studio/components/interfaces/Auth/Users/UserPanel.tsx index 5e9723437f0..35e37044ce6 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserPanel.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserPanel.tsx @@ -15,6 +15,7 @@ import { import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' +import { UserJourney } from './UserJourney' import { UserLogs } from './UserLogs' import { UserOverview } from './UserOverview' import { PANEL_PADDING } from './Users.constants' @@ -30,7 +31,7 @@ export const UserPanel = () => { parseAsString.withOptions({ history: 'push', clearOnDefault: true }) ) - const [view, setView] = useState<'overview' | 'raw' | 'logs'>('overview') + const [view, setView] = useState<'overview' | 'raw' | 'logs' | 'journey'>('overview') const [searchQuery, setSearchQuery] = useState('') const { data: selectedUser, isPending } = useUserQuery({ @@ -67,7 +68,7 @@ export const UserPanel = () => { setView(value as 'overview' | 'raw' | 'logs')} + onValueChange={(value) => setView(value as 'overview' | 'raw' | 'logs' | 'journey')} > {isPending ? (
@@ -91,6 +92,12 @@ export const UserPanel = () => { > Logs + + Journey + { {selectedUser && } + + {selectedUser && } +