Skip to content

Commit 085e68c

Browse files
feat(studio): log drains empty state update (supabase#39977)
* feat: basic page layout * feat: add logo carousel animation * style: small tidy up of carousel animations * chore: tighten up copy on empty state * style: center empty state * chore: clean up old empty state * feat: wire up upgrade plan button * fix: blur on first time item in carousel * fix: exit blur * fix: add missing asChild prop to link nested in button * fix: add target blank to vote here link
1 parent 5cfd10a commit 085e68c

File tree

4 files changed

+239
-39
lines changed

4 files changed

+239
-39
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useState, useEffect } from 'react'
2+
import { motion, AnimatePresence } from 'framer-motion'
3+
import { cn } from 'ui'
4+
import { Datadog, Grafana, Sentry } from 'icons'
5+
import { BracesIcon } from 'lucide-react'
6+
7+
export const AnimatedLogos = () => {
8+
const [currIndex, setCurrIndex] = useState(0)
9+
const timer = 2500
10+
const iconSize = 36
11+
12+
const logos = [
13+
{
14+
id: 'datadog',
15+
name: 'Datadog',
16+
icon: <Datadog fill="currentColor" strokeWidth={0} size={iconSize} />,
17+
},
18+
{
19+
id: 'loki',
20+
name: 'Loki',
21+
icon: <Grafana fill="currentColor" strokeWidth={0} size={iconSize} />,
22+
},
23+
{ id: 'https', name: 'HTTPS', icon: <BracesIcon size={iconSize} /> },
24+
{
25+
id: 'sentry',
26+
name: 'Sentry',
27+
icon: <Sentry fill="currentColor" strokeWidth={0} size={iconSize} />,
28+
},
29+
]
30+
31+
useEffect(() => {
32+
const interval = setInterval(() => {
33+
setCurrIndex((prev) => (prev + 1) % logos.length)
34+
}, timer)
35+
return () => clearInterval(interval)
36+
}, [logos.length])
37+
38+
const getPreviousIndex = () => (currIndex - 1 + logos.length) % logos.length
39+
const getNextIndex = () => (currIndex + 1) % logos.length
40+
41+
const getPosition = (index: number) => {
42+
if (index === currIndex) return 'center'
43+
if (index === getPreviousIndex()) return 'left'
44+
if (index === getNextIndex()) return 'right'
45+
return 'hidden'
46+
}
47+
48+
const logoVariants = {
49+
hidden: {
50+
x: 'calc(-50% + 120px)',
51+
y: '-50%',
52+
scale: 0.6,
53+
opacity: 0,
54+
filter: 'blur(1px)',
55+
},
56+
right: {
57+
x: 'calc(-50% + 80px)',
58+
y: '-50%',
59+
scale: 0.8,
60+
opacity: 0.5,
61+
zIndex: 2,
62+
filter: 'blur(1px)',
63+
},
64+
center: {
65+
x: '-50%',
66+
y: '-50%',
67+
scale: 1,
68+
opacity: 1,
69+
zIndex: 3,
70+
filter: 'blur(0px)',
71+
},
72+
left: {
73+
x: 'calc(-50% - 80px)',
74+
y: '-50%',
75+
scale: 0.8,
76+
opacity: 0.5,
77+
zIndex: 2,
78+
filter: 'blur(1px)',
79+
},
80+
exit: {
81+
x: 'calc(-50% - 120px)',
82+
y: '-50%',
83+
scale: 0.6,
84+
opacity: 0,
85+
filter: 'blur(1px)',
86+
},
87+
}
88+
89+
const visibleIndices = [getPreviousIndex(), currIndex, getNextIndex()]
90+
91+
return (
92+
<div className="relative w-48 h-32 mx-auto mb-8 overflow-hidden">
93+
<AnimatePresence initial={false}>
94+
{logos.map((logo, index) => {
95+
if (!visibleIndices.includes(index)) return null
96+
97+
const position = getPosition(index)
98+
const isCenter = index === currIndex
99+
100+
return (
101+
<motion.div
102+
key={logo.id}
103+
className={cn(
104+
'absolute top-1/2 left-1/2 flex items-center justify-center rounded-lg',
105+
isCenter ? 'w-24 h-24' : 'w-20 h-20'
106+
)}
107+
variants={logoVariants}
108+
initial="hidden"
109+
animate={position}
110+
exit="exit"
111+
transition={{ duration: 0.5, ease: 'easeInOut' }}
112+
>
113+
<span>{logo.icon}</span>
114+
</motion.div>
115+
)
116+
})}
117+
</AnimatePresence>
118+
<div className="absolute -inset-4 bg-gradient-to-r from-background-surface-75 via-transparent to-background-surface-75 z-40" />
119+
</div>
120+
)
121+
}

