@burdenoff/microfe-channel 2026.830.1 → 2026.830.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/ChannelRoot.d.ts.map +1 -1
- package/dist/channel/ChannelRoot.js +32 -35
- package/dist/channel/ChannelRoot.js.map +1 -1
- package/dist/channel/components/AdapterCredentialsForm.d.ts.map +1 -1
- package/dist/channel/components/AdapterCredentialsForm.js +52 -58
- package/dist/channel/components/AdapterCredentialsForm.js.map +1 -1
- package/dist/channel/components/FeatureNotEnabled.d.ts.map +1 -1
- package/dist/channel/components/FeatureNotEnabled.js +27 -22
- package/dist/channel/components/FeatureNotEnabled.js.map +1 -1
- package/dist/channel/components/MarkdownEditor.d.ts.map +1 -1
- package/dist/channel/components/MarkdownEditor.js +78 -77
- package/dist/channel/components/MarkdownEditor.js.map +1 -1
- package/dist/channel/components/PermissionGuard.d.ts.map +1 -1
- package/dist/channel/components/PermissionGuard.js +6 -9
- package/dist/channel/components/PermissionGuard.js.map +1 -1
- package/dist/channel/hooks/useTr.d.ts +17 -0
- package/dist/channel/hooks/useTr.d.ts.map +1 -0
- package/dist/channel/hooks/useTr.js +14 -0
- package/dist/channel/hooks/useTr.js.map +1 -0
- package/dist/channel/modules/channels/i18n.js +24 -24
- package/dist/channel/modules/channels/i18n.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelDashboardPage.js +2 -2
- package/dist/channel/modules/channels/pages/ChannelDashboardPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.js +232 -226
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelUsersPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelUsersPage.js +152 -146
- package/dist/channel/modules/channels/pages/ChannelUsersPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js +112 -114
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/CreateChannelPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/CreateChannelPage.js +300 -269
- package/dist/channel/modules/channels/pages/CreateChannelPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/CreateConnectionPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/CreateConnectionPage.js +108 -102
- package/dist/channel/modules/channels/pages/CreateConnectionPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/EditChannelPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/EditChannelPage.js +384 -343
- package/dist/channel/modules/channels/pages/EditChannelPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/SendMessagePage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/SendMessagePage.js +170 -156
- package/dist/channel/modules/channels/pages/SendMessagePage.js.map +1 -1
- package/dist/channel/pages/ChannelLandingPage.d.ts.map +1 -1
- package/dist/channel/pages/ChannelLandingPage.js +47 -50
- package/dist/channel/pages/ChannelLandingPage.js.map +1 -1
- package/dist/translations/en.d.ts +271 -4
- package/dist/translations/en.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChannelUsersPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelUsersPage.tsx"],"sourcesContent":["/**\n * Channel Users Page\n *\n * Displays user platform connections for a specific channel\n */\n\nimport { useState, useEffect, useCallback, useRef } from 'react';\nimport { useParams, useNavigate } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n Clock,\n User,\n UserCheck,\n Bell,\n BellOff,\n RefreshCw,\n Download,\n ChevronLeft,\n ChevronRight,\n} from 'lucide-react';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n ResponsiveTable,\n IllustratedEmptyState,\n PagePurpose,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n useChannel,\n type UserPlatformConnection,\n type UserPresenceStatus,\n type PairingStatus,\n} from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useExportChannelUsers } from '../hooks/useExport';\n\nconst PRESENCE_CONFIG: Record<UserPresenceStatus, { label: string; color: string }> = {\n ONLINE: { label: 'Online', color: 'text-status-success-text' },\n OFFLINE: { label: 'Offline', color: 'text-text-secondary' },\n AWAY: { label: 'Away', color: 'text-status-warning-text' },\n DND: { label: 'Do Not Disturb', color: 'text-status-error-text' },\n};\n\nconst PAIRING_CONFIG: Record<\n PairingStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n PENDING: {\n label: 'Pending',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: Clock,\n },\n APPROVED: {\n label: 'Approved',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n EXPIRED: { label: 'Expired', color: 'text-text-secondary bg-bg-sunken', icon: XCircle },\n};\n\nexport function ChannelUsersPage() {\n const { channelId } = useParams<{ channelId: string }>();\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n const canExport = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_USER_EXPORT);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportUsers,\n isExporting: isExportingUsers,\n exportError: exportUsersError,\n } = useExportChannelUsers(organizationId ?? undefined, channelId);\n const [isLoadingUsers, setIsLoadingUsers] = useState(true);\n const [users, setUsers] = useState<UserPlatformConnection[]>([]);\n const [limit, setLimit] = useState(25);\n const [total, setTotal] = useState(0);\n const [currentPage, setCurrentPage] = useState(0);\n\n const { channel, isLoading, error, fetchUserPlatformConnections } = useChannel(channelId);\n\n const loadUsers = useCallback(\n async (page: number = 0) => {\n if (!channelId) return;\n setIsLoadingUsers(true);\n try {\n const result = await fetchUserPlatformConnections(limit, page * limit);\n setUsers(result.items);\n setTotal(result.total);\n } finally {\n setIsLoadingUsers(false);\n }\n },\n [channelId, fetchUserPlatformConnections, limit]\n );\n\n const loadUsersRef = useRef(loadUsers);\n useEffect(() => {\n loadUsersRef.current = loadUsers;\n });\n const isFirstPageRender = useRef(true);\n // Prevents the [currentPage] effect from double-loading when limit changes\n // reset the page back to 0 (the [limit] effect already handles that load).\n const limitChangingRef = useRef(false);\n\n useEffect(() => {\n void Promise.resolve().then(() => {\n // Set the guard before resetting page so the [currentPage] effect skips\n // the redundant load that would otherwise fire when page resets to 0.\n limitChangingRef.current = true;\n setCurrentPage(0);\n void loadUsersRef.current(0).finally(() => {\n limitChangingRef.current = false;\n });\n });\n }, [limit]);\n\n useEffect(() => {\n if (isFirstPageRender.current) {\n isFirstPageRender.current = false;\n return;\n }\n if (limitChangingRef.current) return;\n void Promise.resolve().then(() => loadUsersRef.current(currentPage));\n }, [currentPage]);\n\n const totalPages = Math.max(1, Math.ceil(total / limit));\n const hasPrevPage = currentPage > 0;\n const hasNextPage = currentPage < totalPages - 1;\n\n const getPresenceConfig = (status: UserPresenceStatus) => {\n return PRESENCE_CONFIG[status] || PRESENCE_CONFIG.OFFLINE;\n };\n\n const getPairingConfig = (status: PairingStatus) => {\n return PAIRING_CONFIG[status] || PAIRING_CONFIG.EXPIRED;\n };\n\n const userColumns: ResponsiveTableColumn<UserPlatformConnection>[] = [\n {\n key: 'user',\n header: tr('channel.user', 'User'),\n priority: 'primary',\n cell: (user) => (\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 bg-accent-primary/10 rounded-full flex items-center justify-center\">\n <User size={16} className=\"text-accent-primary\" />\n </div>\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {user.externalDisplayName || 'Unknown'}\n </p>\n {user.externalUsername && (\n <p className=\"text-xs text-text-secondary\">{user.externalUsername}</p>\n )}\n </div>\n </div>\n ),\n },\n {\n key: 'platformId',\n header: tr('channel.platformId', 'Platform ID'),\n priority: 'secondary',\n cellClassName: 'text-sm font-mono text-text-secondary',\n cell: (user) => user.externalUserId,\n },\n {\n key: 'source',\n header: tr('channel.source', 'Source'),\n priority: 'secondary',\n cell: (user) => (\n <span className=\"text-xs text-text-secondary bg-bg-sunken px-2 py-1 rounded\">\n {user.source}\n </span>\n ),\n },\n {\n key: 'status',\n header: tr('channel.status', 'Status'),\n priority: 'primary',\n cell: (user) => {\n const presenceConfig = getPresenceConfig(user.presenceStatus as UserPresenceStatus);\n return (\n <div className=\"flex items-center gap-2\">\n <span\n className={`w-2 h-2 rounded-full ${presenceConfig.color.replace('text-', 'bg-')}`}\n />\n <span className={`text-sm ${presenceConfig.color}`}>{presenceConfig.label}</span>\n </div>\n );\n },\n },\n {\n key: 'pairing',\n header: tr('channel.pairing', 'Pairing'),\n priority: 'secondary',\n cell: (user) => {\n const pairingConfig = user.pairingStatus\n ? getPairingConfig(user.pairingStatus as PairingStatus)\n : null;\n const PairingIcon = pairingConfig?.icon ?? XCircle;\n return user.pairingStatus && pairingConfig ? (\n <span\n className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded ${pairingConfig.color}`}\n >\n <PairingIcon size={12} />\n {pairingConfig.label}\n </span>\n ) : (\n <span className=\"text-xs text-text-secondary\">-</span>\n );\n },\n },\n {\n key: 'notifications',\n header: tr('channel.notifications', 'Notifications'),\n priority: 'secondary',\n cell: (user) =>\n user.notificationsEnabled ? (\n <span className=\"inline-flex items-center gap-1\">\n <Bell size={16} className=\"text-status-success-text\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.enabled', 'Enabled')}</span>\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1\">\n <BellOff size={16} className=\"text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.disabled', 'Disabled')}</span>\n </span>\n ),\n },\n {\n key: 'dmAllowed',\n header: tr('channel.dmAllowed', 'DM Allowed'),\n priority: 'secondary',\n cell: (user) =>\n user.dmAllowed ? (\n <span className=\"inline-flex items-center gap-1 text-sm text-status-success-text\">\n <UserCheck size={16} />\n {tr('channel.allowed', 'Allowed')}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <XCircle size={16} />\n {tr('channel.blocked', 'Blocked')}\n </span>\n ),\n },\n ];\n\n // Loading state (only full-screen for initial channel load)\n if (isLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" />\n </div>\n );\n }\n\n // Error state\n if (error || !channel) {\n return (\n <div className=\"p-6\">\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <div>\n <p className=\"text-sm font-medium text-status-error-text\">\n {error || 'Channel not found'}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"ml-auto px-3 py-1 text-sm text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors\"\n >\n Go Back\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.USER_PLATFORM_CONNECTION_READ}>\n <div className=\"space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n aria-label={tr('channel.backToChannel', 'Back to channel')}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} aria-hidden=\"true\" />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-text-primary\">\n {tr('channel.channelUsers', 'Channel Users')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">{channel.name} - User connections</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <select\n value={limit}\n onChange={(e) => setLimit(Number(e.target.value))}\n className=\"px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-primary/50 text-text-primary\"\n >\n <option value={10}>10 results</option>\n <option value={25}>25 results</option>\n <option value={50}>50 results</option>\n <option value={100}>100 results</option>\n </select>\n <CTAOverflowMenu\n actions={[\n {\n label: tr('channel.refresh', 'Refresh'),\n icon: (\n <RefreshCw\n size={16}\n className={isLoadingUsers ? 'animate-spin' : ''}\n aria-hidden=\"true\"\n />\n ),\n disabled: isLoadingUsers,\n onSelect: () => void loadUsers(currentPage),\n },\n ...(canExport\n ? [\n {\n label: tr('channel.export', 'Export'),\n icon: isExportingUsers ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Download size={16} aria-hidden=\"true\" />\n ),\n disabled: isExportingUsers,\n onSelect: () => void exportUsers(),\n },\n ]\n : []),\n ]}\n />\n </div>\n </div>\n\n {/* Page purpose */}\n <PagePurpose icon={<User size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.usersPurpose',\n 'The people reachable through this channel — linked via OAuth or a pairing code on platforms like Slack, Teams or WhatsApp. See who is online, whether their pairing is approved, and whether they have opted into notifications and direct messages. Use it to verify a recipient is actually connected before you target them, or to audit who can receive messages here.'\n )}\n </PagePurpose>\n\n {/* Export Error Banner */}\n {exportUsersError && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{exportUsersError}</p>\n </div>\n )}\n\n {/* Emphasis — connected-users summary */}\n <EmphasisPanel className=\"flex items-center gap-3 p-4\">\n <span className=\"rounded-lg bg-accent-blue-subtle p-2\">\n <UserCheck size={18} className=\"text-accent-blue\" aria-hidden=\"true\" />\n </span>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {total}{' '}\n {total === 1\n ? tr('channel.connectedUserSingular', 'connected user')\n : tr('channel.connectedUsersPlural', 'connected users')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.usersBannerHint',\n 'Users connected to this channel through OAuth or pairing. Notification preferences and DM authorization are managed per user.'\n )}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Users List */}\n <ResponsiveTable\n columns={userColumns}\n data={users}\n rowKey={(user) => user.id}\n emptyState={\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={tr('channel.noUsersConnected', 'No users connected to this channel')}\n />\n }\n className=\"bg-surface border border-border-seam rounded-lg\"\n />\n\n {/* Pagination footer */}\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {total > 0 && (\n <>\n {tr('channel.showing', 'Showing')}{' '}\n <span className=\"font-medium\">\n {currentPage * limit + 1}–{Math.min((currentPage + 1) * limit, total)}\n </span>{' '}\n {tr('channel.of', 'of')} <span className=\"font-medium\">{total}</span>\n </>\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}\n disabled={!hasPrevPage || isLoadingUsers}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronLeft size={16} />\n {tr('channel.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {currentPage + 1} / {totalPages}\n </span>\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}\n disabled={!hasNextPage || isLoadingUsers}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.next', 'Next')}\n <ChevronRight size={16} />\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChannelUsersPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelUsersPage.tsx"],"sourcesContent":["/**\n * Channel Users Page\n *\n * Displays user platform connections for a specific channel\n */\n\nimport { useState, useEffect, useCallback, useRef, useMemo } from 'react';\nimport { useParams, useNavigate } from 'react-router';\nimport { useTr } from '../../../hooks/useTr';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n Clock,\n User,\n UserCheck,\n Bell,\n BellOff,\n RefreshCw,\n Download,\n ChevronLeft,\n ChevronRight,\n} from 'lucide-react';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n ResponsiveTable,\n IllustratedEmptyState,\n PagePurpose,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n useChannel,\n type UserPlatformConnection,\n type UserPresenceStatus,\n type PairingStatus,\n} from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useExportChannelUsers } from '../hooks/useExport';\n\nexport function ChannelUsersPage() {\n const { channelId } = useParams<{ channelId: string }>();\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n const canExport = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_USER_EXPORT);\n const tr = useTr();\n\n const PRESENCE_CONFIG: Record<\n UserPresenceStatus,\n { labelKey: string; labelFallback: string; color: string }\n > = useMemo(\n () => ({\n ONLINE: {\n labelKey: 'channel.presence.online',\n labelFallback: 'Online',\n color: 'text-status-success-text',\n },\n OFFLINE: {\n labelKey: 'channel.presence.offline',\n labelFallback: 'Offline',\n color: 'text-text-secondary',\n },\n AWAY: {\n labelKey: 'channel.presence.away',\n labelFallback: 'Away',\n color: 'text-status-warning-text',\n },\n DND: {\n labelKey: 'channel.presence.dnd',\n labelFallback: 'Do Not Disturb',\n color: 'text-status-error-text',\n },\n }),\n []\n );\n\n const PAIRING_CONFIG: Record<\n PairingStatus,\n { labelKey: string; labelFallback: string; color: string; icon: typeof CheckCircle2 }\n > = useMemo(\n () => ({\n PENDING: {\n labelKey: 'channel.pairingLabel.pending',\n labelFallback: 'Pending',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: Clock,\n },\n APPROVED: {\n labelKey: 'channel.pairingLabel.approved',\n labelFallback: 'Approved',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n EXPIRED: {\n labelKey: 'channel.pairingLabel.expired',\n labelFallback: 'Expired',\n color: 'text-text-secondary bg-bg-sunken',\n icon: XCircle,\n },\n }),\n []\n );\n\n const resultsPerPageOptions = useMemo(\n () => [\n { value: 10, label: tr('channel.resultsPerPage', '{{count}} results', { count: 10 }) },\n { value: 25, label: tr('channel.resultsPerPage', '{{count}} results', { count: 25 }) },\n { value: 50, label: tr('channel.resultsPerPage', '{{count}} results', { count: 50 }) },\n { value: 100, label: tr('channel.resultsPerPage', '{{count}} results', { count: 100 }) },\n ],\n [tr]\n );\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportUsers,\n isExporting: isExportingUsers,\n exportError: exportUsersError,\n } = useExportChannelUsers(organizationId ?? undefined, channelId);\n const [isLoadingUsers, setIsLoadingUsers] = useState(true);\n const [users, setUsers] = useState<UserPlatformConnection[]>([]);\n const [limit, setLimit] = useState(25);\n const [total, setTotal] = useState(0);\n const [currentPage, setCurrentPage] = useState(0);\n\n const { channel, isLoading, error, fetchUserPlatformConnections } = useChannel(channelId);\n\n const loadUsers = useCallback(\n async (page: number = 0) => {\n if (!channelId) return;\n setIsLoadingUsers(true);\n try {\n const result = await fetchUserPlatformConnections(limit, page * limit);\n setUsers(result.items);\n setTotal(result.total);\n } finally {\n setIsLoadingUsers(false);\n }\n },\n [channelId, fetchUserPlatformConnections, limit]\n );\n\n const loadUsersRef = useRef(loadUsers);\n useEffect(() => {\n loadUsersRef.current = loadUsers;\n });\n const isFirstPageRender = useRef(true);\n // Prevents the [currentPage] effect from double-loading when limit changes\n // reset the page back to 0 (the [limit] effect already handles that load).\n const limitChangingRef = useRef(false);\n\n useEffect(() => {\n void Promise.resolve().then(() => {\n // Set the guard before resetting page so the [currentPage] effect skips\n // the redundant load that would otherwise fire when page resets to 0.\n limitChangingRef.current = true;\n setCurrentPage(0);\n void loadUsersRef.current(0).finally(() => {\n limitChangingRef.current = false;\n });\n });\n }, [limit]);\n\n useEffect(() => {\n if (isFirstPageRender.current) {\n isFirstPageRender.current = false;\n return;\n }\n if (limitChangingRef.current) return;\n void Promise.resolve().then(() => loadUsersRef.current(currentPage));\n }, [currentPage]);\n\n const totalPages = Math.max(1, Math.ceil(total / limit));\n const hasPrevPage = currentPage > 0;\n const hasNextPage = currentPage < totalPages - 1;\n\n const getPresenceConfig = (status: UserPresenceStatus) => {\n return PRESENCE_CONFIG[status] || PRESENCE_CONFIG.OFFLINE;\n };\n\n const getPairingConfig = (status: PairingStatus) => {\n return PAIRING_CONFIG[status] || PAIRING_CONFIG.EXPIRED;\n };\n\n const userColumns: ResponsiveTableColumn<UserPlatformConnection>[] = [\n {\n key: 'user',\n header: tr('channel.user', 'User'),\n priority: 'primary',\n cell: (user) => (\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 bg-accent-primary/10 rounded-full flex items-center justify-center\">\n <User size={16} className=\"text-accent-primary\" />\n </div>\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {user.externalDisplayName || tr('channel.unknownUser', 'Unknown')}\n </p>\n {user.externalUsername && (\n <p className=\"text-xs text-text-secondary\">{user.externalUsername}</p>\n )}\n </div>\n </div>\n ),\n },\n {\n key: 'platformId',\n header: tr('channel.platformId', 'Platform ID'),\n priority: 'secondary',\n cellClassName: 'text-sm font-mono text-text-secondary',\n cell: (user) => user.externalUserId,\n },\n {\n key: 'source',\n header: tr('channel.source', 'Source'),\n priority: 'secondary',\n cell: (user) => (\n <span className=\"text-xs text-text-secondary bg-bg-sunken px-2 py-1 rounded\">\n {user.source}\n </span>\n ),\n },\n {\n key: 'status',\n header: tr('channel.status', 'Status'),\n priority: 'primary',\n cell: (user) => {\n const presenceConfig = getPresenceConfig(user.presenceStatus as UserPresenceStatus);\n return (\n <div className=\"flex items-center gap-2\">\n <span\n className={`w-2 h-2 rounded-full ${presenceConfig.color.replace('text-', 'bg-')}`}\n />\n <span className={`text-sm ${presenceConfig.color}`}>\n {tr(presenceConfig.labelKey, presenceConfig.labelFallback)}\n </span>\n </div>\n );\n },\n },\n {\n key: 'pairing',\n header: tr('channel.pairing', 'Pairing'),\n priority: 'secondary',\n cell: (user) => {\n const pairingConfig = user.pairingStatus\n ? getPairingConfig(user.pairingStatus as PairingStatus)\n : null;\n const PairingIcon = pairingConfig?.icon ?? XCircle;\n return user.pairingStatus && pairingConfig ? (\n <span\n className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded ${pairingConfig.color}`}\n >\n <PairingIcon size={12} />\n {tr(pairingConfig.labelKey, pairingConfig.labelFallback)}\n </span>\n ) : (\n <span className=\"text-xs text-text-secondary\">{tr('channel.noPairing', '-')}</span>\n );\n },\n },\n {\n key: 'notifications',\n header: tr('channel.notifications', 'Notifications'),\n priority: 'secondary',\n cell: (user) =>\n user.notificationsEnabled ? (\n <span className=\"inline-flex items-center gap-1\">\n <Bell size={16} className=\"text-status-success-text\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.enabled', 'Enabled')}</span>\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1\">\n <BellOff size={16} className=\"text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.disabled', 'Disabled')}</span>\n </span>\n ),\n },\n {\n key: 'dmAllowed',\n header: tr('channel.dmAllowed', 'DM Allowed'),\n priority: 'secondary',\n cell: (user) =>\n user.dmAllowed ? (\n <span className=\"inline-flex items-center gap-1 text-sm text-status-success-text\">\n <UserCheck size={16} />\n {tr('channel.allowed', 'Allowed')}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <XCircle size={16} />\n {tr('channel.blocked', 'Blocked')}\n </span>\n ),\n },\n ];\n\n // Loading state (only full-screen for initial channel load)\n if (isLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" />\n </div>\n );\n }\n\n // Error state\n if (error || !channel) {\n return (\n <div className=\"p-6\">\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <div>\n <p className=\"text-sm font-medium text-status-error-text\">\n {error || tr('channel.channelNotFound', 'Channel not found')}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"ml-auto px-3 py-1 text-sm text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors\"\n >\n {tr('channel.goBack', 'Go Back')}\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.USER_PLATFORM_CONNECTION_READ}>\n <div className=\"space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n aria-label={tr('channel.backToChannel', 'Back to channel')}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} aria-hidden=\"true\" />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-text-primary\">\n {tr('channel.channelUsers', 'Channel Users')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('channel.usersPageSubtitle', '{{channelName}} - User connections', {\n channelName: channel.name,\n })}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <select\n value={limit}\n onChange={(e) => setLimit(Number(e.target.value))}\n className=\"px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-primary/50 text-text-primary\"\n >\n {resultsPerPageOptions.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <CTAOverflowMenu\n actions={[\n {\n label: tr('channel.refresh', 'Refresh'),\n icon: (\n <RefreshCw\n size={16}\n className={isLoadingUsers ? 'animate-spin' : ''}\n aria-hidden=\"true\"\n />\n ),\n disabled: isLoadingUsers,\n onSelect: () => void loadUsers(currentPage),\n },\n ...(canExport\n ? [\n {\n label: tr('channel.export', 'Export'),\n icon: isExportingUsers ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Download size={16} aria-hidden=\"true\" />\n ),\n disabled: isExportingUsers,\n onSelect: () => void exportUsers(),\n },\n ]\n : []),\n ]}\n />\n </div>\n </div>\n\n {/* Page purpose */}\n <PagePurpose icon={<User size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.usersPurpose',\n 'The people reachable through this channel — linked via OAuth or a pairing code on platforms like Slack, Teams or WhatsApp. See who is online, whether their pairing is approved, and whether they have opted into notifications and direct messages. Use it to verify a recipient is actually connected before you target them, or to audit who can receive messages here.'\n )}\n </PagePurpose>\n\n {/* Export Error Banner */}\n {exportUsersError && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{exportUsersError}</p>\n </div>\n )}\n\n {/* Emphasis — connected-users summary */}\n <EmphasisPanel className=\"flex items-center gap-3 p-4\">\n <span className=\"rounded-lg bg-accent-blue-subtle p-2\">\n <UserCheck size={18} className=\"text-accent-blue\" aria-hidden=\"true\" />\n </span>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {total}{' '}\n {total === 1\n ? tr('channel.connectedUserSingular', 'connected user')\n : tr('channel.connectedUsersPlural', 'connected users')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.usersBannerHint',\n 'Users connected to this channel through OAuth or pairing. Notification preferences and DM authorization are managed per user.'\n )}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Users List */}\n <ResponsiveTable\n columns={userColumns}\n data={users}\n rowKey={(user) => user.id}\n emptyState={\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={tr('channel.noUsersConnected', 'No users connected to this channel')}\n />\n }\n className=\"bg-surface border border-border-seam rounded-lg\"\n />\n\n {/* Pagination footer */}\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {total > 0 && (\n <>\n {tr('channel.showing', 'Showing')}{' '}\n <span className=\"font-medium\">\n {currentPage * limit + 1}–{Math.min((currentPage + 1) * limit, total)}\n </span>{' '}\n {tr('channel.of', 'of')} <span className=\"font-medium\">{total}</span>\n </>\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}\n disabled={!hasPrevPage || isLoadingUsers}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronLeft size={16} />\n {tr('channel.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {currentPage + 1} / {totalPages}\n </span>\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}\n disabled={!hasNextPage || isLoadingUsers}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.next', 'Next')}\n <ChevronRight size={16} />\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConnectionDetailsPage.d.ts","sourceRoot":"","sources":["../../../../../src/channel/modules/channels/pages/ConnectionDetailsPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"ConnectionDetailsPage.d.ts","sourceRoot":"","sources":["../../../../../src/channel/modules/channels/pages/ConnectionDetailsPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAsCH,wBAAgB,qBAAqB,gCAodpC"}
|
|
@@ -1,43 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { AlertCircle as
|
|
12
|
-
import { useNavigate as
|
|
13
|
-
import { usePermissions as
|
|
1
|
+
import { useTr as e } from "../../../hooks/useTr.js";
|
|
2
|
+
import { useChannel as t } from "../../../providers/ChannelProvider.js";
|
|
3
|
+
import { nativeImpact as n, nativeNotify as r } from "../../../../utils/nativeBridge.js";
|
|
4
|
+
import { reportShellError as i } from "../../../utils/shellBridge.js";
|
|
5
|
+
import { getGlobalChannelClient as a } from "../../../graphql/global-client.js";
|
|
6
|
+
import { GetConnectionDocument as ee } from "../../../../generated/global-operations.js";
|
|
7
|
+
import { useChannel as te } from "../../../hooks/useChannels.js";
|
|
8
|
+
import { PermissionGuard as o } from "../../../components/PermissionGuard.js";
|
|
9
|
+
import { CHANNEL_PERMISSIONS as s } from "../../../constants/permissions.js";
|
|
10
|
+
import { useEffect as ne, useMemo as c, useState as l } from "react";
|
|
11
|
+
import { AlertCircle as u, ArrowLeft as d, CheckCircle2 as re, Clock as ie, Edit as f, Loader2 as p, RefreshCw as m, Shield as h, Trash2 as g, XCircle as _ } from "lucide-react";
|
|
12
|
+
import { useNavigate as v, useParams as ae } from "react-router";
|
|
13
|
+
import { usePermissions as oe } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
14
14
|
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
15
|
-
import { CTAOverflowMenu as
|
|
16
|
-
import { TagBadge as
|
|
15
|
+
import { CTAOverflowMenu as se, EmphasisPanel as ce, GlassCard as x, NextSteps as S, PagePurpose as C } from "@burdenoff/fe-libs/ui";
|
|
16
|
+
import { TagBadge as w } from "@burdenoff/fe-libs/tag";
|
|
17
17
|
//#region src/channel/modules/channels/pages/ConnectionDetailsPage.tsx
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
color: "text-status-success-text",
|
|
22
|
-
bgColor: "bg-status-success-bg-subtle",
|
|
23
|
-
label: "Healthy"
|
|
24
|
-
},
|
|
25
|
-
false: {
|
|
26
|
-
icon: g,
|
|
27
|
-
color: "text-status-error-text",
|
|
28
|
-
bgColor: "bg-error-500/10",
|
|
29
|
-
label: "Unhealthy"
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
function D() {
|
|
33
|
-
let l = _(), { hasPermission: g } = ae(), D = g(a.CHANNEL_UPDATE), O = g(a.CHANNEL_EXECUTE), k = v(), { connectionId: A } = k, { authToken: j, apiGatewayUrl: M, organizationId: N } = e(), [P, F] = o(k.channelId), [I, L] = o(null);
|
|
34
|
-
re(() => {
|
|
18
|
+
function T() {
|
|
19
|
+
let T = v(), { hasPermission: E } = oe(), D = E(s.CHANNEL_UPDATE), O = E(s.CHANNEL_EXECUTE), k = ae(), { connectionId: A } = k, { authToken: j, apiGatewayUrl: M, organizationId: N } = t(), [P, F] = l(k.channelId), [I, L] = l(null);
|
|
20
|
+
ne(() => {
|
|
35
21
|
if (P || !A) return;
|
|
36
22
|
let e = !1;
|
|
37
23
|
return (async () => {
|
|
38
24
|
try {
|
|
39
|
-
let t = await
|
|
40
|
-
query:
|
|
25
|
+
let t = await a(M, j, N).query({
|
|
26
|
+
query: ee,
|
|
41
27
|
variables: { id: A },
|
|
42
28
|
fetchPolicy: "network-only"
|
|
43
29
|
});
|
|
@@ -57,23 +43,35 @@ function D() {
|
|
|
57
43
|
j,
|
|
58
44
|
N
|
|
59
45
|
]);
|
|
60
|
-
let R = P, { channel: z, isLoading: B, error: V, runHealthCheck: H, deleteConnection: U } =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
let R = P, { channel: z, isLoading: B, error: V, runHealthCheck: H, deleteConnection: U } = te(R), W = e(), G = c(() => ({
|
|
47
|
+
true: {
|
|
48
|
+
icon: re,
|
|
49
|
+
color: "text-status-success-text",
|
|
50
|
+
bgColor: "bg-status-success-bg-subtle",
|
|
51
|
+
labelKey: "channel.healthLabel.healthy",
|
|
52
|
+
labelFallback: "Healthy"
|
|
53
|
+
},
|
|
54
|
+
false: {
|
|
55
|
+
icon: _,
|
|
56
|
+
color: "text-status-error-text",
|
|
57
|
+
bgColor: "bg-error-500/10",
|
|
58
|
+
labelKey: "channel.healthLabel.unhealthy",
|
|
59
|
+
labelFallback: "Unhealthy"
|
|
60
|
+
}
|
|
61
|
+
}), []), [K, q] = l(!1), [J, Y] = l(!1), [le, X] = l(!1), Z = z?.connection, Q = async () => {
|
|
64
62
|
Y(!0);
|
|
65
63
|
try {
|
|
66
64
|
await H();
|
|
67
65
|
} finally {
|
|
68
66
|
Y(!1);
|
|
69
67
|
}
|
|
70
|
-
},
|
|
68
|
+
}, ue = async () => {
|
|
71
69
|
if (A) {
|
|
72
|
-
|
|
70
|
+
n("medium"), q(!0);
|
|
73
71
|
try {
|
|
74
|
-
await U(A),
|
|
72
|
+
await U(A), r("success"), T(`/channels/${R}`);
|
|
75
73
|
} catch (e) {
|
|
76
|
-
|
|
74
|
+
r("error"), i(e, {
|
|
77
75
|
module: "channel",
|
|
78
76
|
context: { action: "Failed to delete connection" }
|
|
79
77
|
}, "[channel] Failed to delete connection");
|
|
@@ -84,47 +82,47 @@ function D() {
|
|
|
84
82
|
};
|
|
85
83
|
if (B || !P && !I) return /* @__PURE__ */ y("div", {
|
|
86
84
|
className: "flex items-center justify-center min-h-screen",
|
|
87
|
-
children: /* @__PURE__ */ y(
|
|
85
|
+
children: /* @__PURE__ */ y(p, { className: "size-8 animate-spin text-text-secondary" })
|
|
88
86
|
});
|
|
89
87
|
if (I || V || !z) return /* @__PURE__ */ b("div", {
|
|
90
88
|
className: "flex flex-col items-center justify-center min-h-screen gap-4",
|
|
91
89
|
children: [
|
|
92
|
-
/* @__PURE__ */ y(
|
|
90
|
+
/* @__PURE__ */ y(u, { className: "size-12 text-status-error-text" }),
|
|
93
91
|
/* @__PURE__ */ y("h1", {
|
|
94
92
|
className: "text-xl font-semibold",
|
|
95
|
-
children:
|
|
93
|
+
children: W("channel.channelNotFound", "Channel not found")
|
|
96
94
|
}),
|
|
97
95
|
/* @__PURE__ */ y("button", {
|
|
98
96
|
type: "button",
|
|
99
|
-
onClick: () =>
|
|
97
|
+
onClick: () => T("../../list"),
|
|
100
98
|
className: "text-primary hover:underline",
|
|
101
|
-
children:
|
|
99
|
+
children: W("channel.backToChannels", "Back to channels")
|
|
102
100
|
})
|
|
103
101
|
]
|
|
104
102
|
});
|
|
105
103
|
if (!Z) return /* @__PURE__ */ b("div", {
|
|
106
104
|
className: "flex flex-col items-center justify-center min-h-screen gap-4",
|
|
107
105
|
children: [
|
|
108
|
-
/* @__PURE__ */ y(
|
|
106
|
+
/* @__PURE__ */ y(u, { className: "size-12 text-text-secondary" }),
|
|
109
107
|
/* @__PURE__ */ y("h1", {
|
|
110
108
|
className: "text-xl font-semibold",
|
|
111
|
-
children:
|
|
109
|
+
children: W("channel.noConnectionFound", "No connection found")
|
|
112
110
|
}),
|
|
113
111
|
/* @__PURE__ */ y("p", {
|
|
114
112
|
className: "text-sm text-text-secondary",
|
|
115
|
-
children:
|
|
113
|
+
children: W("channel.noConnectionConfigured", "This channel does not have a connection configured.")
|
|
116
114
|
}),
|
|
117
115
|
/* @__PURE__ */ y("button", {
|
|
118
116
|
type: "button",
|
|
119
|
-
onClick: () =>
|
|
117
|
+
onClick: () => T(`/channels/${R}`),
|
|
120
118
|
className: "text-primary hover:underline",
|
|
121
|
-
children:
|
|
119
|
+
children: W("channel.backToChannel", "Back to channel")
|
|
122
120
|
})
|
|
123
121
|
]
|
|
124
122
|
});
|
|
125
|
-
let $ =
|
|
126
|
-
return /* @__PURE__ */ y(
|
|
127
|
-
permission:
|
|
123
|
+
let $ = G[String(Z.isHealthy)] || G.false, de = $.icon;
|
|
124
|
+
return /* @__PURE__ */ y(o, {
|
|
125
|
+
permission: s.CHANNEL_CONNECTION_READ,
|
|
128
126
|
children: /* @__PURE__ */ b("div", {
|
|
129
127
|
className: "min-h-screen bg-bg-surface",
|
|
130
128
|
children: [/* @__PURE__ */ y("div", {
|
|
@@ -137,12 +135,12 @@ function D() {
|
|
|
137
135
|
className: "flex items-center gap-4",
|
|
138
136
|
children: [/* @__PURE__ */ y("button", {
|
|
139
137
|
type: "button",
|
|
140
|
-
onClick: () =>
|
|
138
|
+
onClick: () => T(`/channels/${R}`),
|
|
141
139
|
className: "p-2 hover:bg-bg-sunken rounded-lg transition-colors",
|
|
142
|
-
children: /* @__PURE__ */ y(
|
|
140
|
+
children: /* @__PURE__ */ y(d, { className: "size-5 " })
|
|
143
141
|
}), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h1", {
|
|
144
142
|
className: "text-2xl font-bold",
|
|
145
|
-
children:
|
|
143
|
+
children: W("channel.connectionDetails", "Connection Details")
|
|
146
144
|
}), /* @__PURE__ */ b("p", {
|
|
147
145
|
className: "text-sm text-text-secondary",
|
|
148
146
|
children: [
|
|
@@ -151,21 +149,21 @@ function D() {
|
|
|
151
149
|
z.platform
|
|
152
150
|
]
|
|
153
151
|
})] })]
|
|
154
|
-
}), /* @__PURE__ */ y(
|
|
152
|
+
}), /* @__PURE__ */ y(se, {
|
|
155
153
|
primary: D ? {
|
|
156
|
-
label:
|
|
157
|
-
icon: /* @__PURE__ */ y(
|
|
154
|
+
label: W("channel.editChannel", "Edit Channel"),
|
|
155
|
+
icon: /* @__PURE__ */ y(f, {
|
|
158
156
|
className: "size-4",
|
|
159
157
|
"aria-hidden": "true"
|
|
160
158
|
}),
|
|
161
|
-
onSelect: () =>
|
|
159
|
+
onSelect: () => T(`/channels/${R}/edit`)
|
|
162
160
|
} : void 0,
|
|
163
161
|
actions: O ? [{
|
|
164
|
-
label:
|
|
165
|
-
icon: J ? /* @__PURE__ */ y(
|
|
162
|
+
label: W("channel.runHealthCheck", "Run Health Check"),
|
|
163
|
+
icon: J ? /* @__PURE__ */ y(p, {
|
|
166
164
|
className: "size-4 animate-spin",
|
|
167
165
|
"aria-hidden": "true"
|
|
168
|
-
}) : /* @__PURE__ */ y(
|
|
166
|
+
}) : /* @__PURE__ */ y(m, {
|
|
169
167
|
className: "size-4",
|
|
170
168
|
"aria-hidden": "true"
|
|
171
169
|
}),
|
|
@@ -178,21 +176,21 @@ function D() {
|
|
|
178
176
|
}), /* @__PURE__ */ b("div", {
|
|
179
177
|
className: "container mx-auto px-6 py-8 max-w-5xl space-y-6",
|
|
180
178
|
children: [
|
|
181
|
-
/* @__PURE__ */ y(
|
|
182
|
-
icon: /* @__PURE__ */ y(
|
|
179
|
+
/* @__PURE__ */ y(C, {
|
|
180
|
+
icon: /* @__PURE__ */ y(h, {
|
|
183
181
|
size: 15,
|
|
184
182
|
"aria-hidden": "true"
|
|
185
183
|
}),
|
|
186
|
-
children:
|
|
184
|
+
children: W("channel.connectionDetailsPurpose", "The authenticated link between this channel and its provider — its auth type, whether it is active, when it was last tested, and how soon its token expires. Use it to verify the channel can actually send, to spot a token that needs refreshing before it lapses, or to remove a connection you no longer need.")
|
|
187
185
|
}),
|
|
188
|
-
/* @__PURE__ */ y(
|
|
186
|
+
/* @__PURE__ */ y(S, {
|
|
189
187
|
storageKey: "channel-connection-detail",
|
|
190
188
|
steps: [
|
|
191
189
|
...O ? [{
|
|
192
190
|
id: "health",
|
|
193
|
-
label:
|
|
194
|
-
description:
|
|
195
|
-
icon: /* @__PURE__ */ y(
|
|
191
|
+
label: W("channel.nextRunHealthCheck", "Run a health check"),
|
|
192
|
+
description: W("channel.nextConnectionHealthDesc", "Confirm the credentials still authenticate against the provider."),
|
|
193
|
+
icon: /* @__PURE__ */ y(m, {
|
|
196
194
|
size: 16,
|
|
197
195
|
"aria-hidden": "true"
|
|
198
196
|
}),
|
|
@@ -200,27 +198,27 @@ function D() {
|
|
|
200
198
|
}] : [],
|
|
201
199
|
...D ? [{
|
|
202
200
|
id: "edit",
|
|
203
|
-
label:
|
|
204
|
-
description:
|
|
205
|
-
icon: /* @__PURE__ */ y(
|
|
201
|
+
label: W("channel.nextEditChannel", "Edit the channel"),
|
|
202
|
+
description: W("channel.nextEditChannelDesc", "Update credentials, status or platform metadata for this channel."),
|
|
203
|
+
icon: /* @__PURE__ */ y(f, {
|
|
206
204
|
size: 16,
|
|
207
205
|
"aria-hidden": "true"
|
|
208
206
|
}),
|
|
209
|
-
onClick: () =>
|
|
207
|
+
onClick: () => T(`/channels/${R}/edit`)
|
|
210
208
|
}] : [],
|
|
211
209
|
{
|
|
212
210
|
id: "back",
|
|
213
|
-
label:
|
|
214
|
-
description:
|
|
215
|
-
icon: /* @__PURE__ */ y(
|
|
211
|
+
label: W("channel.nextBackToChannel", "Back to the channel"),
|
|
212
|
+
description: W("channel.nextBackToChannelDesc", "Return to the channel overview to send a test or review deliveries."),
|
|
213
|
+
icon: /* @__PURE__ */ y(d, {
|
|
216
214
|
size: 16,
|
|
217
215
|
"aria-hidden": "true"
|
|
218
216
|
}),
|
|
219
|
-
onClick: () =>
|
|
217
|
+
onClick: () => T(`/channels/${R}`)
|
|
220
218
|
}
|
|
221
219
|
]
|
|
222
220
|
}),
|
|
223
|
-
/* @__PURE__ */ y(
|
|
221
|
+
/* @__PURE__ */ y(ce, {
|
|
224
222
|
className: "p-6",
|
|
225
223
|
children: /* @__PURE__ */ y("div", {
|
|
226
224
|
className: "flex items-center justify-between",
|
|
@@ -228,71 +226,71 @@ function D() {
|
|
|
228
226
|
className: "flex items-center gap-4",
|
|
229
227
|
children: [/* @__PURE__ */ y("div", {
|
|
230
228
|
className: $.color,
|
|
231
|
-
children: /* @__PURE__ */ y(
|
|
229
|
+
children: /* @__PURE__ */ y(de, { className: "size-10 " })
|
|
232
230
|
}), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h2", {
|
|
233
231
|
className: "text-xl font-semibold",
|
|
234
|
-
children: $.
|
|
232
|
+
children: W($.labelKey, $.labelFallback)
|
|
235
233
|
}), /* @__PURE__ */ y("p", {
|
|
236
234
|
className: "text-sm text-text-secondary",
|
|
237
|
-
children: Z.isHealthy ?
|
|
235
|
+
children: Z.isHealthy ? W("channel.connectionWorking", "Connection is working properly") : W("channel.connectionNotWorking", "Connection is not working properly")
|
|
238
236
|
})] })]
|
|
239
237
|
})
|
|
240
238
|
})
|
|
241
239
|
}),
|
|
242
240
|
/* @__PURE__ */ b("div", {
|
|
243
241
|
className: "grid gap-6 lg:grid-cols-3",
|
|
244
|
-
children: [/* @__PURE__ */ b(
|
|
242
|
+
children: [/* @__PURE__ */ b(x, {
|
|
245
243
|
treatment: "plain",
|
|
246
244
|
className: "border-border-seam p-6 space-y-4 lg:col-span-2",
|
|
247
245
|
children: [
|
|
248
246
|
/* @__PURE__ */ y("h3", {
|
|
249
247
|
className: "font-semibold",
|
|
250
|
-
children:
|
|
248
|
+
children: W("channel.connectionInformation", "Connection Information")
|
|
251
249
|
}),
|
|
252
250
|
/* @__PURE__ */ b("div", {
|
|
253
251
|
className: "grid grid-cols-2 gap-4",
|
|
254
252
|
children: [
|
|
255
253
|
/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
|
|
256
254
|
className: "text-sm text-text-secondary",
|
|
257
|
-
children: "Connection ID"
|
|
255
|
+
children: W("connection.connectionId", "Connection ID")
|
|
258
256
|
}), /* @__PURE__ */ y("p", {
|
|
259
257
|
className: "font-mono text-sm",
|
|
260
258
|
children: Z.id
|
|
261
259
|
})] }),
|
|
262
260
|
/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
|
|
263
261
|
className: "text-sm text-text-secondary",
|
|
264
|
-
children: "Auth Type"
|
|
262
|
+
children: W("channel.authType", "Auth Type")
|
|
265
263
|
}), /* @__PURE__ */ b("p", {
|
|
266
264
|
className: "flex items-center gap-2",
|
|
267
|
-
children: [/* @__PURE__ */ y(
|
|
265
|
+
children: [/* @__PURE__ */ y(h, { className: "size-4 text-text-secondary" }), Z.authType]
|
|
268
266
|
})] }),
|
|
269
267
|
/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
|
|
270
268
|
className: "text-sm text-text-secondary",
|
|
271
|
-
children: "Status"
|
|
269
|
+
children: W("channel.status", "Status")
|
|
272
270
|
}), /* @__PURE__ */ y("p", {
|
|
273
271
|
className: Z.isActive ? "text-status-success-text" : "text-text-secondary",
|
|
274
|
-
children: Z.isActive ?
|
|
272
|
+
children: Z.isActive ? W("channel.activeStatus", "Active") : W("channel.inactiveStatus", "Inactive")
|
|
275
273
|
})] }),
|
|
276
274
|
/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
|
|
277
275
|
className: "text-sm text-text-secondary",
|
|
278
|
-
children: "Token Refresh"
|
|
276
|
+
children: W("connection.tokenRefresh", "Token Refresh")
|
|
279
277
|
}), /* @__PURE__ */ y("p", {
|
|
280
278
|
className: Z.needsTokenRefresh ? "text-status-warning-text" : "text-text-secondary",
|
|
281
|
-
children: Z.needsTokenRefresh ?
|
|
279
|
+
children: Z.needsTokenRefresh ? W("channel.tokenRefreshRequired", "Required soon") : W("channel.tokenRefreshNotNeeded", "Not needed")
|
|
282
280
|
})] }),
|
|
283
281
|
Z.lastTestedAt && /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
|
|
284
282
|
className: "text-sm text-text-secondary",
|
|
285
|
-
children: "Last Tested"
|
|
283
|
+
children: W("channel.lastTested", "Last Tested")
|
|
286
284
|
}), /* @__PURE__ */ b("p", {
|
|
287
285
|
className: "flex items-center gap-2",
|
|
288
|
-
children: [/* @__PURE__ */ y(
|
|
286
|
+
children: [/* @__PURE__ */ y(ie, { className: "size-4 text-text-secondary" }), new Date(Z.lastTestedAt).toLocaleString()]
|
|
289
287
|
})] }),
|
|
290
288
|
Z.daysUntilExpiry !== null && Z.daysUntilExpiry !== void 0 && /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
|
|
291
289
|
className: "text-sm text-text-secondary",
|
|
292
|
-
children: "Days Until Token Expiry"
|
|
293
|
-
}), /* @__PURE__ */
|
|
290
|
+
children: W("connection.daysUntilTokenExpiry", "Days Until Token Expiry")
|
|
291
|
+
}), /* @__PURE__ */ y("p", {
|
|
294
292
|
className: Z.daysUntilExpiry < 30 ? "text-status-warning-text" : "text-text-secondary",
|
|
295
|
-
children:
|
|
293
|
+
children: W("channel.duration.days", "{{count}} days", { count: Z.daysUntilExpiry })
|
|
296
294
|
})] })
|
|
297
295
|
]
|
|
298
296
|
}),
|
|
@@ -300,7 +298,7 @@ function D() {
|
|
|
300
298
|
className: "pt-4 border-t",
|
|
301
299
|
children: [/* @__PURE__ */ y("p", {
|
|
302
300
|
className: "text-sm text-text-secondary mb-2",
|
|
303
|
-
children: "Last Test Status"
|
|
301
|
+
children: W("connection.lastTestStatus", "Last Test Status")
|
|
304
302
|
}), /* @__PURE__ */ y("div", {
|
|
305
303
|
className: "p-3 rounded-lg bg-bg-sunken",
|
|
306
304
|
children: /* @__PURE__ */ y("p", {
|
|
@@ -313,11 +311,11 @@ function D() {
|
|
|
313
311
|
className: "pt-4 border-t",
|
|
314
312
|
children: [/* @__PURE__ */ y("p", {
|
|
315
313
|
className: "text-sm text-text-secondary mb-2",
|
|
316
|
-
children:
|
|
314
|
+
children: W("channel.tags", "Tags")
|
|
317
315
|
}), /* @__PURE__ */ y("div", {
|
|
318
316
|
className: "flex flex-wrap gap-1.5",
|
|
319
317
|
"aria-label": "Connection tags",
|
|
320
|
-
children: Z.tagAssignments.map((e) => /* @__PURE__ */ y(
|
|
318
|
+
children: Z.tagAssignments.map((e) => /* @__PURE__ */ y(w, {
|
|
321
319
|
id: e.tagId,
|
|
322
320
|
label: e.tagId,
|
|
323
321
|
size: "md"
|
|
@@ -325,48 +323,48 @@ function D() {
|
|
|
325
323
|
})]
|
|
326
324
|
})
|
|
327
325
|
]
|
|
328
|
-
}), /* @__PURE__ */ b(
|
|
326
|
+
}), /* @__PURE__ */ b(x, {
|
|
329
327
|
treatment: "plain",
|
|
330
328
|
className: "border-border-seam p-6 space-y-4 lg:col-span-1",
|
|
331
329
|
children: [/* @__PURE__ */ y("h3", {
|
|
332
330
|
className: "font-semibold",
|
|
333
|
-
children:
|
|
331
|
+
children: W("channel.actions", "Actions")
|
|
334
332
|
}), /* @__PURE__ */ b("div", {
|
|
335
333
|
className: "flex flex-col gap-3",
|
|
336
334
|
children: [/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
|
|
337
335
|
className: "font-medium",
|
|
338
|
-
children:
|
|
336
|
+
children: W("channel.deleteConnection", "Delete Connection")
|
|
339
337
|
}), /* @__PURE__ */ y("p", {
|
|
340
338
|
className: "text-sm text-text-secondary",
|
|
341
|
-
children:
|
|
342
|
-
})] }),
|
|
339
|
+
children: W("channel.deleteConnectionWarning", "Permanently remove this connection. The channel will become inactive.")
|
|
340
|
+
})] }), le ? /* @__PURE__ */ b("div", {
|
|
343
341
|
className: "flex items-center gap-2",
|
|
344
342
|
children: [
|
|
345
343
|
/* @__PURE__ */ y("span", {
|
|
346
344
|
className: "text-sm text-text-secondary",
|
|
347
|
-
children:
|
|
345
|
+
children: W("channel.confirmDelete", "Confirm delete?")
|
|
348
346
|
}),
|
|
349
347
|
/* @__PURE__ */ b("button", {
|
|
350
348
|
type: "button",
|
|
351
|
-
onClick:
|
|
349
|
+
onClick: ue,
|
|
352
350
|
disabled: K,
|
|
353
351
|
className: "px-4 py-2 rounded-lg bg-status-error-bg text-status-error-text hover:bg-status-error-bg/90 transition-colors flex items-center gap-2",
|
|
354
|
-
children: [K ? /* @__PURE__ */ y(
|
|
352
|
+
children: [K ? /* @__PURE__ */ y(p, { className: "size-4 animate-spin" }) : /* @__PURE__ */ y(g, { className: "size-4 " }), W("channel.delete", "Delete")]
|
|
355
353
|
}),
|
|
356
354
|
/* @__PURE__ */ y("button", {
|
|
357
355
|
type: "button",
|
|
358
356
|
onClick: () => X(!1),
|
|
359
357
|
className: "px-4 py-2 rounded-lg border hover:bg-bg-sunken transition-colors",
|
|
360
|
-
children:
|
|
358
|
+
children: W("channel.cancel", "Cancel")
|
|
361
359
|
})
|
|
362
360
|
]
|
|
363
|
-
}) : /* @__PURE__ */ y(
|
|
364
|
-
permission:
|
|
361
|
+
}) : /* @__PURE__ */ y(o, {
|
|
362
|
+
permission: s.CHANNEL_CONNECTION_DELETE,
|
|
365
363
|
children: /* @__PURE__ */ b("button", {
|
|
366
364
|
type: "button",
|
|
367
365
|
onClick: () => X(!0),
|
|
368
366
|
className: "px-4 py-2 rounded-lg text-status-error-text border border-status-error-border hover:bg-status-error-bg/10 transition-colors flex items-center gap-2",
|
|
369
|
-
children: [/* @__PURE__ */ y(
|
|
367
|
+
children: [/* @__PURE__ */ y(g, { className: "size-4 " }), W("channel.delete", "Delete")]
|
|
370
368
|
})
|
|
371
369
|
})]
|
|
372
370
|
})]
|
|
@@ -378,6 +376,6 @@ function D() {
|
|
|
378
376
|
});
|
|
379
377
|
}
|
|
380
378
|
//#endregion
|
|
381
|
-
export {
|
|
379
|
+
export { T as ConnectionDetailsPage };
|
|
382
380
|
|
|
383
381
|
//# sourceMappingURL=ConnectionDetailsPage.js.map
|