This commit is contained in:
Alan Daniel committed 2026-02-15 18:28:49 -04:00
1 parent e58ea5ab1b
commit a7cfb94674
11 files changed
+873 -320

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
export { SupaMock } from './src/SupaMock'
export type { SupaMockProps } from './src/types'
export type { SupaMockProps, SupaMockOrganization } from './src/types'
+2
View File
@@ -11,6 +11,7 @@ export function SupaMock({
organizationName,
organizationPlan,
branchName,
organizations,
}: SupaMockProps) {
return (
<div
@@ -26,6 +27,7 @@ export function SupaMock({
organizationName={organizationName}
organizationPlan={organizationPlan}
branchName={branchName}
organizations={organizations}
>
<MockDashboardLayout />
</MockProviders>
+8 -36
View File
@@ -1,6 +1,8 @@
import { Badge, Button, cn } from 'ui'
import { Boxes, Box, ChevronsUpDown, Plug } from 'lucide-react'
import { Plug } from 'lucide-react'
import { Button, cn } from 'ui'
import { useMockProject } from '../providers/MockProjectContext'
import { BranchDropdown, OrgDropdown, ProjectDropdown } from './MockHeaderDropdowns'
const LayoutHeaderDivider = ({ className }: { className?: string }) => (
<span className={cn('text-border-stronger pr-2', className)}>
@@ -21,7 +23,7 @@ const LayoutHeaderDivider = ({ className }: { className?: string }) => (
)
export function MockHeader() {
const { project } = useMockProject()
const { project, organizations } = useMockProject()
return (
<header className="flex h-12 items-center flex-shrink-0 border-b">
@@ -77,44 +79,14 @@ export function MockHeader() {
</div>
<div className="flex items-center pl-2">
{/* Organization */}
<LayoutHeaderDivider />
<div className="flex items-center gap-2 flex-shrink-0 text-sm">
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
<span className="text-foreground">{project.organization.name}</span>
<Badge variant="default">{project.organization.plan}</Badge>
</div>
<Button
type="text"
className="px-1.5 py-4 [&_svg]:w-5 [&_svg]:h-5 ml-1"
iconRight={<ChevronsUpDown strokeWidth={1.5} />}
/>
<OrgDropdown project={project} organizations={organizations} />
{/* Project */}
<LayoutHeaderDivider />
<div className="flex items-center gap-2 flex-shrink-0 text-sm">
<Box size={14} strokeWidth={1.5} className="text-foreground-lighter" />
<span className="text-foreground">{project.name}</span>
</div>
<Button
type="text"
className="px-1.5 py-4 [&_svg]:w-5 [&_svg]:h-5 ml-1"
iconRight={<ChevronsUpDown strokeWidth={1.5} />}
/>
<ProjectDropdown project={project} />
{/* Branch */}
<LayoutHeaderDivider />
<div className="flex items-center gap-2 flex-shrink-0 text-sm">
<span className="text-foreground">{project.branchName}</span>
<Badge variant="warning" className="mt-[1px]">
Production
</Badge>
</div>
<Button
type="text"
className="px-1.5 py-4 [&_svg]:w-5 [&_svg]:h-5 ml-1"
iconRight={<ChevronsUpDown strokeWidth={1.5} />}
/>
<BranchDropdown project={project} />
{/* Connect button */}
<div className="ml-3 flex items-center">
@@ -0,0 +1,198 @@
import {
Box,
Boxes,
Check,
ChevronsUpDown,
GitBranch,
ListFilter,
MessageCircle,
Plus,
} from 'lucide-react'
import { useState } from 'react'
import {
Badge,
Command_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
CommandSeparator_Shadcn_,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
} from 'ui'
import type { MockProject, SupaMockOrganization } from '../types'
// ─── Organization Dropdown ──────────────────────────────────────────────
export function OrgDropdown({
project,
organizations,
}: {
project: MockProject
organizations: SupaMockOrganization[]
}) {
const [open, setOpen] = useState(false)
const hasOrgs = organizations.length > 0
return (
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center gap-2 flex-shrink-0 text-sm cursor-pointer">
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
<span className="text-foreground">{project.organization.name}</span>
<Badge variant="default">{project.organization.plan}</Badge>
<ChevronsUpDown size={16} strokeWidth={1.5} className="text-foreground-muted" />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-64" side="bottom" align="start">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find organization..." />
<CommandList_Shadcn_>
<CommandGroup_Shadcn_>
{hasOrgs ? (
organizations.map((org) => {
const isCurrent =
org.slug === project.organization.slug ||
org.name === project.organization.name
return (
<CommandItem_Shadcn_
key={org.slug}
onSelect={() => {
window.open(
`https://supabase.com/dashboard/org/${org.slug}`,
'_blank',
'noopener,noreferrer'
)
setOpen(false)
}}
className="flex items-center justify-between gap-2 cursor-pointer"
>
<span className="truncate">{org.name}</span>
{isCurrent && (
<Check size={14} strokeWidth={2} className="flex-shrink-0" />
)}
</CommandItem_Shadcn_>
)
})
) : (
<CommandItem_Shadcn_ className="flex items-center justify-between gap-2">
<span>{project.organization.name}</span>
<Check size={14} strokeWidth={2} className="flex-shrink-0" />
</CommandItem_Shadcn_>
)}
</CommandGroup_Shadcn_>
<CommandSeparator_Shadcn_ />
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_ className="cursor-pointer">
All Organizations
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
<CommandSeparator_Shadcn_ />
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_ className="flex items-center gap-2 cursor-pointer">
<Plus size={14} strokeWidth={1.5} />
<span>New organization</span>
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
// ─── Project Dropdown ───────────────────────────────────────────────────
export function ProjectDropdown({ project }: { project: MockProject }) {
const [open, setOpen] = useState(false)
return (
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center gap-2 flex-shrink-0 text-sm cursor-pointer">
<Box size={14} strokeWidth={1.5} className="text-foreground-lighter" />
<span className="text-foreground">{project.name}</span>
<ChevronsUpDown size={16} strokeWidth={1.5} className="text-foreground-muted" />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-64" side="bottom" align="start">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find project..." />
<CommandList_Shadcn_>
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_ className="flex items-center justify-between gap-2">
<span>{project.name}</span>
<Check size={14} strokeWidth={2} className="flex-shrink-0" />
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
<CommandSeparator_Shadcn_ />
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_ className="flex items-center gap-2 cursor-pointer">
<Plus size={14} strokeWidth={1.5} />
<span>New project</span>
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
// ─── Branch Dropdown ────────────────────────────────────────────────────
export function BranchDropdown({ project }: { project: MockProject }) {
const [open, setOpen] = useState(false)
return (
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center gap-2 flex-shrink-0 text-sm cursor-pointer">
<span className="text-foreground">{project.branchName}</span>
<Badge variant="warning" className="mt-[1px]">
Production
</Badge>
<ChevronsUpDown size={16} strokeWidth={1.5} className="text-foreground-muted" />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-64" side="bottom" align="start">
<Command_Shadcn_>
<CommandList_Shadcn_>
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_ className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<GitBranch size={14} strokeWidth={1.5} className="text-warning" />
<span>{project.branchName}</span>
</div>
<Check size={14} strokeWidth={2} className="flex-shrink-0" />
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
<CommandSeparator_Shadcn_ />
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_ className="flex items-center gap-2 cursor-pointer">
<Plus size={14} strokeWidth={1.5} />
<span>Create branch</span>
</CommandItem_Shadcn_>
<CommandItem_Shadcn_ className="flex items-center gap-2 cursor-pointer">
<ListFilter size={14} strokeWidth={1.5} />
<span>Manage branches</span>
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
<CommandSeparator_Shadcn_ />
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_ className="flex items-center gap-2 cursor-pointer">
<MessageCircle size={14} strokeWidth={1.5} />
<div className="flex flex-col">
<span>Branching feedback</span>
<span className="text-xs text-foreground-lighter">Join GitHub Discussion</span>
</div>
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
@@ -1,6 +1,6 @@
import { createContext, useContext, useMemo, type ReactNode } from 'react'
import type { MockProject, MockProjectContextType } from '../types'
import type { MockProject, MockProjectContextType, SupaMockOrganization } from '../types'
const MockProjectContext = createContext<MockProjectContextType | null>(null)
@@ -17,6 +17,7 @@ interface MockProjectProviderProps {
organizationName?: string
organizationPlan?: string
branchName?: string
organizations?: SupaMockOrganization[]
children: ReactNode
}
@@ -38,6 +39,7 @@ export function MockProjectProvider({
organizationName,
organizationPlan,
branchName,
organizations = [],
children,
}: MockProjectProviderProps) {
const project = useMemo<MockProject>(
@@ -64,5 +66,9 @@ export function MockProjectProvider({
[projectName, organizationName, organizationPlan, branchName]
)
return <MockProjectContext.Provider value={{ project }}>{children}</MockProjectContext.Provider>
return (
<MockProjectContext.Provider value={{ project, organizations }}>
{children}
</MockProjectContext.Provider>
)
}
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react'
import { MockRouterProvider } from '../router/MockRouterContext'
import type { SupaMockOrganization } from '../types'
import { MockProjectProvider } from './MockProjectContext'
import { MockUserProvider } from './MockUserContext'
@@ -10,6 +11,7 @@ interface MockProvidersProps {
organizationName?: string
organizationPlan?: string
branchName?: string
organizations?: SupaMockOrganization[]
children: ReactNode
}
@@ -19,6 +21,7 @@ export function MockProviders({
organizationName,
organizationPlan,
branchName,
organizations,
children,
}: MockProvidersProps) {
return (
@@ -28,6 +31,7 @@ export function MockProviders({
organizationName={organizationName}
organizationPlan={organizationPlan}
branchName={branchName}
organizations={organizations}
>
<MockUserProvider>{children}</MockUserProvider>
</MockProjectProvider>
+28 -50
View File
@@ -97,58 +97,36 @@ interface MockAuthUser {
}
const MOCK_NAMES: (string | null)[] = [
null,
null,
null,
null,
'Afonso Resendes',
null,
'Aisulu Kalshabekova',
null,
'Alan Daniel',
null,
null,
null,
null,
null,
'Alyssa Gerasimoff',
null,
null,
null,
null,
'asd',
null,
null,
null,
'Ben Miller',
'Alice Johnson',
'Bob Smith',
'Carol Williams',
'Dave Brown',
'Eve Davis',
'Frank Wilson',
'Grace Lee',
'Henry Taylor',
'Ivy Chen',
'Jack Martin',
'Karen White',
'Leo Garcia',
'Mia Robinson',
'Noah Clark',
'Olivia Hall',
'Paul Adams',
'Quinn Foster',
'Ruby Scott',
'Sam Turner',
'Tina Brooks',
'Uma Patel',
'Victor Hayes',
'Wendy Ross',
'Xander Cole',
]
const MOCK_EMAILS = [
'a@thesimple.co',
'a34mail2@gmail.com',
'abdulrahimmirani@gmail.com',
'abhi.jain@sjsu.edu',
'adaaanniel@gmail.com',
'adambowker98@gmail.com',
'admin@admin.com',
'afischbeck@gmx.com',
'afonsoresendes03@gmail.com',
'agrancini@snapchat.com',
'aisulu@icecore.ai',
'alaister@supabase.io',
'alan.daniel@supabase.io',
'alejandra@domu.ai',
'alek1122t@gmail.com',
'alhwyn@alhwyn.com',
'aliciaww@gmail.com',
'alroma79@gmail.com',
'alvinapink@virgilian.com',
'alyssag022@gmail.com',
'amicus_nays.5y@icloud.com',
'ananya.mac98@gmail.com',
'anasjapananime7711@gmail.com',
'ben.miller@example.com',
]
const MOCK_EMAILS = MOCK_NAMES.map((name) => {
const [first, last] = name!.toLowerCase().split(' ')
return `${first}.${last}@example.com`
})
const MOCK_USERS: MockAuthUser[] = Array.from({ length: 24 }, (_, i) => ({
id: mockUUID(i + 7700),
+343 -105
View File
@@ -1,5 +1,6 @@
import { Auth, Database, Realtime, Storage } from 'icons'
import { Activity, ExternalLink, Shield } from 'lucide-react'
import { ChevronLeft, ExternalLink, Shield, X } from 'lucide-react'
import { useMemo, useState } from 'react'
import {
Badge,
Button,
@@ -23,28 +24,116 @@ import { useMockProject } from '../providers/MockProjectContext'
// ─── Mock Data ──────────────────────────────────────────────────────────
function seededRandom(seed: number): number {
const x = Math.sin(seed + 1) * 10000
return x - Math.floor(x)
}
const MOCK_STATS = { tables: 20, functions: 15, replicas: 0 }
const MOCK_USAGE_DATA = Array.from({ length: 24 }, (_, i) => ({
hour: `${i}:00`,
rest: Math.floor(Math.random() * 200 + 50),
auth: Math.floor(Math.random() * 80 + 10),
storage: Math.floor(Math.random() * 60 + 5),
realtime: Math.floor(Math.random() * 40 + 5),
}))
type TimeRange = '60min' | '24h' | '7d'
const MOCK_SECURITY_LINTS = [
'Function `public.sudo` has a role mutable search_path',
'Function `public.sudo` has a role mutable search_path',
'Function `public.get_storage_base_path` has a role mutable search_path',
'Function `public.euro_worker` has a role mutable search_path',
'Function `public.get_secret` has a role mutable search_path',
'Function `public.add_cron_job` has a role mutable search_path',
const TIME_RANGE_OPTIONS: { key: TimeRange; label: string; description: string }[] = [
{ key: '60min', label: 'Last 60 minutes', description: 'Statistics for last 60 minutes' },
{ key: '24h', label: 'Last 24 hours', description: 'Statistics for last 24 hours' },
{ key: '7d', label: 'Last 7 days', description: 'Statistics for last 7 days' },
]
function generateUsageData(range: TimeRange) {
const offset = range === '60min' ? 100 : range === '7d' ? 200 : 0
const count = range === '60min' ? 12 : range === '7d' ? 7 : 24
const restScale = range === '60min' ? 30 : range === '7d' ? 1400 : 200
const restBase = range === '60min' ? 5 : range === '7d' ? 300 : 50
const authScale = range === '60min' ? 10 : range === '7d' ? 500 : 80
const authBase = range === '60min' ? 1 : range === '7d' ? 60 : 10
const storageScale = range === '60min' ? 8 : range === '7d' ? 400 : 60
const storageBase = range === '60min' ? 0 : range === '7d' ? 30 : 5
const realtimeScale = range === '60min' ? 6 : range === '7d' ? 300 : 40
const realtimeBase = range === '60min' ? 1 : range === '7d' ? 20 : 5
return Array.from({ length: count }, (_, i) => ({
rest: Math.floor(seededRandom((i + offset) * 4) * restScale + restBase),
auth: Math.floor(seededRandom((i + offset) * 4 + 1) * authScale + authBase),
storage: Math.floor(seededRandom((i + offset) * 4 + 2) * storageScale + storageBase),
realtime: Math.floor(seededRandom((i + offset) * 4 + 3) * realtimeScale + realtimeBase),
}))
}
interface SecurityLint {
summary: string
title: string
severity: 'warning' | 'error'
entity: string
issue: string
description: string
}
const MOCK_SECURITY_LINTS: SecurityLint[] = [
{
summary: 'Table `public.products` has a row level security policy `Allow read...',
title: 'Auth RLS Initialization Plan',
severity: 'warning',
entity: 'public.products',
issue:
'Table `public.products` has a row level security policy `Allow read access` that re-evaluates `current_setting()` or `auth.<function>()` for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing `auth.<function>()` with `(select auth.<function>())`.',
description:
'Detects if calls to `current_setting()` and `auth.<function>()` in RLS policies are being unnecessarily re-evaluated for each row.',
},
{
summary: 'Table `public.products` has a row level security policy `Allow insert...',
title: 'Auth RLS Initialization Plan',
severity: 'warning',
entity: 'public.products',
issue:
'Table `public.products` has a row level security policy `Allow insert` that re-evaluates `current_setting()` or `auth.<function>()` for each row. This produces suboptimal query performance at scale.',
description:
'Detects if calls to `current_setting()` and `auth.<function>()` in RLS policies are being unnecessarily re-evaluated for each row.',
},
{
summary: 'Table `public.orders` has a row level security policy `Allow auth...',
title: 'Auth RLS Initialization Plan',
severity: 'warning',
entity: 'public.orders',
issue:
'Table `public.orders` has a row level security policy `Allow auth users` that re-evaluates `current_setting()` or `auth.<function>()` for each row.',
description:
'Detects if calls to `current_setting()` and `auth.<function>()` in RLS policies are being unnecessarily re-evaluated for each row.',
},
{
summary: 'Table `public.orders` has a row level security policy `Allow user...',
title: 'Auth RLS Initialization Plan',
severity: 'warning',
entity: 'public.orders',
issue:
'Table `public.orders` has a row level security policy `Allow user access` that re-evaluates `current_setting()` or `auth.<function>()` for each row.',
description:
'Detects if calls to `current_setting()` and `auth.<function>()` in RLS policies are being unnecessarily re-evaluated for each row.',
},
{
summary: 'Function `public.get_config` has a role mutable search_path',
title: 'Function Search Path Mutable',
severity: 'warning',
entity: 'public.get_config',
issue:
'Function `public.get_config` has a role mutable `search_path`, meaning the function could potentially be hijacked by a malicious user.',
description:
'Detects functions where the search_path is mutable, which could allow search path injection attacks.',
},
{
summary: 'Function `public.run_task` has a role mutable search_path',
title: 'Function Search Path Mutable',
severity: 'warning',
entity: 'public.run_task',
issue:
'Function `public.run_task` has a role mutable `search_path`, meaning the function could potentially be hijacked by a malicious user.',
description:
'Detects functions where the search_path is mutable, which could allow search path injection attacks.',
},
]
const MOCK_SLOW_QUERIES = [
{ query: 'select edge.generate_embedding($1) limit $2', avgTime: '2.15s', calls: 8 },
{ query: 'select public.euro_worker( jsonb_build_object($1,$2))', avgTime: '4.10s', calls: 12 },
{ query: 'select public.compute_aggregates($1) limit $2', avgTime: '2.15s', calls: 8 },
{ query: 'select public.sync_records(jsonb_build_object($1,$2))', avgTime: '4.10s', calls: 12 },
{ query: 'SELECT name FROM pg_timezone_names', avgTime: '0.47s', calls: 192 },
{ query: 'with table_info as ( select n.nspname::text as schem...', avgTime: '5.37s', calls: 1 },
{ query: 'with table_info as ( select n.nspname::text as schem...', avgTime: '5.26s', calls: 1 },
@@ -52,21 +141,36 @@ const MOCK_SLOW_QUERIES = [
// ─── Mini Bar Chart (mocked) ────────────────────────────────────────────
function MiniBarChart({ data, color = '#3ECF8E' }: { data: number[]; color?: string }) {
function MiniBarChart({
data,
color = '#3ECF8E',
animationKey,
}: {
data: number[]
color?: string
animationKey?: string
}) {
const max = Math.max(...data, 1)
return (
<div className="flex items-end gap-[2px] h-20 w-full">
<div key={animationKey} className="flex items-end gap-[2px] h-20 w-full">
{data.map((val, i) => (
<div
key={i}
className="flex-1 rounded-sm min-h-[2px]"
className="flex-1 rounded-sm min-h-[2px] origin-bottom animate-[bar-grow_0.2s_ease-in-out_both]"
style={{
height: `${(val / max) * 100}%`,
backgroundColor: color,
opacity: 0.7,
animationDelay: `${i * 15}ms`,
}}
/>
))}
<style>{`
@keyframes bar-grow {
from { transform: scaleY(0); }
to { transform: scaleY(1); }
}
`}</style>
</div>
)
}
@@ -79,12 +183,14 @@ function UsagePanel({
metricLabel,
data,
total,
animationKey,
}: {
icon: React.ReactNode
title: string
metricLabel: string
data: number[]
total: number
animationKey?: string
}) {
return (
<div className="rounded-md border bg-surface-100 shadow-sm overflow-hidden">
@@ -97,7 +203,7 @@ function UsagePanel({
<p className="text-xs text-foreground-lighter mb-1">{metricLabel}</p>
<p className="text-xl tabular-nums text-foreground">{total.toLocaleString()}</p>
</div>
<MiniBarChart data={data} />
<MiniBarChart data={data} animationKey={animationKey} />
</div>
</div>
)
@@ -105,7 +211,89 @@ function UsagePanel({
// ─── Advisor Widget ─────────────────────────────────────────────────────
function MockAdvisorWidget() {
function InlineCode({ children }: { children: string }) {
return <code className="text-xs bg-surface-300 px-1 py-0.5 rounded font-mono">{children}</code>
}
function RenderWithCode({ text }: { text: string }) {
return (
<>
{text
.split('`')
.map((part, i) =>
i % 2 === 1 ? <InlineCode key={i}>{part}</InlineCode> : <span key={i}>{part}</span>
)}
</>
)
}
function LintDetailPanel({ lint, onClose }: { lint: SecurityLint; onClose: () => void }) {
return (
<div className="absolute top-0 right-0 h-full w-[420px] border-l bg-dash-sidebar flex flex-col z-10">
<div className="flex items-center gap-2 px-4 min-h-[46px] border-b">
<button
onClick={onClose}
className="text-foreground-lighter hover:text-foreground transition-colors"
>
<ChevronLeft size={16} strokeWidth={1.5} />
</button>
<span className="text-sm truncate flex-1">{lint.title}</span>
<Badge variant="warning" className="uppercase !text-[10px] flex-shrink-0">
{lint.severity}
</Badge>
<button
onClick={onClose}
className="text-foreground-lighter hover:text-foreground transition-colors ml-1"
>
<X size={16} strokeWidth={1.5} />
</button>
</div>
<div className="flex-1 overflow-y-auto px-5 py-6 space-y-6">
<div className="space-y-2">
<p className="text-sm text-foreground-light">Entity</p>
<div className="inline-flex items-center gap-1.5 bg-surface-200 border rounded px-2 py-1">
<Database size={12} strokeWidth={1.5} className="text-foreground-muted" />
<span className="text-sm font-mono">{lint.entity}</span>
</div>
</div>
<div className="space-y-2">
<p className="text-sm text-foreground-light">Issue</p>
<p className="text-sm text-foreground-lighter leading-relaxed">
<RenderWithCode text={lint.issue} />
{'. '}
See <button className="text-foreground-light underline underline-offset-2">
docs
</button>{' '}
for more info.
</p>
</div>
<div className="space-y-2">
<p className="text-sm text-foreground-light">Description</p>
<p className="text-sm text-foreground-lighter leading-relaxed">
<RenderWithCode text={lint.description} />
</p>
</div>
<div className="space-y-3">
<p className="text-sm text-foreground-light">Resolve</p>
<div className="flex items-center gap-2">
<Button type="alternative" size="tiny">
Ask Assistant
</Button>
<Button type="default" size="tiny">
View policies
</Button>
<button className="flex items-center gap-1 text-xs text-foreground-lighter hover:text-foreground transition-colors ml-1">
Learn more
<ExternalLink size={12} strokeWidth={1.5} />
</button>
</div>
</div>
</div>
</div>
)
}
function MockAdvisorWidget({ onSelectLint }: { onSelectLint: (lint: SecurityLint) => void }) {
const totalIssues = MOCK_SECURITY_LINTS.length
return (
@@ -141,14 +329,20 @@ function MockAdvisorWidget() {
<TabsContent value="security" className="p-0 mt-0 h-full">
<ul>
{MOCK_SECURITY_LINTS.map((lint, i) => (
<li key={i} className="text-sm w-full border-b my-0 last:border-b-0 group px-4">
<li
key={i}
className="text-sm w-full border-b my-0 last:border-b-0 group px-4 cursor-pointer hover:bg-surface-200 transition-colors"
onClick={() => onSelectLint(lint)}
>
<div className="flex items-center gap-2 py-3">
<Shield
size={14}
strokeWidth={1.5}
className="text-foreground-muted flex-shrink-0"
/>
<p className="font-mono text-xs text-foreground-light truncate">{lint}</p>
<p className="font-mono text-xs text-foreground-light truncate">
{lint.summary}
</p>
</div>
</li>
))}
@@ -206,100 +400,144 @@ function MockAdvisorWidget() {
export function HomeScreen() {
const { project } = useMockProject()
const [timeRange, setTimeRange] = useState<TimeRange>('24h')
const [rangeDropdownOpen, setRangeDropdownOpen] = useState(false)
const [selectedLint, setSelectedLint] = useState<SecurityLint | null>(null)
const restTotal = MOCK_USAGE_DATA.reduce((sum, d) => sum + d.rest, 0)
const authTotal = MOCK_USAGE_DATA.reduce((sum, d) => sum + d.auth, 0)
const storageTotal = MOCK_USAGE_DATA.reduce((sum, d) => sum + d.storage, 0)
const realtimeTotal = MOCK_USAGE_DATA.reduce((sum, d) => sum + d.realtime, 0)
const usageData = useMemo(() => generateUsageData(timeRange), [timeRange])
const selectedOption = TIME_RANGE_OPTIONS.find((o) => o.key === timeRange)!
const restTotal = usageData.reduce((sum, d) => sum + d.rest, 0)
const authTotal = usageData.reduce((sum, d) => sum + d.auth, 0)
const storageTotal = usageData.reduce((sum, d) => sum + d.storage, 0)
const realtimeTotal = usageData.reduce((sum, d) => sum + d.realtime, 0)
return (
<div className="w-full px-4">
{/* Section 1: Header */}
<div className="py-8 border-b border-muted">
<div className="mx-auto max-w-7xl">
<div className="flex flex-col md:flex-row md:items-center gap-6 justify-between w-full">
<div className="flex flex-col md:flex-row md:items-end gap-3">
<h1 className="text-3xl">{project.name}</h1>
<div className="flex items-center gap-x-2 mb-1">
<Badge variant="default">Nano</Badge>
<div className="relative w-full h-full">
<div className="w-full h-full overflow-auto px-4">
{/* Section 1: Header */}
<div className="py-8 border-b border-muted">
<div className="mx-auto max-w-7xl">
<div className="flex flex-col md:flex-row md:items-center gap-6 justify-between w-full">
<div className="flex flex-col md:flex-row md:items-end gap-3">
<h1 className="text-3xl">{project.name}</h1>
<div className="flex items-center gap-x-2 mb-1">
<Badge variant="default">Nano</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-x-6">
<div className="flex flex-col gap-y-1">
<span className="text-foreground-light text-sm">Tables</span>
<p className="text-2xl tabular-nums">{MOCK_STATS.tables}</p>
</div>
<div className="flex flex-col gap-y-1">
<span className="text-foreground-light text-sm">Functions</span>
<p className="text-2xl tabular-nums">{MOCK_STATS.functions}</p>
</div>
<div className="flex flex-col gap-y-1">
<span className="text-foreground-light text-sm">Replicas</span>
<p className="text-2xl tabular-nums">{MOCK_STATS.replicas}</p>
</div>
<div className="ml-6 pl-6 border-l flex items-center">
<Button type="default" className="rounded-full">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-brand" />
Project Status
</div>
</Button>
<div className="flex items-center gap-x-6">
<div className="flex flex-col gap-y-1">
<span className="text-foreground-light text-sm">Tables</span>
<p className="text-2xl tabular-nums">{MOCK_STATS.tables}</p>
</div>
<div className="flex flex-col gap-y-1">
<span className="text-foreground-light text-sm">Functions</span>
<p className="text-2xl tabular-nums">{MOCK_STATS.functions}</p>
</div>
<div className="flex flex-col gap-y-1">
<span className="text-foreground-light text-sm">Replicas</span>
<p className="text-2xl tabular-nums">{MOCK_STATS.replicas}</p>
</div>
<div className="ml-6 pl-6 border-l flex items-center">
<Button type="default" className="rounded-full">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-brand" />
Project Status
</div>
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Section 2: Usage */}
<div className="py-16 ">
<div className="mx-auto max-w-7xl space-y-16 @container">
<div className="space-y-6">
<div className="flex flex-row items-center gap-x-2">
<Button type="default" size="small">
Last 24 hours
<span className="ml-1 text-foreground-lighter">&#9662;</span>
</Button>
<span className="text-xs text-foreground-light">Statistics for last 24 hours</span>
</div>
<div
style={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
className="grid gap-4"
>
<UsagePanel
icon={<Database strokeWidth={1.5} size={16} />}
title="Database"
metricLabel="REST Requests"
data={MOCK_USAGE_DATA.map((d) => d.rest)}
total={restTotal}
/>
<UsagePanel
icon={<Auth strokeWidth={1.5} size={16} />}
title="Auth"
metricLabel="Auth Requests"
data={MOCK_USAGE_DATA.map((d) => d.auth)}
total={authTotal}
/>
<UsagePanel
icon={<Storage strokeWidth={1.5} size={16} />}
title="Storage"
metricLabel="Storage Requests"
data={MOCK_USAGE_DATA.map((d) => d.storage)}
total={storageTotal}
/>
<UsagePanel
icon={<Realtime strokeWidth={1.5} size={16} />}
title="Realtime"
metricLabel="Realtime Connections"
data={MOCK_USAGE_DATA.map((d) => d.realtime)}
total={realtimeTotal}
/>
{/* Section 2: Usage */}
<div className="py-16 ">
<div className="mx-auto max-w-7xl space-y-16 @container">
<div className="space-y-6">
<div className="flex flex-row items-center gap-x-2">
<div className="relative">
<Button
type="default"
size="tiny"
onClick={() => setRangeDropdownOpen(!rangeDropdownOpen)}
>
{selectedOption.label}
<span className="ml-1 text-foreground-lighter">&#9662;</span>
</Button>
{rangeDropdownOpen && (
<div className="absolute top-full left-0 mt-1 z-50 min-w-[180px] rounded-md border border-default bg-overlay shadow-md py-1">
{TIME_RANGE_OPTIONS.map((opt) => (
<button
key={opt.key}
onClick={() => {
setTimeRange(opt.key)
setRangeDropdownOpen(false)
}}
className="flex items-center gap-2 w-full px-3 py-1.5 text-sm text-foreground-light hover:bg-surface-200 hover:text-foreground transition-colors"
>
<div className="w-3 flex justify-center">
{opt.key === timeRange && (
<div className="w-1.5 h-1.5 rounded-full bg-foreground" />
)}
</div>
<span>{opt.label}</span>
</button>
))}
</div>
)}
</div>
<span className="text-xs text-foreground-light">{selectedOption.description}</span>
</div>
<div
style={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
className="grid gap-4"
>
<UsagePanel
icon={<Database strokeWidth={1.5} size={16} />}
title="Database"
metricLabel="REST Requests"
data={usageData.map((d) => d.rest)}
total={restTotal}
animationKey={timeRange}
/>
<UsagePanel
icon={<Auth strokeWidth={1.5} size={16} />}
title="Auth"
metricLabel="Auth Requests"
data={usageData.map((d) => d.auth)}
total={authTotal}
animationKey={timeRange}
/>
<UsagePanel
icon={<Storage strokeWidth={1.5} size={16} />}
title="Storage"
metricLabel="Storage Requests"
data={usageData.map((d) => d.storage)}
total={storageTotal}
animationKey={timeRange}
/>
<UsagePanel
icon={<Realtime strokeWidth={1.5} size={16} />}
title="Realtime"
metricLabel="Realtime Connections"
data={usageData.map((d) => d.realtime)}
total={realtimeTotal}
animationKey={timeRange}
/>
</div>
</div>
{/* Advisor */}
<MockAdvisorWidget onSelectLint={setSelectedLint} />
</div>
{/* Advisor */}
<MockAdvisorWidget />
</div>
</div>
{/* Right-side lint detail panel */}
{selectedLint && (
<LintDetailPanel lint={selectedLint} onClose={() => setSelectedLint(null)} />
)}
</div>
)
}
@@ -1,7 +1,9 @@
import {
ArrowDownNarrowWide,
Book,
ChevronLeft,
ChevronRight,
Copy,
Download,
File,
FolderClosed,
@@ -15,7 +17,21 @@ import {
Upload,
} from 'lucide-react'
import { useState } from 'react'
import { Badge, Button, cn } from 'ui'
import {
Badge,
Button,
Card,
CardContent,
CardFooter,
cn,
Switch,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
// ─── Storage Menu ───────────────────────────────────────────────────────
@@ -42,7 +58,7 @@ const STORAGE_MENU: StorageMenuGroup[] = [
},
{
title: 'Configuration',
items: [{ name: 'S3', key: 's3', disabled: true }],
items: [{ name: 'S3', key: 's3' }],
},
]
@@ -93,16 +109,16 @@ function generateFiles(count: number): MockFile[] {
const MOCK_BUCKETS: MockBucket[] = [
{
id: 'submissions',
id: 'uploads',
isPublic: true,
policies: 0,
fileSizeLimit: '100 MB',
allowedMimeTypes: 'image/png, image/jpeg, image/webp, video/mp4',
folders: ['logos', 'screenshots'],
folders: ['thumbnails', 'originals'],
files: generateFiles(15),
},
{
id: 'images',
id: 'assets',
isPublic: true,
policies: 4,
fileSizeLimit: '100 MB',
@@ -111,7 +127,7 @@ const MOCK_BUCKETS: MockBucket[] = [
files: generateFiles(28),
},
{
id: 'submission-media',
id: 'media',
isPublic: true,
policies: 4,
fileSizeLimit: '100 MB',
@@ -487,6 +503,128 @@ function BucketsContent({ onOpenBucket }: { onOpenBucket: (bucket: MockBucket) =
)
}
// ─── S3 Configuration ───────────────────────────────────────────────────
function CopyInput({ value }: { value: string }) {
return (
<div className="flex items-center rounded-md border border-default bg-surface-300 h-8 overflow-hidden">
<input
name={value}
readOnly
value={value}
className="flex-1 bg-transparent text-sm text-foreground-light pl-2 border-none !outline-none !shadow-none font-mono min-w-0"
/>
<button className="flex items-center gap-1.5 px-3 h-full text-foreground-light hover:text-foreground border-l border-default transition-colors flex-shrink-0 bg-surface-200">
<Copy size={12} strokeWidth={1.5} />
<span className="text-sm">Copy</span>
</button>
</div>
)
}
function S3Content() {
return (
<div className="flex-1 overflow-auto">
<div className="mx-auto max-w-[1200px] w-full px-6 xl:px-10">
{/* Page Title */}
<div className="pt-12 pb-4">
<h1 className="text-2xl">S3 Configuration</h1>
</div>
{/* Connection Section */}
<div className="pt-12 last:pb-12 flex flex-col gap-6">
<div className="flex flex-row justify-between items-center">
<div className="flex flex-col gap-1">
<h2 className="text-lg">Connection</h2>
<p className="text-sm text-foreground-light">
Connect to your bucket using any S3-compatible service via the S3 protocol
</p>
</div>
<Button type="default" size="tiny" icon={<Book size={14} strokeWidth={1.5} />}>
Docs
</Button>
</div>
<Card>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex flex-col gap-0.5">
<span className="text-sm text-foreground">S3 protocol connection</span>
<span className="text-sm text-foreground-light">
Allow clients to connect to Supabase Storage via the S3 protocol
</span>
</div>
<Switch size="large" checked={true} />
</div>
</CardContent>
<CardContent>
<div className="flex items-center justify-between gap-8">
<span className="text-sm text-foreground whitespace-nowrap">Endpoint</span>
<div className="md:w-1/2 min-w-[400px]">
<CopyInput value="https://rtlybsbjjaewcbcjqgfu.storage.supabase.co/storage/v1/s3" />
</div>
</div>
</CardContent>
<CardContent>
<div className="flex items-center justify-between gap-8">
<span className="text-sm text-foreground whitespace-nowrap">Region</span>
<div className="md:w-1/2 min-w-[400px]">
<CopyInput value="us-east-1" />
</div>
</div>
</CardContent>
<CardFooter className="justify-end">
<Button type="primary" size="small">
Save
</Button>
</CardFooter>
</Card>
</div>
{/* Access Keys Section */}
<div className="pt-12 last:pb-12 flex flex-col gap-6">
<div className="flex flex-row justify-between items-center">
<div className="flex flex-col gap-1">
<h2 className="text-lg">Access keys</h2>
<p className="text-sm text-foreground-light">
Manage your access keys for this project
</p>
</div>
<Button type="default" size="tiny" icon={<Plus size={14} strokeWidth={1.5} />}>
New access key
</Button>
</div>
<Card>
<Table className="mt-0">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Key ID</TableHead>
<TableHead>Created at</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={3} className="!rounded-b-md overflow-hidden">
<p className="text-sm text-foreground">No access keys created</p>
<p className="text-sm text-foreground-light">
There are no access keys associated with your project yet
</p>
</TableCell>
</TableRow>
</TableBody>
</Table>
</Card>
</div>
</div>
</div>
)
}
// ─── Storage Screen ─────────────────────────────────────────────────────
export function StorageScreen() {
@@ -569,6 +707,8 @@ export function StorageScreen() {
) : (
<BucketsContent onOpenBucket={setOpenBucket} />
)
) : activeKey === 's3' ? (
<S3Content />
) : (
<div className="flex-1 flex items-center justify-center">
<p className="text-foreground-lighter text-sm">
@@ -33,114 +33,122 @@ interface MockColumn {
isPrimary?: boolean
}
const HACKATHON_IDS = ['f47ac10b-58cc-4372-a567-0e02b2c3d500', '1a93ba5-f676-4d65-baa8-2fa3fdf...']
const MOCK_NAMES = [
'SuperDuperAi',
'Matthew Habtezgi',
'Snap Banana',
'BonAppeTeam',
'Ittybit',
'Peyote',
'Table Review',
'ScamSniper',
'5-Dee Studios',
'Moldable',
'Pitched App',
'MomentoAI',
'Svelte Supabase Auth',
"Women's Wellness Tracker",
'Repatch',
'My Events Super Base',
'WhatTheStack?!',
'PaintPal',
'HotOrSlop',
'Linkus',
'Expressify',
'Supa Events',
'honk honk',
'Alicia',
'jsonMartin',
'Kimchi',
'Interfaze',
'MexicoGPT',
'call/cc',
'DevDash',
'SupaFlow',
'CodeBuddy',
'AIssistant',
'DataVault',
'CloudPilot',
'PixelForge',
'StreamLine',
'NeuralNest',
'ByteCraft',
'FormStack',
'QueryMaster',
'SyncWave',
'BlinkDB',
'HexaGrid',
'VectorVault',
'GraphQL Hero',
'AuthGuard',
'EdgeRunner',
'LambdaLab',
'ServerPulse',
const CATEGORY_IDS = [
'c1a2b3c4-d5e6-4f70-a890-1b2c3d4e5f60',
'a9b8c7d6-e5f4-4321-b098-7654321fedcb',
]
const MOCK_DESCRIPTIONS = [
'AI Filmmaking Storyboard in Figma',
'A mail platform for AI agents',
'An AI-Powered Tower Defense Game Ger...',
'2 homo sapiens with hands full of fingers.',
null,
null,
'A Supabase-powered web app connectin...',
null,
'WZRD.Studio : Creative Interface to gene...',
'make your app customizable',
'Pitched reimagines online dating by lettin...',
'Choose Your Mentor. Change Your Life. P...',
"I'm working on extending the (now unsup...",
'This application tracks health and wellne...',
'Autogenerate AI patchnote newsletters a...',
'My Events Super Base',
'WhatTheStack?! is the simplest way to ch...',
'A friendly painting app for Spectacles. Dr...',
null,
null,
"It's augmentative communication mobile/...",
null,
'kill zombie costs on AWS instantly',
null,
null,
null,
'We think postgres should be able to call /...',
'Building AI native solutions for LATAM',
null,
null,
null,
'Your AI pair programming companion',
'An intelligent assistant for developers',
'Secure data storage solution',
'Cloud infrastructure manager',
'Design tool for pixel art',
'Workflow automation platform',
'Neural network playground',
'Byte-level data manipulation',
'Dynamic form builder',
'Advanced database query tool',
'Real-time data synchronization',
'Lightning fast database',
'Hexagonal grid system',
'Vector embedding storage',
'GraphQL schema generator',
'Authentication guard middleware',
'Edge computing framework',
'Serverless function lab',
'Server monitoring dashboard',
const PRODUCT_NAMES = [
'Wireless Headphones',
'Mechanical Keyboard',
'USB-C Hub',
'Standing Desk',
'Monitor Light Bar',
'Ergonomic Mouse',
'Webcam HD',
'Desk Mat XL',
'Cable Organizer',
'Laptop Stand',
'Portable Charger',
'Smart Plug',
'LED Strip Lights',
'Bluetooth Speaker',
'Noise Machine',
'Screen Protector',
'Phone Mount',
'Ring Light',
'Drawing Tablet',
'Mesh Wi-Fi Router',
'External SSD',
'Docking Station',
'Trackpad',
'Desk Shelf',
'Air Purifier',
'Desk Fan',
'Footrest',
'Wrist Rest',
'Monitor Arm',
'Surge Protector',
'Desk Lamp',
'Wireless Charger',
'Noise-Canceling Earbuds',
'Smart Thermostat',
'Portable Monitor',
'Keyboard Tray',
'Cable Sleeve',
'Headphone Stand',
'Document Scanner',
'Label Maker',
'Pen Holder',
'Whiteboard',
'Timer Cube',
'Desk Clock',
'Power Strip',
'HDMI Adapter',
'Privacy Screen',
'Clip-on Light',
'Mini Projector',
'Card Reader',
]
const PRODUCT_DESCRIPTIONS: (string | null)[] = [
'Over-ear with active noise cancellation',
'Cherry MX switches, RGB backlight',
'7-in-1 adapter with 4K HDMI output',
'Electric height-adjustable, 60x30 inch',
null,
null,
'1080p with autofocus and built-in mic',
null,
'Premium felt, 90x40cm',
'Silicone cable clips, pack of 12',
'Aluminum alloy, adjustable height',
'10000mAh, USB-C PD fast charging',
'Wi-Fi enabled, energy monitoring',
null,
'10W stereo, waterproof IPX7',
'White noise and nature sounds',
null,
null,
'Tempered glass, matte finish',
null,
'Magnetic car mount, MagSafe compatible',
null,
null,
null,
'10-inch with stylus, pressure sensitive',
'Tri-band, covers up to 5000 sq ft',
null,
null,
null,
'1TB NVMe, USB 3.2 Gen 2',
'Triple display support, 100W PD',
'Multi-touch, Bluetooth 5.0',
'Bamboo desktop organizer',
'HEPA filter, covers 400 sq ft',
'Rechargeable, oscillating, USB powered',
'Adjustable tilt, anti-slip surface',
'Memory foam, for 60% keyboards',
'Gas spring, VESA 75/100 compatible',
'8-outlet, 2 USB ports, 6ft cord',
'Adjustable color temperature, CRI 95+',
'15W Qi fast charge, slim profile',
'ANC, 30hr battery, multipoint pairing',
'Smart scheduling, energy saving',
'15.6 inch, 1080p IPS, USB-C powered',
'Clamp-on, adjustable height',
'Neoprene, 10ft length, zip closure',
'Walnut wood, universal fit',
'Duplex scanning, 600 DPI, Wi-Fi',
'Thermal, Bluetooth, label tape included',
'Bamboo, 4 compartments',
]
function seededRandom(seed: number): number {
const x = Math.sin(seed + 1) * 10000
return x - Math.floor(x)
}
function generateUUID(seed: number): string {
const hex = (n: number) => n.toString(16).padStart(8, '0')
return `${hex(seed)}-${hex(seed * 7).slice(0, 4)}-${hex(seed * 13).slice(0, 4)}-${hex(seed * 17).slice(0, 4)}-${hex(
@@ -156,53 +164,53 @@ function generateTimestamp(seed: number): string {
return base.toISOString().replace('T', ' ').replace('Z', '+00')
}
function generateSubmissionRows(): Record<string, string | null>[] {
function generateProductRows(): Record<string, string | null>[] {
const rows: Record<string, string | null>[] = []
for (let i = 0; i < 100; i++) {
rows.push({
id: generateUUID(i + 1000),
hackathon_id: HACKATHON_IDS[i % 2],
name: MOCK_NAMES[i % MOCK_NAMES.length],
description: MOCK_DESCRIPTIONS[i % MOCK_DESCRIPTIONS.length],
category_id: CATEGORY_IDS[i % 2],
name: PRODUCT_NAMES[i % PRODUCT_NAMES.length],
description: PRODUCT_DESCRIPTIONS[i % PRODUCT_DESCRIPTIONS.length],
created_at: generateTimestamp(i),
})
}
return rows
}
const SUBMISSIONS_TABLE: MockTable = {
const PRODUCTS_TABLE: MockTable = {
id: 1,
name: 'submissions',
name: 'products',
hasRls: true,
columns: [
{ name: 'id', type: 'uuid', isPrimary: true },
{ name: 'hackathon_id', type: 'uuid' },
{ name: 'category_id', type: 'uuid' },
{ name: 'name', type: 'text' },
{ name: 'description', type: 'text' },
{ name: 'created_at', type: 'timestamptz' },
],
rows: generateSubmissionRows(),
rows: generateProductRows(),
}
const TEAMS_TABLE: MockTable = {
const ORDERS_TABLE: MockTable = {
id: 2,
name: 'teams',
name: 'orders',
hasRls: true,
columns: [
{ name: 'id', type: 'uuid', isPrimary: true },
{ name: 'name', type: 'text' },
{ name: 'submission_id', type: 'uuid' },
{ name: 'product_id', type: 'uuid' },
{ name: 'quantity', type: 'int4' },
{ name: 'created_at', type: 'timestamptz' },
],
rows: Array.from({ length: 100 }, (_, i) => ({
id: generateUUID(i + 5000),
name: `Team ${MOCK_NAMES[i % MOCK_NAMES.length]}`,
submission_id: generateUUID(i + 1000),
product_id: generateUUID(i + 1000),
quantity: String(Math.floor(seededRandom(i * 3) * 10) + 1),
created_at: generateTimestamp(i + 50),
})),
}
const ALL_TABLES: MockTable[] = [SUBMISSIONS_TABLE, TEAMS_TABLE]
const ALL_TABLES: MockTable[] = [PRODUCTS_TABLE, ORDERS_TABLE]
// ─── Mock Data Grid ─────────────────────────────────────────────────────
@@ -447,7 +455,7 @@ function MockDataGrid({ table }: { table: MockTable }) {
// ─── Table Editor Screen ────────────────────────────────────────────────
export function TableEditorScreen() {
const [selectedTableId, setSelectedTableId] = useState<number | null>(SUBMISSIONS_TABLE.id)
const [selectedTableId, setSelectedTableId] = useState<number | null>(PRODUCTS_TABLE.id)
const [searchText, setSearchText] = useState('')
const selectedTable = ALL_TABLES.find((t) => t.id === selectedTableId) ?? null
+7
View File
@@ -1,5 +1,10 @@
import type { ComponentType, ReactNode } from 'react'
export interface SupaMockOrganization {
name: string
slug: string
}
export interface SupaMockProps {
defaultScreen?: string
className?: string
@@ -7,6 +12,7 @@ export interface SupaMockProps {
organizationName?: string
organizationPlan?: string
branchName?: string
organizations?: SupaMockOrganization[]
}
export interface MockRoute {
@@ -45,6 +51,7 @@ export interface MockUser {
export interface MockProjectContextType {
project: MockProject
organizations: SupaMockOrganization[]
}
export interface MockUserContextType {