diff --git a/surfsense_web/components/Logo.tsx b/surfsense_web/components/Logo.tsx index 79799942b..58f8d1c9f 100644 --- a/surfsense_web/components/Logo.tsx +++ b/surfsense_web/components/Logo.tsx @@ -7,7 +7,13 @@ import { cn } from "@/lib/utils"; export const Logo = ({ className }: { className?: string }) => { return ( - logo + logo ); }; 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..43c03e03c 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,21 @@ export const ConnectorCard: FC = ({ onConnect, onManage, }) => { + // Get connector status + const { + getConnectorStatus, + isConnectorEnabled, + getConnectorWarning, + getConnectorStatusMessage, + shouldShowWarnings, + } = useConnectorStatus(); + + const status = getConnectorStatus(connectorType); + const isEnabled = isConnectorEnabled(connectorType); + const warning = getConnectorWarning(connectorType); + const statusMessage = getConnectorStatusMessage(connectorType); + const showWarnings = shouldShowWarnings(); + // Extract count from active task message during indexing const indexingCount = extractIndexedCount(activeTask?.message); @@ -123,6 +140,11 @@ export const ConnectorCard: FC = ({ ); } + // Show status message if available and connector is not connected + if (!isConnected && statusMessage) { + return {statusMessage}; + } + if (isConnected) { // Show last indexed date for connected connectors if (lastIndexedAt) { @@ -136,12 +158,35 @@ export const ConnectorCard: FC = ({ return Never indexed; } + // Show warning message if available and warnings are enabled + if (warning && showWarnings) { + return {warning}; + } + return description; }; return ( -
-
+
+
{connectorType ? ( getConnectorIcon(connectorType, "size-6") ) : id === "youtube-crawler" ? ( @@ -153,6 +198,9 @@ export const ConnectorCard: FC = ({
{title} + {showWarnings && status.status !== "active" && ( + + )}
{getStatusContent()}
{isConnected && documentCount !== undefined && ( @@ -179,10 +227,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" ? ( 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..0fc48dfb1 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-status-badge.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { AlertTriangle, Ban, Wrench } from "lucide-react"; +import type { FC } from "react"; +import type { ConnectorStatus } from "../config/connector-status-config"; +import { cn } from "@/lib/utils"; + +interface ConnectorStatusBadgeProps { + status: ConnectorStatus; + className?: string; +} + +export const ConnectorStatusBadge: FC = ({ status, className }) => { + if (status === "active") { + return null; + } + + const getBadgeConfig = () => { + switch (status) { + case "warning": + return { + icon: AlertTriangle, + className: "text-yellow-500 dark:text-yellow-400", + title: "Warning", + }; + case "disabled": + return { + icon: Ban, + className: "text-red-500 dark:text-red-400", + title: "Disabled", + }; + case "maintenance": + return { + icon: Wrench, + className: "text-orange-500 dark:text-orange-400", + title: "Maintenance", + }; + case "deprecated": + return { + icon: AlertTriangle, + className: "text-amber-500 dark:text-amber-400", + title: "Deprecated", + }; + default: + return null; + } + }; + + const config = getBadgeConfig(); + if (!config) return null; + + const Icon = config.icon; + + 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.ts b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.ts new file mode 100644 index 000000000..5f3f1fee7 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.ts @@ -0,0 +1,114 @@ +/** + * Connector Status Configuration + * + * This configuration allows managing connector statuses in the frontend without backend changes. + * Statuses control warnings, disabling connectors, and displaying status messages. + */ + +import { z } from "zod"; + +// 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, + warning: z.string().nullable().optional(), + statusMessage: z.string().nullable().optional(), + disableReason: 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; + +/** + * Default status configuration for all connectors + * Connectors not listed here default to "active" and enabled + * + * This config is validated at runtime using the Zod schema above + */ +const rawConnectorStatusConfig = { + connectorStatuses: { + // Example: Disabled connector + // "SLACK_CONNECTOR": { + // enabled: false, + // status: "disabled", + // warning: null, + // statusMessage: "Slack connector is currently unavailable due to API changes", + // disableReason: "maintenance", + // }, + // Example: Connector with warning + // "NOTION_CONNECTOR": { + // enabled: true, + // status: "warning", + // warning: "Rate limits may apply", + // statusMessage: "Notion API rate limits are currently active. Some requests may be delayed.", + // disableReason: null, + // }, + // Example: Connector in maintenance + // "TEAMS_CONNECTOR": { + // enabled: false, + // status: "maintenance", + // warning: "Under maintenance", + // statusMessage: "Temporarily unavailable for maintenance", + // disableReason: "maintenance", + // }, + }, + globalSettings: { + showWarnings: true, + allowManualOverride: false, + }, +}; + +// Validate the config at module load time (development only) +// In production, this will throw if config is invalid +export const connectorStatusConfig: ConnectorStatusConfigFile = + connectorStatusConfigFileSchema.parse(rawConnectorStatusConfig); + +/** + * 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", + warning: null, + statusMessage: null, + disableReason: 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..bb781c879 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-status.ts @@ -0,0 +1,63 @@ +"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 warning message for a connector (if any) + */ + const getConnectorWarning = (connectorType: string | undefined): string | null => { + return getConnectorStatus(connectorType).warning || null; + }; + + /** + * 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, + getConnectorWarning, + 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 e45f24d11..df21c0eb5 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,8 @@ 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"; +import { ConnectorWarningBanner } from "../components/connector-warning-banner"; interface ConnectorAccountsListViewProps { connectorType: string; @@ -65,43 +67,57 @@ export const ConnectorAccountsListView: FC = ({ onAddAccount, isConnecting = false, }) => { + // Get connector status + const { isConnectorEnabled, getConnectorWarning, getConnectorStatusMessage, shouldShowWarnings } = + useConnectorStatus(); + + const isEnabled = isConnectorEnabled(connectorType); + const warning = getConnectorWarning(connectorType); + const statusMessage = getConnectorStatusMessage(connectorType); + const showWarnings = shouldShowWarnings(); + // Filter connectors to only show those of this type const typeConnectors = connectors.filter((c) => c.connector_type === connectorType); return (
{/* Header */} -
-
-
- -
-
- {getConnectorIcon(connectorType, "size-5")} -
-
-

{connectorTitle} Accounts

-

- {typeConnectors.length} connected account{typeConnectors.length !== 1 ? "s" : ""} -

-
+
+ {/* Back button */} + + + {/* Connector header */} +
+
+
+ {getConnectorIcon(connectorType, "size-7")} +
+
+

+ {connectorTitle} +

+

+ {statusMessage || "Manage your connector settings and sync configuration"} +

{/* Add Account Button with dashed border */}
{/* Content */} -
+
+ {/* Warning Banner */} + {warning && showWarnings && ( + + )} {/* Connected Accounts Grid */}
{typeConnectors.map((connector) => {