IntegrationPanels.tsx346 lines · main
1// @ts-nocheck
2import dayjs from 'dayjs'
3import { ArrowRight, ExternalLink, Github } from 'lucide-react'
4import Image from 'next/legacy/image'
5import Link from 'next/link'
6import { forwardRef, HTMLAttributes, ReactNode, RefAttributes } from 'react'
7import { Badge, Button, cn } from 'ui'
8
9import { Markdown } from '@/components/interfaces/Markdown'
10import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
11import type {
12 Integration,
13 IntegrationProjectConnection,
14} from '@/data/integrations/integrations.types'
15import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
16import { BASE_PATH } from '@/lib/constants'
17import { getIntegrationConfigurationUrl } from '@/lib/integration-utils'
18
19const ICON_STROKE_WIDTH = 2
20const ICON_SIZE = 14
21
22export interface IntegrationInstallationProps extends RefAttributes<HTMLLIElement> {
23 title: string
24 integration: Integration
25 disabled?: boolean
26}
27
28type HandleIconType = Integration['integration']['name'] | 'Briven'
29
30const HandleIcon = ({ type, className }: { type: HandleIconType; className?: string }) => {
31 switch (type) {
32 case 'GitHub':
33 return <Github strokeWidth={ICON_STROKE_WIDTH} size={ICON_SIZE} />
34 break
35 // case 'Netlify':
36 // return <Square strokeWidth={ICON_STROKE_WIDTH} size={ICON_SIZE} />
37 // break
38 case 'Vercel':
39 return (
40 <svg
41 xmlns="http://www.w3.org/2000/svg"
42 fill="white"
43 viewBox="0 0 512 512"
44 className={cn('w-3.5', className)}
45 >
46 <path fillRule="evenodd" d="M256,48,496,464H16Z" />
47 </svg>
48 )
49
50 break
51 case 'Briven':
52 return <img src={`${BASE_PATH}/img/briven-logo.svg`} alt="Briven" className="w-3.5"></img>
53 break
54
55 default:
56 return <></>
57 break
58 }
59}
60
61const Avatar = ({ src }: { src: string | undefined }) => {
62 return (
63 <div className="relative border shadow-lg w-8 h-8 rounded-full overflow-hidden">
64 <Image
65 src={src || ''}
66 width={30}
67 height={30}
68 layout="fill"
69 alt="avatar"
70 className="relative"
71 />
72 </div>
73 )
74}
75
76export const IntegrationInstallation = forwardRef<HTMLLIElement, IntegrationInstallationProps>(
77 ({ integration, disabled, ...props }, ref) => {
78 const IntegrationIconBlock = () => {
79 return (
80 <div className="bg-black text-white w-8 h-8 rounded-sm flex items-center justify-center">
81 <HandleIcon type={integration.integration.name} />
82 </div>
83 )
84 }
85
86 return (
87 <li
88 ref={ref}
89 key={integration.id}
90 className="bg-surface-100 border shadow-xs flex justify-between items-center px-8 py-4 rounded-lg"
91 {...props}
92 >
93 <div className="flex gap-6 items-center">
94 <div className="flex gap-3 items-center">
95 <div className="flex -space-x-1">
96 <IntegrationIconBlock />
97 <Avatar src={integration?.metadata?.account.avatar} />
98 </div>
99 </div>
100 <div className="flex flex-col gap-0">
101 <div className="flex items-center gap-2">
102 <span className="text-foreground text-sm font-medium">
103 {integration.metadata?.account.name ||
104 (integration.metadata !== undefined &&
105 'gitHubConnectionOwner' in integration.metadata &&
106 integration.metadata?.gitHubConnectionOwner)}
107 </span>
108
109 <Badge>{integration.metadata?.account.type}</Badge>
110 </div>
111 <div className="flex flex-col gap-0">
112 <span className="text-foreground-lighter text-xs">
113 Created {dayjs(integration.inserted_at).fromNow()}
114 </span>
115 <span className="text-foreground-lighter text-xs">
116 Added by {integration?.added_by?.primary_email}
117 </span>
118 </div>
119 </div>
120 </div>
121
122 <Button asChild disabled={disabled} type="default" iconRight={<ExternalLink />}>
123 {disabled ? (
124 <p>Manage</p>
125 ) : (
126 <Link
127 href={getIntegrationConfigurationUrl(integration)}
128 target="_blank"
129 rel="noopener noreferrer"
130 >
131 Manage
132 </Link>
133 )}
134 </Button>
135 </li>
136 )
137 }
138)
139
140export interface IntegrationConnectionProps extends HTMLAttributes<HTMLLIElement> {
141 connection: IntegrationProjectConnection
142 type: Integration['integration']['name']
143 actions?: ReactNode
144 showNode?: boolean
145 orientation?: 'horizontal' | 'vertical'
146}
147
148export const IntegrationConnection = forwardRef<HTMLLIElement, IntegrationConnectionProps>(
149 (
150 { connection, type, actions, showNode = true, orientation = 'horizontal', className, ...props },
151 ref
152 ) => {
153 const { data: project } = useProjectDetailQuery({ ref: connection.briven_project_ref })
154
155 return (
156 <li
157 ref={ref}
158 key={connection.id}
159 {...props}
160 className={cn(showNode && 'pl-8 ml-6 border-l border-muted', 'relative')}
161 >
162 {showNode && (
163 <div className="absolute w-8 rounded-bl-full border-b border-l border-muted h-10 -left-px"></div>
164 )}
165 <div
166 className={cn(
167 orientation === 'horizontal'
168 ? 'flex items-center justify-between gap-2'
169 : 'flex flex-col gap-3',
170 'bg-surface-100 border shadow-xs px-6 py-4 rounded-lg',
171 className
172 )}
173 >
174 <div className="flex flex-col gap-1 min-w-0">
175 <div className="flex items-center gap-2">
176 <div className="shrink-0 flex gap-x-2 items-center max-w-40 ">
177 <HandleIcon type={'Briven'} />
178 <span title={project?.name} className="text-sm truncate">
179 {project?.name}
180 </span>
181 </div>
182
183 <ArrowRight
184 size={14}
185 className="shrink-0 text-foreground-lighter"
186 strokeWidth={1.5}
187 />
188
189 <div className="flex-1 min-w-0 flex gap-2 items-center">
190 {!connection?.metadata?.framework ? (
191 <div className="bg-black text-white w-4 h-4 rounded-sm flex items-center justify-center">
192 <HandleIcon type={type} className={'w-2.5!'} />
193 </div>
194 ) : (
195 <img
196 src={`${BASE_PATH}/img/icons/frameworks/${connection.metadata.framework}.svg`}
197 width={21}
198 height={21}
199 alt={`icon`}
200 />
201 )}
202 {type === 'GitHub' ? (
203 <a
204 title={connection.metadata.name}
205 href={`https://github.com/${connection.metadata?.name}`}
206 className="text-sm truncate"
207 target="_blank"
208 rel="noreferrer"
209 >
210 {connection.metadata?.name}
211 </a>
212 ) : (
213 <span title={connection.metadata.name} className="text-sm truncate">
214 {connection.metadata?.name}
215 </span>
216 )}
217 </div>
218 </div>
219
220 <div className="flex flex-col gap-0">
221 <span className="text-foreground-lighter text-xs">
222 Connected {dayjs(connection?.inserted_at).fromNow()}
223 </span>
224 <span className="text-foreground-lighter text-xs">
225 Added by {connection?.added_by?.primary_email}
226 </span>
227 </div>
228 </div>
229
230 <div className="shrink-0">{actions}</div>
231 </div>
232 </li>
233 )
234 }
235)
236
237export const IntegrationConnectionOption = forwardRef<HTMLLIElement, IntegrationConnectionProps>(
238 ({ connection, type, ...props }, ref) => {
239 const { data: project } = useProjectDetailQuery({ ref: connection.briven_project_ref })
240
241 return (
242 <li
243 ref={ref}
244 key={connection.id}
245 {...props}
246 className={cn(
247 'bg-surface-100 border shadow-xs flex justify-between items-center px-8 py-4 rounded-lg'
248 )}
249 >
250 <div className="flex flex-col gap-1">
251 <div className="flex gap-2 items-center">
252 <HandleIcon type={'Briven'} />
253 <span className="text-sm">{project?.name}</span>
254 <ArrowRight size={14} className="text-foreground-lighter" strokeWidth={1.5} />
255 <HandleIcon type={type} />
256 <span className="text-sm">{connection.metadata.name}</span>
257 </div>
258
259 <span className="text-foreground-lighter text-xs">
260 Connected {dayjs(connection.inserted_at).fromNow()}
261 </span>
262 </div>
263
264 <Button type="default">Connect</Button>
265 </li>
266 )
267 }
268)
269
270export const EmptyIntegrationConnection = forwardRef<
271 HTMLDivElement,
272 HTMLAttributes<HTMLDivElement> & {
273 showNode?: boolean
274 onClick: () => void
275 disabled?: boolean
276 }
277>(({ className, showNode = true, onClick, disabled, ...props }, ref) => {
278 return (
279 <div
280 ref={ref}
281 {...props}
282 className={cn(
283 showNode && 'ml-6 pl-8 mt-4 border-l',
284 'relative pb-2',
285 'last:border-l-transparent',
286 className
287 )}
288 >
289 {showNode && (
290 <div className="absolute w-8 rounded-bl-full border-b border-l border-muted h-14 -top-4 -left-px"></div>
291 )}
292 <div
293 className={cn(
294 'w-full',
295 'border border-dashed bg-surface-100 border-overlay',
296 'flex h-20 px-10 rounded-lg justify-center items-center'
297 )}
298 >
299 <ButtonTooltip
300 type="default"
301 disabled={disabled}
302 onClick={() => onClick()}
303 tooltip={{
304 content: {
305 side: 'bottom',
306 text: disabled ? 'Additional permissions required to add connection' : undefined,
307 },
308 }}
309 >
310 Add new project connection
311 </ButtonTooltip>
312 </div>
313 </div>
314 )
315})
316
317interface IntegrationConnectionHeader extends React.HTMLAttributes<HTMLDivElement> {
318 name?: string
319 markdown?: string
320 showNode?: boolean
321}
322
323export const IntegrationConnectionHeader = forwardRef<HTMLDivElement, IntegrationConnectionHeader>(
324 ({ className, markdown = '', showNode = true, ...props }, ref) => {
325 return (
326 <div
327 {...props}
328 ref={ref}
329 className={cn(
330 showNode && 'border-l border-muted ml-6 pl-8',
331 'py-4 prose text-sm',
332 className
333 )}
334 >
335 {props.title && <h5 className="text-foreground">{props.title}</h5>}
336 <Markdown content={markdown} />
337 </div>
338 )
339 }
340)
341
342IntegrationInstallation.displayName = 'IntegrationInstallation'
343IntegrationConnection.displayName = 'IntegrationConnection'
344IntegrationConnectionHeader.displayName = 'IntegrationConnectionHeader'
345EmptyIntegrationConnection.displayName = 'EmptyIntegrationConnection'
346IntegrationConnectionOption.displayName = 'IntegrationConnectionOption'