diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx index e8fe6da33..fa4b8feb6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx @@ -8,6 +8,8 @@ import { Button } from "@/components/ui/button"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { LogActiveTask } from "@/contracts/types/log.types"; import { cn } from "@/lib/utils"; +import { useConnectorStatus } from "../hooks/use-connector-status"; +import { ConnectorStatusBadge } from "./connector-status-badge"; interface ConnectorCardProps { id: string; @@ -104,6 +106,15 @@ export const ConnectorCard: FC = ({ onConnect, onManage, }) => { + // Get connector status + const { getConnectorStatus, isConnectorEnabled, getConnectorStatusMessage, shouldShowWarnings } = + useConnectorStatus(); + + const status = getConnectorStatus(connectorType); + const isEnabled = isConnectorEnabled(connectorType); + const statusMessage = getConnectorStatusMessage(connectorType); + const showWarnings = shouldShowWarnings(); + // Extract count from active task message during indexing const indexingCount = extractIndexedCount(activeTask?.message); @@ -139,9 +150,23 @@ export const ConnectorCard: FC = ({ return description; }; - return ( -
-
+ const cardContent = ( +
+
{connectorType ? ( getConnectorIcon(connectorType, "size-6") ) : id === "youtube-crawler" ? ( @@ -151,8 +176,15 @@ export const ConnectorCard: FC = ({ )}
-
+
{title} + {showWarnings && status.status !== "active" && ( + + )}
{getStatusContent()}
{isConnected && documentCount !== undefined && ( @@ -179,10 +211,12 @@ export const ConnectorCard: FC = ({ !isConnected && "shadow-xs" )} onClick={isConnected ? onManage : onConnect} - disabled={isConnecting} + disabled={isConnecting || !isEnabled} > {isConnecting ? ( + ) : !isEnabled ? ( + "Unavailable" ) : isConnected ? ( "Manage" ) : id === "youtube-crawler" ? ( @@ -195,4 +229,6 @@ export const ConnectorCard: FC = ({
); + + return cardContent; }; diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-status-badge.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-status-badge.tsx new file mode 100644 index 000000000..ecc3a11cd --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-status-badge.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { AlertTriangle, Ban, Wrench } from "lucide-react"; +import type { FC } from "react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import type { ConnectorStatus } from "../config/connector-status-config"; +import { cn } from "@/lib/utils"; + +interface ConnectorStatusBadgeProps { + status: ConnectorStatus; + statusMessage?: string | null; + className?: string; +} + +export const ConnectorStatusBadge: FC = ({ + status, + statusMessage, + className, +}) => { + if (status === "active") { + return null; + } + + const getBadgeConfig = () => { + switch (status) { + case "warning": + return { + icon: AlertTriangle, + className: "text-yellow-500 dark:text-yellow-400", + defaultTitle: "Warning", + }; + case "disabled": + return { + icon: Ban, + className: "text-red-500 dark:text-red-400", + defaultTitle: "Disabled", + }; + case "maintenance": + return { + icon: Wrench, + className: "text-orange-500 dark:text-orange-400", + defaultTitle: "Maintenance", + }; + case "deprecated": + return { + icon: AlertTriangle, + className: "ext-slate-500 dark:text-slate-400", + defaultTitle: "Deprecated", + }; + default: + return null; + } + }; + + const config = getBadgeConfig(); + if (!config) return null; + + const Icon = config.icon; + // Show statusMessage in tooltip for warning, deprecated, disabled, and maintenance statuses + const shouldUseTooltip = + (status === "warning" || + status === "deprecated" || + status === "disabled" || + status === "maintenance") && + statusMessage; + const tooltipTitle = shouldUseTooltip ? statusMessage : config.defaultTitle; + + // Use Tooltip component for statuses with statusMessage, native title for others + if (shouldUseTooltip) { + return ( + + + + + + + + {statusMessage} + + + ); + } + + return ( + + + + ); +}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-warning-banner.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-warning-banner.tsx new file mode 100644 index 000000000..d1de3e37e --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-warning-banner.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { AlertTriangle, X } from "lucide-react"; +import type { FC } from "react"; +import { useState } from "react"; +import { cn } from "@/lib/utils"; + +interface ConnectorWarningBannerProps { + warning: string; + statusMessage?: string | null; + onDismiss?: () => void; + className?: string; +} + +export const ConnectorWarningBanner: FC = ({ + warning, + statusMessage, + onDismiss, + className, +}) => { + const [isDismissed, setIsDismissed] = useState(false); + + if (isDismissed) return null; + + const handleDismiss = () => { + setIsDismissed(true); + onDismiss?.(); + }; + + return ( +
+ +
+

{warning}

+ {statusMessage && ( +

{statusMessage}

+ )} +
+ {onDismiss && ( + + )} +
+ ); +}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.example.json b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.example.json new file mode 100644 index 000000000..ad2a914f3 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.example.json @@ -0,0 +1,28 @@ +{ + "connectorStatuses": { + "SLACK_CONNECTOR": { + "enabled": false, + "status": "disabled", + "statusMessage": "Unavailable due to API changes" + }, + "NOTION_CONNECTOR": { + "enabled": true, + "status": "warning", + "statusMessage": "Rate limits may apply" + }, + "TEAMS_CONNECTOR": { + "enabled": false, + "status": "maintenance", + "statusMessage": "Temporarily unavailable for maintenance" + }, + "JIRA_CONNECTOR": { + "enabled": false, + "status": "deprecated", + "statusMessage": "Deprecated" + } + }, + "globalSettings": { + "showWarnings": true, + "allowManualOverride": false + } +} diff --git a/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.json b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.json new file mode 100644 index 000000000..470ff22e9 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.json @@ -0,0 +1,7 @@ +{ + "connectorStatuses": {}, + "globalSettings": { + "showWarnings": true, + "allowManualOverride": false + } +} diff --git a/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.ts b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.ts new file mode 100644 index 000000000..06e98d927 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.ts @@ -0,0 +1,74 @@ +/** + * Connector Status Configuration + * + * Manages connector statuses (disable/enable, status messages). Edit connector-status-config.json to configure. + * Valid status values: "active", "warning", "disabled", "deprecated", "maintenance". + * Unlisted connectors default to "active" and enabled. See connector-status-config.example.json for reference. + */ + +import { z } from "zod"; +import rawConnectorStatusConfigData from "./connector-status-config.json"; + +// Zod schemas for runtime validation and type safety +export const connectorStatusSchema = z.enum([ + "active", + "warning", + "disabled", + "deprecated", + "maintenance", +]); + +export const connectorStatusConfigSchema = z.object({ + enabled: z.boolean(), + status: connectorStatusSchema, + statusMessage: z.string().nullable().optional(), +}); + +export const connectorStatusMapSchema = z.record(z.string(), connectorStatusConfigSchema); + +export const connectorStatusConfigFileSchema = z.object({ + connectorStatuses: connectorStatusMapSchema, + globalSettings: z.object({ + showWarnings: z.boolean(), + allowManualOverride: z.boolean(), + }), +}); + +// TypeScript types inferred from Zod schemas +export type ConnectorStatus = z.infer; +export type ConnectorStatusConfig = z.infer; +export type ConnectorStatusMap = z.infer; +export type ConnectorStatusConfigFile = z.infer; + +/** + * Validated at runtime via Zod schema; invalid JSON throws at module load time. + */ +export const connectorStatusConfig: ConnectorStatusConfigFile = + connectorStatusConfigFileSchema.parse(rawConnectorStatusConfigData); + +/** + * Get default status config for a connector (when not in config file) + * Returns a validated default config + */ +export function getDefaultConnectorStatus(): ConnectorStatusConfig { + return connectorStatusConfigSchema.parse({ + enabled: true, + status: "active", + statusMessage: null, + }); +} + +/** + * Validate a connector status config object + * Useful for validating config loaded from external sources + */ +export function validateConnectorStatusConfig(config: unknown): ConnectorStatusConfigFile { + return connectorStatusConfigFileSchema.parse(config); +} + +/** + * Validate a single connector status config + */ +export function validateSingleConnectorStatus(config: unknown): ConnectorStatusConfig { + return connectorStatusConfigSchema.parse(config); +} diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-status.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-status.ts new file mode 100644 index 000000000..7d48d4369 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-status.ts @@ -0,0 +1,55 @@ +"use client"; + +import { useMemo } from "react"; +import { + type ConnectorStatusConfig, + connectorStatusConfig, + getDefaultConnectorStatus, +} from "../config/connector-status-config"; + +/** + * Hook to get connector status information + */ +export function useConnectorStatus() { + /** + * Get status configuration for a specific connector type + */ + const getConnectorStatus = (connectorType: string | undefined): ConnectorStatusConfig => { + if (!connectorType) { + return getDefaultConnectorStatus(); + } + + return connectorStatusConfig.connectorStatuses[connectorType] || getDefaultConnectorStatus(); + }; + + /** + * Check if a connector is enabled + */ + const isConnectorEnabled = (connectorType: string | undefined): boolean => { + return getConnectorStatus(connectorType).enabled; + }; + + /** + * Get status message for a connector + */ + const getConnectorStatusMessage = (connectorType: string | undefined): string | null => { + return getConnectorStatus(connectorType).statusMessage || null; + }; + + /** + * Check if warnings should be shown globally + */ + const shouldShowWarnings = (): boolean => { + return connectorStatusConfig.globalSettings.showWarnings; + }; + + return useMemo( + () => ({ + getConnectorStatus, + isConnectorEnabled, + getConnectorStatusMessage, + shouldShowWarnings, + }), + [] + ); +} diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx index 141de0848..74dd51929 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx @@ -9,6 +9,7 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; import { cn } from "@/lib/utils"; import { getConnectorDisplayName } from "../tabs/all-connectors-tab"; +import { useConnectorStatus } from "../hooks/use-connector-status"; interface ConnectorAccountsListViewProps { connectorType: string; @@ -65,13 +66,19 @@ export const ConnectorAccountsListView: FC = ({ onAddAccount, isConnecting = false, }) => { + // Get connector status + const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus(); + + const isEnabled = isConnectorEnabled(connectorType); + const statusMessage = getConnectorStatusMessage(connectorType); + // Filter connectors to only show those of this type const typeConnectors = connectors.filter((c) => c.connector_type === connectorType); return (
{/* Header */} -
+
{/* Back button */}
@@ -101,21 +108,23 @@ export const ConnectorAccountsListView: FC = ({ @@ -123,7 +132,7 @@ export const ConnectorAccountsListView: FC = ({
{/* Content */} -
+
{/* Connected Accounts Grid */}
{typeConnectors.map((connector) => { diff --git a/surfsense_web/components/layout/ui/header/Header.tsx b/surfsense_web/components/layout/ui/header/Header.tsx index a03761ef5..0bdb9b423 100644 --- a/surfsense_web/components/layout/ui/header/Header.tsx +++ b/surfsense_web/components/layout/ui/header/Header.tsx @@ -24,7 +24,7 @@ export function Header({ {/* Left side - Mobile menu trigger + Breadcrumb */}
{mobileMenuTrigger} - {breadcrumb} +
{breadcrumb}
{/* Right side - Actions */}