uniform connector UX across all connector types

This commit is contained in:
CREDO23 2026-04-22 11:22:04 +02:00
parent dfa40b8801
commit a4bc621c2a
8 changed files with 82 additions and 61 deletions

View file

@ -8,6 +8,7 @@ import { Spinner } from "@/components/ui/spinner";
import { EnumConnectorName } from "@/contracts/enums/connector"; import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
import { useConnectorStatus } from "../hooks/use-connector-status"; import { useConnectorStatus } from "../hooks/use-connector-status";
import { ConnectorStatusBadge } from "./connector-status-badge"; import { ConnectorStatusBadge } from "./connector-status-badge";
@ -55,6 +56,7 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
onManage, onManage,
}) => { }) => {
const isMCP = connectorType === EnumConnectorName.MCP_CONNECTOR; const isMCP = connectorType === EnumConnectorName.MCP_CONNECTOR;
const isLive = !!connectorType && LIVE_CONNECTOR_TYPES.has(connectorType);
// Get connector status // Get connector status
const { getConnectorStatus, isConnectorEnabled, getConnectorStatusMessage, shouldShowWarnings } = const { getConnectorStatus, isConnectorEnabled, getConnectorStatusMessage, shouldShowWarnings } =
useConnectorStatus(); useConnectorStatus();
@ -123,14 +125,14 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
</span> </span>
) : ( ) : (
<> <>
<span>{formatDocumentCount(documentCount)}</span> {!isLive && <span>{formatDocumentCount(documentCount)}</span>}
{!isLive && accountCount !== undefined && accountCount > 0 && (
<span className="text-muted-foreground/50"></span>
)}
{accountCount !== undefined && accountCount > 0 && ( {accountCount !== undefined && accountCount > 0 && (
<> <span>
<span className="text-muted-foreground/50"></span> {accountCount} {accountCount === 1 ? "Account" : "Accounts"}
<span> </span>
{accountCount} {accountCount === 1 ? "Account" : "Accounts"}
</span>
</>
)} )}
</> </>
)} )}

View file

