mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 13:49:09 +03:00
refinements
This commit is contained in:
1 parent
1ff78a5e11
commit
d7ab35e844
17 files changed
+269
-929
No files matched your search
@@ -1,84 +0,0 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'common'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useMigrationsQuery } from 'data/database/migrations-query'
|
||||
import { useBackupsQuery } from 'data/database/backups-query'
|
||||
import { ServiceStatus } from './ServiceStatus'
|
||||
|
||||
export default function ActivityStats() {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { ref } = useParams()
|
||||
|
||||
const { data: branchesData, isLoading: isLoadingBranches } = useBranchesQuery({
|
||||
projectRef: project?.parent_project_ref ?? project?.ref,
|
||||
})
|
||||
const latestBranch = (branchesData ?? [])
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at ?? b.updated_at).valueOf() -
|
||||
new Date(a.created_at ?? a.updated_at).valueOf()
|
||||
)[0]
|
||||
|
||||
const { data: migrationsData, isLoading: isLoadingMigrations } = useMigrationsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const latestMigration = (migrationsData ?? [])[0]
|
||||
|
||||
const { data: backupsData, isLoading: isLoadingBackups } = useBackupsQuery({
|
||||
projectRef: project?.ref,
|
||||
})
|
||||
const latestBackup = (backupsData?.backups ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf())[0]
|
||||
|
||||
return (
|
||||
<div className="@container">
|
||||
<div className="grid grid-cols-2 gap-4 @md:grid-cols-4">
|
||||
<div className="block">
|
||||
<p className="heading-meta text-foreground-light">Status</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<ServiceStatus />
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/project/${ref}/branches`} className="block">
|
||||
<h4 className="heading-meta text-foreground-light mb-1">Last branch</h4>
|
||||
<p className="text-foreground">
|
||||
{isLoadingBranches
|
||||
? 'Loading...'
|
||||
: latestBranch?.created_at
|
||||
? dayjs(latestBranch.created_at).fromNow()
|
||||
: latestBranch?.updated_at
|
||||
? dayjs(latestBranch.updated_at).fromNow()
|
||||
: 'No branches'}
|
||||
</p>
|
||||
</Link>
|
||||
<Link href={`/project/${ref}/database/migrations`} className="block">
|
||||
<p className="heading-meta text-foreground-light mb-1">Last migration</p>
|
||||
<p className="text-foreground">
|
||||
{isLoadingMigrations
|
||||
? 'Loading...'
|
||||
: latestMigration
|
||||
? dayjs(latestMigration.version, 'YYYYMMDDHHmmss').fromNow()
|
||||
: 'No migrations'}
|
||||
</p>
|
||||
</Link>
|
||||
<Link href={`/project/${ref}/database/backups/scheduled`} className="block">
|
||||
<p className="heading-meta text-foreground-light mb-1">Last backup</p>
|
||||
<p className="text-foreground">
|
||||
{isLoadingBackups
|
||||
? 'Loading...'
|
||||
: backupsData?.pitr_enabled
|
||||
? 'PITR enabled'
|
||||
: latestBackup
|
||||
? dayjs(latestBackup.inserted_at).fromNow()
|
||||
: 'No backups'}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { AdvisorWidget } from './AdvisorWidget'
|
||||
|
||||
export default function AdvisorSection() {
|
||||
return <AdvisorWidget />
|
||||
}
|
||||
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'common'
|
||||
import { DEFAULT_CHART_CONFIG } from 'components/ui/QueryBlock/QueryBlock'
|
||||
import { AnalyticsInterval } from 'data/analytics/constants'
|
||||
import { useContentQuery } from 'data/content/content-query'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
Command_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
} from 'ui'
|
||||
import { Row } from 'ui-patterns'
|
||||
import { ReportBlock } from 'components/interfaces/Reports/ReportBlock/ReportBlock'
|
||||
import dayjs from 'dayjs'
|
||||
import type { Dashboards } from 'types'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Content, useContentQuery as useReportQuery } from 'data/content/content-query'
|
||||
import { useContentUpsertMutation } from 'data/content/content-upsert-mutation'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragStartEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext, rectSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable'
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
|
||||
type CustomReportSectionProps = {}
|
||||
|
||||
export default function CustomReportSection({}: CustomReportSectionProps) {
|
||||
const startDate = dayjs().subtract(7, 'day').toISOString()
|
||||
const endDate = dayjs().toISOString()
|
||||
const [snippetSearch, setSnippetSearch] = useState('')
|
||||
const { ref } = useParams()
|
||||
const { profile } = useProfile()
|
||||
|
||||
// Load first report
|
||||
const { data: reportsData } = useReportQuery({ projectRef: ref, type: 'report', limit: 1 })
|
||||
const firstReport = reportsData?.content?.[0] as Content | undefined
|
||||
const reportContent = firstReport?.content as Dashboards.Content | undefined
|
||||
const [editableReport, setEditableReport] = useState<Dashboards.Content | undefined>(
|
||||
reportContent
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (reportContent) setEditableReport(reportContent)
|
||||
}, [reportContent])
|
||||
|
||||
const canUpdateReport = useCheckPermissions(PermissionAction.UPDATE, 'user_content', {
|
||||
resource: {
|
||||
type: 'report',
|
||||
visibility: (firstReport as any)?.visibility,
|
||||
owner_id: (firstReport as any)?.owner_id,
|
||||
},
|
||||
subject: { id: profile?.id },
|
||||
})
|
||||
|
||||
const { mutate: upsertContent } = useContentUpsertMutation()
|
||||
|
||||
const persistReport = (updated: Dashboards.Content) => {
|
||||
if (!ref || !firstReport) return
|
||||
upsertContent({ projectRef: ref, payload: { ...firstReport, content: updated } })
|
||||
}
|
||||
|
||||
const { data: snippetsData, isLoading: isLoadingSnippets } = useContentQuery({
|
||||
projectRef: ref,
|
||||
type: 'sql',
|
||||
name: snippetSearch.length === 0 ? undefined : snippetSearch,
|
||||
})
|
||||
const snippets = (snippetsData?.content ?? []) as any[]
|
||||
|
||||
// Drag and drop reordering for report blocks
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
|
||||
|
||||
const handleDragStart = (_event: DragStartEvent) => {}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (!editableReport || !active || !over || active.id === over.id) return
|
||||
const items = editableReport.layout.map((x: any) => String(x.id))
|
||||
const oldIndex = items.indexOf(String(active.id))
|
||||
const newIndex = items.indexOf(String(over.id))
|
||||
if (oldIndex === -1 || newIndex === -1) return
|
||||
|
||||
// Reorder and recompute a simple 2-column grid like GridResize does (x,y with w=1,h=1)
|
||||
const moved = arrayMove(editableReport.layout, oldIndex, newIndex)
|
||||
const recomputed = moved.map((block: any, idx: number) => ({
|
||||
...block,
|
||||
x: idx % 2,
|
||||
y: Math.floor(idx / 2),
|
||||
w: 1,
|
||||
h: 1,
|
||||
}))
|
||||
const updated = { ...editableReport, layout: recomputed }
|
||||
setEditableReport(updated)
|
||||
persistReport(updated)
|
||||
}
|
||||
|
||||
const addSnippetToReport = (snippet: any) => {
|
||||
if (!editableReport) return
|
||||
const current = [...editableReport.layout]
|
||||
let x = 0
|
||||
let y: number | null = null
|
||||
const chartsByY = current.reduce((acc: Record<string, any[]>, item: any) => {
|
||||
const key = String(item.y)
|
||||
acc[key] = acc[key] ? [...acc[key], item] : [item]
|
||||
return acc
|
||||
}, {})
|
||||
const yValues = Object.keys(chartsByY)
|
||||
if (yValues.length === 0) {
|
||||
y = 0
|
||||
} else {
|
||||
for (const yValue of yValues) {
|
||||
const totalWidthTaken = chartsByY[yValue].reduce((a, b) => a + b.w, 0)
|
||||
if (2 - totalWidthTaken >= 1) {
|
||||
y = Number(yValue)
|
||||
x = totalWidthTaken
|
||||
break
|
||||
}
|
||||
}
|
||||
if (y === null) {
|
||||
y = Number(yValues[yValues.length - 1]) + 1
|
||||
}
|
||||
}
|
||||
current.push({
|
||||
x,
|
||||
y: y as number,
|
||||
w: 1,
|
||||
h: 1,
|
||||
id: snippet.id,
|
||||
label: snippet.name,
|
||||
attribute: `snippet_${snippet.id}` as Dashboards.ChartType,
|
||||
provider: undefined as any,
|
||||
chart_type: 'bar',
|
||||
chartConfig: DEFAULT_CHART_CONFIG,
|
||||
})
|
||||
const updated = { ...editableReport, layout: current }
|
||||
setEditableReport(updated)
|
||||
persistReport(updated)
|
||||
}
|
||||
|
||||
const handleRemoveChart = ({ metric }: { metric: { key: string } }) => {
|
||||
if (!editableReport) return
|
||||
const nextLayout = editableReport.layout.filter((x) => x.attribute !== (metric.key as any))
|
||||
const updated = { ...editableReport, layout: nextLayout }
|
||||
setEditableReport(updated)
|
||||
persistReport(updated)
|
||||
}
|
||||
|
||||
const handleUpdateChart = (
|
||||
id: string,
|
||||
{ chart, chartConfig }: { chart?: Partial<Dashboards.Chart>; chartConfig?: Partial<any> }
|
||||
) => {
|
||||
if (!editableReport) return
|
||||
const currentChart = editableReport.layout.find((x) => x.id === id)
|
||||
if (!currentChart) return
|
||||
const updatedChart: Dashboards.Chart = { ...currentChart, ...(chart ?? {}) }
|
||||
if (chartConfig) {
|
||||
updatedChart.chartConfig = { ...(currentChart.chartConfig ?? {}), ...chartConfig }
|
||||
}
|
||||
const updatedLayouts = editableReport.layout.map((x) => (x.id === id ? updatedChart : x))
|
||||
const updated = { ...editableReport, layout: updatedLayouts }
|
||||
setEditableReport(updated)
|
||||
persistReport(updated)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="heading-section">At a glance</h3>
|
||||
{canUpdateReport ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" icon={<Plus />}>
|
||||
Add block
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-80 p-0">
|
||||
<Command_Shadcn_ shouldFilter={false}>
|
||||
<CommandInput_Shadcn_
|
||||
autoFocus
|
||||
placeholder="Search snippets..."
|
||||
value={snippetSearch}
|
||||
onValueChange={setSnippetSearch}
|
||||
/>
|
||||
<CommandList_Shadcn_>
|
||||
{isLoadingSnippets ? (
|
||||
<CommandEmpty_Shadcn_>Loading…</CommandEmpty_Shadcn_>
|
||||
) : snippets.length === 0 ? (
|
||||
<CommandEmpty_Shadcn_>No snippets found</CommandEmpty_Shadcn_>
|
||||
) : null}
|
||||
<CommandGroup_Shadcn_>
|
||||
{snippets.map((snippet: any) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={snippet.id}
|
||||
onSelect={() => addSnippetToReport(snippet)}
|
||||
>
|
||||
{snippet.name}
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative">
|
||||
{(() => {
|
||||
const layout = editableReport?.layout ?? []
|
||||
if (layout.length === 0) {
|
||||
return (
|
||||
<div className="flex min-h-[270px] items-center justify-center rounded border-2 border-dashed p-16 border-default">
|
||||
{canUpdateReport ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<Plus size={14} />}>
|
||||
Add your first chart
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="center" className="w-80 p-0">
|
||||
<Command_Shadcn_ shouldFilter={false}>
|
||||
<CommandInput_Shadcn_
|
||||
autoFocus
|
||||
placeholder="Search snippets..."
|
||||
value={snippetSearch}
|
||||
onValueChange={setSnippetSearch}
|
||||
/>
|
||||
<CommandList_Shadcn_>
|
||||
{isLoadingSnippets ? (
|
||||
<CommandEmpty_Shadcn_>Loading…</CommandEmpty_Shadcn_>
|
||||
) : snippets.length === 0 ? (
|
||||
<CommandEmpty_Shadcn_>No snippets found</CommandEmpty_Shadcn_>
|
||||
) : null}
|
||||
<CommandGroup_Shadcn_>
|
||||
{snippets.map((snippet: any) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={snippet.id}
|
||||
onSelect={() => addSnippetToReport(snippet)}
|
||||
>
|
||||
{snippet.name}
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">No charts set up yet in report</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={(editableReport?.layout ?? []).map((x: any) => String(x.id))}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
<Row columns={[3, 2, 1]}>
|
||||
{layout.map((item: any) => (
|
||||
<SortableReportBlock key={item.id} id={String(item.id)}>
|
||||
{({ attributes, listeners }: { attributes: any; listeners: any }) => (
|
||||
<div className="h-64">
|
||||
<ReportBlock
|
||||
key={item.id}
|
||||
item={item}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
interval={
|
||||
(editableReport?.interval as AnalyticsInterval) ??
|
||||
('1d' as AnalyticsInterval)
|
||||
}
|
||||
disableUpdate={false}
|
||||
isRefreshing={false}
|
||||
onRemoveChart={handleRemoveChart}
|
||||
onUpdateChart={(config) => handleUpdateChart(item.id, config as any)}
|
||||
dragAttributes={attributes}
|
||||
dragListeners={listeners}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SortableReportBlock>
|
||||
))}
|
||||
</Row>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SortableReportBlock({
|
||||
id,
|
||||
children,
|
||||
}: {
|
||||
id: string
|
||||
children:
|
||||
| ((args: { attributes: any; listeners: any; isDragging: boolean }) => ReactNode)
|
||||
| ReactNode
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id,
|
||||
})
|
||||
|
||||
const style: CSSProperties = {
|
||||
transform: transform
|
||||
? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`
|
||||
: undefined,
|
||||
transition,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={isDragging ? 'opacity-70 will-change-transform' : 'will-change-transform'}
|
||||
>
|
||||
{typeof children === 'function'
|
||||
? (children as any)({ attributes, listeners, isDragging })
|
||||
: children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { Check } from 'lucide-react'
|
||||
import { cn, Button, Card, CardContent, CardHeader, CardTitle } from 'ui'
|
||||
import { Row } from 'ui-patterns'
|
||||
|
||||
export type GettingStartedAction = {
|
||||
label: string
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
variant?: React.ComponentProps<typeof Button>['type']
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export type GettingStartedStep = {
|
||||
key: string
|
||||
status: 'complete' | 'incomplete'
|
||||
imageUrl?: string
|
||||
imageAlt?: string
|
||||
title: string
|
||||
description: string
|
||||
actions: GettingStartedAction[]
|
||||
}
|
||||
|
||||
export interface GettingStartedProps {
|
||||
steps: GettingStartedStep[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function GettingStarted({ steps, className }: GettingStartedProps) {
|
||||
return (
|
||||
<section className={cn('w-full', className)}>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="heading-section">Getting started</h3>
|
||||
<Button size="small" type="outline">
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
<Row columns={[3, 2, 1]} className="items-stretch">
|
||||
{steps.map((step) => (
|
||||
<Card key={step.key} className="overflow-hidden h-full">
|
||||
<CardHeader className={cn('py-3 px-4 border-b', 'bg-surface-100')}>
|
||||
<CardTitle
|
||||
className={cn(
|
||||
'text-xs font-mono uppercase flex items-center gap-1',
|
||||
step.status === 'complete' ? 'text-brand' : 'text-foreground-light'
|
||||
)}
|
||||
>
|
||||
{step.status === 'complete' && <Check size={14} className="text-brand" />}
|
||||
<span>{step.status}</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
{step.imageUrl ? (
|
||||
<img src={step.imageUrl} alt={step.imageAlt ?? ''} className="w-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-28 bg-surface-200" />
|
||||
)}
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm text-foreground">{step.title}</h4>
|
||||
<p className="text-sm text-foreground-light">{step.description}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{step.actions.map((action, i) => {
|
||||
const content = (
|
||||
<Button
|
||||
key={`${step.key}-action-${i}`}
|
||||
type={action.variant ?? 'default'}
|
||||
icon={action.icon}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
)
|
||||
if (action.href) {
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
key={`${step.key}-action-${i}`}
|
||||
type={action.variant ?? 'default'}
|
||||
icon={action.icon}
|
||||
>
|
||||
<Link href={action.href}>{action.label}</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span key={`${step.key}-action-${i}`} onClick={action.onClick}>
|
||||
{content}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Row>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default GettingStarted
|
||||
@@ -1,93 +0,0 @@
|
||||
import dayjs from 'dayjs'
|
||||
import GettingStarted, { GettingStartedStep } from './GettingStarted'
|
||||
import { useParams } from 'common'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
|
||||
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
|
||||
export default function GettingStartedSection() {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { ref } = useParams()
|
||||
|
||||
const { data: tablesData } = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: 'public',
|
||||
})
|
||||
const { data: functionsData } = useEdgeFunctionsQuery({ projectRef: project?.ref })
|
||||
const { data: replicasData } = useReadReplicasQuery({ projectRef: project?.ref })
|
||||
|
||||
const tablesCount = Math.max(0, tablesData?.length ?? 0)
|
||||
const functionsCount = Math.max(0, functionsData?.length ?? 0)
|
||||
const replicasCount = Math.max(0, (replicasData?.length ?? 1) - 1)
|
||||
|
||||
return (
|
||||
<GettingStarted
|
||||
steps={
|
||||
[
|
||||
{
|
||||
key: 'design-db',
|
||||
status: tablesCount > 0 ? 'complete' : 'incomplete',
|
||||
title: 'Design your database',
|
||||
description:
|
||||
'Start building your app by creating tables with the table editor or describe what you want with the assistant',
|
||||
imageUrl: '/img/getting-started/one.png',
|
||||
actions: [
|
||||
{ label: 'View tables', href: `/project/${ref}/editor`, variant: 'default' },
|
||||
{ label: 'Do it for me', href: `/project/${ref}/sql/templates`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'add-data',
|
||||
status: 'incomplete',
|
||||
title: 'Add some data',
|
||||
description:
|
||||
'Use the table editor to create some sample data, or ask the Assistant to generate some data for you.',
|
||||
imageUrl: '/img/getting-started/two.jpg',
|
||||
actions: [
|
||||
{ label: 'Table editor', href: `/project/${ref}/editor`, variant: 'default' },
|
||||
{ label: 'Run SQL', href: `/project/${ref}/sql/new`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'implement-auth',
|
||||
status: 'incomplete',
|
||||
title: 'Implement auth',
|
||||
description: 'Set up authentication and manage users using Supabase Auth.',
|
||||
imageUrl: '/img/getting-started/four.webp',
|
||||
actions: [
|
||||
{ label: 'Users', href: `/project/${ref}/auth/users`, variant: 'default' },
|
||||
{ label: 'Auth settings', href: `/project/${ref}/auth/settings`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'implement-storage',
|
||||
status: 'incomplete',
|
||||
title: 'Implement storage',
|
||||
description: 'Create buckets and manage files with Supabase Storage.',
|
||||
imageUrl: '/img/getting-started/three.webp',
|
||||
actions: [
|
||||
{ label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' },
|
||||
{ label: 'Policies', href: `/project/${ref}/storage/policies`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'connect-app',
|
||||
status: 'incomplete',
|
||||
title: 'Connect your app',
|
||||
description:
|
||||
'Install a client library and connect to your Supabase project to start querying your data',
|
||||
imageUrl: '/img/getting-started/one.png',
|
||||
actions: [
|
||||
{ label: 'Client libraries', href: `/project/${ref}/settings/api`, variant: 'default' },
|
||||
{ label: 'Quickstarts', href: `/project/${ref}/sql/quickstarts`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
] satisfies GettingStartedStep[]
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { GripVertical } from 'lucide-react'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
|
||||
type SortableSectionProps = {
|
||||
id: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function SortableSection({ id, children }: SortableSectionProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id,
|
||||
})
|
||||
|
||||
const style: CSSProperties = {
|
||||
transform: transform
|
||||
? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`
|
||||
: undefined,
|
||||
transition,
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="relative will-change-transform">
|
||||
<button
|
||||
aria-label="Drag to reorder section"
|
||||
className="absolute -left-6 top-2 text-foreground-muted hover:text-foreground focus:outline-none cursor-grab active:cursor-grabbing"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<GripVertical size={14} />
|
||||
</button>
|
||||
<div className={isDragging ? 'opacity-70' : undefined}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SortableSection
|
||||
@@ -1,98 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import ActivityStats from 'components/interfaces/Home/ActivityStats'
|
||||
import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner'
|
||||
import InstanceConfiguration from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration'
|
||||
import { Badge, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { ProjectPausedState } from 'components/layouts/ProjectLayout/PausedState/ProjectPausedState'
|
||||
|
||||
interface TopSectionProps {
|
||||
projectName: string
|
||||
isMainBranch?: boolean
|
||||
parentProject?: { ref?: string; name?: string } | null
|
||||
isOrioleDb?: boolean
|
||||
project: any
|
||||
organization: any
|
||||
projectRef?: string
|
||||
isPaused: boolean
|
||||
}
|
||||
|
||||
const TopSection = ({
|
||||
projectName,
|
||||
isMainBranch,
|
||||
parentProject,
|
||||
isOrioleDb,
|
||||
project,
|
||||
organization,
|
||||
projectRef,
|
||||
isPaused,
|
||||
}: TopSectionProps) => {
|
||||
return (
|
||||
<div className="pt-12 px-8">
|
||||
<div className="mx-auto max-w-7xl flex flex-col gap-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-32 w-full items-center">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex flex-col md:flex-row md:items-end gap-3 w-full">
|
||||
<div>
|
||||
{!isMainBranch && (
|
||||
<Link
|
||||
href={`/project/${parentProject?.ref}`}
|
||||
className="text-sm text-foreground-light"
|
||||
>
|
||||
{parentProject?.name}
|
||||
</Link>
|
||||
)}
|
||||
<h1 className="text-3xl">{projectName}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 mb-1">
|
||||
{isOrioleDb && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="warning">OrioleDB</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start" className="max-w-80 text-center">
|
||||
This project is using Postgres with OrioleDB which is currently in preview and
|
||||
not suitable for production workloads. View our{' '}
|
||||
<InlineLink href="https://supabase.com/docs/guides/database/orioledb">
|
||||
documentation
|
||||
</InlineLink>{' '}
|
||||
for all limitations.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ComputeBadgeWrapper
|
||||
project={{
|
||||
ref: project?.ref,
|
||||
organization_slug: organization?.slug,
|
||||
cloud_provider: project?.cloud_provider,
|
||||
infra_compute_size: project?.infra_compute_size,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<ActivityStats />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Link href={`/project/${projectRef}/settings/infrastructure`}>
|
||||
<div className="w-full h-[400px] border rounded-md overflow-hidden cursor-pointer hover:bg">
|
||||
<div
|
||||
className="h-[500px] scale-[0.8] origin-top-left pointer-events-none"
|
||||
style={{ width: 'calc(100% / 0.8)' }}
|
||||
>
|
||||
<InstanceConfiguration />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<ProjectUpgradeFailedBanner />
|
||||
{isPaused && <ProjectPausedState />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopSection
|
||||
@@ -5,6 +5,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useMigrationsQuery } from 'data/database/migrations-query'
|
||||
import { useBackupsQuery } from 'data/database/backups-query'
|
||||
import { Skeleton } from 'ui'
|
||||
import { ServiceStatus } from './ServiceStatus'
|
||||
|
||||
export default function ActivityStats() {
|
||||
@@ -47,35 +48,41 @@ export default function ActivityStats() {
|
||||
<Link href={`/project/${ref}/branches`} className="block">
|
||||
<h4 className="heading-meta text-foreground-light mb-1">Last branch</h4>
|
||||
<p className="text-foreground">
|
||||
{isLoadingBranches
|
||||
? 'Loading...'
|
||||
: latestBranch?.created_at
|
||||
? dayjs(latestBranch.created_at).fromNow()
|
||||
: latestBranch?.updated_at
|
||||
? dayjs(latestBranch.updated_at).fromNow()
|
||||
: 'No branches'}
|
||||
{isLoadingBranches ? (
|
||||
<Skeleton className="h-4 w-16 mt-2" />
|
||||
) : latestBranch?.created_at ? (
|
||||
dayjs(latestBranch.created_at).fromNow()
|
||||
) : latestBranch?.updated_at ? (
|
||||
dayjs(latestBranch.updated_at).fromNow()
|
||||
) : (
|
||||
'No branches'
|
||||
)}
|
||||
</p>
|
||||
</Link>
|
||||
<Link href={`/project/${ref}/database/migrations`} className="block">
|
||||
<p className="heading-meta text-foreground-light mb-1">Last migration</p>
|
||||
<p className="text-foreground">
|
||||
{isLoadingMigrations
|
||||
? 'Loading...'
|
||||
: latestMigration
|
||||
? dayjs(latestMigration.version, 'YYYYMMDDHHmmss').fromNow()
|
||||
: 'No migrations'}
|
||||
{isLoadingMigrations ? (
|
||||
<Skeleton className="h-4 w-16 mt-2" />
|
||||
) : latestMigration ? (
|
||||
dayjs(latestMigration.version, 'YYYYMMDDHHmmss').fromNow()
|
||||
) : (
|
||||
'No migrations'
|
||||
)}
|
||||
</p>
|
||||
</Link>
|
||||
<Link href={`/project/${ref}/database/backups/scheduled`} className="block">
|
||||
<p className="heading-meta text-foreground-light mb-1">Last backup</p>
|
||||
<p className="text-foreground">
|
||||
{isLoadingBackups
|
||||
? 'Loading...'
|
||||
: backupsData?.pitr_enabled
|
||||
? 'PITR enabled'
|
||||
: latestBackup
|
||||
? dayjs(latestBackup.inserted_at).fromNow()
|
||||
: 'No backups'}
|
||||
{isLoadingBackups ? (
|
||||
<Skeleton className="h-4 w-16 mt-2" />
|
||||
) : backupsData?.pitr_enabled ? (
|
||||
'PITR enabled'
|
||||
) : latestBackup ? (
|
||||
dayjs(latestBackup.inserted_at).fromNow()
|
||||
) : (
|
||||
'No backups'
|
||||
)}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Shield } from 'lucide-react'
|
||||
import { BarChart, Shield } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -118,10 +118,16 @@ export const AdvisorWidget = () => {
|
||||
const levelLabel = 'Error'
|
||||
return (
|
||||
<Card key={lint.cache_key} className="h-full flex flex-col items-stretch h-64">
|
||||
<CardHeader className="border-b-0 shrink-0 flex flex-row gap-2 space-y-0 justify-between items-center py-2">
|
||||
<CardHeader className="border-b-0 shrink-0 flex flex-row gap-2 space-y-0 justify-between items-center">
|
||||
<Link
|
||||
href={`/project/${projectRef}/advisors/${advisor}?id=${lint.cache_key}&preset=${lint.level}`}
|
||||
className="flex flex-row items-center gap-3"
|
||||
>
|
||||
{lint.categories[0] === 'SECURITY' ? (
|
||||
<Shield size={16} strokeWidth={1.5} className="text-foreground-muted" />
|
||||
) : (
|
||||
<BarChart size={16} strokeWidth={1.5} className="text-foreground-muted" />
|
||||
)}
|
||||
<CardTitle className="text-foreground-light">{lint.categories[0]}</CardTitle>
|
||||
</Link>
|
||||
<ButtonTooltip
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from 'next/link'
|
||||
import { Check } from 'lucide-react'
|
||||
import { cn, Button, Card, CardContent, CardHeader, CardTitle } from 'ui'
|
||||
import { cn, Button, Card, CardContent, CardHeader, CardTitle, Badge } from 'ui'
|
||||
import { Row } from 'ui-patterns'
|
||||
|
||||
export type GettingStartedAction = {
|
||||
@@ -14,8 +14,7 @@ export type GettingStartedAction = {
|
||||
export type GettingStartedStep = {
|
||||
key: string
|
||||
status: 'complete' | 'incomplete'
|
||||
imageUrl?: string
|
||||
imageAlt?: string
|
||||
icon?: React.ReactNode
|
||||
title: string
|
||||
description: string
|
||||
actions: GettingStartedAction[]
|
||||
@@ -38,27 +37,20 @@ export function GettingStarted({ steps, className }: GettingStartedProps) {
|
||||
<Row columns={[3, 2, 1]} className="items-stretch">
|
||||
{steps.map((step) => (
|
||||
<Card key={step.key} className="overflow-hidden h-full">
|
||||
<CardHeader className={cn('py-3 px-4 border-b', 'bg-surface-100')}>
|
||||
<CardTitle
|
||||
className={cn(
|
||||
'text-xs font-mono uppercase flex items-center gap-1',
|
||||
step.status === 'complete' ? 'text-brand' : 'text-foreground-light'
|
||||
)}
|
||||
>
|
||||
{step.status === 'complete' && <Check size={14} className="text-brand" />}
|
||||
<span>{step.status}</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
{step.imageUrl ? (
|
||||
<img src={step.imageUrl} alt={step.imageAlt ?? ''} className="w-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-28 bg-surface-200" />
|
||||
)}
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm text-foreground">{step.title}</h4>
|
||||
<p className="text-sm text-foreground-light">{step.description}</p>
|
||||
<CardHeader className="flex flex-row space-y-0 justify-between items-center border-b-0">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
{step.icon && <div>{step.icon}</div>}
|
||||
<CardTitle className="text-foreground-light">{step.title}</CardTitle>
|
||||
</div>
|
||||
<Badge
|
||||
variant={step.status === 'complete' ? 'success' : 'default'}
|
||||
className="capitalize"
|
||||
>
|
||||
{step.status}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-16">
|
||||
<p className="text-base text-foreground">{step.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{step.actions.map((action, i) => {
|
||||
const content = (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useParams } from 'common'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import GettingStarted, { GettingStartedStep } from './GettingStarted'
|
||||
import { Code, Database, File, Table, User } from 'lucide-react'
|
||||
|
||||
export default function GettingStartedSection() {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
@@ -22,9 +23,9 @@ export default function GettingStartedSection() {
|
||||
key: 'design-db',
|
||||
status: tablesCount > 0 ? 'complete' : 'incomplete',
|
||||
title: 'Design your database',
|
||||
icon: <Database strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description:
|
||||
'Start building your app by creating tables with the table editor or describe what you want with the assistant',
|
||||
imageUrl: '/img/getting-started/one.png',
|
||||
'Start building your app by creating tables with the table editor, sql editor or describe what you want with the assistant',
|
||||
actions: [
|
||||
{ label: 'View tables', href: `/project/${ref}/editor`, variant: 'default' },
|
||||
{ label: 'Do it for me', href: `/project/${ref}/sql/templates`, variant: 'default' },
|
||||
@@ -34,9 +35,9 @@ export default function GettingStartedSection() {
|
||||
key: 'add-data',
|
||||
status: 'incomplete',
|
||||
title: 'Add some data',
|
||||
icon: <Table strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description:
|
||||
'Use the table editor to create some sample data, or ask the Assistant to generate some data for you.',
|
||||
imageUrl: '/img/getting-started/two.jpg',
|
||||
actions: [
|
||||
{ label: 'Table editor', href: `/project/${ref}/editor`, variant: 'default' },
|
||||
{ label: 'Run SQL', href: `/project/${ref}/sql/new`, variant: 'default' },
|
||||
@@ -46,8 +47,8 @@ export default function GettingStartedSection() {
|
||||
key: 'implement-auth',
|
||||
status: 'incomplete',
|
||||
title: 'Implement auth',
|
||||
icon: <User strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description: 'Set up authentication and manage users using Supabase Auth.',
|
||||
imageUrl: '/img/getting-started/four.webp',
|
||||
actions: [
|
||||
{ label: 'Users', href: `/project/${ref}/auth/users`, variant: 'default' },
|
||||
{ label: 'Auth settings', href: `/project/${ref}/auth/settings`, variant: 'default' },
|
||||
@@ -57,8 +58,8 @@ export default function GettingStartedSection() {
|
||||
key: 'implement-storage',
|
||||
status: 'incomplete',
|
||||
title: 'Implement storage',
|
||||
icon: <File strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description: 'Create buckets and manage files with Supabase Storage.',
|
||||
imageUrl: '/img/getting-started/three.webp',
|
||||
actions: [
|
||||
{ label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' },
|
||||
{ label: 'Policies', href: `/project/${ref}/storage/policies`, variant: 'default' },
|
||||
@@ -68,9 +69,9 @@ export default function GettingStartedSection() {
|
||||
key: 'connect-app',
|
||||
status: 'incomplete',
|
||||
title: 'Connect your app',
|
||||
icon: <Code strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description:
|
||||
'Install a client library and connect to your Supabase project to start querying your data',
|
||||
imageUrl: '/img/getting-started/one.png',
|
||||
actions: [
|
||||
{ label: 'Client libraries', href: `/project/${ref}/settings/api`, variant: 'default' },
|
||||
{ label: 'Quickstarts', href: `/project/${ref}/sql/quickstarts`, variant: 'default' },
|
||||
@@ -79,4 +80,4 @@ export default function GettingStartedSection() {
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useProjectLogRequestsCountQuery } from 'data/analytics/project-log-requ
|
||||
import { useProjectLogStatsQuery } from 'data/analytics/project-log-stats-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import ProjectUsage from './ProjectUsage'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
|
||||
export const ProjectUsageSection = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
@@ -18,7 +19,17 @@ export const ProjectUsageSection = () => {
|
||||
})
|
||||
|
||||
if (isLoading || isLogsStatsLoading) {
|
||||
return <ProjectUsageLoadingState />
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ShimmeringLoader className="w-40 h-7" />
|
||||
<div className="flex flex-col md:flex-row gap-2 md:gap-4">
|
||||
<ShimmeringLoader className="w-full h-[258px] py-0" />
|
||||
<ShimmeringLoader className="w-full h-[258px] py-0" />
|
||||
<ShimmeringLoader className="w-full h-[258px] py-0" />
|
||||
<ShimmeringLoader className="w-full h-[258px] py-0" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
Skeleton,
|
||||
} from 'ui'
|
||||
|
||||
const SERVICE_STATUS_THRESHOLD = 5 // minutes
|
||||
@@ -280,13 +281,15 @@ export const ServiceStatus = () => {
|
||||
|
||||
const anyUnhealthy = services.some((service) => !service.isHealthy)
|
||||
const anyComingUp = services.some((service) => service.status === 'COMING_UP')
|
||||
const overallStatusLabel = isLoadingChecks
|
||||
? 'Checking status'
|
||||
: anyUnhealthy
|
||||
? 'Unhealthy'
|
||||
: anyComingUp || isProjectNew
|
||||
? 'Coming up...'
|
||||
: 'Healthy'
|
||||
const overallStatusLabel = isLoadingChecks ? (
|
||||
<Skeleton className="h-4 w-16 mt-2" />
|
||||
) : anyUnhealthy ? (
|
||||
'Unhealthy'
|
||||
) : anyComingUp || isProjectNew ? (
|
||||
'Coming up...'
|
||||
) : (
|
||||
'Healthy'
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover_Shadcn_ modal={false} open={open} onOpenChange={setOpen}>
|
||||
@@ -294,9 +297,8 @@ export const ServiceStatus = () => {
|
||||
<Button
|
||||
className="p-0 border-0 h-auto text-base"
|
||||
icon={
|
||||
isLoadingChecks || (!allServicesOperational && isProjectNew && isMigrationLoading) ? (
|
||||
<LoaderIcon />
|
||||
) : (
|
||||
isLoadingChecks ||
|
||||
(!allServicesOperational && isProjectNew && isMigrationLoading) ? null : (
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
allServicesOperational ? 'bg-brand' : 'bg-warning'
|
||||
|
||||
@@ -83,7 +83,7 @@ const TopSection = ({
|
||||
className="h-[500px] scale-[0.8] origin-top-left pointer-events-none"
|
||||
style={{ width: 'calc(100% / 0.8)' }}
|
||||
>
|
||||
<InstanceConfiguration />
|
||||
<InstanceConfiguration diagramOnly />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
+105
-91
@@ -40,7 +40,11 @@ import MapView from './MapView'
|
||||
import { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal'
|
||||
import { useShowNewReplicaPanel } from './use-show-new-replica'
|
||||
|
||||
const InstanceConfigurationUI = () => {
|
||||
interface InstanceConfigurationUIProps {
|
||||
diagramOnly?: boolean
|
||||
}
|
||||
|
||||
const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationUIProps) => {
|
||||
const reactFlow = useReactFlow()
|
||||
const isOrioleDb = useIsOrioleDb()
|
||||
const { resolvedTheme } = useTheme()
|
||||
@@ -210,9 +214,9 @@ const InstanceConfigurationUI = () => {
|
||||
}, [isSuccessReplicas, isSuccessLoadBalancers, nodes, edges, view])
|
||||
|
||||
return (
|
||||
<div className="nowheel border-y">
|
||||
<div className={diagramOnly ? 'h-full' : 'nowheel border-y'}>
|
||||
<div
|
||||
className={`h-[500px] w-full relative ${
|
||||
className={`${diagramOnly ? 'h-full' : 'h-[500px]'} w-full relative ${
|
||||
isSuccessReplicas && !isLoadingProject ? '' : 'flex items-center justify-center px-28'
|
||||
}`}
|
||||
>
|
||||
@@ -224,70 +228,72 @@ const InstanceConfigurationUI = () => {
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve replicas" />}
|
||||
{isSuccessReplicas && !isLoadingProject && (
|
||||
<>
|
||||
<div className="z-10 absolute top-4 right-4 flex items-center justify-center gap-x-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
disabled={!canManageReplicas || isOrioleDb}
|
||||
className={cn(replicas.length > 0 ? 'rounded-r-none' : '')}
|
||||
onClick={() => setShowNewReplicaPanel(true)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canManageReplicas
|
||||
? 'You need additional permissions to deploy replicas'
|
||||
: isOrioleDb
|
||||
? 'Read replicas are not supported with OrioleDB'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Deploy a new replica
|
||||
</ButtonTooltip>
|
||||
{replicas.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<ChevronDown size={16} />}
|
||||
className="px-1 rounded-l-none border-l-0"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52 *:space-x-2">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
|
||||
Resize databases
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setShowDeleteAllModal(true)}>
|
||||
<div>Remove all replicas</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{!diagramOnly && (
|
||||
<div className="z-10 absolute top-4 right-4 flex items-center justify-center gap-x-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
disabled={!canManageReplicas || isOrioleDb}
|
||||
className={cn(replicas.length > 0 ? 'rounded-r-none' : '')}
|
||||
onClick={() => setShowNewReplicaPanel(true)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canManageReplicas
|
||||
? 'You need additional permissions to deploy replicas'
|
||||
: isOrioleDb
|
||||
? 'Read replicas are not supported with OrioleDB'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Deploy a new replica
|
||||
</ButtonTooltip>
|
||||
{replicas.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<ChevronDown size={16} />}
|
||||
className="px-1 rounded-l-none border-l-0"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52 *:space-x-2">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
|
||||
Resize databases
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setShowDeleteAllModal(true)}>
|
||||
<div>Remove all replicas</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
{project?.cloud_provider === 'AWS' && (
|
||||
<div className="flex items-center justify-center">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Network size={15} />}
|
||||
className={`rounded-r-none transition ${
|
||||
view === 'flow' ? 'opacity-100' : 'opacity-50'
|
||||
}`}
|
||||
onClick={() => setView('flow')}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Globe2 size={15} />}
|
||||
className={`rounded-l-none transition ${
|
||||
view === 'map' ? 'opacity-100' : 'opacity-50'
|
||||
}`}
|
||||
onClick={() => setView('map')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{project?.cloud_provider === 'AWS' && (
|
||||
<div className="flex items-center justify-center">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Network size={15} />}
|
||||
className={`rounded-r-none transition ${
|
||||
view === 'flow' ? 'opacity-100' : 'opacity-50'
|
||||
}`}
|
||||
onClick={() => setView('flow')}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Globe2 size={15} />}
|
||||
className={`rounded-l-none transition ${
|
||||
view === 'map' ? 'opacity-100' : 'opacity-50'
|
||||
}`}
|
||||
onClick={() => setView('map')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{view === 'flow' ? (
|
||||
<ReactFlow
|
||||
fitView
|
||||
@@ -322,41 +328,49 @@ const InstanceConfigurationUI = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DeployNewReplicaPanel
|
||||
visible={showNewReplicaPanel}
|
||||
selectedDefaultRegion={newReplicaRegion}
|
||||
onSuccess={() => setRefetchInterval(5000)}
|
||||
onClose={() => {
|
||||
setNewReplicaRegion(undefined)
|
||||
setShowNewReplicaPanel(false)
|
||||
}}
|
||||
/>
|
||||
{!diagramOnly && (
|
||||
<>
|
||||
<DeployNewReplicaPanel
|
||||
visible={showNewReplicaPanel}
|
||||
selectedDefaultRegion={newReplicaRegion}
|
||||
onSuccess={() => setRefetchInterval(5000)}
|
||||
onClose={() => {
|
||||
setNewReplicaRegion(undefined)
|
||||
setShowNewReplicaPanel(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
<DropReplicaConfirmationModal
|
||||
selectedReplica={selectedReplicaToDrop}
|
||||
onSuccess={() => setRefetchInterval(5000)}
|
||||
onCancel={() => setSelectedReplicaToDrop(undefined)}
|
||||
/>
|
||||
<DropReplicaConfirmationModal
|
||||
selectedReplica={selectedReplicaToDrop}
|
||||
onSuccess={() => setRefetchInterval(5000)}
|
||||
onCancel={() => setSelectedReplicaToDrop(undefined)}
|
||||
/>
|
||||
|
||||
<DropAllReplicasConfirmationModal
|
||||
visible={showDeleteAllModal}
|
||||
onSuccess={() => setRefetchInterval(5000)}
|
||||
onCancel={() => setShowDeleteAllModal(false)}
|
||||
/>
|
||||
<DropAllReplicasConfirmationModal
|
||||
visible={showDeleteAllModal}
|
||||
onSuccess={() => setRefetchInterval(5000)}
|
||||
onCancel={() => setShowDeleteAllModal(false)}
|
||||
/>
|
||||
|
||||
<RestartReplicaConfirmationModal
|
||||
selectedReplica={selectedReplicaToRestart}
|
||||
onSuccess={() => setRefetchInterval(5000)}
|
||||
onCancel={() => setSelectedReplicaToRestart(undefined)}
|
||||
/>
|
||||
<RestartReplicaConfirmationModal
|
||||
selectedReplica={selectedReplicaToRestart}
|
||||
onSuccess={() => setRefetchInterval(5000)}
|
||||
onCancel={() => setSelectedReplicaToRestart(undefined)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const InstanceConfiguration = () => {
|
||||
interface InstanceConfigurationProps {
|
||||
diagramOnly?: boolean
|
||||
}
|
||||
|
||||
const InstanceConfiguration = ({ diagramOnly = false }: InstanceConfigurationProps) => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<InstanceConfigurationUI />
|
||||
<InstanceConfigurationUI diagramOnly={diagramOnly} />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useProjectByRefQuery,
|
||||
useSelectedProjectQuery,
|
||||
} from 'hooks/misc/useSelectedProject'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
@@ -64,12 +65,10 @@ const Home: NextPageWithLayout = () => {
|
||||
projectName = project.name
|
||||
}
|
||||
|
||||
const [sectionOrder, setSectionOrder] = useState<string[]>([
|
||||
'getting-started',
|
||||
'usage',
|
||||
'advisor',
|
||||
'custom-report',
|
||||
])
|
||||
const [sectionOrder, setSectionOrder] = useLocalStorage<string[]>(
|
||||
`home-section-order-${project?.ref || 'default'}`,
|
||||
['getting-started', 'usage', 'advisor', 'custom-report']
|
||||
)
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
@@ -106,11 +105,7 @@ const Home: NextPageWithLayout = () => {
|
||||
{!isPaused && (
|
||||
<div className="py-16 px-8">
|
||||
<div className="mx-auto max-w-7xl space-y-16">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={sectionOrder} strategy={verticalListSortingStrategy}>
|
||||
{sectionOrder.map((id) => {
|
||||
if (id === 'getting-started') {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<svg width="330" height="271" viewBox="0 0 330 271" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_26_2)">
|
||||
<mask id="mask0_26_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-159" width="330" height="430">
|
||||
<path d="M330 -159H0V271H330V-159Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_26_2)">
|
||||
<mask id="mask1_26_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="-72" y="35" width="501" height="307">
|
||||
<path d="M428.9 35.31H-71.3203V341.446H428.9V35.31Z" fill="url(#paint0_linear_26_2)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_26_2)">
|
||||
<path opacity="0.6" d="M56.9363 456.864C55.0738 455.729 58.4661 446.079 66.7467 428.868C75.0273 411.656 87.9158 387.498 104.11 358.81C120.304 330.122 139.235 297.928 159.003 265.468C178.78 232.993 198.693 201.405 216.734 173.844C234.791 146.292 250.345 123.755 261.833 108.507C273.329 93.243 280.353 85.814 282.216 86.949M29.7378 437.282C22.0017 430.736 20.8579 416.662 26.4116 396.477C31.9809 376.301 44.0671 350.704 61.447 322.289C78.8269 293.875 100.91 263.633 125.455 234.617C150.009 205.585 176.155 178.795 201.271 156.927C226.402 135.067 249.614 118.891 268.59 110.051C287.566 101.21 301.634 100.003 309.379 106.533M3.20403 410.554C-7.94815 397.166 -11.2031 377.622 -6.26454 353.873C-1.32594 330.124 11.667 303.003 31.3826 275.237C51.0983 247.471 76.8517 220.038 106.057 195.697C135.262 171.356 166.889 150.975 197.741 136.589C228.609 122.211 257.618 114.321 281.884 113.729C306.142 113.153 324.785 119.878 335.922 133.258M-22.4581 372.044C-33.3277 351.212 -35.1363 326.108 -27.6923 299.27C-20.2483 272.432 -3.8149 244.81 19.9338 219.162C43.698 193.523 73.9446 170.773 107.648 153.187C141.352 135.601 177.334 123.84 211.952 119.025C246.586 114.218 278.641 116.573 304.901 125.824C331.16 135.076 350.709 150.938 361.578 171.77M-42.7743 314.456C-48.2735 287.019 -43.8643 258.09 -29.9902 230.587C-16.1161 203.084 6.7291 177.968 36.2619 157.782C65.7947 137.596 100.962 123.031 138.234 115.554C175.506 108.078 213.569 107.972 248.601 115.224C283.633 122.475 314.391 136.839 337.788 156.865C361.186 176.891 376.404 201.902 381.903 229.34M-43.4928 233.108C-38.0184 203.028 -22.7933 175.282 0.658211 152.651C24.0942 130.012 54.9289 113.285 90.0679 104.137C125.198 95.005 163.382 93.771 200.78 100.593C238.187 107.4 273.487 122.016 303.128 142.955C332.768 163.894 355.742 190.414 369.689 219.867C383.636 249.32 388.11 280.648 382.636 310.728M-5.64198 144.625C21.9477 106.658 66.8567 83.639 119.227 80.655C171.598 77.671 227.12 94.963 273.586 128.711C320.051 162.459 353.668 209.929 367.02 260.638C380.363 311.363 372.376 361.189 344.802 399.164M61.3434 84.313C99.8723 62.089 148.231 60.525 195.775 79.995C243.318 99.464 286.152 138.33 314.846 188.093C343.549 237.841 355.768 294.387 348.805 345.288C341.842 396.189 316.291 437.274 277.762 459.498M127.004 59.562C165.252 51.897 206.413 66.91 241.443 101.321C276.473 135.732 302.483 186.68 313.776 242.996C325.069 299.311 320.687 356.368 301.627 401.606C282.567 446.843 250.368 476.575 212.121 484.24M179.177 55.558C210.013 56.932 238.58 81.045 258.588 122.58C278.595 164.114 288.389 219.698 285.858 277.073C283.303 334.455 268.618 388.933 245.003 428.539C221.388 468.146 190.807 489.635 159.97 488.26M219.804 61.258C239.872 66.037 253.815 92.833 258.582 135.715C263.349 178.596 258.544 234.08 245.211 289.956C231.886 345.817 211.127 397.509 187.519 433.634C163.912 469.758 139.377 487.369 119.318 482.574M253.079 72.088C261.369 75.556 260.504 99.926 250.701 139.845C240.898 179.764 222.957 231.967 200.809 284.955C178.661 337.943 154.135 387.401 132.598 422.426C111.086 457.443 94.3421 475.185 86.0524 471.716M95.4135 475.397C74.2746 467.692 53.7477 456.293 36.0327 442.422M174.386 488.421C141.03 489.174 101.973 478.33 66.0165 458.363C30.0595 438.396 0.23185 410.968 -16.7855 382.263M225.997 481.002C183.579 492.452 124.337 481.019 70.1613 450.935C15.9857 420.851 -25.0361 376.607 -37.7393 334.548M268.893 464.345C246.532 475.886 216.188 479.649 181.886 475.134C147.584 470.619 110.902 458.053 76.7424 439.084C42.5825 420.115 12.5162 395.636 -9.44046 368.89C-31.4057 342.159 -44.2526 314.412 -46.2763 289.33M305.246 440.71C283.394 458.27 249.804 466.088 209.925 462.895C170.045 459.701 126.223 445.684 85.5878 423.119C44.9523 400.554 9.88843 370.765 -13.904 338.602C-37.6965 306.438 -48.8162 273.791 -45.4619 245.96M335.338 411.267C315.677 434.665 280.44 446.799 236.392 445.344C192.336 443.905 142.533 428.979 96.5374 403.437C50.5417 377.895 11.5441 343.511 -12.9669 306.874C-37.4779 270.237 -45.805 233.912 -36.3459 204.869M358.903 377.045C348.313 396.115 329.168 409.969 303.39 417.261C277.596 424.545 246.094 424.99 212.019 418.533C177.944 412.077 142.515 398.97 109.27 380.509C76.0237 362.047 46.1694 338.902 22.6759 313.39C-0.817509 287.879 -17.1082 260.894 -24.5451 235.157C-31.9974 209.412 -30.3341 185.83 -19.7527 166.775M358.903 377.045C331.021 427.254 284.322 464.338 229.096 480.128C173.871 495.919 114.65 489.119 64.425 461.229C14.2154 433.347 -22.8689 386.648 -38.6676 331.438C-54.4577 276.213 -47.6584 216.992 -19.7682 166.766C8.11344 116.557 54.8132 79.473 110.023 63.674C165.248 47.884 224.469 54.683 274.694 82.573C324.904 110.455 361.988 157.155 377.787 212.365C393.577 267.59 386.785 326.835 358.903 377.045ZM375.496 338.951C368.268 361.165 350.965 378.089 325.575 387.786C300.193 397.467 267.772 399.53 232.028 393.707C196.275 387.9 158.648 374.444 123.481 354.916C88.299 335.379 57.002 310.561 33.1726 283.282C9.34321 256.003 -6.06826 227.398 -11.2523 200.744C-16.4518 174.081 -11.2172 150.455 3.81191 132.552M384.572 297.858C381.Line truncated
|
||||
<path d="M96.0839 -3.58803C129.821 7.72697 155.729 34.229 169.369 55.373C49.8789 55.63 -46.9314 152.472 -47.1379 271.975C-71.4456 262.419 -89.2904 248.817 -100.644 232.238C-112.052 215.579 -116.919 195.897 -115.188 174.25C-111.724 130.946 -81.8496 79.773 -25.091 29.321C20.2709 -11.001 62.1568 -14.966 96.0839 -3.58803Z" fill="url(#paint1_radial_26_2)" fill-opacity="0.08" stroke="url(#paint2_radial_26_2)" stroke-width="0.522968"/>
|
||||
</g>
|
||||
<path d="M163.815 130.173C168.96 128.532 182.524 124.584 195.618 121.922" stroke="url(#paint3_linear_26_2)" stroke-width="0.522968"/>
|
||||
<path opacity="0.6" d="M163.743 130.032C145.537 135.219 120.666 145.679 101.45 156.562C98.5928 150.936 92.5914 137.549 91.4453 129.014" stroke="url(#paint4_linear_26_2)" stroke-width="0.629955"/>
|
||||
<path opacity="0.6" d="M131.988 141.584C109.209 150.463 67.2367 175.448 46.0713 194.25" stroke="url(#paint5_linear_26_2)" stroke-width="0.629955"/>
|
||||
<path d="M163.45 129.852C160.552 124.745 155.015 111.79 156.05 100.82C157.085 89.849 161.432 84.106 162.726 82.501C143.941 79.551 103.163 77.947 70.9746 89.642" stroke="url(#paint6_linear_26_2)" stroke-width="0.629955"/>
|
||||
<path d="M107 257.714C113.591 247.473 136.106 222.182 146.594 210.603C136.483 202.56 125 189.684 120.428 184.179C136.916 171.022 164.082 154.53 175.604 147.929C172.568 144.3 166.025 135.617 163.637 130.305" stroke="url(#paint7_linear_26_2)" stroke-width="0.629955"/>
|
||||
<path d="M208.664 264.039C215.773 251.086 229.419 210.035 235.78 191.283C207.668 179.623 187.65 162.055 175.596 147.866" stroke="url(#paint8_linear_26_2)" stroke-width="0.629955"/>
|
||||
<path d="M285.525 235.502C259.949 234.085 234.914 225.28 225.594 221.056C228.046 214.322 233.523 198.947 235.811 191.32" stroke="url(#paint9_linear_26_2)" stroke-width="0.629955"/>
|
||||
<path d="M163.741 66.905V129.695" stroke="url(#paint10_linear_26_2)" stroke-width="0.581638"/>
|
||||
<path d="M166.436 129.951C166.436 128.294 165.099 126.951 163.449 126.951C161.799 126.951 160.462 128.294 160.462 129.951C160.462 131.608 161.799 132.951 163.449 132.951C165.099 132.951 166.436 131.608 166.436 129.951Z" fill="#EDEDED"/>
|
||||
<path d="M288.509 235.501C288.509 233.844 287.172 232.501 285.522 232.501C283.872 232.501 282.535 233.844 282.535 235.501C282.535 237.158 283.872 238.501 285.522 238.501C287.172 238.501 288.509 237.158 288.509 235.501Z" fill="#EDEDED"/>
|
||||
<path d="M49.0591 194.25C49.0591 192.593 47.7217 191.25 46.072 191.25C44.4223 191.25 43.085 192.593 43.085 194.25C43.085 195.907 44.4223 197.25 46.072 197.25C47.7217 197.25 49.0591 195.907 49.0591 194.25Z" fill="#EDEDED"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_26_2" x1="130.011" y1="0.67701" x2="290.2" y2="210.686" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial_26_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(63.4042 147.574) rotate(-132.571) scale(103.192 103.372)">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial_26_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(35.3474 120.408) rotate(-113.962) scale(61.4062 126.439)">
|
||||
<stop stop-color="#D9D9D9"/>
|
||||
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint3_linear_26_2" x1="161.952" y1="128.31" x2="200.942" y2="123.386" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_26_2" x1="176.291" y1="132.992" x2="146.084" y2="158.019" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_26_2" x1="142.094" y1="139.08" x2="49.0046" y2="215.208" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_26_2" x1="163.45" y1="127.878" x2="151.98" y2="101.371" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_26_2" x1="163.939" y1="158.43" x2="120.03" y2="247.478" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_26_2" x1="211.598" y1="199.54" x2="210.244" y2="264.039" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_26_2" x1="252.624" y1="227.02" x2="289.44" y2="239.696" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_26_2" x1="nan" y1="nan" x2="nan" y2="nan" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_26_2">
|
||||
<rect width="330" height="271" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
Reference in new issue
Block a user