@@ -2,24 +2,65 @@ import React, { useState, useMemo, useEffect } from 'react';
22import Link from '@docusaurus/Link' ;
33import useBaseUrl from '@docusaurus/useBaseUrl' ;
44import CUICard from '@site/src/components/CUICard' ;
5- // @ts -ignore
6- import integrationsData from '@site/static/integrations.json' ;
7- // @ts -ignore
8- import integrationsCustomData from '@site/static/integrations_custom.json' ;
95import styles from './styles.module.scss' ;
106
7+ type CMSIntegrationData = {
8+ id : number ;
9+ attributes : {
10+ name : string ;
11+ slug : string ;
12+ category : string ;
13+ supportLevel : string ;
14+ docsLink ?: string ;
15+ logo ?: {
16+ data ?: {
17+ attributes : {
18+ url : string ;
19+ } ;
20+ } ;
21+ } ;
22+ logo_dark ?: {
23+ data ?: {
24+ attributes : {
25+ url : string ;
26+ } ;
27+ } ;
28+ } ;
29+ } ;
30+ } ;
31+
1132type IntegrationData = {
1233 slug : string ;
34+ docsLink ?: string ;
1335 integration_logo : string ;
1436 integration_type : string [ ] ;
1537 integration_title ?: string ;
1638 integration_tier ?: string ;
1739} ;
1840
1941function IntegrationCard ( { integration } : { integration : IntegrationData } ) {
42+ // Convert ClickHouse docs URLs to relative links
43+ const getNavigationLink = ( docsLink : string | undefined , slug : string ) : string => {
44+ if ( ! docsLink ) {
45+ return slug ;
46+ }
47+
48+ // Check if it's a ClickHouse docs URL
49+ const clickhouseDocsMatch = docsLink . match ( / h t t p s : \/ \/ c l i c k h o u s e \. c o m \/ d o c s \/ ( .+ ) / ) ;
50+ if ( clickhouseDocsMatch ) {
51+ // Convert to relative link by removing the domain and keeping everything after /docs
52+ return `/${ clickhouseDocsMatch [ 1 ] } ` ;
53+ }
54+
55+ // For external URLs, return as-is
56+ return docsLink ;
57+ } ;
58+
59+ const linkTo = getNavigationLink ( integration . docsLink , integration . slug ) ;
60+
2061 return (
2162 < Link
22- to = { integration . slug }
63+ to = { linkTo }
2364 style = { { textDecoration : 'none' , color : 'inherit' } }
2465 >
2566 < CUICard style = { { position : 'relative' } } >
@@ -38,7 +79,7 @@ function IntegrationCard({ integration }: { integration: IntegrationData }) {
3879 < CUICard . Body >
3980 < CUICard . Header >
4081 < img
41- src = { useBaseUrl ( integration . integration_logo ) }
82+ src = { integration . integration_logo }
4283 alt = { `${ integration . integration_title || integration . slug } logo` }
4384 />
4485 </ CUICard . Header >
@@ -65,20 +106,77 @@ function IntegrationCards({ integrations }: { integrations: IntegrationData[] })
65106 ) ;
66107}
67108
68- export function IntegrationGrid ( ) {
69- // Combine integrations from both JSON files and normalize logo paths
70- const integrations : IntegrationData [ ] = useMemo ( ( ) => {
71- // Process custom integrations to fix logo paths
72- const processedCustomData = integrationsCustomData . map ( integration => ( {
73- ...integration ,
74- integration_logo : integration . integration_logo . startsWith ( '/static/' )
75- ? integration . integration_logo . replace ( '/static/' , '/' )
76- : integration . integration_logo
77- } ) ) ;
78-
79- return [ ...integrationsData , ...processedCustomData ] ;
109+ // Helper function to transform CMS data to the expected format
110+ function transformCMSData ( cmsData : CMSIntegrationData [ ] ) : IntegrationData [ ] {
111+ // Mapping from CMS category to display-friendly integration type
112+ const categoryMapping : { [ key : string ] : string } = {
113+ 'AI_ML' : 'AI/ML' ,
114+ 'CLICKPIPES' : 'ClickPipes' ,
115+ 'DATA_INGESTION' : 'Data ingestion' ,
116+ 'DATA_INTEGRATION' : 'Data integration' ,
117+ 'DATA_MANAGEMENT' : 'Data management' ,
118+ 'DATA_VISUALIZATION' : 'Data visualization' ,
119+ 'LANGUAGE_CLIENT' : 'Language client' ,
120+ 'SECURITY_GOVERNANCE' : 'Security governance' ,
121+ 'SQL_CLIENT' : 'SQL client'
122+ } ;
123+
124+ return cmsData . map ( item => {
125+ // Map category to integration_type array
126+ const integrationTypes = item . attributes . category ? [ categoryMapping [ item . attributes . category ] || item . attributes . category ] : [ ] ;
127+
128+ // Map supportLevel to integration_tier
129+ const integrationTier = item . attributes . supportLevel ?. toLowerCase ( ) || '' ;
130+
131+ return {
132+ slug : item . attributes . slug . startsWith ( '/' ) ? item . attributes . slug : `/${ item . attributes . slug } ` ,
133+ docsLink : item . attributes . docsLink ,
134+ integration_logo : item . attributes . logo ?. data ?. attributes . url ? `https://cms.clickhouse-dev.com:1337${ item . attributes . logo . data . attributes . url } ` : '' ,
135+ integration_type : integrationTypes ,
136+ integration_title : item . attributes . name ,
137+ integration_tier : integrationTier
138+ } ;
139+ } ) ;
140+ }
141+
142+ // Custom hook for fetching CMS data
143+ function useCMSIntegrations ( ) {
144+ const [ integrations , setIntegrations ] = useState < IntegrationData [ ] > ( [ ] ) ;
145+ const [ loading , setLoading ] = useState ( true ) ;
146+ const [ error , setError ] = useState < string | null > ( null ) ;
147+
148+ useEffect ( ( ) => {
149+ const fetchIntegrations = async ( ) => {
150+ try {
151+ setLoading ( true ) ;
152+ const response = await fetch ( 'https://cms.clickhouse-dev.com:1337/api/integrations?populate[]=logo&populate[]=logo_dark' ) ;
153+
154+ if ( ! response . ok ) {
155+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
156+ }
157+
158+ const data = await response . json ( ) ;
159+ const transformedData = transformCMSData ( data . data || [ ] ) ;
160+ setIntegrations ( transformedData ) ;
161+ setError ( null ) ;
162+ } catch ( err ) {
163+ console . error ( 'Failed to fetch integrations from CMS:' , err ) ;
164+ setError ( err instanceof Error ? err . message : 'Failed to fetch integrations' ) ;
165+ setIntegrations ( [ ] ) ;
166+ } finally {
167+ setLoading ( false ) ;
168+ }
169+ } ;
170+
171+ fetchIntegrations ( ) ;
80172 } , [ ] ) ;
81173
174+ return { integrations, loading, error } ;
175+ }
176+
177+ export function IntegrationGrid ( ) {
178+ const { integrations, loading, error } = useCMSIntegrations ( ) ;
179+
82180 // Initialize state from localStorage or default values
83181 const [ searchTerm , setSearchTerm ] = useState ( ( ) => {
84182 if ( typeof window !== 'undefined' ) {
@@ -230,10 +328,30 @@ export function IntegrationGrid() {
230328 return grouped ;
231329 } , [ filteredIntegrations ] ) ;
232330
331+ // Handle loading state
332+ if ( loading ) {
333+ return (
334+ < div style = { { textAlign : 'center' , padding : '2rem' } } >
335+ < p > Loading integrations...</ p >
336+ </ div >
337+ ) ;
338+ }
339+
340+ // Handle error state
341+ if ( error ) {
342+ return (
343+ < div style = { { textAlign : 'center' , padding : '2rem' , color : 'var(--ifm-color-danger)' } } >
344+ < p > Failed to load integrations: { error } </ p >
345+ < p > Please try refreshing the page.</ p >
346+ </ div >
347+ ) ;
348+ }
349+
350+ // Handle empty state
233351 if ( integrations . length === 0 ) {
234352 return (
235353 < div style = { { textAlign : 'center' , padding : '2rem' } } >
236- < p > No integrations found with complete metadata .</ p >
354+ < p > No integrations found.</ p >
237355 </ div >
238356 ) ;
239357 }
0 commit comments