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:
Anish Sarkar 2026-01-09 18:55:50 +05:30
parent 2e8d3fd721
commit 924d18896a
7 changed files with 400 additions and 29 deletions

View file

@ -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>
);
};

View file

@ -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" ? (

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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);
}

View file

@ -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,
}),
[]
);
}

View file

@ -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) => {