Skip to content

Add Strapi-Response-Format header for v4 backward compatibility during v5 migration#5579

Open
vineethasok wants to merge 1 commit intomainfrom
va-v4-strapi-header
Open

Add Strapi-Response-Format header for v4 backward compatibility during v5 migration#5579
vineethasok wants to merge 1 commit intomainfrom
va-v4-strapi-header

Conversation

@vineethasok
Copy link
Contributor

Summary

  • Adds the Strapi-Response-Format: v4 header to all Strapi API requests to maintain backward-compatible response format during the Strapi v4 → v5 migration

Context

Strapi v5 changes the default REST API response structure (flattening the data.attributes nesting). This header preserves the v4 response format so the existing codebase continues to work without requiring a full migration of all data-access code at once.

Notes

Test plan

  • Verify Strapi API responses return the expected v4 format in preview/staging
  • Confirm no regressions in pages that consume Strapi data

We would replace the api with strapi client after v5 upgradation

Apply consistent code formatting (double quotes, spacing, trailing commas) and minor refactors across IntegrationGrid.tsx. Improve CMS data handling and resilience: normalize slug/logo fields, map CMS categories, deduplicate grouped results, and better localStorage initialization. Harden remote fetch with AbortController timeout, added Accept and Strapi-Response-Format headers, clearer logging/error handling, and fallback JSON usage for faster initial render. No functional UI changes apart from these robustness and formatting improvements.
@vineethasok vineethasok self-assigned this Feb 23, 2026
@vineethasok vineethasok requested a review from a team as a code owner February 23, 2026 16:44
@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clickhouse-docs Ready Ready Preview, Comment Feb 23, 2026 4:51pm
4 Skipped Deployments
Project Deployment Actions Updated (UTC)
clickhouse-docs-jp Ignored Ignored Feb 23, 2026 4:51pm
clickhouse-docs-ko Ignored Ignored Feb 23, 2026 4:51pm
clickhouse-docs-ru Ignored Ignored Feb 23, 2026 4:51pm
clickhouse-docs-zh Ignored Ignored Feb 23, 2026 4:51pm

Request Review

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to add the Strapi-Response-Format: v4 header to Strapi API requests to maintain backward-compatible response format during the Strapi v4 → v5 migration. The PR description indicates this header preserves the v4 response format so existing data-access code continues to work without requiring immediate migration to handle v5's flattened response structure.

Changes:

  • Added Strapi-Response-Format: v4 header to the Strapi integrations API fetch call
  • Applied code formatting changes throughout the file (quote style standardization)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1 to +859
@@ -44,13 +44,18 @@ function IntegrationCard({ integration }: { integration: IntegrationData }) {
const { colorMode } = useColorMode();

// Convert ClickHouse docs URLs to relative links
const getNavigationLink = (docsLink: string | undefined, slug: string): string => {
const getNavigationLink = (
docsLink: string | undefined,
slug: string,
): string => {
if (!docsLink) {
return slug;
}

// Check if it's a ClickHouse docs URL
const clickhouseDocsMatch = docsLink.match(/https:\/\/clickhouse\.com\/docs\/(.+)/);
const clickhouseDocsMatch = docsLink.match(
/https:\/\/clickhouse\.com\/docs\/(.+)/,
);
if (clickhouseDocsMatch) {
// Convert to relative link by removing the domain and keeping everything after /docs
return `/${clickhouseDocsMatch[1]}`;
@@ -63,7 +68,7 @@ function IntegrationCard({ integration }: { integration: IntegrationData }) {
// Select appropriate logo based on theme and availability
const getLogoSrc = (): string => {
// If we're in dark mode and dark logo is available, use it
if (colorMode === 'dark' && integration.integration_logo_dark) {
if (colorMode === "dark" && integration.integration_logo_dark) {
return integration.integration_logo_dark;
}
// Otherwise, use the regular logo
@@ -73,54 +78,53 @@ function IntegrationCard({ integration }: { integration: IntegrationData }) {
const linkTo = getNavigationLink(integration.docsLink, integration.slug);

// Check if this is an external link (not to clickhouse.com/docs)
const isExternalLink = linkTo.startsWith('http') && !linkTo.includes('clickhouse.com/docs');
const isExternalLink =
linkTo.startsWith("http") && !linkTo.includes("clickhouse.com/docs");

return (
<div className={styles.cardWrapper}>
<Link
to={linkTo}
style={{ textDecoration: 'none', color: 'inherit' }}
>
<CUICard style={{ position: 'relative' }}>
<Link to={linkTo} style={{ textDecoration: "none", color: "inherit" }}>
<CUICard style={{ position: "relative" }}>
{/* Tier Icon in top right corner */}
{integration.integration_tier && integration.integration_tier !== 'community' && (
<div style={{
position: 'absolute',
top: '12px',
right: '12px',
zIndex: 1,
opacity: 0.7
}}>
{getTierIcon(integration.integration_tier)}
</div>
)}
{integration.integration_tier &&
integration.integration_tier !== "community" && (
<div
style={{
position: "absolute",
top: "12px",
right: "12px",
zIndex: 1,
opacity: 0.7,
}}
>
{getTierIcon(integration.integration_tier)}
</div>
)}
<CUICard.Body>
<CUICard.Header>
<img
src={getLogoSrc()}
alt={`${integration.integration_title || integration.slug} logo`}
/>
</CUICard.Header>
<CUICard.Footer>
{integration.integration_title}
</CUICard.Footer>
<CUICard.Header>
<img
src={getLogoSrc()}
alt={`${integration.integration_title || integration.slug} logo`}
/>
</CUICard.Header>
<CUICard.Footer>{integration.integration_title}</CUICard.Footer>
</CUICard.Body>
</CUICard>
</Link>
{/* External link overlay */}
{isExternalLink && (
<div className={styles.externalLinkOverlay}>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
d='M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14'
/>
</svg>
</div>
@@ -129,15 +133,25 @@ function IntegrationCard({ integration }: { integration: IntegrationData }) {
);
}

function IntegrationCards({ integrations }: { integrations: IntegrationData[] }) {
function IntegrationCards({
integrations,
}: {
integrations: IntegrationData[];
}) {
return (
<div className={styles.integrationGrid} style={{
display: 'grid',
gap: '24px',
margin: '32px 0'
}}>
<div
className={styles.integrationGrid}
style={{
display: "grid",
gap: "24px",
margin: "32px 0",
}}
>
{integrations.map((integration, index) => (
<IntegrationCard key={`${integration.slug}-${integration.integration_title || index}`} integration={integration} />
<IntegrationCard
key={`${integration.slug}-${integration.integration_title || index}`}
integration={integration}
/>
))}
</div>
);
@@ -147,32 +161,40 @@ function IntegrationCards({ integrations }: { integrations: IntegrationData[] })
function transformCMSData(cmsData: CMSIntegrationData[]): IntegrationData[] {
// Mapping from CMS category to display-friendly integration type
const categoryMapping: { [key: string]: string } = {
'AI_ML': 'AI/ML',
'CLICKPIPES': 'ClickPipes',
'DATA_INGESTION': 'Data ingestion',
'DATA_INTEGRATION': 'Data integration',
'DATA_MANAGEMENT': 'Data management',
'DATA_VISUALIZATION': 'Data visualization',
'LANGUAGE_CLIENT': 'Language client',
'SECURITY_GOVERNANCE': 'Security governance',
'SQL_CLIENT': 'SQL client'
AI_ML: "AI/ML",
CLICKPIPES: "ClickPipes",
DATA_INGESTION: "Data ingestion",
DATA_INTEGRATION: "Data integration",
DATA_MANAGEMENT: "Data management",
DATA_VISUALIZATION: "Data visualization",
LANGUAGE_CLIENT: "Language client",
SECURITY_GOVERNANCE: "Security governance",
SQL_CLIENT: "SQL client",
};

return cmsData.map(item => {
return cmsData.map((item) => {
// Map category to integration_type array
const integrationTypes = item.attributes.category ? [categoryMapping[item.attributes.category] || item.attributes.category] : [];
const integrationTypes = item.attributes.category
? [categoryMapping[item.attributes.category] || item.attributes.category]
: [];

// Map supportLevel to integration_tier
const integrationTier = item.attributes.supportLevel?.toLowerCase() || '';
const integrationTier = item.attributes.supportLevel?.toLowerCase() || "";

return {
slug: item.attributes.slug.startsWith('/') ? item.attributes.slug : `/${item.attributes.slug}`,
slug: item.attributes.slug.startsWith("/")
? item.attributes.slug
: `/${item.attributes.slug}`,
docsLink: item.attributes.docsLink,
integration_logo: item.attributes.logo?.data?.attributes.url ? `https://cms.clickhouse-dev.com:1337${item.attributes.logo.data.attributes.url}` : '',
integration_logo_dark: item.attributes.logo_dark?.data?.attributes.url ? `https://cms.clickhouse-dev.com:1337${item.attributes.logo_dark.data.attributes.url}` : undefined,
integration_logo: item.attributes.logo?.data?.attributes.url
? `https://cms.clickhouse-dev.com:1337${item.attributes.logo.data.attributes.url}`
: "",
integration_logo_dark: item.attributes.logo_dark?.data?.attributes.url
? `https://cms.clickhouse-dev.com:1337${item.attributes.logo_dark.data.attributes.url}`
: undefined,
integration_type: integrationTypes,
integration_title: item.attributes.name,
integration_tier: integrationTier
integration_tier: integrationTier,
};
});
}
@@ -185,14 +207,14 @@ function useCMSIntegrations() {
const [integrations, setIntegrations] = useState<IntegrationData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fallbackPath = useBaseUrl('/integrations-fallback.json');
const fallbackPath = useBaseUrl("/integrations-fallback.json");

useEffect(() => {
const fetchIntegrations = async () => {
// Step 1: Load fallback data first for immediate display
try {
const fallbackResponse = await fetch(fallbackPath, {
cache: 'no-cache' // Always revalidate with server to get fresh data
cache: "no-cache", // Always revalidate with server to get fresh data
});

if (fallbackResponse.ok) {
@@ -201,12 +223,15 @@ function useCMSIntegrations() {
setIntegrations(transformedData);
setError(null);
setLoading(false); // Show content immediately with fallback data
console.log('Loaded fallback integrations data');
console.log("Loaded fallback integrations data");
} else {
console.warn('Fallback file not available, will try CMS only');
console.warn("Fallback file not available, will try CMS only");
}
} catch (fallbackErr) {
console.error('Failed to load fallback integrations data:', fallbackErr);
console.error(
"Failed to load fallback integrations data:",
fallbackErr,
);
// Continue to try CMS even if fallback fails
}

@@ -216,18 +241,19 @@ function useCMSIntegrations() {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
console.log('CMS request timed out after 8 seconds');
console.log("CMS request timed out after 8 seconds");
}, 8000); // 8 second timeout

const response = await fetch(
'https://cms.clickhouse-dev.com:1337/api/integrations?populate[]=logo&populate[]=logo_dark',
"https://cms.clickhouse-dev.com:1337/api/integrations?populate[]=logo&populate[]=logo_dark",
{
signal: controller.signal,
// Add headers to help with CORS and caching
headers: {
'Accept': 'application/json',
}
}
Accept: "application/json",
"Strapi-Response-Format": "v4",
},
},
);

clearTimeout(timeoutId);
@@ -242,20 +268,27 @@ function useCMSIntegrations() {
// Update with fresh CMS data
setIntegrations(transformedData);
setError(null);
console.log('Successfully updated with fresh CMS data');
console.log("Successfully updated with fresh CMS data");
} catch (cmsErr) {
// CMS fetch failed, but that's okay - we already have fallback data
if (cmsErr instanceof Error) {
if (cmsErr.name === 'AbortError') {
console.log('CMS request was aborted due to timeout, using fallback data');
if (cmsErr.name === "AbortError") {
console.log(
"CMS request was aborted due to timeout, using fallback data",
);
} else {
console.error('Error loading integrations from CMS:', cmsErr.message);
console.error(
"Error loading integrations from CMS:",
cmsErr.message,
);
}
}

// Only set error if we don't have any integrations data at all
if (integrations.length === 0) {
setError('Unable to load integrations. Please try refreshing the page.');
setError(
"Unable to load integrations. Please try refreshing the page.",
);
}
}
}
@@ -274,65 +307,65 @@ export function IntegrationGrid() {

// Initialize state from localStorage or default values
const [searchTerm, setSearchTerm] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('integrations-search') || '';
if (typeof window !== "undefined") {
return localStorage.getItem("integrations-search") || "";
}
return '';
return "";
});

const [selectedFilter, setSelectedFilter] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('integrations-filter') || 'All';
if (typeof window !== "undefined") {
return localStorage.getItem("integrations-filter") || "All";
}
return 'All';
return "All";
});

const [selectedTier, setSelectedTier] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('integrations-tier') || 'All';
if (typeof window !== "undefined") {
return localStorage.getItem("integrations-tier") || "All";
}
return 'All';
return "All";
});

// Save to localStorage whenever state changes
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('integrations-search', searchTerm);
if (typeof window !== "undefined") {
localStorage.setItem("integrations-search", searchTerm);
}
}, [searchTerm]);

useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('integrations-filter', selectedFilter);
if (typeof window !== "undefined") {
localStorage.setItem("integrations-filter", selectedFilter);
}
}, [selectedFilter]);

useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('integrations-tier', selectedTier);
if (typeof window !== "undefined") {
localStorage.setItem("integrations-tier", selectedTier);
}
}, [selectedTier]);

// Get unique integration types for filter buttons
const integrationTypes = useMemo(() => {
const types = new Set<string>();
integrations.forEach(integration => {
integration.integration_type.forEach(type => {
integrations.forEach((integration) => {
integration.integration_type.forEach((type) => {
types.add(type);
});
});

// Custom sort order
const sortOrder = [
'Language client',
'ClickPipes',
'Data ingestion',
'Data visualization',
'AI/ML',
'Data integration',
'Data management',
'Security governance',
'SQL client'
"Language client",
"ClickPipes",
"Data ingestion",
"Data visualization",
"AI/ML",
"Data integration",
"Data management",
"Security governance",
"SQL client",
];

return Array.from(types).sort((a, b) => {
@@ -356,14 +389,14 @@ export function IntegrationGrid() {
// Get unique integration tiers for tier filter buttons
const integrationTiers = useMemo(() => {
const tiers = new Set<string>();
integrations.forEach(integration => {
integrations.forEach((integration) => {
if (integration.integration_tier) {
tiers.add(integration.integration_tier);
}
});

// Custom sort order for tiers
const tierSortOrder = ['core', 'partner', 'community'];
const tierSortOrder = ["core", "partner", "community"];

return Array.from(tiers).sort((a, b) => {
const indexA = tierSortOrder.indexOf(a);
@@ -385,25 +418,29 @@ export function IntegrationGrid() {

// Filter and group integrations
const filteredIntegrations = useMemo(() => {
const filtered = integrations.filter(integration => {
const matchesSearch = integration.integration_title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
integration.slug.toLowerCase().includes(searchTerm.toLowerCase());
const filtered = integrations.filter((integration) => {
const matchesSearch =
integration.integration_title
?.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
integration.slug.toLowerCase().includes(searchTerm.toLowerCase());

const matchesFilter = selectedFilter === 'All' ||
integration.integration_type.some(type => type === selectedFilter);
const matchesFilter =
selectedFilter === "All" ||
integration.integration_type.some((type) => type === selectedFilter);

const matchesTier = selectedTier === 'All' ||
integration.integration_tier === selectedTier;
const matchesTier =
selectedTier === "All" || integration.integration_tier === selectedTier;

return matchesSearch && matchesFilter && matchesTier;
});

// Sort filtered results by tier first, then by title
return filtered.sort((a, b) => {
// Define tier priority order
const tierOrder = ['core', 'partner', 'community', ''];
const tierA = a.integration_tier || '';
const tierB = b.integration_tier || '';
const tierOrder = ["core", "partner", "community", ""];
const tierA = a.integration_tier || "";
const tierB = b.integration_tier || "";

const tierIndexA = tierOrder.indexOf(tierA);
const tierIndexB = tierOrder.indexOf(tierB);
@@ -414,21 +451,31 @@ export function IntegrationGrid() {
}

// If tiers are the same, sort by title alphabetically
return (a.integration_title || '').localeCompare(b.integration_title || '');
return (a.integration_title || "").localeCompare(
b.integration_title || "",
);
});
}, [integrations, searchTerm, selectedFilter, selectedTier]);

// Group integrations by type for sectioned view
const groupedIntegrations = useMemo(() => {
const grouped = new Map<string, IntegrationData[]>();

filteredIntegrations.forEach(integration => {
integration.integration_type.forEach(type => {
filteredIntegrations.forEach((integration) => {
integration.integration_type.forEach((type) => {
if (!grouped.has(type)) {
grouped.set(type, []);
}
// Avoid duplicates in the same section using slug + title combination
if (!grouped.get(type)?.find(item => item.slug === integration.slug && item.integration_title === integration.integration_title)) {
if (
!grouped
.get(type)
?.find(
(item) =>
item.slug === integration.slug &&
item.integration_title === integration.integration_title,
)
) {
grouped.get(type)?.push(integration);
}
});
@@ -438,9 +485,9 @@ export function IntegrationGrid() {
grouped.forEach((integrationsArray, key) => {
integrationsArray.sort((a, b) => {
// Define tier priority order
const tierOrder = ['core', 'partner', 'community', ''];
const tierA = a.integration_tier || '';
const tierB = b.integration_tier || '';
const tierOrder = ["core", "partner", "community", ""];
const tierA = a.integration_tier || "";
const tierB = b.integration_tier || "";

const tierIndexA = tierOrder.indexOf(tierA);
const tierIndexB = tierOrder.indexOf(tierB);
@@ -451,7 +498,9 @@ export function IntegrationGrid() {
}

// If tiers are the same, sort by title alphabetically
return (a.integration_title || '').localeCompare(b.integration_title || '');
return (a.integration_title || "").localeCompare(
b.integration_title || "",
);
});
});

@@ -461,7 +510,7 @@ export function IntegrationGrid() {
// Handle loading state
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<div style={{ textAlign: "center", padding: "2rem" }}>
<p>Loading integrations...</p>
</div>
);
@@ -470,7 +519,13 @@ export function IntegrationGrid() {
// Handle error state
if (error) {
return (
<div style={{ textAlign: 'center', padding: '2rem', color: 'var(--ifm-color-danger)' }}>
<div
style={{
textAlign: "center",
padding: "2rem",
color: "var(--ifm-color-danger)",
}}
>
<p>Failed to load integrations: {error}</p>
<p>Please try refreshing the page.</p>
</div>
@@ -480,7 +535,7 @@ export function IntegrationGrid() {
// Handle empty state
if (integrations.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<div style={{ textAlign: "center", padding: "2rem" }}>
<p>No integrations found.</p>
</div>
);
@@ -492,70 +547,80 @@ export function IntegrationGrid() {
<div className={styles.searchContainer}>
<svg
className={styles.searchIcon}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
/>
</svg>
<input
type="text"
placeholder="Search by integration"
type='text'
placeholder='Search by integration'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className={styles.searchInput}
/>
</div>

{/* Filter Buttons */}
<div className={styles.filterContainer} style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div
className={styles.filterContainer}
style={{ display: "flex", flexDirection: "column", gap: "12px" }}
>
{/* Integration Type Filters - First Row */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
<button
className={`${styles.filterButton} ${selectedFilter === 'All' ? styles.active : ''}`}
onClick={() => setSelectedFilter('All')}
style={{ padding: '6px 12px', fontSize: '0.875rem' }}
className={`${styles.filterButton} ${selectedFilter === "All" ? styles.active : ""}`}
onClick={() => setSelectedFilter("All")}
style={{ padding: "6px 12px", fontSize: "0.875rem" }}
>
All
</button>
{integrationTypes.map(type => (
{integrationTypes.map((type) => (
<button
key={type}
className={`${styles.filterButton} ${selectedFilter === type ? styles.active : ''}`}
className={`${styles.filterButton} ${selectedFilter === type ? styles.active : ""}`}
onClick={() => setSelectedFilter(type)}
style={{ padding: '6px 12px', fontSize: '0.875rem' }}
style={{ padding: "6px 12px", fontSize: "0.875rem" }}
>
{getProperCapitalization(type)}
</button>
))}
</div>

{/* Integration Tier Filters - Second Row */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '12px'}}>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "8px",
marginTop: "12px",
}}
>
<button
className={`${styles.filterButton} ${selectedTier === 'All' ? styles.active : ''}`}
onClick={() => setSelectedTier('All')}
style={{ padding: '6px 12px', fontSize: '0.875rem' }}
className={`${styles.filterButton} ${selectedTier === "All" ? styles.active : ""}`}
onClick={() => setSelectedTier("All")}
style={{ padding: "6px 12px", fontSize: "0.875rem" }}
>
All tiers
</button>
{integrationTiers.map(tier => (
{integrationTiers.map((tier) => (
<button
key={tier}
className={`${styles.filterButton} ${selectedTier === tier ? styles.active : ''}`}
className={`${styles.filterButton} ${selectedTier === tier ? styles.active : ""}`}
onClick={() => setSelectedTier(tier)}
style={{
padding: '6px 12px',
fontSize: '0.875rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
padding: "6px 12px",
fontSize: "0.875rem",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{getTierIcon(tier, true)}
@@ -566,21 +631,21 @@ export function IntegrationGrid() {
</div>

{/* Integration Sections */}
{selectedFilter === 'All' ? (
{selectedFilter === "All" ? (
// Show sections grouped by type
Array.from(groupedIntegrations.entries())
.sort(([a], [b]) => {
// Use the same sort order as the filter buttons
const sortOrder = [
'Language client',
'ClickPipes',
'Data ingestion',
'Data visualization',
'AI/ML',
'Data integration',
'Data management',
'Security governance',
'SQL client'
"Language client",
"ClickPipes",
"Data ingestion",
"Data visualization",
"AI/ML",
"Data integration",
"Data management",
"Security governance",
"SQL client",
];

const indexA = sortOrder.indexOf(a);
@@ -632,34 +697,106 @@ export function IntegrationGrid() {
}

function getTierIcon(tier: string, withMargin = false): React.ReactNode {
const marginStyle = withMargin ? { marginRight: '6px' } : {};
const marginStyle = withMargin ? { marginRight: "6px" } : {};

switch (tier) {
case 'core':
case "core":
return (
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style={marginStyle}>
<path d="M1.30762 1.39073C1.30762 1.3103 1.37465 1.22986 1.46849 1.22986H2.64824C2.72868 1.22986 2.80912 1.29689 2.80912 1.39073V14.4886C2.80912 14.5691 2.74209 14.6495 2.64824 14.6495H1.46849C1.38805 14.6495 1.30762 14.5825 1.30762 14.4886V1.39073Z" fill="currentColor" />
<path d="M4.2832 1.39073C4.2832 1.3103 4.35023 1.22986 4.44408 1.22986H5.62383C5.70427 1.22986 5.7847 1.29689 5.7847 1.39073V14.4886C5.7847 14.5691 5.71767 14.6495 5.62383 14.6495H4.44408C4.36364 14.6495 4.2832 14.5825 4.2832 14.4886V1.39073Z" fill="currentColor" />
<path d="M7.25977 1.39073C7.25977 1.3103 7.3268 1.22986 7.42064 1.22986H8.60039C8.68083 1.22986 8.76127 1.29689 8.76127 1.39073V14.4886C8.76127 14.5691 8.69423 14.6495 8.60039 14.6495H7.42064C7.3402 14.6495 7.25977 14.5825 7.25977 14.4886V1.39073Z" fill="currentColor" />
<path d="M10.2354 1.39073C10.2354 1.3103 10.3024 1.22986 10.3962 1.22986H11.576C11.6564 1.22986 11.7369 1.29689 11.7369 1.39073V14.4886C11.7369 14.5691 11.6698 14.6495 11.576 14.6495H10.3962C10.3158 14.6495 10.2354 14.5825 10.2354 14.4886V1.39073Z" fill="currentColor" />
<path d="M13.2256 6.6057C13.2256 6.52526 13.2926 6.44482 13.3865 6.44482H14.5662C14.6466 6.44482 14.7271 6.51186 14.7271 6.6057V9.27354C14.7271 9.35398 14.6601 9.43442 14.5662 9.43442H13.3865C13.306 9.43442 13.2256 9.36739 13.2256 9.27354V6.6057Z" fill="currentColor" />
<svg
width='20'
height='20'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
style={marginStyle}
>
<path
d='M1.30762 1.39073C1.30762 1.3103 1.37465 1.22986 1.46849 1.22986H2.64824C2.72868 1.22986 2.80912 1.29689 2.80912 1.39073V14.4886C2.80912 14.5691 2.74209 14.6495 2.64824 14.6495H1.46849C1.38805 14.6495 1.30762 14.5825 1.30762 14.4886V1.39073Z'
fill='currentColor'
/>
<path
d='M4.2832 1.39073C4.2832 1.3103 4.35023 1.22986 4.44408 1.22986H5.62383C5.70427 1.22986 5.7847 1.29689 5.7847 1.39073V14.4886C5.7847 14.5691 5.71767 14.6495 5.62383 14.6495H4.44408C4.36364 14.6495 4.2832 14.5825 4.2832 14.4886V1.39073Z'
fill='currentColor'
/>
<path
d='M7.25977 1.39073C7.25977 1.3103 7.3268 1.22986 7.42064 1.22986H8.60039C8.68083 1.22986 8.76127 1.29689 8.76127 1.39073V14.4886C8.76127 14.5691 8.69423 14.6495 8.60039 14.6495H7.42064C7.3402 14.6495 7.25977 14.5825 7.25977 14.4886V1.39073Z'
fill='currentColor'
/>
<path
d='M10.2354 1.39073C10.2354 1.3103 10.3024 1.22986 10.3962 1.22986H11.576C11.6564 1.22986 11.7369 1.29689 11.7369 1.39073V14.4886C11.7369 14.5691 11.6698 14.6495 11.576 14.6495H10.3962C10.3158 14.6495 10.2354 14.5825 10.2354 14.4886V1.39073Z'
fill='currentColor'
/>
<path
d='M13.2256 6.6057C13.2256 6.52526 13.2926 6.44482 13.3865 6.44482H14.5662C14.6466 6.44482 14.7271 6.51186 14.7271 6.6057V9.27354C14.7271 9.35398 14.6601 9.43442 14.5662 9.43442H13.3865C13.306 9.43442 13.2256 9.36739 13.2256 9.27354V6.6057Z'
fill='currentColor'
/>
</svg>
);
case 'partner':
case "partner":
return (
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style={marginStyle}>
<polyline points="12.5 9.5 10 12 6 11 2.5 8.5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1"/>
<polyline points="4.54 4.41 8 3.5 11.46 4.41" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1"/>
<path d="M2.15,3.78 L0.55,6.95 A0.5,0.5 0,0,0 0.77,7.62 L2.5,8.5 L4.54,4.41 L2.82,3.55 A0.5,0.5 0,0,0 2.15,3.78 Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1"/>
<path d="M13.5,8.5 L15.23,7.62 A0.5,0.5 0,0,0 15.45,6.95 L13.85,3.78 A0.5,0.5 0,0,0 13.18,3.55 L11.46,4.41 Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1"/>
<path d="M11.5,4.5 L9,4.5 L6.15,7.27 A0.5,0.5 0,0,0 6.24,8.05 C7.33,8.74 8.81,8.72 10,7.5 L12.5,9.5 L13.5,8.5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1"/>
<polyline points="7.75 13.5 5.15 12.85 3.5 11.67" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1"/>
<svg
width='20'
height='20'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
style={marginStyle}
>
<polyline
points='12.5 9.5 10 12 6 11 2.5 8.5'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='1'
/>
<polyline
points='4.54 4.41 8 3.5 11.46 4.41'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='1'
/>
<path
d='M2.15,3.78 L0.55,6.95 A0.5,0.5 0,0,0 0.77,7.62 L2.5,8.5 L4.54,4.41 L2.82,3.55 A0.5,0.5 0,0,0 2.15,3.78 Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='1'
/>
<path
d='M13.5,8.5 L15.23,7.62 A0.5,0.5 0,0,0 15.45,6.95 L13.85,3.78 A0.5,0.5 0,0,0 13.18,3.55 L11.46,4.41 Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='1'
/>
<path
d='M11.5,4.5 L9,4.5 L6.15,7.27 A0.5,0.5 0,0,0 6.24,8.05 C7.33,8.74 8.81,8.72 10,7.5 L12.5,9.5 L13.5,8.5'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='1'
/>
<polyline
points='7.75 13.5 5.15 12.85 3.5 11.67'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='1'
/>
</svg>
);
case 'community':
case "community":
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256" style={marginStyle}>
<path d="M244.8,150.4a8,8,0,0,1-11.2-1.6A51.6,51.6,0,0,0,192,128a8,8,0,0,1-7.37-4.89,8,8,0,0,1,0-6.22A8,8,0,0,1,192,112a24,24,0,1,0-23.24-30,8,8,0,1,1-15.5-4A40,40,0,1,1,219,117.51a67.94,67.94,0,0,1,27.43,21.68A8,8,0,0,1,244.8,150.4ZM190.92,212a8,8,0,1,1-13.84,8,57,57,0,0,0-98.16,0,8,8,0,1,1-13.84-8,72.06,72.06,0,0,1,33.74-29.92,48,48,0,1,1,58.36,0A72.06,72.06,0,0,1,190.92,212ZM128,176a32,32,0,1,0-32-32A32,32,0,0,0,128,176ZM72,120a8,8,0,0,0-8-8A24,24,0,1,1,87.24,82a8,8,0,1,0,15.5-4A40,40,0,1,0,37,117.51,67.94,67.94,0,0,0,9.6,139.19a8,8,0,1,0,12.8,9.61A51.6,51.6,0,0,1,64,128,8,8,0,0,0,72,120Z"></path>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
fill='currentColor'
viewBox='0 0 256 256'
style={marginStyle}
>
<path d='M244.8,150.4a8,8,0,0,1-11.2-1.6A51.6,51.6,0,0,0,192,128a8,8,0,0,1-7.37-4.89,8,8,0,0,1,0-6.22A8,8,0,0,1,192,112a24,24,0,1,0-23.24-30,8,8,0,1,1-15.5-4A40,40,0,1,1,219,117.51a67.94,67.94,0,0,1,27.43,21.68A8,8,0,0,1,244.8,150.4ZM190.92,212a8,8,0,1,1-13.84,8,57,57,0,0,0-98.16,0,8,8,0,1,1-13.84-8,72.06,72.06,0,0,1,33.74-29.92,48,48,0,1,1,58.36,0A72.06,72.06,0,0,1,190.92,212ZM128,176a32,32,0,1,0-32-32A32,32,0,0,0,128,176ZM72,120a8,8,0,0,0-8-8A24,24,0,1,1,87.24,82a8,8,0,1,0,15.5-4A40,40,0,1,0,37,117.51,67.94,67.94,0,0,0,9.6,139.19a8,8,0,1,0,12.8,9.61A51.6,51.6,0,0,1,64,128,8,8,0,0,0,72,120Z'></path>
</svg>
);
default:
@@ -670,16 +807,16 @@ function getTierIcon(tier: string, withMargin = false): React.ReactNode {
function getProperCapitalization(text: string): string {
// Handle special cases
const specialCases: { [key: string]: string } = {
'ai/ml': 'AI/ML',
'clickpipes': 'ClickPipes',
'data ingestion': 'Data ingestion',
'data integration': 'Data integration',
'data management': 'Data management',
'data visualization': 'Data visualization',
'deployment method': 'Deployment method',
'language client': 'Language client',
'security governance': 'Security governance',
'sql client': 'SQL client'
"ai/ml": "AI/ML",
clickpipes: "ClickPipes",
"data ingestion": "Data ingestion",
"data integration": "Data integration",
"data management": "Data management",
"data visualization": "Data visualization",
"deployment method": "Deployment method",
"language client": "Language client",
"security governance": "Security governance",
"sql client": "SQL client",
};

const lowerText = text.toLowerCase();
@@ -688,20 +825,35 @@ function getProperCapitalization(text: string): string {
}

// Default capitalization
return text.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
return text
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}

function getSectionDescription(type: string): string {
const descriptions: { [key: string]: string } = {
'ClickPipes': 'ClickPipes is an integration engine that makes ingesting massive volumes of data from a diverse set of sources as simple as clicking a few buttons.',
'Data ingestion': 'Streamline your data pipelines with ClickHouse! Seamless integrations ensure efficient ingestion, optimizing real-time analytics.',
'Data visualization': 'Illuminate your data stories! ClickHouse integrations enhance visualization, making insights more vivid & actionable.',
'SQL client': 'Access and query ClickHouse databases using familiar SQL client tools and interfaces.',
'Language client': 'Code in your comfort zone! ClickHouse\'s language client integrations make data access fluent across multiple programming languages.',
'AI/ML': 'Leverage ClickHouse for machine learning and AI workloads with integrated ML tools and frameworks.',
'Data management': 'Manage, monitor, and optimize your ClickHouse data with specialized management tools.',
'Data integration': 'Integrate ClickHouse with your existing data infrastructure and workflows.',
'Security governance': 'Implement security and governance frameworks for your ClickHouse environment.'
ClickPipes:
"ClickPipes is an integration engine that makes ingesting massive volumes of data from a diverse set of sources as simple as clicking a few buttons.",
"Data ingestion":
"Streamline your data pipelines with ClickHouse! Seamless integrations ensure efficient ingestion, optimizing real-time analytics.",
"Data visualization":
"Illuminate your data stories! ClickHouse integrations enhance visualization, making insights more vivid & actionable.",
"SQL client":
"Access and query ClickHouse databases using familiar SQL client tools and interfaces.",
"Language client":
"Code in your comfort zone! ClickHouse's language client integrations make data access fluent across multiple programming languages.",
"AI/ML":
"Leverage ClickHouse for machine learning and AI workloads with integrated ML tools and frameworks.",
"Data management":
"Manage, monitor, and optimize your ClickHouse data with specialized management tools.",
"Data integration":
"Integrate ClickHouse with your existing data infrastructure and workflows.",
"Security governance":
"Implement security and governance frameworks for your ClickHouse environment.",
};
return descriptions[type] || 'Integrate ClickHouse with specialized tools and services.';
} No newline at end of file
return (
descriptions[type] ||
"Integrate ClickHouse with specialized tools and services."
);
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR includes extensive formatting changes (quote style conversion from double to single quotes throughout the entire file) that are unrelated to the stated purpose of adding the Strapi-Response-Format header. This makes the diff much larger and harder to review. Consider separating formatting changes into a separate PR to keep changes focused and easier to review. The only functional change related to the PR description is the addition of the "Strapi-Response-Format: v4" header at lines 253-254.

Copilot uses AI. Check for mistakes.
Comment on lines 247 to +256
const response = await fetch(
'https://cms.clickhouse-dev.com:1337/api/integrations?populate[]=logo&populate[]=logo_dark',
"https://cms.clickhouse-dev.com:1337/api/integrations?populate[]=logo&populate[]=logo_dark",
{
signal: controller.signal,
// Add headers to help with CORS and caching
headers: {
'Accept': 'application/json',
}
}
Accept: "application/json",
"Strapi-Response-Format": "v4",
},
},
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Strapi-Response-Format header has been added to this API call, but there's another Strapi API call in the codebase (src/theme/DocItem/TOC/Desktop/index.js line 52) that calls https://cms.clickhouse-dev.com:1337/api/docs-ad without this header. To ensure consistent v4 response format across all Strapi API calls during the migration, that endpoint should also include the header. Consider adding the header to all Strapi API calls in the codebase.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants