From 51b33e5f2183f11952126f5826abbe1514c3b065 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 10 Mar 2026 20:18:26 +0000 Subject: [PATCH 1/7] Add config selector step in the hub --- .../integration-picker/IntegrationPicker.tsx | 33 ++- .../components/IntegrationPickerContent.tsx | 59 ++-- .../components/IntegrationPickerTitle.tsx | 36 ++- .../views/AuthConfigSelectionView.tsx | 276 ++++++++++++++++++ .../components/views/index.ts | 1 + .../hooks/useIntegrationPicker.ts | 88 +++++- src/modules/integration-picker/queries.ts | 11 + src/modules/integration-picker/types.ts | 25 ++ 8 files changed, 498 insertions(+), 31 deletions(-) create mode 100644 src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx diff --git a/src/modules/integration-picker/IntegrationPicker.tsx b/src/modules/integration-picker/IntegrationPicker.tsx index 1b9e899..d7d0b65 100644 --- a/src/modules/integration-picker/IntegrationPicker.tsx +++ b/src/modules/integration-picker/IntegrationPicker.tsx @@ -39,6 +39,10 @@ export const IntegrationPicker: React.FC = ({ accountData, connectorData, selectedIntegration, + selectedProvider, + uniqueProviderIntegrations, + providerIntegrations, + hasOnlyOneProvider, fields, guide, @@ -54,6 +58,9 @@ export const IntegrationPicker: React.FC = ({ // Actions setSelectedIntegration, + setSelectedProvider, + handleProviderSelect, + handleCreateNewAuthConfig, setFormData, setIsFormValid, handleConnect, @@ -82,10 +89,16 @@ export const IntegrationPicker: React.FC = ({ }, [hubData]); const onBack = () => { - setSelectedIntegration(null); - resetConnectionState(); - setSelectedCategory(null); - setSearch(''); + if (selectedIntegration) { + // From form → auth config selection + setSelectedIntegration(null); + resetConnectionState(); + } else if (selectedProvider) { + // From auth config selection → provider list + setSelectedProvider(null); + setSelectedCategory(null); + setSearch(''); + } }; return ( @@ -126,13 +139,16 @@ export const IntegrationPicker: React.FC = ({ hasOnlyOneIntegration } connectorData={connectorData?.config ?? null} + selectedProvider={selectedProvider} + hasOnlyOneProvider={hasOnlyOneProvider} + uniqueProviderIntegrations={uniqueProviderIntegrations} /> ) } height={height} padding="0" headerConfig={ - selectedIntegration + selectedIntegration || selectedProvider ? undefined : { padding: '0', @@ -145,12 +161,17 @@ export const IntegrationPicker: React.FC = ({ hasError={hasError} connectionState={connectionState} selectedIntegration={selectedIntegration} + selectedProvider={selectedProvider} + uniqueProviderIntegrations={uniqueProviderIntegrations} + providerIntegrations={providerIntegrations} connectorData={connectorData?.config ?? null} hubData={hubData ?? null} fields={fields} errorHubData={(errorHubData as Error) ?? null} errorConnectorData={(errorConnectorData as Error) ?? null} - onSelect={setSelectedIntegration} + onProviderSelect={handleProviderSelect} + onAuthConfigSelect={setSelectedIntegration} + onCreateNewAuthConfig={handleCreateNewAuthConfig} onChange={setFormData} onValidationChange={handleValidationChange} selectedCategory={selectedCategory} diff --git a/src/modules/integration-picker/components/IntegrationPickerContent.tsx b/src/modules/integration-picker/components/IntegrationPickerContent.tsx index c1fbfaf..25cabd8 100644 --- a/src/modules/integration-picker/components/IntegrationPickerContent.tsx +++ b/src/modules/integration-picker/components/IntegrationPickerContent.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { ConnectorConfig, ConnectorConfigField, HubData, Integration } from '../types'; +import { AuthConfigSelectionView } from './views/AuthConfigSelectionView'; import { ErrorView } from './views/ErrorView'; import { IntegrationFormView } from './views/IntegrationFormView'; import { IntegrationListView } from './views/IntegrationListView'; @@ -21,6 +22,9 @@ interface IntegrationPickerContentProps { // Data selectedIntegration: Integration | null; + selectedProvider: string | null; + uniqueProviderIntegrations: Integration[]; + providerIntegrations: Integration[]; connectorData: ConnectorConfig | null; hubData: HubData | null; fields: ConnectorConfigField[]; @@ -32,7 +36,9 @@ interface IntegrationPickerContentProps { errorConnectorData: Error | null; // Actions - onSelect: (integration: Integration) => void; + onProviderSelect: (integration: Integration) => void; + onAuthConfigSelect: (integration: Integration) => void; + onCreateNewAuthConfig?: () => void; onChange: (data: Record) => void; onValidationChange?: (isValid: boolean) => void; editingSecrets?: Set; @@ -44,6 +50,9 @@ export const IntegrationPickerContent: React.FC = hasError, connectionState, selectedIntegration, + selectedProvider, + uniqueProviderIntegrations, + providerIntegrations, connectorData, hubData, fields, @@ -51,7 +60,9 @@ export const IntegrationPickerContent: React.FC = search, errorHubData, errorConnectorData, - onSelect, + onProviderSelect, + onAuthConfigSelect, + onCreateNewAuthConfig, onChange, onValidationChange, editingSecrets, @@ -90,23 +101,8 @@ export const IntegrationPickerContent: React.FC = return ; } - // Integration selection flow - if (!selectedIntegration) { - if (!hubData?.integrations.length) { - return ; - } - return ( - - ); - } - - // Form view (when integration is selected and connector data is available) - if (connectorData) { + // Step 4: Form view (when auth config is selected and connector data is available) + if (selectedIntegration && connectorData) { return ( = ); } - // Fallback - return null; + // Step 3.5: Auth Config selection (provider selected, auth config not yet selected) + if (selectedProvider && !selectedIntegration) { + return ( + + ); + } + + // Step 3: Provider/Connector selection + if (!hubData?.integrations.length) { + return ; + } + return ( + + ); }; diff --git a/src/modules/integration-picker/components/IntegrationPickerTitle.tsx b/src/modules/integration-picker/components/IntegrationPickerTitle.tsx index ba30d5f..ab0ec3c 100644 --- a/src/modules/integration-picker/components/IntegrationPickerTitle.tsx +++ b/src/modules/integration-picker/components/IntegrationPickerTitle.tsx @@ -1,4 +1,5 @@ -import { ConnectorConfig, HubData } from '../types'; +import { Button, Typography } from '@stackone/malachite'; +import { ConnectorConfig, HubData, Integration } from '../types'; import CardTitle from './cardTitle'; import { IntegrationListHeader } from './views/IntegrationListView'; @@ -14,6 +15,9 @@ interface IntegrationPickerTitleProps { onCategoryChange: (category: string | null) => void; onSearchChange: (search: string) => void; hideBackButton?: boolean; + selectedProvider: string | null; + hasOnlyOneProvider: boolean; + uniqueProviderIntegrations: Integration[]; } export const IntegrationPickerTitle: React.FC = ({ @@ -28,7 +32,11 @@ export const IntegrationPickerTitle: React.FC = ({ onCategoryChange, onSearchChange, hideBackButton, + selectedProvider, + hasOnlyOneProvider, + uniqueProviderIntegrations, }) => { + // Step 4: Form title (when connector data is loaded after auth config selection) if (connectorData) { return ( = ({ ); } + // Step 3.5: Auth Config selection title + if (selectedProvider) { + const showBackButton = !hasOnlyOneProvider && !accountData; + return ( +
+ {showBackButton && ( +
+ ); + } + + // Step 3: Provider list header const shouldShowListHeader = !isLoading && !hasError && hubData?.integrations; if (!shouldShowListHeader) { @@ -47,7 +79,7 @@ export const IntegrationPickerTitle: React.FC = ({ return ( = { + api_key: 'API Key', + oauth2: 'OAuth', + oauth: 'OAuth', + bearer: 'Bearer', + basic: 'Basic Auth', + saml: 'SAML', + oidc: 'OIDC', + custom: 'Custom', +}; + +function getAuthTypeLabel(key: string): string { + if (!key) return 'Unknown'; + return ( + AUTH_TYPE_LABELS[key.toLowerCase()] || + key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + ); +} + +const statusBadgeStyle = (active: boolean): React.CSSProperties => ({ + display: 'inline-flex', + alignItems: 'center', + padding: '2px 8px', + borderRadius: '9999px', + fontSize: '12px', + fontWeight: 500, + backgroundColor: active ? '#dcfce7' : '#f3f4f6', + color: active ? '#166534' : '#6b7280', + whiteSpace: 'nowrap', +}); + +const authTypeBadgeStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + padding: '2px 8px', + borderRadius: '4px', + fontSize: '12px', + border: '1px solid var(--malachite-border-color, #e5e7eb)', + backgroundColor: 'var(--malachite-card-background, #fff)', +}; + +const cardStyle: React.CSSProperties = { + border: '1px solid var(--malachite-border-color, #e5e7eb)', + borderRadius: '8px', + padding: '16px', + width: '100%', + boxSizing: 'border-box', +}; + +const tabActiveStyle: React.CSSProperties = { + padding: '4px 12px', + borderBottom: '2px solid #000', + fontWeight: 500, + fontSize: '14px', +}; + +interface AuthConfigCardProps { + integration: Integration; + onSelect: (integration: Integration) => void; +} + +const AuthConfigCard: React.FC = ({ integration, onSelect }) => { + const [expanded, setExpanded] = useState(false); + + const actionsCount = integration.actions_count ?? integration.actions?.length ?? 0; + const accountCount = integration.account_count ?? 0; + const hasActions = integration.actions && integration.actions.length > 0; + + return ( +
+ {/* Row 1: Name + Status badge + chevron + Select */} + + + {integration.name} + + {integration.active ? 'Enabled' : 'Disabled'} + + + + {hasActions && ( + + + + + {/* Row 2: Auth type + Version (left-aligned) */} +
+ + {getAuthTypeLabel(integration.authentication_config_key)} + + Version {integration.version} +
+ + {/* Row 3: Stats */} +
+ + {actionsCount} {actionsCount === 1 ? 'action' : 'actions'} ·{' '} + {accountCount} {accountCount === 1 ? 'account' : 'accounts'} + +
+ + {/* Expanded: Actions table (only when actions data exists) */} + {expanded && hasActions && ( +
+
+ Actions {actionsCount} +
+ + + + + + + + + {integration.actions?.map((action) => ( + + + + + ))} + +
+ Name + + View +
+ {action.name} + + {action.url ? ( + + ↗ + + ) : ( + + )} +
+
+ )} +
+ ); +}; + +interface AuthConfigSelectionViewProps { + integrations: Integration[]; + onSelect: (integration: Integration) => void; + onCreateNew?: () => void; +} + +export const AuthConfigSelectionView: React.FC = ({ + integrations, + onSelect, + onCreateNew, +}) => { + return ( + +
+ + Select an existing auth config to link an account, or create a new one. + +
+
+ {integrations.map((integration) => ( + + ))} +
+ {onCreateNew && ( +
+ +
+ )} +
+ ); +}; diff --git a/src/modules/integration-picker/components/views/index.ts b/src/modules/integration-picker/components/views/index.ts index 299c751..657610c 100644 --- a/src/modules/integration-picker/components/views/index.ts +++ b/src/modules/integration-picker/components/views/index.ts @@ -1,3 +1,4 @@ +export { AuthConfigSelectionView } from './AuthConfigSelectionView'; export { IntegrationListView } from './IntegrationListView'; export { IntegrationFormView } from './IntegrationFormView'; export { LoadingView } from './LoadingView'; diff --git a/src/modules/integration-picker/hooks/useIntegrationPicker.ts b/src/modules/integration-picker/hooks/useIntegrationPicker.ts index b92738f..90a699d 100644 --- a/src/modules/integration-picker/hooks/useIntegrationPicker.ts +++ b/src/modules/integration-picker/hooks/useIntegrationPicker.ts @@ -6,11 +6,13 @@ import { getAccountData, getConnectorConfig, getHubData, + getProviderActions, updateAccount, } from '../queries'; import { ConnectorConfigField, Integration, + IntegrationAction, isFalconConnectorConfig, isLegacyConnectorConfig, } from '../types'; @@ -60,6 +62,7 @@ export const useIntegrationPicker = ({ dashboardUrl, }: UseIntegrationPickerProps) => { const [selectedIntegration, setSelectedIntegration] = useState(null); + const [selectedProvider, setSelectedProvider] = useState(null); const [formData, setFormData] = useState>({}); const [editingSecrets, setEditingSecrets] = useState>(new Set()); @@ -238,15 +241,17 @@ export const useIntegrationPicker = ({ }, [accountData, hubData]); useEffect(() => { - if (hubData && !selectedIntegration) { + if (hubData && !selectedIntegration && !selectedProvider) { const activeIntegrations = hubData.integrations.filter( (integration) => integration.active, ); if (activeIntegrations.length === 1) { + // Single integration total - auto-select both provider and config + setSelectedProvider(activeIntegrations[0].provider); setSelectedIntegration(activeIntegrations[0]); } } - }, [hubData, selectedIntegration]); + }, [hubData, selectedIntegration, selectedProvider]); const { data: connectorData, @@ -271,6 +276,78 @@ export const useIntegrationPicker = ({ ...RETRY_CONFIG, }); + // Fetch provider actions when a provider is selected (for auth config cards) + const { data: providerActionsData } = useQuery({ + queryKey: ['providerActions', selectedProvider], + queryFn: async () => { + if (!selectedProvider) return null; + return getProviderActions(baseUrl, token, selectedProvider); + }, + enabled: Boolean(selectedProvider) && !selectedIntegration, + ...RETRY_CONFIG, + }); + + // Deduplicated list of integrations (one per provider) for the connector list + const uniqueProviderIntegrations = useMemo(() => { + if (!hubData) return []; + const seen = new Set(); + return hubData.integrations.filter((integration) => { + if (seen.has(integration.provider)) return false; + seen.add(integration.provider); + return true; + }); + }, [hubData]); + + // All integrations for the selected provider, enriched with actions data + const providerIntegrations = useMemo(() => { + if (!selectedProvider || !hubData) return []; + const integrations = hubData.integrations.filter((i) => i.provider === selectedProvider); + + if (!providerActionsData?.length) return integrations; + + // Find the matching provider meta (strict match by provider key AND version) + return integrations.map((integration) => { + const providerMeta = providerActionsData.find( + (p) => p.key === integration.provider && p.version === integration.version, + ); + + if (!providerMeta?.actions?.length) return integration; + + const actions: IntegrationAction[] = providerMeta.actions.map((a) => ({ + name: a.name, + })); + + return { + ...integration, + actions, + actions_count: + integration.actions_count ?? providerMeta.actions_count ?? actions.length, + }; + }); + }, [selectedProvider, hubData, providerActionsData]); + + const hasOnlyOneProvider = useMemo(() => { + if (!hubData) return false; + const activeProviders = new Set( + hubData.integrations.filter((i) => i.active).map((i) => i.provider), + ); + return activeProviders.size <= 1; + }, [hubData]); + + const handleProviderSelect = useCallback((integration: Integration) => { + setSelectedProvider(integration.provider); + }, []); + + const handleCreateNewAuthConfig = useCallback(() => { + if (!dashboardUrl) return; + const params = new URLSearchParams(); + if (selectedProvider) { + params.set('connectorKey', selectedProvider); + } + const query = params.toString(); + window.open(`${dashboardUrl}/auth_configs${query ? `?${query}` : ''}`, '_blank'); + }, [dashboardUrl, selectedProvider]); + const { fields, guide } = useMemo(() => { if (!connectorData || !selectedIntegration) { const fields: ConnectorConfigField[] = []; @@ -696,6 +773,10 @@ export const useIntegrationPicker = ({ accountData, connectorData, selectedIntegration, + selectedProvider, + uniqueProviderIntegrations, + providerIntegrations, + hasOnlyOneProvider, fields, guide, @@ -713,6 +794,9 @@ export const useIntegrationPicker = ({ // Actions setSelectedIntegration, + setSelectedProvider, + handleProviderSelect, + handleCreateNewAuthConfig, setFormData: setFormDataCallback, setIsFormValid, handleConnect, diff --git a/src/modules/integration-picker/queries.ts b/src/modules/integration-picker/queries.ts index 60aa351..21f5cf9 100644 --- a/src/modules/integration-picker/queries.ts +++ b/src/modules/integration-picker/queries.ts @@ -5,6 +5,7 @@ import { ConnectorConfig, HubConnectorConfig, HubData, + ProviderActionsResponse, } from './types'; export const getHubData = async (token: string, baseUrl: string, provider?: string) => { @@ -85,6 +86,16 @@ export const updateAccount = async ({ }); }; +export const getProviderActions = async (baseUrl: string, token: string, provider: string) => { + return await getRequest({ + url: `${baseUrl}/hub/actions?provider=${encodeURIComponent(provider)}`, + headers: { + 'Content-Type': 'application/json', + 'x-hub-session-token': token, + }, + }); +}; + export const getAccountData = async (baseUrl: string, token: string, accountId: string) => { return await getRequest({ url: `${baseUrl}/hub/accounts/${accountId}`, diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index eba5aa9..26363af 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -1,3 +1,8 @@ +export interface IntegrationAction { + name: string; + url?: string; +} + export interface Integration { active: boolean; name: string; @@ -8,6 +13,11 @@ export interface Integration { environment: string; integration_id: string; logo_url: string; + connector_name?: string; + account_count?: number; + actions_count?: number; + triggers_count?: number; + actions?: IntegrationAction[]; } export interface HubData { @@ -110,6 +120,21 @@ export interface AccountData { integrationId: string; } +export interface ProviderActionMeta { + id?: string; + key?: string; + name: string; + description?: string; +} + +export interface ProviderActionsResponse { + key: string; + version: string; + name?: string; + actions?: ProviderActionMeta[]; + actions_count?: number; +} + export interface AccountCreationResponse { id: string; provider: string; From dcb602606a805fead8afdcc705cfb6d32a536ca2 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 10 Mar 2026 22:41:34 +0000 Subject: [PATCH 2/7] Couple of changes --- package-lock.json | 53 ---- package.json | 3 +- .../integration-picker/IntegrationPicker.tsx | 4 +- .../components/IntegrationPickerContent.tsx | 6 +- .../views/AuthConfigSelectionView.tsx | 256 ++++++++---------- src/modules/integration-picker/types.ts | 1 + 6 files changed, 113 insertions(+), 210 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ea3d1f..6326be0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@stackone/expressions": "^0.16.0", - "@stackone/malachite": "^0.24.2", "@tanstack/react-query": "^5.77.2", "use-deep-compare-effect": "^1.8.1" }, @@ -1206,20 +1205,6 @@ "jsonpath-plus": "^10.3.0" } }, - "node_modules/@stackone/malachite": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@stackone/malachite/-/malachite-0.24.2.tgz", - "integrity": "sha512-B9PKzQ7/UtOWos/yw0jqMwFHiNT1jLxhRowc59dTzvlW4B/1G02Kb1ER79DmRaCmohoP8UxmCdNYvb6b2FnRCA==", - "dependencies": { - "@uiw/react-json-view": "^2.0.0-alpha.35", - "marked-react": "^3.0.0" - }, - "peerDependencies": { - "react": "18.3.1", - "react-dom": "18.3.1", - "react-hook-form": "7.60.0" - } - }, "node_modules/@stackone/utils": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/@stackone/utils/-/utils-0.8.4.tgz", @@ -1527,20 +1512,6 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, - "node_modules/@uiw/react-json-view": { - "version": "2.0.0-alpha.39", - "resolved": "https://registry.npmjs.org/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.39.tgz", - "integrity": "sha512-D9MHNan56WhtdAsmjtE9x18YLY0JSMnh0a6Ji0/2sVXCF456ZVumYLdx2II7hLQOgRMa4QMaHloytpTUHxsFRw==", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.10.0", - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", @@ -2483,30 +2454,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/marked": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", - "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/marked-react": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/marked-react/-/marked-react-3.0.2.tgz", - "integrity": "sha512-uLuH5P5i1H+igH+Y7toWX+sM2UGDdYMIPZ9JtBNajkyFiQBoAMJJY7gBlm3nHXHiPDZHkhZ9p/cb9XO0F1qnCA==", - "license": "MIT", - "dependencies": { - "marked": "^16.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || >=17.0.0" - } - }, "node_modules/mathjs": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.12.0.tgz", diff --git a/package.json b/package.json index bb8000b..628fa24 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "license": "MIT", "dependencies": { "@stackone/expressions": "^0.16.0", - "@stackone/malachite": "^0.24.2", "@tanstack/react-query": "^5.77.2", "use-deep-compare-effect": "^1.8.1" }, @@ -61,4 +60,4 @@ "tslib": "^2.8.1", "typescript": "^5.8.3" } -} \ No newline at end of file +} diff --git a/src/modules/integration-picker/IntegrationPicker.tsx b/src/modules/integration-picker/IntegrationPicker.tsx index d7d0b65..f89bdb5 100644 --- a/src/modules/integration-picker/IntegrationPicker.tsx +++ b/src/modules/integration-picker/IntegrationPicker.tsx @@ -90,11 +90,11 @@ export const IntegrationPicker: React.FC = ({ const onBack = () => { if (selectedIntegration) { - // From form → auth config selection + // From form to auth config selection setSelectedIntegration(null); resetConnectionState(); } else if (selectedProvider) { - // From auth config selection → provider list + // From auth config selection to provider list setSelectedProvider(null); setSelectedCategory(null); setSearch(''); diff --git a/src/modules/integration-picker/components/IntegrationPickerContent.tsx b/src/modules/integration-picker/components/IntegrationPickerContent.tsx index 25cabd8..da7da60 100644 --- a/src/modules/integration-picker/components/IntegrationPickerContent.tsx +++ b/src/modules/integration-picker/components/IntegrationPickerContent.tsx @@ -101,7 +101,7 @@ export const IntegrationPickerContent: React.FC = return ; } - // Step 4: Form view (when auth config is selected and connector data is available) + // Form view (when auth config is selected and connector data is available) if (selectedIntegration && connectorData) { return ( = ); } - // Step 3.5: Auth Config selection (provider selected, auth config not yet selected) + // Auth Config selection (provider selected, auth config not yet selected) if (selectedProvider && !selectedIntegration) { return ( = ); } - // Step 3: Provider/Connector selection + // Provider/Connector selection if (!hubData?.integrations.length) { return ; } diff --git a/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx b/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx index 8bbb8a5..f894b89 100644 --- a/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx +++ b/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx @@ -1,12 +1,17 @@ import { Button, + Card, CustomIcons, + Divider, Flex, FlexAlign, FlexDirection, FlexGapSize, FlexJustify, Padded, + Spacer, + Table, + Tag, Typography, } from '@stackone/malachite'; import React, { useState } from 'react'; @@ -31,43 +36,6 @@ function getAuthTypeLabel(key: string): string { ); } -const statusBadgeStyle = (active: boolean): React.CSSProperties => ({ - display: 'inline-flex', - alignItems: 'center', - padding: '2px 8px', - borderRadius: '9999px', - fontSize: '12px', - fontWeight: 500, - backgroundColor: active ? '#dcfce7' : '#f3f4f6', - color: active ? '#166534' : '#6b7280', - whiteSpace: 'nowrap', -}); - -const authTypeBadgeStyle: React.CSSProperties = { - display: 'inline-flex', - alignItems: 'center', - padding: '2px 8px', - borderRadius: '4px', - fontSize: '12px', - border: '1px solid var(--malachite-border-color, #e5e7eb)', - backgroundColor: 'var(--malachite-card-background, #fff)', -}; - -const cardStyle: React.CSSProperties = { - border: '1px solid var(--malachite-border-color, #e5e7eb)', - borderRadius: '8px', - padding: '16px', - width: '100%', - boxSizing: 'border-box', -}; - -const tabActiveStyle: React.CSSProperties = { - padding: '4px 12px', - borderBottom: '2px solid #000', - fontWeight: 500, - fontSize: '14px', -}; - interface AuthConfigCardProps { integration: Integration; onSelect: (integration: Integration) => void; @@ -81,7 +49,7 @@ const AuthConfigCard: React.FC = ({ integration, onSelect } const hasActions = integration.actions && integration.actions.length > 0; return ( -
+ {/* Row 1: Name + Status badge + chevron + Select */} = ({ integration, onSelect } gapSize={FlexGapSize.Small} > {integration.name} - - {integration.active ? 'Enabled' : 'Disabled'} - + = ({ integration, onSelect } - {/* Row 2: Auth type + Version (left-aligned) */} -
- - {getAuthTypeLabel(integration.authentication_config_key)} - - Version {integration.version} + {/* Row 2: Auth type + Version */} +
+ + + + Version {integration.version} + +
{/* Row 3: Stats */} -
+
{actionsCount} {actionsCount === 1 ? 'action' : 'actions'} ·{' '} {accountCount} {accountCount === 1 ? 'account' : 'accounts'}
- {/* Expanded: Actions table (only when actions data exists) */} + {/* Expanded: Actions table */} {expanded && hasActions && ( -
-
- Actions {actionsCount} + <> +
+ + Actions {actionsCount} +
- - - - - - - - - {integration.actions?.map((action) => ( - - -
- Name - - View -
- {action.name} - - {action.url ? ( +
+ + value ? ( ) : ( - - )} - - - ))} - -
-
+ + ↗ + + ), + }, + ]} + data={(integration.actions ?? []).map((action) => ({ + name: action.name, + url: action.url ?? '', + }))} + /> + + )} - + ); }; @@ -231,46 +193,40 @@ export const AuthConfigSelectionView: React.FC = ( onCreateNew, }) => { return ( - -
- - Select an existing auth config to link an account, or create a new one. - -
-
- {integrations.map((integration) => ( - - ))} -
- {onCreateNew && ( -
- -
- )} + + + + + Select an existing auth config to link an account, or create a new one. + + {onCreateNew && ( + + )} + + + {integrations.map((integration) => ( + + ))} + + ); }; diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 26363af..77fcf29 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -10,6 +10,7 @@ export interface Integration { type: string; version: string; authentication_config_key: string; + authentication_config_label?: string | null; environment: string; integration_id: string; logo_url: string; From c485b28e6d7102e01fac2f4a4b83faa7c46551ac Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Mar 2026 11:14:29 +0000 Subject: [PATCH 3/7] Couple of cleanups --- package-lock.json | 53 +++++++++++++++++++ package.json | 1 + .../integration-picker/IntegrationPicker.tsx | 2 +- .../views/AuthConfigSelectionView.tsx | 32 ++--------- .../hooks/useIntegrationPicker.ts | 1 + src/modules/integration-picker/types.ts | 1 + 6 files changed, 60 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6326be0..8b4ae29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@stackone/expressions": "^0.16.0", + "@stackone/malachite": "^0.24.2", "@tanstack/react-query": "^5.77.2", "use-deep-compare-effect": "^1.8.1" }, @@ -1205,6 +1206,20 @@ "jsonpath-plus": "^10.3.0" } }, + "node_modules/@stackone/malachite": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@stackone/malachite/-/malachite-0.24.2.tgz", + "integrity": "sha512-B9PKzQ7/UtOWos/yw0jqMwFHiNT1jLxhRowc59dTzvlW4B/1G02Kb1ER79DmRaCmohoP8UxmCdNYvb6b2FnRCA==", + "dependencies": { + "@uiw/react-json-view": "^2.0.0-alpha.35", + "marked-react": "^3.0.0" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1", + "react-hook-form": "7.60.0" + } + }, "node_modules/@stackone/utils": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/@stackone/utils/-/utils-0.8.4.tgz", @@ -1512,6 +1527,20 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "node_modules/@uiw/react-json-view": { + "version": "2.0.0-alpha.41", + "resolved": "https://registry.npmjs.org/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.41.tgz", + "integrity": "sha512-botRpQ5AgymYEsqXSdT2/1LefAJEYfMntvdnx1SqhTQCTW9HygeFZXx9inkYqUmiQZ3+0QlZnodjBvwnUfZhVA==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.10.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", @@ -2454,6 +2483,30 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/marked-react": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/marked-react/-/marked-react-3.0.2.tgz", + "integrity": "sha512-uLuH5P5i1H+igH+Y7toWX+sM2UGDdYMIPZ9JtBNajkyFiQBoAMJJY7gBlm3nHXHiPDZHkhZ9p/cb9XO0F1qnCA==", + "license": "MIT", + "dependencies": { + "marked": "^16.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || >=17.0.0" + } + }, "node_modules/mathjs": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.12.0.tgz", diff --git a/package.json b/package.json index 628fa24..97c6335 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "license": "MIT", "dependencies": { "@stackone/expressions": "^0.16.0", + "@stackone/malachite": "^0.24.2", "@tanstack/react-query": "^5.77.2", "use-deep-compare-effect": "^1.8.1" }, diff --git a/src/modules/integration-picker/IntegrationPicker.tsx b/src/modules/integration-picker/IntegrationPicker.tsx index f89bdb5..8d656e6 100644 --- a/src/modules/integration-picker/IntegrationPicker.tsx +++ b/src/modules/integration-picker/IntegrationPicker.tsx @@ -171,7 +171,7 @@ export const IntegrationPicker: React.FC = ({ errorConnectorData={(errorConnectorData as Error) ?? null} onProviderSelect={handleProviderSelect} onAuthConfigSelect={setSelectedIntegration} - onCreateNewAuthConfig={handleCreateNewAuthConfig} + onCreateNewAuthConfig={dashboardUrl ? handleCreateNewAuthConfig : undefined} onChange={setFormData} onValidationChange={handleValidationChange} selectedCategory={selectedCategory} diff --git a/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx b/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx index f894b89..f2e0cff 100644 --- a/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx +++ b/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx @@ -2,7 +2,6 @@ import { Button, Card, CustomIcons, - Divider, Flex, FlexAlign, FlexDirection, @@ -136,42 +135,17 @@ const AuthConfigCard: React.FC = ({ integration, onSelect } - value ? ( - - ↗ - - ) : ( - - ↗ - - ), - }, ]} data={(integration.actions ?? []).map((action) => ({ + id: action.id ?? action.name, name: action.name, - url: action.url ?? '', }))} /> @@ -199,7 +173,7 @@ export const AuthConfigSelectionView: React.FC = ( direction={FlexDirection.Horizontal} align={FlexAlign.Center} justify={FlexJustify.SpaceBetween} - fullWidth + width="100%" gapSize={FlexGapSize.Small} > diff --git a/src/modules/integration-picker/hooks/useIntegrationPicker.ts b/src/modules/integration-picker/hooks/useIntegrationPicker.ts index 90a699d..d9e44cd 100644 --- a/src/modules/integration-picker/hooks/useIntegrationPicker.ts +++ b/src/modules/integration-picker/hooks/useIntegrationPicker.ts @@ -314,6 +314,7 @@ export const useIntegrationPicker = ({ if (!providerMeta?.actions?.length) return integration; const actions: IntegrationAction[] = providerMeta.actions.map((a) => ({ + id: a.id, name: a.name, })); diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 77fcf29..1698c5e 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -1,4 +1,5 @@ export interface IntegrationAction { + id?: string; name: string; url?: string; } From 0ed95b9c98b4c6f4a39e93327cc99872bfe07f5f Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Mar 2026 13:55:01 +0000 Subject: [PATCH 4/7] Added description to actions table --- .../views/AuthConfigSelectionView.tsx | 22 +++++++++++++++++++ .../hooks/useIntegrationPicker.ts | 1 + src/modules/integration-picker/types.ts | 1 + 3 files changed, 24 insertions(+) diff --git a/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx b/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx index f2e0cff..4893291 100644 --- a/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx +++ b/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx @@ -135,17 +135,39 @@ const AuthConfigCard: React.FC = ({ integration, onSelect }
{ + const description = value as string; + return description ? ( + + {description} + + ) : ( + '\u2014' + ); + }, }, ]} data={(integration.actions ?? []).map((action) => ({ id: action.id ?? action.name, name: action.name, + description: action.description ?? '', }))} /> diff --git a/src/modules/integration-picker/hooks/useIntegrationPicker.ts b/src/modules/integration-picker/hooks/useIntegrationPicker.ts index d9e44cd..6861571 100644 --- a/src/modules/integration-picker/hooks/useIntegrationPicker.ts +++ b/src/modules/integration-picker/hooks/useIntegrationPicker.ts @@ -316,6 +316,7 @@ export const useIntegrationPicker = ({ const actions: IntegrationAction[] = providerMeta.actions.map((a) => ({ id: a.id, name: a.name, + description: a.description, })); return { diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 1698c5e..2a903f0 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -1,6 +1,7 @@ export interface IntegrationAction { id?: string; name: string; + description?: string; url?: string; } From ec60fdd10421fb0fdd355cf31bc1b48608ea0172 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Mar 2026 18:22:26 +0000 Subject: [PATCH 5/7] Couple more cleanups --- .../components/views/AuthConfigSelectionView.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx b/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx index 4893291..4788ae9 100644 --- a/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx +++ b/src/modules/integration-picker/components/views/AuthConfigSelectionView.tsx @@ -49,7 +49,6 @@ const AuthConfigCard: React.FC = ({ integration, onSelect } return ( - {/* Row 1: Name + Status badge + chevron + Select */} = ({ integration, onSelect } - {/* Row 2: Auth type + Version */}
= ({ integration, onSelect }
- {/* Row 3: Stats */}
{actionsCount} {actionsCount === 1 ? 'action' : 'actions'} ·{' '} @@ -123,7 +120,6 @@ const AuthConfigCard: React.FC = ({ integration, onSelect }
- {/* Expanded: Actions table */} {expanded && hasActions && ( <>
From a961e7a59c83da915218da7adbc14025bd553c5a Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Mar 2026 18:34:09 +0000 Subject: [PATCH 6/7] Remove unused comments --- .../integration-picker/components/IntegrationPickerTitle.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/modules/integration-picker/components/IntegrationPickerTitle.tsx b/src/modules/integration-picker/components/IntegrationPickerTitle.tsx index ab0ec3c..350bcbf 100644 --- a/src/modules/integration-picker/components/IntegrationPickerTitle.tsx +++ b/src/modules/integration-picker/components/IntegrationPickerTitle.tsx @@ -36,7 +36,6 @@ export const IntegrationPickerTitle: React.FC = ({ hasOnlyOneProvider, uniqueProviderIntegrations, }) => { - // Step 4: Form title (when connector data is loaded after auth config selection) if (connectorData) { return ( = ({ ); } - // Step 3.5: Auth Config selection title if (selectedProvider) { const showBackButton = !hasOnlyOneProvider && !accountData; return ( @@ -70,7 +68,6 @@ export const IntegrationPickerTitle: React.FC = ({ ); } - // Step 3: Provider list header const shouldShowListHeader = !isLoading && !hasError && hubData?.integrations; if (!shouldShowListHeader) { From 03c73a9c5abc4b26e42c9600a62b0705a74822c4 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Mar 2026 23:15:10 +0000 Subject: [PATCH 7/7] Deduplicate provider list by version --- .../integration-picker/IntegrationPicker.tsx | 2 ++ .../hooks/useIntegrationPicker.ts | 32 +++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/modules/integration-picker/IntegrationPicker.tsx b/src/modules/integration-picker/IntegrationPicker.tsx index 8d656e6..1793a17 100644 --- a/src/modules/integration-picker/IntegrationPicker.tsx +++ b/src/modules/integration-picker/IntegrationPicker.tsx @@ -59,6 +59,7 @@ export const IntegrationPicker: React.FC = ({ // Actions setSelectedIntegration, setSelectedProvider, + setSelectedVersion, handleProviderSelect, handleCreateNewAuthConfig, setFormData, @@ -96,6 +97,7 @@ export const IntegrationPicker: React.FC = ({ } else if (selectedProvider) { // From auth config selection to provider list setSelectedProvider(null); + setSelectedVersion(null); setSelectedCategory(null); setSearch(''); } diff --git a/src/modules/integration-picker/hooks/useIntegrationPicker.ts b/src/modules/integration-picker/hooks/useIntegrationPicker.ts index 6861571..1efbe1a 100644 --- a/src/modules/integration-picker/hooks/useIntegrationPicker.ts +++ b/src/modules/integration-picker/hooks/useIntegrationPicker.ts @@ -63,6 +63,7 @@ export const useIntegrationPicker = ({ }: UseIntegrationPickerProps) => { const [selectedIntegration, setSelectedIntegration] = useState(null); const [selectedProvider, setSelectedProvider] = useState(null); + const [selectedVersion, setSelectedVersion] = useState(null); const [formData, setFormData] = useState>({}); const [editingSecrets, setEditingSecrets] = useState>(new Set()); @@ -248,6 +249,7 @@ export const useIntegrationPicker = ({ if (activeIntegrations.length === 1) { // Single integration total - auto-select both provider and config setSelectedProvider(activeIntegrations[0].provider); + setSelectedVersion(activeIntegrations[0].version); setSelectedIntegration(activeIntegrations[0]); } } @@ -287,21 +289,26 @@ export const useIntegrationPicker = ({ ...RETRY_CONFIG, }); - // Deduplicated list of integrations (one per provider) for the connector list + // Deduplicated list of integrations (one per provider+version) for the connector list const uniqueProviderIntegrations = useMemo(() => { if (!hubData) return []; const seen = new Set(); return hubData.integrations.filter((integration) => { - if (seen.has(integration.provider)) return false; - seen.add(integration.provider); + const key = `${integration.provider}:${integration.version}`; + if (seen.has(key)) return false; + seen.add(key); return true; }); }, [hubData]); - // All integrations for the selected provider, enriched with actions data + // All integrations for the selected provider+version, enriched with actions data const providerIntegrations = useMemo(() => { if (!selectedProvider || !hubData) return []; - const integrations = hubData.integrations.filter((i) => i.provider === selectedProvider); + const integrations = hubData.integrations.filter( + (i) => + i.provider === selectedProvider && + (!selectedVersion || i.version === selectedVersion), + ); if (!providerActionsData?.length) return integrations; @@ -326,18 +333,19 @@ export const useIntegrationPicker = ({ integration.actions_count ?? providerMeta.actions_count ?? actions.length, }; }); - }, [selectedProvider, hubData, providerActionsData]); + }, [selectedProvider, selectedVersion, hubData, providerActionsData]); const hasOnlyOneProvider = useMemo(() => { if (!hubData) return false; - const activeProviders = new Set( - hubData.integrations.filter((i) => i.active).map((i) => i.provider), + const activeProviderVersions = new Set( + hubData.integrations.filter((i) => i.active).map((i) => `${i.provider}:${i.version}`), ); - return activeProviders.size <= 1; + return activeProviderVersions.size <= 1; }, [hubData]); const handleProviderSelect = useCallback((integration: Integration) => { setSelectedProvider(integration.provider); + setSelectedVersion(integration.version); }, []); const handleCreateNewAuthConfig = useCallback(() => { @@ -346,9 +354,12 @@ export const useIntegrationPicker = ({ if (selectedProvider) { params.set('connectorKey', selectedProvider); } + if (selectedVersion) { + params.set('connectorVersion', selectedVersion); + } const query = params.toString(); window.open(`${dashboardUrl}/auth_configs${query ? `?${query}` : ''}`, '_blank'); - }, [dashboardUrl, selectedProvider]); + }, [dashboardUrl, selectedProvider, selectedVersion]); const { fields, guide } = useMemo(() => { if (!connectorData || !selectedIntegration) { @@ -797,6 +808,7 @@ export const useIntegrationPicker = ({ // Actions setSelectedIntegration, setSelectedProvider, + setSelectedVersion, handleProviderSelect, handleCreateNewAuthConfig, setFormData: setFormDataCallback,