Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,12 @@ export const TableList = ({
{!isSchemaLocked && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
<Button
type="default"
className="px-1"
icon={<MoreVertical />}
aria-label={`Table ${x.name} actions`}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useQuery } from '@tanstack/react-query'
import { FeatureFlagContext, IS_PLATFORM } from 'common'
import { fullImageUrl } from 'common/marketplace-client'
import { Boxes } from 'lucide-react'
Expand All @@ -9,10 +8,36 @@ import { cn } from 'ui'

import { INTEGRATIONS, Loading, type IntegrationDefinition } from './Integrations.constants'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { marketplaceIntegrationsQueryOptions } from '@/data/marketplace/integrations-query'
import {
useMarketplaceIntegrationsQuery,
type MarketplaceIntegration,
} from '@/data/marketplace/integrations-query'
import { useCLIReleaseVersionQuery } from '@/data/misc/cli-release-version-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'

const renderMarketplaceLogo = (listingLogo?: string | null) => {
const MarketplaceLogo = ({ className, ...props }: { className?: string } = {}) => (
<div className="relative w-full h-full">
{listingLogo ? (
<Image
fill
src={fullImageUrl(listingLogo)}
alt=""
className={cn('p-2', className)}
{...props}
/>
) : (
<Boxes className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
)}
</div>
)
return MarketplaceLogo
}

function isForeignDataWrapper(integration: MarketplaceIntegration) {
return integration.categories.some((c) => c?.slug === 'foreign-data-wrapper')
}

/**
* [Joshen] Returns a combination of
* - Marketplace integrations retrieved remotely (Only if feature flag enabled)
Expand All @@ -26,118 +51,108 @@ export const useAvailableIntegrations = () => {
const { data: cliData } = useCLIReleaseVersionQuery()
const isCLI = !!cliData?.current

const { data, error } = useQuery({
...marketplaceIntegrationsQueryOptions(),
enabled: isMarketplaceEnabled,
})
const { data, error } = useMarketplaceIntegrationsQuery({ enabled: isMarketplaceEnabled })
const isPending = IS_PLATFORM && (!hasLoaded || (isMarketplaceEnabled && !data && !error))
const isSuccess = !IS_PLATFORM || (hasLoaded && (!isMarketplaceEnabled || (!!data && !error)))
const isError = IS_PLATFORM && isMarketplaceEnabled && !!error

// [Joshen] Format marketplace integrations into existing ones for now
// Likely that we might need to change, but can look into separately
// Wrappers from marketplace are excluded here — they are merged into the
// hardcoded studio wrappers below as content overrides.
const marketplaceIntegrations: IntegrationDefinition[] = useMemo(
() =>
(data ?? [])?.map((integration) => {
const {
id: listingId,
slug,
categories,
featured,
title,
description,
documentation_url: docsUrl,
website_url: siteUrl,
installation_url: installUrl,
installation_url_type: installUrlType,
installation_identification_method: installMethod,
secret_key_prefix: secretKeyPrefix,
edge_function_secret_name: edgeFunctionSecretName,
images,
content,
partner_name: authorName,
listing_logo: listingLogo,
} = integration

const status = undefined
const author = { name: authorName ?? '', websiteUrl: '' }

return {
id: slug ?? '',
name: title ?? '',
status,
featured: !!featured,
type: 'oauth' as const, // Currently marketplace only supports oauth apps
source: 'Partner' as const,
categories: Array.isArray(categories)
? (categories as Array<{ slug: string }>).map((x) => x.slug)
: [],
content,
files: images?.map((image) => fullImageUrl(image)),
description,
docsUrl,
siteUrl,
installUrl,
installUrlType: installUrlType ?? undefined,
installIdentificationMethod: installMethod ?? undefined,
secretKeyPrefix: secretKeyPrefix ?? undefined,
edgeFunctionSecretName: edgeFunctionSecretName ?? undefined,
listingId: listingId ?? undefined,
author,
requiredExtensions: [],
icon: ({ className, ...props } = {}) => (
<div className="relative w-full h-full">
{listingLogo ? (
<Image
fill
src={fullImageUrl(listingLogo)}
alt=""
className={cn('p-2', className)}
{...props}
/>
) : (
<Boxes
className={cn('inset-0 p-2 text-black w-full h-full', className)}
{...props}
/>
)}
</div>
),
navigation: [
{
route: 'overview',
label: 'Overview',
(data ?? [])
?.filter((integration) => !isForeignDataWrapper(integration))
.map((integration) => {
const {
id: listingId,
slug,
categories,
featured,
title,
description,
documentation_url: docsUrl,
website_url: siteUrl,
installation_url: installUrl,
installation_url_type: installUrlType,
installation_identification_method: installMethod,
secret_key_prefix: secretKeyPrefix,
edge_function_secret_name: edgeFunctionSecretName,
images,
content,
partner_name: authorName,
listing_logo: listingLogo,
} = integration

const status = undefined
const author = { name: authorName ?? '', websiteUrl: '' }

return {
id: slug ?? '',
name: title ?? '',
status,
featured: !!featured,
type: 'oauth' as const, // Currently marketplace only supports oauth apps
source: 'Partner' as const,
categories: categories.map((x) => x.slug),
content,
files: images?.map((image) => fullImageUrl(image)),
description,
docsUrl,
siteUrl,
installUrl,
installUrlType: installUrlType ?? undefined,
installIdentificationMethod: installMethod ?? undefined,
secretKeyPrefix: secretKeyPrefix ?? undefined,
edgeFunctionSecretName: edgeFunctionSecretName ?? undefined,
listingId: listingId ?? undefined,
author,
requiredExtensions: [],
icon: renderMarketplaceLogo(listingLogo),
navigation: [
{
route: 'overview',
label: 'Overview',
},
],
navigate: ({ pageId = 'overview' }) => {
switch (pageId) {
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab').then(
(mod) => mod.MarketplaceIntegrationOverviewTab
),
{
loading: Loading,
}
)
}
return null
},
],
navigate: ({ pageId = 'overview' }) => {
switch (pageId) {
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab').then(
(mod) => mod.MarketplaceIntegrationOverviewTab
),
{
loading: Loading,
}
)
}
return null
},
}
}),
}
}),
[data]
)

// Marketplace wrapper listings keyed by their studio-equivalent id
// (marketplace uses dash-separated slugs, studio uses underscore-separated ids).
const marketplaceWrappers = useMemo(() => {
const map: Record<string, MarketplaceIntegration> = {}
;(data ?? []).forEach((integration) => {
if (!isForeignDataWrapper(integration)) return
map[integration.slug.replaceAll('-', '_')] = integration
})
return map
}, [data])

// [Joshen] Existing integrations that are defined within studio
// Available integrations are all integrations that can be installed. If an integration can't be installed (needed
// extensions are not available on this DB image), the UI will provide a tooltip explaining why.
const allIntegrations = useMemo(() => {
return INTEGRATIONS.filter((integration) => {
if (
!integrationsWrappers &&
(integration.type === 'wrapper' || integration.id.endsWith('_wrapper'))
) {
if (!integrationsWrappers && integration.type === 'wrapper') {
return false
}

Expand All @@ -146,8 +161,41 @@ export const useAvailableIntegrations = () => {
}

return true
}).map((integration) => {
const isWrapper = integration.type === 'wrapper'
if (!isWrapper) return integration

const marketplaceWrapper = marketplaceWrappers[integration.id]
if (!marketplaceWrapper) return integration

const {
title,
description,
content,
documentation_url: docsUrl,
website_url: siteUrl,
images,
partner_name: authorName,
listing_logo: listingLogo,
} = marketplaceWrapper

const overrides = {
name: title,
description,
content,
docsUrl,
siteUrl,
author: authorName ? { name: authorName, websiteUrl: '' } : undefined,
files: images?.map((image) => fullImageUrl(image)),
icon: listingLogo ? renderMarketplaceLogo(listingLogo) : undefined,
}

return {
...integration,
...Object.fromEntries(Object.entries(overrides).filter(([, v]) => v != null)),
}
})
}, [integrationsWrappers, isCLI])
}, [integrationsWrappers, isCLI, marketplaceWrappers])

const dataWithMarketplace = useMemo(() => {
return [...marketplaceIntegrations, ...allIntegrations].sort((a, b) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useQuery } from '@tanstack/react-query'
import { parseAsString, parseAsStringEnum, useQueryState } from 'nuqs'
import { useMemo, useRef } from 'react'
import { Button, Card, ShadowScrollArea, Table, TableBody, TableHeader } from 'ui'
Expand Down Expand Up @@ -33,7 +32,7 @@ import { useInstalledIntegrations } from '@/components/interfaces/Integrations/L
import { useIntegrationFilteringAndSort } from '@/components/interfaces/Integrations/Landing/useIntegrationFilteringAndSort'
import { AlertError } from '@/components/ui/AlertError'
import { DocsButton } from '@/components/ui/DocsButton'
import { marketplaceCategoriesQueryOptions } from '@/data/marketplace/integration-categories-query'
import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { DOCS_URL } from '@/lib/constants'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
Expand Down Expand Up @@ -89,7 +88,7 @@ export const MarketplaceIndex = () => {
const isLoading = isLoadingAvailable || isLoadingInstalled
const isSuccess = isSuccessAvailable && isSuccessInstalled

const { data: marketplaceCategories = [] } = useQuery(marketplaceCategoriesQueryOptions())
const { data: marketplaceCategories = [] } = useMarketplaceCategoriesQuery()

const categoryOptions = useMemo(() => {
// Start with marketplace DB categories
Expand Down
23 changes: 9 additions & 14 deletions apps/studio/components/layouts/ProjectIntegrationsLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useQuery } from '@tanstack/react-query'
import { IS_PLATFORM, useFeatureFlags, useParams } from 'common'
import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
Expand All @@ -10,8 +9,8 @@ import { useInstalledIntegrations } from '@/components/interfaces/Integrations/L
import { ProjectLayout } from '@/components/layouts/ProjectLayout'
import AlertError from '@/components/ui/AlertError'
import { ProductMenu } from '@/components/ui/ProductMenu'
import { marketplaceCategoriesQueryOptions } from '@/data/marketplace/integration-categories-query'
import { marketplaceIntegrationsQueryOptions } from '@/data/marketplace/integrations-query'
import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query'
import { useMarketplaceIntegrationsQuery } from '@/data/marketplace/integrations-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { withAuth } from '@/hooks/misc/withAuth'

Expand Down Expand Up @@ -54,19 +53,15 @@ const IntegrationCategoriesMenu = ({ page }: { page: string }) => {
const pageKey = categoryParam || page

const { integrationsWrappers: showWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const { data: categories = [], isPending: isPendingCategories } = useQuery(
marketplaceCategoriesQueryOptions({ enabled: isMarketplaceEnabled })
)
const { data: listings = [], isPending: isPendingListings } = useQuery(
marketplaceIntegrationsQueryOptions({ enabled: isMarketplaceEnabled })
)
const { data: categories = [], isPending: isPendingCategories } = useMarketplaceCategoriesQuery({
enabled: isMarketplaceEnabled,
})
const { data: listings = [], isPending: isPendingListings } = useMarketplaceIntegrationsQuery({
enabled: isMarketplaceEnabled,
})

const populatedCategoryIds = new Set(
listings.flatMap((listing) =>
Array.isArray(listing.categories)
? (listing.categories as Array<{ id: string }>).map((c) => c.id)
: []
)
listings.flatMap((listing) => listing.categories.map((c) => c.id))
)
const nonEmptyCategories = categories.filter(
(category) => category.id && populatedCategoryIds.has(category.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export const TableMenuEmptyState = () => {
description="Any tables or views you create will be listed here."
className="mx-4"
>
<div className="top-0 left-6 flex flex-col opacity-50 cursor-not-allowed bg-dash-sidebar h-content -mb-7 pointer-events-none scale-75">
<div className="relative h-content">
<div className="top-0 left-6 flex flex-col opacity-50 cursor-not-allowed bg-dash-sidebar -mb-7 pointer-events-none scale-75">
<div className="relative">
<div className="absolute inset-0 pointer-events-none z-10">
<div className="absolute inset-0 bg-linear-to-t from-transparent from-80% to-100% to-background-surface-100 dark:to-background-surface-75" />
<div className="absolute inset-0 bg-linear-to-r from-transparent from-50% to-100% to-background-surface-100 dark:to-background-surface-75" />
Expand Down
Loading
Loading