mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-16 21:05:20 +02:00
feat: implement connector status management and warnings, ran frontend linting
- Added a new hook `useConnectorStatus` to manage connector status information. - Introduced `ConnectorStatusBadge` and `ConnectorWarningBanner` components for displaying status and warnings. - Updated `ConnectorCard` and `ConnectorAccountsListView` to utilize the new status management features, including conditional rendering based on connector status and warnings. - Created a configuration file for connector statuses to streamline status management across the application.
This commit is contained in:
parent
2e8d3fd721
commit
924d18896a
7 changed files with 400 additions and 29 deletions
|
|
@ -7,7 +7,13 @@ import { cn } from "@/lib/utils";
|
|||
export const Logo = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<Link href="/">
|
||||
<Image src="/icon-128.svg" className={cn("dark:invert", className)} alt="logo" width={128} height={128} />
|
||||
<Image
|
||||
src="/icon-128.svg"
|
||||
className={cn("dark:invert", className)}
|
||||
alt="logo"
|
||||
width={128}
|
||||
height={128}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<ConnectorCardProps> = ({
|
|||
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<ConnectorCardProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
// Show status message if available and connector is not connected
|
||||
if (!isConnected && statusMessage) {
|
||||
return <span className="text-[10px] text-muted-foreground">{statusMessage}</span>;
|
||||
}
|
||||
|
||||
if (isConnected) {
|
||||
// Show last indexed date for connected connectors
|
||||
if (lastIndexedAt) {
|
||||
|
|
@ -136,12 +158,35 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
return <span className="whitespace-nowrap text-[10px]">Never indexed</span>;
|
||||
}
|
||||
|
||||
// Show warning message if available and warnings are enabled
|
||||
if (warning && showWarnings) {
|
||||
return <span className="text-[10px] text-yellow-600 dark:text-yellow-500">{warning}</span>;
|
||||
}
|
||||
|
||||
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">
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex items-center gap-4 p-4 rounded-xl text-left transition-all duration-200 w-full border",
|
||||
!isEnabled
|
||||
? "opacity-50 border-border/50 bg-slate-400/5 dark:bg-white/5 cursor-not-allowed"
|
||||
: 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",
|
||||
!isEnabled
|
||||
? "bg-slate-400/5 dark:bg-white/5 border-slate-400/5 dark:border-white/5 opacity-50"
|
||||
: 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" ? (
|
||||
|
|
@ -153,6 +198,9 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[14px] font-semibold leading-tight truncate">{title}</span>
|
||||
{showWarnings && status.status !== "active" && (
|
||||
<ConnectorStatusBadge status={status.status} />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-1">{getStatusContent()}</div>
|
||||
{isConnected && documentCount !== undefined && (
|
||||
|
|
@ -179,10 +227,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" ? (
|
||||
|
|
|
|||
|
|
@ -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<ConnectorStatusBadgeProps> = ({ 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 (
|
||||
<div
|
||||
className={cn("flex items-center justify-center shrink-0", className)}
|
||||
title={config.title}
|
||||
>
|
||||
<Icon className={cn("size-3.5", config.className)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,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<typeof connectorStatusSchema>;
|
||||
export type ConnectorStatusConfig = z.infer<typeof connectorStatusConfigSchema>;
|
||||
export type ConnectorStatusMap = z.infer<typeof connectorStatusMapSchema>;
|
||||
export type ConnectorStatusConfigFile = z.infer<typeof connectorStatusConfigFileSchema>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
|
@ -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<ConnectorAccountsListViewProps> = ({
|
|||
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 (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-12 pt-6 sm:pt-10 pb-4 border-b border-border/50 bg-muted">
|
||||
<div className="flex items-center justify-between gap-4 sm:pr-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 rounded-full shrink-0"
|
||||
onClick={onBack}
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-slate-400/5 dark:bg-white/5 border border-slate-400/5 dark:border-white/5">
|
||||
{getConnectorIcon(connectorType, "size-5")}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{connectorTitle} Accounts</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{typeConnectors.length} connected account{typeConnectors.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-6 sm:px-12 pt-8 sm:pt-10 pb-4 border-b border-border/50 bg-muted">
|
||||
{/* Back button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground mb-6 w-fit"
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to connectors
|
||||
</button>
|
||||
|
||||
{/* Connector header */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-6">
|
||||
<div className="flex gap-4 flex-1 w-full sm:w-auto">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10 border border-primary/20 shrink-0">
|
||||
{getConnectorIcon(connectorType, "size-7")}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight text-wrap whitespace-normal">
|
||||
{connectorTitle}
|
||||
</h2>
|
||||
<p className="text-xs sm:text-base text-muted-foreground mt-1">
|
||||
{statusMessage || "Manage your connector settings and sync configuration"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Add Account Button with dashed border */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAddAccount}
|
||||
disabled={isConnecting}
|
||||
disabled={isConnecting || !isEnabled}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-lg mr-4 border-2 border-dashed border-border/70 text-left transition-all duration-200",
|
||||
"border-primary/50 hover:bg-primary/5",
|
||||
"flex items-center gap-2 px-3 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"
|
||||
)}
|
||||
>
|
||||
|
|
@ -120,7 +136,11 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-4 sm:px-12 py-6 sm:py-8">
|
||||
<div className="flex-1 overflow-y-auto px-6 sm:px-12 py-6 sm:py-8">
|
||||
{/* Warning Banner */}
|
||||
{warning && showWarnings && (
|
||||
<ConnectorWarningBanner warning={warning} statusMessage={statusMessage} />
|
||||
)}
|
||||
{/* Connected Accounts Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{typeConnectors.map((connector) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue