Skip to content

Commit f9027a2

Browse files
joshenlimjordienr
andauthored
Chore/custom content connect UI frameworks (supabase#38090)
* Init custom content hook * Implement useCustomContent hook similarly to useIsFeatureEnabled, and implement extension of organization documents * Attempt to type things nicely * Add support for custom content example projects * Add support for custom content logs explorer default query * nit * Add support for custom content connect UI frameworks * Reset custom-content.json * Smol nit * set custom-content to null --------- Co-authored-by: Jordi Enric <[email protected]>
1 parent b8b01ef commit f9027a2

File tree

12 files changed

+212
-42
lines changed

12 files changed

+212
-42
lines changed

apps/studio/components/interfaces/Connect/Connect.constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export type ConnectionType = {
6464
label: string
6565
guideLink?: string
6666
children: ConnectionType[]
67+
files?: {
68+
name: string
69+
content: string
70+
}[]
6771
}
6872

6973
export const FRAMEWORKS: ConnectionType[] = [

apps/studio/components/interfaces/Connect/Connect.tsx

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
99
import Panel from 'components/ui/Panel'
1010
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
1111
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
12+
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
1213
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
1314
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
1415
import { PROJECT_STATUS } from 'lib/constants'
@@ -30,20 +31,31 @@ import {
3031
} from 'ui'
3132
import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './Connect.constants'
3233
import { getContentFilePath } from './Connect.utils'
33-
import ConnectDropdown from './ConnectDropdown'
34-
import ConnectTabContent from './ConnectTabContent'
34+
import { ConnectDropdown } from './ConnectDropdown'
35+
import { ConnectTabContent } from './ConnectTabContent'
36+
import { ConnectTabContentCustom } from './ConnectTabContentCustom'
3537

3638
export const Connect = () => {
3739
const { ref: projectRef } = useParams()
3840
const { data: selectedProject } = useSelectedProjectQuery()
3941
const isActiveHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY
4042

43+
const { connectFrameworks } = useCustomContent(['connect:frameworks'])
44+
const connectionTypes = !connectFrameworks
45+
? CONNECTION_TYPES
46+
: [
47+
{ key: 'direct', label: 'Connection String', obj: [] },
48+
connectFrameworks,
49+
{ key: 'orms', label: 'ORMs', obj: ORMS },
50+
]
51+
const frameworks = !connectFrameworks ? FRAMEWORKS : connectFrameworks.obj
52+
4153
const [showConnect, setShowConnect] = useQueryState(
4254
'showConnect',
4355
parseAsBoolean.withDefault(false)
4456
)
4557

46-
const [connectionObject, setConnectionObject] = useState<ConnectionType[]>(FRAMEWORKS)
58+
const [connectionObject, setConnectionObject] = useState<ConnectionType[]>(frameworks)
4759
const [selectedParent, setSelectedParent] = useState(connectionObject[0].key) // aka nextjs
4860
const [selectedChild, setSelectedChild] = useState(
4961
connectionObject.find((item) => item.key === selectedParent)?.children[0]?.key ?? ''
@@ -54,6 +66,8 @@ export const Connect = () => {
5466
?.children.find((child) => child.key === selectedChild)?.children[0]?.key || ''
5567
)
5668

69+
const isFrameworkSelected = frameworks.some((x) => x.key === selectedParent)
70+
5771
const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: showConnect })
5872
const { can: canReadAPIKeys } = useAsyncCheckProjectPermissions(
5973
PermissionAction.READ,
@@ -109,8 +123,8 @@ export const Connect = () => {
109123

110124
function handleConnectionType(type: string) {
111125
if (type === 'frameworks') {
112-
setConnectionObject(FRAMEWORKS)
113-
handleConnectionTypeChange(FRAMEWORKS)
126+
setConnectionObject(frameworks)
127+
handleConnectionTypeChange(frameworks)
114128
}
115129

116130
if (type === 'mobiles') {
@@ -207,14 +221,14 @@ export const Connect = () => {
207221

208222
<Tabs_Shadcn_ defaultValue="direct" onValueChange={(value) => handleConnectionType(value)}>
209223
<TabsList_Shadcn_ className={cn('flex overflow-x-scroll gap-x-4', DIALOG_PADDING_X)}>
210-
{CONNECTION_TYPES.map((type) => (
224+
{connectionTypes.map((type) => (
211225
<TabsTrigger_Shadcn_ key={type.key} value={type.key} className="px-0">
212226
{type.label}
213227
</TabsTrigger_Shadcn_>
214228
))}
215229
</TabsList_Shadcn_>
216230

217-
{CONNECTION_TYPES.map((type) => {
231+
{connectionTypes.map((type) => {
218232
const hasChildOptions =
219233
(connectionObject.find((parent) => parent.key === selectedParent)?.children.length ||
220234
0) > 0
@@ -290,11 +304,18 @@ export const Connect = () => {
290304
<p className="text-xs text-foreground-lighter my-3">
291305
Add the following files below to your application
292306
</p>
293-
<ConnectTabContent
294-
projectKeys={projectKeys}
295-
filePath={filePath}
296-
className="rounded-b-none"
297-
/>
307+
{!!connectFrameworks && isFrameworkSelected ? (
308+
<ConnectTabContentCustom
309+
projectKeys={projectKeys}
310+
framework={frameworks.find((x) => x.key === selectedParent)}
311+
/>
312+
) : (
313+
<ConnectTabContent
314+
projectKeys={projectKeys}
315+
filePath={filePath}
316+
className="rounded-b-none"
317+
/>
318+
)}
298319
<Panel.Notice
299320
className="border border-t-0 rounded-lg rounded-t-none"
300321
title="New API keys coming 2025"

apps/studio/components/interfaces/Connect/ConnectDropdown.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@ import {
1414
Popover_Shadcn_,
1515
cn,
1616
} from 'ui'
17+
import { ConnectionType } from './Connect.constants'
1718
import { ConnectionIcon } from './ConnectionIcon'
1819

1920
interface ConnectDropdownProps {
2021
state: string
2122
updateState: (state: string) => void
2223
label: string
23-
items: any[]
24+
items: ConnectionType[]
2425
}
2526

26-
const ConnectDropdown = ({
27+
export const ConnectDropdown = ({
2728
state,
2829
updateState,
2930
label,
@@ -53,11 +54,7 @@ const ConnectDropdown = ({
5354
iconRight={<ChevronDown strokeWidth={1.5} />}
5455
>
5556
<div className="flex items-center gap-2">
56-
{selectedItem?.icon ? (
57-
<ConnectionIcon connection={selectedItem.icon} />
58-
) : (
59-
<Box size={12} />
60-
)}
57+
{selectedItem?.icon ? <ConnectionIcon icon={selectedItem.icon} /> : <Box size={12} />}
6158
{selectedItem?.label}
6259
</div>
6360
</Button>
@@ -79,7 +76,7 @@ const ConnectDropdown = ({
7976
}}
8077
className="flex gap-2 items-center"
8178
>
82-
{item.icon ? <ConnectionIcon connection={item.icon} /> : <Box size={12} />}
79+
{item.icon ? <ConnectionIcon icon={item.icon} /> : <Box size={12} />}
8380
{item.label}
8481
<Check
8582
size={15}
@@ -94,5 +91,3 @@ const ConnectDropdown = ({
9491
</Popover_Shadcn_>
9592
)
9693
}
97-
98-
export default ConnectDropdown

apps/studio/components/interfaces/Connect/ConnectTabContent.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface ConnectContentTabProps extends HTMLAttributes<HTMLDivElement> {
2727
}
2828
}
2929

30-
const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
30+
export const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
3131
({ projectKeys, filePath, ...props }, ref) => {
3232
const { ref: projectRef } = useParams()
3333
const { data: selectedOrg } = useSelectedOrganizationQuery()
@@ -102,5 +102,3 @@ const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
102102
)
103103

104104
ConnectTabContent.displayName = 'ConnectTabContent'
105-
106-
export default ConnectTabContent
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useEffect, useState } from 'react'
2+
3+
import { cn, SimpleCodeBlock } from 'ui'
4+
import { ConnectionType } from './Connect.constants'
5+
import { projectKeys } from './Connect.types'
6+
import {
7+
ConnectTabContent,
8+
ConnectTabs,
9+
ConnectTabTrigger,
10+
ConnectTabTriggers,
11+
} from './ConnectTabs'
12+
13+
interface ConnectTabContentCustomProps {
14+
projectKeys: projectKeys
15+
framework?: ConnectionType
16+
}
17+
18+
export const ConnectTabContentCustom = ({
19+
projectKeys,
20+
framework,
21+
}: ConnectTabContentCustomProps) => {
22+
const { files = [] } = framework ?? {}
23+
24+
const [selectedTab, setSelectedTab] = useState<string>()
25+
26+
useEffect(() => {
27+
if (framework?.files) setSelectedTab(framework.files[0].name)
28+
}, [framework])
29+
30+
return (
31+
<div className={cn('border rounded-lg rounded-b-none')}>
32+
<ConnectTabs value={selectedTab} onValueChange={setSelectedTab}>
33+
<ConnectTabTriggers>
34+
{files.map((x) => (
35+
<ConnectTabTrigger key={`${x.name}-tab`} value={x.name} />
36+
))}
37+
</ConnectTabTriggers>
38+
39+
{files.map((x) => {
40+
const format = x.name.split('.')[1] ?? 'bash'
41+
const content = x.content
42+
.replaceAll('{{apiUrl}}', projectKeys.apiUrl ?? '')
43+
.replaceAll('{{anonKey}}', projectKeys.anonKey ?? '')
44+
.replaceAll('{{publishableKey}}', projectKeys.publishableKey ?? '')
45+
return (
46+
<ConnectTabContent key={`${x.name}-content`} value={x.name}>
47+
<SimpleCodeBlock className={format} parentClassName="min-h-72">
48+
{content}
49+
</SimpleCodeBlock>
50+
</ConnectTabContent>
51+
)
52+
})}
53+
</ConnectTabs>
54+
</div>
55+
)
56+
}

apps/studio/components/interfaces/Connect/ConnectTabs.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,26 @@ interface ConnectTabTriggersProps {
1212

1313
interface ConnectFileTabProps {
1414
children: ReactNode[]
15+
value?: string
16+
onValueChange?: (value: string) => void
1517
}
1618

1719
interface ConnectTabContentProps {
1820
children: ReactNode
1921
value: string
2022
}
21-
const ConnectTabs = ({ children }: ConnectFileTabProps) => {
23+
const ConnectTabs = ({ children, value, onValueChange }: ConnectFileTabProps) => {
2224
const firstChild = children[0]
2325

2426
const defaultValue = isValidElement(firstChild)
2527
? (firstChild.props as any)?.children[0]?.props?.value || ''
2628
: null
2729

28-
return <Tabs_Shadcn_ defaultValue={defaultValue}>{children}</Tabs_Shadcn_>
30+
return (
31+
<Tabs_Shadcn_ defaultValue={defaultValue} value={value} onValueChange={onValueChange}>
32+
{children}
33+
</Tabs_Shadcn_>
34+
)
2935
}
3036

3137
const ConnectTabTrigger = ({ value }: ConnectTabTriggerProps) => {

apps/studio/components/interfaces/Connect/ConnectionIcon.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,29 @@ import Image from 'next/image'
44
import { BASE_PATH } from 'lib/constants'
55

66
interface ConnectionIconProps {
7-
connection: any
7+
icon: string
88
}
99

10-
export const ConnectionIcon = ({ connection }: ConnectionIconProps) => {
10+
export const ConnectionIcon = ({ icon }: ConnectionIconProps) => {
1111
const { resolvedTheme } = useTheme()
1212

13-
const imageFolder = ['ionic-angular'].includes(connection) ? 'icons/frameworks' : 'libraries'
13+
const imageFolder = ['ionic-angular'].includes(icon) ? 'icons/frameworks' : 'libraries'
1414
const imageExtension = imageFolder === 'icons/frameworks' ? '' : '-icon'
15-
16-
return (
17-
<Image
18-
className="transition-all group-hover:scale-110"
19-
src={`${BASE_PATH}/img/${imageFolder}/${connection.toLowerCase()}${
20-
['expo', 'nextjs', 'prisma', 'drizzle', 'astro', 'remix'].includes(connection.toLowerCase())
15+
const iconImgSrc = icon.startsWith('http')
16+
? icon
17+
: `${BASE_PATH}/img/${imageFolder}/${icon.toLowerCase()}${
18+
['expo', 'nextjs', 'prisma', 'drizzle', 'astro', 'remix'].includes(icon.toLowerCase())
2119
? resolvedTheme?.includes('dark')
2220
? '-dark'
2321
: ''
2422
: ''
25-
}${imageExtension}.svg`}
26-
alt={`${connection} logo`}
23+
}${imageExtension}.svg`
24+
25+
return (
26+
<Image
27+
className="transition-all group-hover:scale-110"
28+
src={iconImgSrc}
29+
alt={`${icon} logo`}
2730
width={14}
2831
height={14}
2932
/>

apps/studio/components/interfaces/Connect/content/nextjs/app/supabasejs/content.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types'
22

33
import {
4+
ConnectTabContent,
45
ConnectTabs,
56
ConnectTabTrigger,
67
ConnectTabTriggers,
7-
ConnectTabContent,
88
} from 'components/interfaces/Connect/ConnectTabs'
99
import { SimpleCodeBlock } from 'ui'
1010

@@ -154,4 +154,6 @@ export const createClient = (request: NextRequest) => {
154154
)
155155
}
156156

157+
// [Joshen] Used as a dynamic import
158+
// eslint-disable-next-line no-restricted-exports
157159
export default ContentFile

apps/studio/hooks/custom-content/CustomContent.types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { ConnectionType } from 'components/interfaces/Connect/Connect.constants'
2+
13
export type CustomContentTypes = {
24
organizationLegalDocuments: {
35
id: string
46
name: string
57
description: string
68
action: { text: string; url: string }
79
}[]
10+
811
projectHomepageExampleProjects: {
912
title: string
1013
description: string
@@ -13,4 +16,18 @@ export type CustomContentTypes = {
1316
}[]
1417

1518
logsDefaultQuery: string
19+
20+
/**
21+
* When declaring files for each framework, there are 3 properties that can be dynamically rendered into the file content using handlebar notation:
22+
* - {{apiUrl}}: The API URL of the project
23+
* - {{anonKey}}: The anonymous key of the project (if still using legacy API keys)
24+
* - {{publishableKey}}: The publishable API key of the project (if using new API keys)
25+
*
26+
* These could be helpful in rendering, for e.g an environment file like `.env`
27+
*/
28+
connectFrameworks: {
29+
key: string
30+
label: string
31+
obj: ConnectionType[]
32+
}
1633
}

apps/studio/hooks/custom-content/custom-content.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@
55

66
"project_homepage:example_projects": null,
77

8-
"logs:default_query": null
8+
"logs:default_query": null,
9+
10+
"connect:frameworks": null
911
}

0 commit comments

Comments
 (0)