mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-02 19:55:18 +02:00
Merge pull request #681 from AnishSarkar22/feat/config-connectors
feat: Config for connectors
This commit is contained in:
commit
9655db1995
9 changed files with 373 additions and 16 deletions
|
|
@ -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<ConnectorCardProps> = ({
|
|||
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<ConnectorCardProps> = ({
|
|||
return description;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group relative flex items-center gap-4 p-4 rounded-xl text-left transition-all duration-200 w-full border border-border bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg transition-colors shrink-0 bg-slate-400/5 dark:bg-white/5 border border-slate-400/5 dark:border-white/5">
|
||||
const cardContent = (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex items-center gap-4 p-4 rounded-xl text-left transition-all duration-200 w-full border",
|
||||
status.status === "warning"
|
||||
? "border-yellow-500/30 bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
|
||||
: "border-border bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-12 w-12 items-center justify-center rounded-lg transition-colors shrink-0 border",
|
||||
status.status === "warning"
|
||||
? "bg-yellow-500/10 border-yellow-500/20 bg-slate-400/5 dark:bg-white/5 border-slate-400/5 dark:border-white/5"
|
||||
: "bg-slate-400/5 dark:bg-white/5 border-slate-400/5 dark:border-white/5"
|
||||
)}
|
||||
>
|
||||
{connectorType ? (
|
||||
getConnectorIcon(connectorType, "size-6")
|
||||
) : id === "youtube-crawler" ? (
|
||||
|
|
@ -151,8 +176,15 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[14px] font-semibold leading-tight truncate">{title}</span>
|
||||
{showWarnings && status.status !== "active" && (
|
||||
<ConnectorStatusBadge
|
||||
status={status.status}
|
||||
statusMessage={statusMessage}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-1">{getStatusContent()}</div>
|
||||
{isConnected && documentCount !== undefined && (
|
||||
|
|
@ -179,10 +211,12 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
!isConnected && "shadow-xs"
|
||||
)}
|
||||
onClick={isConnected ? onManage : onConnect}
|
||||
disabled={isConnecting}
|
||||
disabled={isConnecting || !isEnabled}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
) : !isEnabled ? (
|
||||
"Unavailable"
|
||||
) : isConnected ? (
|
||||
"Manage"
|
||||
) : id === "youtube-crawler" ? (
|
||||
|
|
@ -195,4 +229,6 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return cardContent;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<ConnectorStatusBadgeProps> = ({
|
||||
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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={cn("inline-flex items-center justify-center shrink-0", className)}>
|
||||
<Icon className={cn("size-3.5", config.className)} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs">
|
||||
{statusMessage}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("inline-flex items-center justify-center shrink-0", className)}
|
||||
title={tooltipTitle}
|
||||
>
|
||||
<Icon className={cn("size-3.5", config.className)} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<ConnectorWarningBannerProps> = ({
|
||||
warning,
|
||||
statusMessage,
|
||||
onDismiss,
|
||||
className,
|
||||
}) => {
|
||||
const [isDismissed, setIsDismissed] = useState(false);
|
||||
|
||||
if (isDismissed) return null;
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsDismissed(true);
|
||||
onDismiss?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10 dark:bg-yellow-500/5 mb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<AlertTriangle className="size-4 text-yellow-600 dark:text-yellow-500 shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[12px] font-medium text-yellow-900 dark:text-yellow-200">{warning}</p>
|
||||
{statusMessage && (
|
||||
<p className="text-[11px] text-yellow-700 dark:text-yellow-300 mt-1">{statusMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
className="shrink-0 p-0.5 rounded hover:bg-yellow-500/20 transition-colors"
|
||||
aria-label="Dismiss warning"
|
||||
>
|
||||
<X className="size-3.5 text-yellow-700 dark:text-yellow-300" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"connectorStatuses": {},
|
||||
"globalSettings": {
|
||||
"showWarnings": true,
|
||||
"allowManualOverride": false
|
||||
}
|
||||
}
|
||||
|
|
@ -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<typeof connectorStatusSchema>;
|
||||
export type ConnectorStatusConfig = z.infer<typeof connectorStatusConfigSchema>;
|
||||
export type ConnectorStatusMap = z.infer<typeof connectorStatusMapSchema>;
|
||||
export type ConnectorStatusConfigFile = z.infer<typeof connectorStatusConfigFileSchema>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
|
@ -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<ConnectorAccountsListViewProps> = ({
|
|||
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 (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="px-6 sm:px-12 pt-8 sm:pt-10 pb-4 border-b border-border/50 bg-muted">
|
||||
<div className="px-6 sm:px-12 pt-8 sm:pt-10 pb-1 sm:pb-4 border-b border-border/50 bg-muted">
|
||||
{/* Back button */}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -93,7 +100,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
{connectorTitle}
|
||||
</h2>
|
||||
<p className="text-xs sm:text-base text-muted-foreground mt-1">
|
||||
Manage your connector settings and sync configuration
|
||||
{statusMessage || "Manage your connector settings and sync configuration"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -101,21 +108,23 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onAddAccount}
|
||||
disabled={isConnecting}
|
||||
disabled={isConnecting || !isEnabled}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-lg border-2 border-dashed border-border/70 text-left transition-all duration-200 shrink-0 self-center sm:self-auto sm:w-auto",
|
||||
"border-primary/50 hover:bg-primary/5",
|
||||
"flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg border-2 border-dashed text-left transition-all duration-200 shrink-0 self-center sm:self-auto sm:w-auto",
|
||||
!isEnabled
|
||||
? "border-border/30 opacity-50 cursor-not-allowed"
|
||||
: "border-primary/50 hover:bg-primary/5",
|
||||
isConnecting && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary/10 shrink-0">
|
||||
<div className="flex h-5 w-5 sm:h-6 sm:w-6 items-center justify-center rounded-md bg-primary/10 shrink-0">
|
||||
{isConnecting ? (
|
||||
<Loader2 className="size-3.5 animate-spin text-primary" />
|
||||
<Loader2 className="size-3 sm:size-3.5 animate-spin text-primary" />
|
||||
) : (
|
||||
<Plus className="size-3.5 text-primary" />
|
||||
<Plus className="size-3 sm:size-3.5 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[12px] font-medium">
|
||||
<span className="text-[11px] sm:text-[12px] font-medium">
|
||||
{isConnecting ? "Connecting..." : "Add Account"}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -123,7 +132,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 sm:px-12 py-6 sm:py-8">
|
||||
<div className="flex-1 overflow-y-auto px-6 sm:px-12 pt-0 sm:pt-6 pb-6 sm:pb-8">
|
||||
{/* Connected Accounts Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{typeConnectors.map((connector) => {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function Header({
|
|||
{/* Left side - Mobile menu trigger + Breadcrumb */}
|
||||
<div className="flex flex-1 items-center gap-2 min-w-0">
|
||||
{mobileMenuTrigger}
|
||||
{breadcrumb}
|
||||
<div className="hidden md:block">{breadcrumb}</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Actions */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue