mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 20:35:07 +03:00
detaisls
This commit is contained in:
1 parent
e58ea5ab1b
commit
a7cfb94674
11 files changed
+873
-320
No files matched your search
@@ -1,2 +1,2 @@
|
||||
export { SupaMock } from './src/SupaMock'
|
||||
export type { SupaMockProps } from './src/types'
|
||||
export type { SupaMockProps, SupaMockOrganization } from './src/types'
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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">▾</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">▾</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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user