apps/studio/components/interfaces/LogDrains/LogDrains.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { MoreHorizontal, Pencil, TrashIcon } from 'lucide-react'
2-
import Link from 'next/link'
32
import { useState } from 'react'
43
import { toast } from 'sonner'
54

@@ -10,7 +9,6 @@ import Panel from 'components/ui/Panel'
109
import { useDeleteLogDrainMutation } from 'data/log-drains/delete-log-drain-mutation'
1110
import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query'
1211
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
13-
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
1412
import {
1513
Button,
1614
DropdownMenu,
@@ -27,6 +25,7 @@ import {
2725
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
2826
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
2927
import { LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants'
28+
import { LogDrainsEmpty } from './LogDrainsEmpty'
3029

3130
export function LogDrains({
3231
onNewDrainClick,
@@ -35,8 +34,6 @@ export function LogDrains({
3534
onNewDrainClick: (src: LogDrainType) => void
3635
onUpdateDrainClick: (drain: LogDrainData) => void
3736
}) {
38-
const { data: org } = useSelectedOrganizationQuery()
39-
4037
const { isLoading: orgPlanLoading, plan } = useCurrentOrgPlan()
4138
const logDrainsEnabled = !orgPlanLoading && (plan?.id === 'team' || plan?.id === 'enterprise')
4239

@@ -71,16 +68,7 @@ export function LogDrains({
7168
})
7269

7370
if (!orgPlanLoading && !logDrainsEnabled) {
74-
return (
75-
<CardButton
76-
title="Upgrade to a Team Plan"
77-
description="Upgrade to a Team or Enterprise Plan to use Log Drains"
78-
>
79-
<Button className="mt-2" asChild>
80-
<Link href={`/org/${org?.slug}/billing`}>Upgrade to Team</Link>
81-
</Button>
82-
</CardButton>
83-
)
71+
return <LogDrainsEmpty />
8472
}
8573

8674
if (isLoading || orgPlanLoading) {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Button, Card, cn } from 'ui'
2+
import Link from 'next/link'
3+
import { AnimatedLogos } from './AnimatedLogos'
4+
import Image from 'next/image'
5+
import { BASE_PATH } from 'lib/constants'
6+
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
7+
8+
export const LogDrainsEmpty = () => {
9+
const items = [
10+
{
11+
step: 1,
12+
title: 'Log drain pricing',
13+
description:
14+
'Log Drains are available as a project Add-On for all Team and Enterprise users. Each Log Drain costs $60 per month.',
15+
label: 'See our pricing',
16+
link: 'https://supabase.com/docs/guides/platform/manage-your-usage/log-drains',
17+
},
18+
{
19+
step: 2,
20+
title: 'Connect to your drain',
21+
description:
22+
'We offer support for multiple destinations including HTTPS, Datadog, Loki and Sentry.',
23+
label: 'Read our documentation',
24+
link: 'https://supabase.com/docs/guides/telemetry/log-drains',
25+
},
26+
]
27+
28+
return (
29+
<div className="flex grow h-full pt-16">
30+
<div className="flex grow items-center justify-center p-12 @container">
31+
<div className="w-full max-w-4xl flex flex-col items-center gap-0">
32+
<div className="text-center mb-12">
33+
<AnimatedLogos />
34+
<h2 className="heading-section mb-1">Capture your logs, your way</h2>
35+
<p className="text-foreground-light mb-6">
36+
Upgrade to a Team or Enterprise Plan to send your logs to your preferred platform
37+
</p>
38+
<UpgradePlanButton type="primary" plan="Team" source="log-drains-empty-state">
39+
Upgrade plan
40+
</UpgradePlanButton>
41+
</div>
42+
<Card className="grid grid-cols-1 @xl:grid-cols-2 bg divide-x mb-8">
43+
{items.map((item, i) => (
44+
<div className="flex flex-col h-full p-6" key={i}>
45+
<div className="flex items-center gap-3 mb-2">
46+
<span
47+
className={cn(
48+
'text-xs shrink-0 font-mono text-foreground-light w-7 h-7 bg border flex items-center justify-center rounded-md'
49+
)}
50+
>
51+
{item.step}
52+
</span>
53+
<h3 className="heading-default">{item.title}</h3>
54+
</div>
55+
<p className="text-foreground-light text-sm mb-4 flex-1">{item.description}</p>
56+
<Button type="default" className="w-full" asChild>
57+
<Link href={item.link} target="_blank">
58+
{item.label}
59+
</Link>
60+
</Button>
61+
</div>
62+
))}
63+
</Card>
64+
<div className="flex items-center justify-center gap-1.5 text-sm">
65+
<Image
66+
className={cn('dark:invert text-muted')}
67+
src={`${BASE_PATH}/img/icons/github-icon.svg`}
68+
width={16}
69+
height={16}
70+
alt="GitHub icon"
71+
/>
72+
<p className="text-foreground-light">
73+
Don't see your preferred drain?{' '}
74+
<Link
75+
href="https://github.com/orgs/supabase/discussions/28324?sort=top"
76+
className="text-foreground underline underline-offset-2 decoration-foreground-muted hover:decoration-foreground transition-all"
77+
target="_blank"
78+
>
79+
Vote here
80+
</Link>
81+
</p>
82+
</div>
83+
</div>
84+
</div>
85+
</div>
86+
)
87+
}

apps/studio/pages/project/[ref]/settings/log-drains.tsx

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -81,32 +81,36 @@ const LogDrainsSettings: NextPageWithLayout = () => {
8181
return (
8282
<>
8383
<ScaffoldContainer>
84-
<ScaffoldHeader className="flex flex-row justify-between">
85-
<div>
86-
<ScaffoldTitle>Log Drains</ScaffoldTitle>
87-
<ScaffoldDescription>
88-
Send your project logs to third party destinations
89-
</ScaffoldDescription>
90-
</div>
91-
<div className="flex items-center justify-end gap-2">
92-
<DocsButton href={`${DOCS_URL}/guides/platform/log-drains`} />
93-
94-
{!(logDrains?.length === 0) && (
95-
<Button
96-
disabled={!logDrainsEnabled || !canManageLogDrains}
97-
onClick={() => {
98-
setSelectedLogDrain(null)
99-
setMode('create')
100-
setOpen(true)
101-
}}
102-
type="primary"
103-
>
104-
Add destination
105-
</Button>
106-
)}
107-
</div>
108-
</ScaffoldHeader>
84+
{(logDrainsEnabled || planLoading) && (
85+
<ScaffoldHeader className="flex flex-row justify-between">
86+
<div>
87+
<ScaffoldTitle>Log Drains</ScaffoldTitle>
88+
<ScaffoldDescription>
89+
Send your project logs to third party destinations
90+
</ScaffoldDescription>
91+
</div>
92+
93+
<div className="flex items-center justify-end gap-2">
94+
<DocsButton href={`${DOCS_URL}/guides/platform/log-drains`} />
95+
96+
{!(logDrains?.length === 0) && (
97+
<Button
98+
disabled={!logDrainsEnabled || !canManageLogDrains}
99+
onClick={() => {
100+
setSelectedLogDrain(null)
101+
setMode('create')
102+
setOpen(true)
103+
}}
104+
type="primary"
105+
>
106+
Add destination
107+
</Button>
108+
)}
109+
</div>
110+
</ScaffoldHeader>
111+
)}
109112
</ScaffoldContainer>
113+
110114
<ScaffoldContainer className="flex flex-col gap-10" bottomPadding>
111115
<LogDrainDestinationSheetForm
112116
mode={mode}

0 commit comments

Comments
 (0)