@ -53,8 +53,7 @@ export const DiscordConfig: FC<DiscordConfigProps> = ({ connector }) => {
return () => document.removeEventListener("visibilitychange", handleVisibilityChange); return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
}, [connector?.id, fetchChannels]); }, [connector?.id, fetchChannels]);
// Separate channels by indexing capability const accessible = channels.filter((ch) => ch.can_index);
const readyToIndex = channels.filter((ch) => ch.can_index);
const needsPermissions = channels.filter((ch) => !ch.can_index); const needsPermissions = channels.filter((ch) => !ch.can_index);
// Format last fetched time // Format last fetched time
@ -80,7 +79,7 @@ export const DiscordConfig: FC<DiscordConfigProps> = ({ connector }) => {
</div> </div>
<div className="text-xs sm:text-sm"> <div className="text-xs sm:text-sm">
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm"> <p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
The bot needs &quot;Read Message History&quot; permission to index channels. Ask a The bot needs &quot;Read Message History&quot; permission to access channels. Ask a
server admin to grant this permission for channels shown below. server admin to grant this permission for channels shown below.
</p> </p>
</div> </div>
@ -127,18 +126,18 @@ export const DiscordConfig: FC<DiscordConfigProps> = ({ connector }) => {
</div> </div>
) : ( ) : (
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 overflow-hidden"> <div className="rounded-xl bg-slate-400/5 dark:bg-white/5 overflow-hidden">
{/* Ready to index */} {/* Accessible channels */}
{readyToIndex.length > 0 && ( {accessible.length > 0 && (
<div className={cn("p-3", needsPermissions.length > 0 && "border-b border-border")}> <div className={cn("p-3", needsPermissions.length > 0 && "border-b border-border")}>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<CheckCircle2 className="size-3.5 text-emerald-500" /> <CheckCircle2 className="size-3.5 text-emerald-500" />
<span className="text-[11px] font-medium">Ready to index</span> <span className="text-[11px] font-medium">Accessible</span>
<span className="text-[10px] text-muted-foreground"> <span className="text-[10px] text-muted-foreground">
{readyToIndex.length} {readyToIndex.length === 1 ? "channel" : "channels"} {accessible.length} {accessible.length === 1 ? "channel" : "channels"}
</span> </span>
</div> </div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{readyToIndex.map((channel) => ( {accessible.map((channel) => (
<ChannelPill key={channel.id} channel={channel} /> <ChannelPill key={channel.id} channel={channel} />
))} ))}
</div> </div>
@ -150,7 +149,7 @@ export const DiscordConfig: FC<DiscordConfigProps> = ({ connector }) => {
<div className="p-3"> <div className="p-3">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<AlertCircle className="size-3.5 text-amber-500" /> <AlertCircle className="size-3.5 text-amber-500" />
<span className="text-[11px] font-medium">Grant permissions to index</span> <span className="text-[11px] font-medium">Needs permissions</span>
<span className="text-[10px] text-muted-foreground"> <span className="text-[10px] text-muted-foreground">
{needsPermissions.length}{" "} {needsPermissions.length}{" "}
{needsPermissions.length === 1 ? "channel" : "channels"} {needsPermissions.length === 1 ? "channel" : "channels"}

View file

@ -6,25 +6,23 @@ import type { ConnectorConfigProps } from "../index";
export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => { export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
const serviceName = connector.config?.mcp_service as string | undefined; const serviceName = connector.config?.mcp_service as string | undefined;
const displayName = serviceName
? serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
: "this service";
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3"> <div className="rounded-xl border border-border bg-emerald-500/5 p-4 flex items-start gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 shrink-0 mt-0.5"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 shrink-0 mt-0.5">
<CheckCircle2 className="size-4 text-emerald-500" /> <CheckCircle2 className="size-4 text-emerald-500" />
</div> </div>
<div className="text-xs sm:text-sm"> <div className="text-xs sm:text-sm">
<p className="font-medium text-xs sm:text-sm">Connected via MCP</p> <p className="font-medium text-xs sm:text-sm">Connected</p>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm"> <p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
Your agent can search, read, and take actions in{" "} Your agent can search, read, and take actions in {displayName}.
{serviceName
? serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
: "this service"}{" "}
in real time. No background indexing needed.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
); );
}; };

View file

@ -18,9 +18,9 @@ export const TeamsConfig: FC<TeamsConfigProps> = () => {
<div className="text-xs sm:text-sm"> <div className="text-xs sm:text-sm">
<p className="font-medium text-xs sm:text-sm">Microsoft Teams Access</p> <p className="font-medium text-xs sm:text-sm">Microsoft Teams Access</p>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm"> <p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
SurfSense will index messages from Teams channels that you have access to. The app can Your agent can search and read messages from Teams channels you have access to,
only read messages from teams and channels where you are a member. Make sure you're a and send messages on your behalf. Make sure you&#39;re a member of the teams
member of the teams you want to index before connecting. you want to interact with.
</p> </p>
</div> </div>
</div> </div>

View file

@ -16,6 +16,7 @@ import { DateRangeSelector } from "../../components/date-range-selector";
import { PeriodicSyncConfig } from "../../components/periodic-sync-config"; import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
import { SummaryConfig } from "../../components/summary-config"; import { SummaryConfig } from "../../components/summary-config";
import { VisionLLMConfig } from "../../components/vision-llm-config"; import { VisionLLMConfig } from "../../components/vision-llm-config";
import { LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants";
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab"; import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
import { type ConnectorConfigProps, getConnectorConfigComponent } from "../index"; import { type ConnectorConfigProps, getConnectorConfigComponent } from "../index";
@ -119,6 +120,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
}, [searchSpaceId, searchSpaceIdAtom, reauthEndpoint, connector.id]); }, [searchSpaceId, searchSpaceIdAtom, reauthEndpoint, connector.id]);
const isMCPBacked = Boolean(connector.config?.server_config); const isMCPBacked = Boolean(connector.config?.server_config);
const isLive = isMCPBacked || LIVE_CONNECTOR_TYPES.has(connector.connector_type);
// Get connector-specific config component (MCP-backed connectors use a generic view) // Get connector-specific config component (MCP-backed connectors use a generic view)
const ConnectorConfigComponent = useMemo(() => { const ConnectorConfigComponent = useMemo(() => {
@ -228,8 +230,8 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
{getConnectorDisplayName(connector.name)} {getConnectorDisplayName(connector.name)}
</h2> </h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1"> <p className="text-xs sm:text-base text-muted-foreground mt-1">
{isMCPBacked {isLive
? "Connected — your agent can interact with this service in real time" ? "Manage your connected account"
: "Manage your connector settings and sync configuration"} : "Manage your connector settings and sync configuration"}
</p> </p>
</div> </div>
@ -381,10 +383,12 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
{/* Fixed Footer - Action buttons */} {/* Fixed Footer - Action buttons */}
<div className="flex-shrink-0 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 sm:gap-0 px-6 sm:px-12 py-6 sm:py-6 bg-muted border-t border-border"> <div className="flex-shrink-0 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 sm:gap-0 px-6 sm:px-12 py-6 sm:py-6 bg-muted border-t border-border">
{showDisconnectConfirm ? ( {showDisconnectConfirm ? (
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 sm:flex-initial"> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 sm:flex-initial">
<span className="text-xs sm:text-sm text-muted-foreground sm:whitespace-nowrap"> <span className="text-xs sm:text-sm text-muted-foreground sm:whitespace-nowrap">
Are you sure? {isLive
? "Your agent will lose access to this service."
: "This will remove all indexed data."}
</span> </span>
<div className="flex items-center gap-2 sm:gap-3"> <div className="flex items-center gap-2 sm:gap-3">
<Button <Button

View file

@ -1,5 +1,22 @@
import { EnumConnectorName } from "@/contracts/enums/connector"; import { EnumConnectorName } from "@/contracts/enums/connector";
/**
* Connectors that operate in real time (no background indexing).
* Used to adjust UI: hide sync controls, show "Connected" instead of doc counts.
*/
export const LIVE_CONNECTOR_TYPES = new Set<string>([
EnumConnectorName.LINEAR_CONNECTOR,
EnumConnectorName.SLACK_CONNECTOR,
EnumConnectorName.JIRA_CONNECTOR,
EnumConnectorName.CLICKUP_CONNECTOR,
EnumConnectorName.AIRTABLE_CONNECTOR,
EnumConnectorName.DISCORD_CONNECTOR,
EnumConnectorName.TEAMS_CONNECTOR,
EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR,
EnumConnectorName.GOOGLE_GMAIL_CONNECTOR,
EnumConnectorName.LUMA_CONNECTOR,
]);
// OAuth Connectors (Quick Connect) // OAuth Connectors (Quick Connect)
export const OAUTH_CONNECTORS = [ export const OAUTH_CONNECTORS = [
{ {
@ -13,7 +30,7 @@ export const OAUTH_CONNECTORS = [
{ {
id: "google-gmail-connector", id: "google-gmail-connector",
title: "Gmail", title: "Gmail",
description: "Search through your emails", description: "Search and read your emails",
connectorType: EnumConnectorName.GOOGLE_GMAIL_CONNECTOR, connectorType: EnumConnectorName.GOOGLE_GMAIL_CONNECTOR,
authEndpoint: "/api/v1/auth/google/gmail/connector/add/", authEndpoint: "/api/v1/auth/google/gmail/connector/add/",
selfHostedOnly: true, selfHostedOnly: true,
@ -21,7 +38,7 @@ export const OAUTH_CONNECTORS = [
{ {
id: "google-calendar-connector", id: "google-calendar-connector",
title: "Google Calendar", title: "Google Calendar",
description: "Search through your events", description: "Search and manage your events",
connectorType: EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR, connectorType: EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR,
authEndpoint: "/api/v1/auth/google/calendar/connector/add/", authEndpoint: "/api/v1/auth/google/calendar/connector/add/",
selfHostedOnly: true, selfHostedOnly: true,
@ -29,7 +46,7 @@ export const OAUTH_CONNECTORS = [
{ {
id: "airtable-connector", id: "airtable-connector",
title: "Airtable", title: "Airtable",
description: "Search your Airtable bases", description: "Search, read, and manage records",
connectorType: EnumConnectorName.AIRTABLE_CONNECTOR, connectorType: EnumConnectorName.AIRTABLE_CONNECTOR,
authEndpoint: "/api/v1/auth/mcp/airtable/connector/add/", authEndpoint: "/api/v1/auth/mcp/airtable/connector/add/",
}, },
@ -43,21 +60,21 @@ export const OAUTH_CONNECTORS = [
{ {
id: "linear-connector", id: "linear-connector",
title: "Linear", title: "Linear",
description: "Search issues & projects", description: "Search, read, and manage issues & projects",
connectorType: EnumConnectorName.LINEAR_CONNECTOR, connectorType: EnumConnectorName.LINEAR_CONNECTOR,
authEndpoint: "/api/v1/auth/mcp/linear/connector/add/", authEndpoint: "/api/v1/auth/mcp/linear/connector/add/",
}, },
{ {
id: "slack-connector", id: "slack-connector",
title: "Slack", title: "Slack",
description: "Search Slack messages", description: "Search, read, and send messages",
connectorType: EnumConnectorName.SLACK_CONNECTOR, connectorType: EnumConnectorName.SLACK_CONNECTOR,
authEndpoint: "/api/v1/auth/mcp/slack/connector/add/", authEndpoint: "/api/v1/auth/mcp/slack/connector/add/",
}, },
{ {
id: "teams-connector", id: "teams-connector",
title: "Microsoft Teams", title: "Microsoft Teams",
description: "Search Teams messages", description: "Search, read, and send messages",
connectorType: EnumConnectorName.TEAMS_CONNECTOR, connectorType: EnumConnectorName.TEAMS_CONNECTOR,
authEndpoint: "/api/v1/auth/teams/connector/add/", authEndpoint: "/api/v1/auth/teams/connector/add/",
}, },
@ -78,14 +95,14 @@ export const OAUTH_CONNECTORS = [
{ {
id: "discord-connector", id: "discord-connector",
title: "Discord", title: "Discord",
description: "Search Discord messages", description: "Search, read, and send messages",
connectorType: EnumConnectorName.DISCORD_CONNECTOR, connectorType: EnumConnectorName.DISCORD_CONNECTOR,
authEndpoint: "/api/v1/auth/discord/connector/add/", authEndpoint: "/api/v1/auth/discord/connector/add/",
}, },
{ {
id: "jira-connector", id: "jira-connector",
title: "Jira", title: "Jira",
description: "Search Jira issues", description: "Search, read, and manage issues",
connectorType: EnumConnectorName.JIRA_CONNECTOR, connectorType: EnumConnectorName.JIRA_CONNECTOR,
authEndpoint: "/api/v1/auth/mcp/jira/connector/add/", authEndpoint: "/api/v1/auth/mcp/jira/connector/add/",
}, },
@ -99,7 +116,7 @@ export const OAUTH_CONNECTORS = [
{ {
id: "clickup-connector", id: "clickup-connector",
title: "ClickUp", title: "ClickUp",
description: "Search ClickUp tasks", description: "Search, read, and manage tasks",
connectorType: EnumConnectorName.CLICKUP_CONNECTOR, connectorType: EnumConnectorName.CLICKUP_CONNECTOR,
authEndpoint: "/api/v1/auth/mcp/clickup/connector/add/", authEndpoint: "/api/v1/auth/mcp/clickup/connector/add/",
}, },
@ -138,7 +155,7 @@ export const OTHER_CONNECTORS = [
{ {
id: "luma-connector", id: "luma-connector",
title: "Luma", title: "Luma",
description: "Search Luma events", description: "Search and manage events",
connectorType: EnumConnectorName.LUMA_CONNECTOR, connectorType: EnumConnectorName.LUMA_CONNECTOR,
}, },
{ {

View file

@ -9,7 +9,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { getDocumentTypeLabel } from "@/lib/documents/document-type-labels"; import { getDocumentTypeLabel } from "@/lib/documents/document-type-labels";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { COMPOSIO_CONNECTORS, OAUTH_CONNECTORS } from "../constants/connector-constants"; import { COMPOSIO_CONNECTORS, LIVE_CONNECTOR_TYPES, OAUTH_CONNECTORS } from "../constants/connector-constants";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
import { getConnectorDisplayName } from "./all-connectors-tab"; import { getConnectorDisplayName } from "./all-connectors-tab";
@ -156,6 +156,7 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
{/* OAuth Connectors - Grouped by Type */} {/* OAuth Connectors - Grouped by Type */}
{filteredOAuthConnectorTypes.map(([connectorType, typeConnectors]) => { {filteredOAuthConnectorTypes.map(([connectorType, typeConnectors]) => {
const { title } = getOAuthConnectorTypeInfo(connectorType); const { title } = getOAuthConnectorTypeInfo(connectorType);
const isLive = LIVE_CONNECTOR_TYPES.has(connectorType);
const isAnyIndexing = typeConnectors.some((c: SearchSourceConnector) => const isAnyIndexing = typeConnectors.some((c: SearchSourceConnector) =>
indexingConnectorIds.has(c.id) indexingConnectorIds.has(c.id)
); );
@ -202,8 +203,12 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
</p> </p>
) : ( ) : (
<p className="text-[10px] text-muted-foreground mt-1 flex items-center gap-1.5"> <p className="text-[10px] text-muted-foreground mt-1 flex items-center gap-1.5">
<span>{formatDocumentCount(documentCount)}</span> {!isLive && (
<span className="text-muted-foreground/50"></span> <>
<span>{formatDocumentCount(documentCount)}</span>
<span className="text-muted-foreground/50"></span>
</>
)}
<span> <span>
{accountCount} {accountCount === 1 ? "Account" : "Accounts"} {accountCount} {accountCount === 1 ? "Account" : "Accounts"}
</span> </span>
@ -230,6 +235,7 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
documentTypeCounts documentTypeCounts
); );
const isMCPConnector = connector.connector_type === "MCP_CONNECTOR"; const isMCPConnector = connector.connector_type === "MCP_CONNECTOR";
const isLive = LIVE_CONNECTOR_TYPES.has(connector.connector_type);
return ( return (
<div <div
key={`connector-${connector.id}`} key={`connector-${connector.id}`}
@ -261,7 +267,7 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
<Spinner size="xs" /> <Spinner size="xs" />
Syncing Syncing
</p> </p>
) : !isMCPConnector ? ( ) : !isLive && !isMCPConnector ? (
<p className="text-[10px] text-muted-foreground mt-1"> <p className="text-[10px] text-muted-foreground mt-1">
{formatDocumentCount(documentCount)} {formatDocumentCount(documentCount)}
</p> </p>

View file

@ -13,6 +13,7 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { authenticatedFetch } from "@/lib/auth-utils"; import { authenticatedFetch } from "@/lib/auth-utils";
import { formatRelativeDate } from "@/lib/format-date"; import { formatRelativeDate } from "@/lib/format-date";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
import { useConnectorStatus } from "../hooks/use-connector-status"; import { useConnectorStatus } from "../hooks/use-connector-status";
import { getConnectorDisplayName } from "../tabs/all-connectors-tab"; import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
@ -43,12 +44,8 @@ interface ConnectorAccountsListViewProps {
addButtonText?: string; addButtonText?: string;
} }
/** function isLiveConnector(connectorType: string): boolean {
* Check if a connector type is indexable return LIVE_CONNECTOR_TYPES.has(connectorType) || connectorType === "MCP_CONNECTOR";
*/
function isIndexableConnector(connectorType: string): boolean {
const nonIndexableTypes = ["MCP_CONNECTOR"];
return !nonIndexableTypes.includes(connectorType);
} }
export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
@ -149,7 +146,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
{connectorTitle} {connectorTitle}
</h2> </h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1"> <p className="text-xs sm:text-base text-muted-foreground mt-1">
{statusMessage || "Manage your connector settings and sync configuration"} {statusMessage || "Manage your connected accounts"}
</p> </p>
</div> </div>
</div> </div>
@ -234,15 +231,13 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
<Spinner size="xs" /> <Spinner size="xs" />
Syncing Syncing
</p> </p>
) : ( ) : !isLiveConnector(connector.connector_type) ? (
<p className="text-[10px] text-muted-foreground mt-1 whitespace-nowrap truncate"> <p className="text-[10px] mt-1 whitespace-nowrap truncate text-muted-foreground">
{isIndexableConnector(connector.connector_type) {connector.last_indexed_at
? connector.last_indexed_at ? `Last indexed: ${formatRelativeDate(connector.last_indexed_at)}`
? `Last indexed: ${formatRelativeDate(connector.last_indexed_at)}` : "Never indexed"}
: "Never indexed"
: "Active"}
</p> </p>
)} ) : null}
</div> </div>
{isAuthExpired ? ( {isAuthExpired ? (
<Button <Button