661 lines
28 KiB
TypeScript
661 lines
28 KiB
TypeScript
/**
|
|
* OAuth Token Status Component
|
|
* Compact terminal-themed component for displaying OAuth token monitoring status
|
|
* with platform-specific execution logs in expanded sections
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
IconButton,
|
|
Tooltip,
|
|
CircularProgress,
|
|
Collapse,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableRow,
|
|
Chip,
|
|
Divider,
|
|
} from '@mui/material';
|
|
import {
|
|
RefreshCw,
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertTriangle,
|
|
Info,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
} from 'lucide-react';
|
|
import { useAuth } from '@clerk/clerk-react';
|
|
import {
|
|
getOAuthTokenStatus,
|
|
manualRefreshToken,
|
|
getOAuthTokenExecutionLogs,
|
|
OAuthTokenStatusResponse,
|
|
ManualRefreshResponse,
|
|
ExecutionLog,
|
|
ExecutionLogsResponse,
|
|
} from '../../api/oauthTokenMonitoring';
|
|
import {
|
|
TerminalPaper,
|
|
TerminalTypography,
|
|
TerminalChip,
|
|
TerminalChipSuccess,
|
|
TerminalChipError,
|
|
TerminalChipWarning,
|
|
TerminalAlert,
|
|
TerminalTableCell,
|
|
TerminalTableRow,
|
|
terminalColors,
|
|
} from './terminalTheme';
|
|
|
|
interface OAuthTokenStatusProps {
|
|
compact?: boolean;
|
|
}
|
|
|
|
interface PlatformLogs {
|
|
[platform: string]: {
|
|
logs: ExecutionLog[];
|
|
loading: boolean;
|
|
error: string | null;
|
|
};
|
|
}
|
|
|
|
const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) => {
|
|
const { userId } = useAuth();
|
|
const [status, setStatus] = useState<OAuthTokenStatusResponse | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [expandedPlatform, setExpandedPlatform] = useState<string | null>(null);
|
|
const [platformLogs, setPlatformLogs] = useState<PlatformLogs>({});
|
|
const [hoveredLogId, setHoveredLogId] = useState<number | null>(null);
|
|
|
|
const fetchStatus = async () => {
|
|
if (!userId) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
const response = await getOAuthTokenStatus(userId);
|
|
setStatus(response);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to fetch token status');
|
|
console.error('Error fetching OAuth token status:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchPlatformLogs = async (platform: string) => {
|
|
if (!userId) return;
|
|
|
|
// Initialize platform logs state if not exists
|
|
if (!platformLogs[platform]) {
|
|
setPlatformLogs(prev => ({
|
|
...prev,
|
|
[platform]: { logs: [], loading: false, error: null }
|
|
}));
|
|
}
|
|
|
|
setPlatformLogs(prev => ({
|
|
...prev,
|
|
[platform]: { ...prev[platform], loading: true, error: null }
|
|
}));
|
|
|
|
try {
|
|
const response = await getOAuthTokenExecutionLogs(userId, platform, 10, 0); // Get latest 10 logs
|
|
|
|
if (response.success && response.data) {
|
|
setPlatformLogs(prev => ({
|
|
...prev,
|
|
[platform]: {
|
|
logs: response.data.logs || [],
|
|
loading: false,
|
|
error: null
|
|
}
|
|
}));
|
|
}
|
|
} catch (err: any) {
|
|
setPlatformLogs(prev => ({
|
|
...prev,
|
|
[platform]: {
|
|
...prev[platform],
|
|
loading: false,
|
|
error: err.message || 'Failed to fetch logs'
|
|
}
|
|
}));
|
|
console.error(`Error fetching logs for ${platform}:`, err);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchStatus();
|
|
|
|
// Poll for status updates every 2 minutes
|
|
const interval = setInterval(fetchStatus, 120000);
|
|
return () => clearInterval(interval);
|
|
}, [userId]);
|
|
|
|
// Fetch logs when platform is expanded
|
|
useEffect(() => {
|
|
if (expandedPlatform && userId) {
|
|
fetchPlatformLogs(expandedPlatform);
|
|
}
|
|
}, [expandedPlatform, userId]);
|
|
|
|
const handleRefresh = async (platform: string) => {
|
|
if (!userId) return;
|
|
|
|
try {
|
|
setRefreshing(platform);
|
|
setError(null);
|
|
const response: ManualRefreshResponse = await manualRefreshToken(userId, platform);
|
|
|
|
// Refresh status after manual refresh
|
|
await fetchStatus();
|
|
|
|
// Refresh logs if platform is expanded
|
|
if (expandedPlatform === platform) {
|
|
await fetchPlatformLogs(platform);
|
|
}
|
|
|
|
if (response.success) {
|
|
console.log(`Token refresh successful for ${platform}`);
|
|
} else {
|
|
console.error(`Token refresh failed for ${platform}:`, response.data.execution_result.error_message);
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.message || `Failed to refresh ${platform} token`);
|
|
console.error(`Error refreshing ${platform} token:`, err);
|
|
} finally {
|
|
setRefreshing(null);
|
|
}
|
|
};
|
|
|
|
const handleExpandPlatform = (platform: string) => {
|
|
if (expandedPlatform === platform) {
|
|
setExpandedPlatform(null);
|
|
} else {
|
|
setExpandedPlatform(platform);
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (taskStatus: string | null, connected: boolean) => {
|
|
if (!connected) {
|
|
return <XCircle size={16} color={terminalColors.error} />;
|
|
}
|
|
|
|
if (!taskStatus || taskStatus === 'not_created') {
|
|
return <Info size={16} color={terminalColors.info} />;
|
|
}
|
|
|
|
switch (taskStatus) {
|
|
case 'active':
|
|
return <CheckCircle size={16} color={terminalColors.success} />;
|
|
case 'failed':
|
|
return <XCircle size={16} color={terminalColors.error} />;
|
|
case 'paused':
|
|
return <AlertTriangle size={16} color={terminalColors.warning} />;
|
|
default:
|
|
return <Info size={16} color={terminalColors.primary} />;
|
|
}
|
|
};
|
|
|
|
const getStatusChip = (taskStatus: string | null, connected: boolean) => {
|
|
if (!connected) {
|
|
return <TerminalChipError label="Not Connected" size="small" />;
|
|
}
|
|
|
|
if (!taskStatus || taskStatus === 'not_created') {
|
|
return <TerminalChip label={taskStatus || 'Not Created'} size="small" />;
|
|
}
|
|
|
|
switch (taskStatus) {
|
|
case 'active':
|
|
return <TerminalChipSuccess label="Active" size="small" />;
|
|
case 'failed':
|
|
return <TerminalChipError label="Failed" size="small" />;
|
|
case 'paused':
|
|
return <TerminalChipWarning label="Paused" size="small" />;
|
|
default:
|
|
return <TerminalChip label={taskStatus} size="small" />;
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string | null) => {
|
|
if (!dateString) return 'Never';
|
|
try {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString();
|
|
} catch {
|
|
return dateString;
|
|
}
|
|
};
|
|
|
|
const getPlatformDisplayName = (platform: string) => {
|
|
const names: { [key: string]: string } = {
|
|
gsc: 'GSC',
|
|
bing: 'Bing',
|
|
wordpress: 'WP',
|
|
wix: 'Wix',
|
|
};
|
|
return names[platform] || platform.toUpperCase();
|
|
};
|
|
|
|
const getLogStatusChip = (logStatus: string) => {
|
|
switch (logStatus) {
|
|
case 'success':
|
|
return <TerminalChipSuccess label="Success" size="small" />;
|
|
case 'failed':
|
|
return <TerminalChipError label="Failed" size="small" />;
|
|
case 'running':
|
|
return <TerminalChipWarning label="Running" size="small" />;
|
|
default:
|
|
return <Chip label={logStatus} size="small" />;
|
|
}
|
|
};
|
|
|
|
const formatLogResult = (resultData: any): string => {
|
|
if (!resultData) return 'N/A';
|
|
if (typeof resultData === 'string') {
|
|
try {
|
|
resultData = JSON.parse(resultData);
|
|
} catch {
|
|
return resultData.substring(0, 50);
|
|
}
|
|
}
|
|
|
|
if (resultData.token_status) {
|
|
return `Token: ${resultData.token_status}`;
|
|
}
|
|
if (resultData.platform) {
|
|
return `Platform: ${resultData.platform}`;
|
|
}
|
|
const str = JSON.stringify(resultData);
|
|
return str.length > 60 ? str.substring(0, 60) + '...' : str;
|
|
};
|
|
|
|
if (loading && !status) {
|
|
return (
|
|
<TerminalPaper sx={{ p: 2 }}>
|
|
<Box display="flex" justifyContent="center" alignItems="center" p={2}>
|
|
<CircularProgress size={20} sx={{ color: terminalColors.primary }} />
|
|
</Box>
|
|
</TerminalPaper>
|
|
);
|
|
}
|
|
|
|
if (!status) {
|
|
return null;
|
|
}
|
|
|
|
const platforms = ['gsc', 'bing', 'wordpress', 'wix'];
|
|
|
|
return (
|
|
<TerminalPaper sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
|
<TerminalTypography variant="h6" component="h3">
|
|
OAuth Token Status
|
|
</TerminalTypography>
|
|
<Tooltip title="Refresh status">
|
|
<IconButton
|
|
size="small"
|
|
onClick={fetchStatus}
|
|
disabled={loading}
|
|
sx={{
|
|
color: terminalColors.primary,
|
|
border: `1px solid ${terminalColors.primary}`,
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
|
},
|
|
'&:disabled': {
|
|
color: '#004400',
|
|
borderColor: '#004400',
|
|
}
|
|
}}
|
|
>
|
|
<RefreshCw size={16} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
|
|
{error && (
|
|
<TerminalAlert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
|
|
{error}
|
|
</TerminalAlert>
|
|
)}
|
|
|
|
<Box sx={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
|
|
<Table size="small" sx={{ '& .MuiTableCell-root': { color: terminalColors.primary, borderColor: terminalColors.primary + '40' } }}>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Platform</TableCell>
|
|
<TableCell>Status</TableCell>
|
|
<TableCell>Last Check</TableCell>
|
|
<TableCell align="right">Actions</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{platforms.map((platform) => {
|
|
const platformStatus = status.data.platform_status[platform];
|
|
const task = platformStatus?.monitoring_task;
|
|
const isExpanded = expandedPlatform === platform;
|
|
const logs = platformLogs[platform];
|
|
|
|
return (
|
|
<React.Fragment key={platform}>
|
|
<TableRow
|
|
sx={{
|
|
cursor: 'pointer',
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(0, 255, 0, 0.05)',
|
|
}
|
|
}}
|
|
>
|
|
<TableCell>
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
{getStatusIcon(task?.status || null, platformStatus?.connected || false)}
|
|
<TerminalTypography variant="body2" fontWeight="medium">
|
|
{getPlatformDisplayName(platform)}
|
|
</TerminalTypography>
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Box display="flex" alignItems="center" gap={1} flexWrap="wrap">
|
|
{getStatusChip(task?.status || null, platformStatus?.connected || false)}
|
|
{task?.last_success && (
|
|
<Tooltip title={`Last successful: ${formatDate(task.last_success)}`}>
|
|
<Chip
|
|
label={`✓ ${formatDate(task.last_success).split(',')[0].trim()}`}
|
|
size="small"
|
|
sx={{
|
|
backgroundColor: terminalColors.success + '40',
|
|
color: terminalColors.success,
|
|
fontFamily: 'monospace',
|
|
fontSize: '0.65rem',
|
|
height: '20px',
|
|
border: `1px solid ${terminalColors.success}40`,
|
|
'& .MuiChip-label': {
|
|
padding: '0 6px'
|
|
}
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
{task?.next_check && (
|
|
<Tooltip title={`Next check: ${formatDate(task.next_check)}`}>
|
|
<Chip
|
|
label={`⏱ ${formatDate(task.next_check).split(',')[0].trim()}`}
|
|
size="small"
|
|
sx={{
|
|
backgroundColor: terminalColors.info + '40',
|
|
color: terminalColors.info,
|
|
fontFamily: 'monospace',
|
|
fontSize: '0.65rem',
|
|
height: '20px',
|
|
border: `1px solid ${terminalColors.info}40`,
|
|
'& .MuiChip-label': {
|
|
padding: '0 6px'
|
|
}
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
|
|
{formatDate(task?.last_check || null)}
|
|
</TerminalTypography>
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
<Box display="flex" gap={0.5} justifyContent="flex-end">
|
|
<Tooltip title={isExpanded ? "Hide details" : "Show details"}>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => handleExpandPlatform(platform)}
|
|
sx={{
|
|
color: terminalColors.primary,
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
|
}
|
|
}}
|
|
>
|
|
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
</IconButton>
|
|
</Tooltip>
|
|
{platformStatus?.connected && (
|
|
<Tooltip title="Manually refresh token">
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => handleRefresh(platform)}
|
|
disabled={refreshing === platform}
|
|
sx={{
|
|
color: terminalColors.primary,
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
|
},
|
|
'&:disabled': {
|
|
color: '#004400',
|
|
}
|
|
}}
|
|
>
|
|
{refreshing === platform ? (
|
|
<CircularProgress size={14} sx={{ color: terminalColors.primary }} />
|
|
) : (
|
|
<RefreshCw size={14} />
|
|
)}
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell colSpan={4} sx={{ py: 0, border: 0 }}>
|
|
<Collapse in={isExpanded}>
|
|
<Box p={2} sx={{ backgroundColor: 'rgba(0, 255, 0, 0.05)', borderLeft: `2px solid ${terminalColors.primary}` }}>
|
|
{task?.failure_reason && (
|
|
<TerminalAlert severity="error" sx={{ mb: 1 }}>
|
|
<TerminalTypography variant="body2" fontWeight="bold">
|
|
Last Failure:
|
|
</TerminalTypography>
|
|
<TerminalTypography variant="body2">
|
|
{task.failure_reason}
|
|
</TerminalTypography>
|
|
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
|
|
{formatDate(task.last_failure || null)}
|
|
</TerminalTypography>
|
|
</TerminalAlert>
|
|
)}
|
|
{/* OAuth Monitoring Logs Section */}
|
|
{platformStatus?.connected && (
|
|
<>
|
|
<Divider sx={{ my: 1.5, borderColor: terminalColors.primary + '40' }} />
|
|
<TerminalTypography variant="subtitle2" fontWeight="bold" mb={1}>
|
|
🔐 Monitoring Logs
|
|
</TerminalTypography>
|
|
|
|
{logs?.loading ? (
|
|
<Box display="flex" alignItems="center" gap={1} p={1}>
|
|
<CircularProgress size={16} sx={{ color: terminalColors.primary }} />
|
|
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
|
|
Loading logs...
|
|
</TerminalTypography>
|
|
</Box>
|
|
) : logs?.error ? (
|
|
<TerminalAlert severity="error" sx={{ mb: 1 }}>
|
|
<TerminalTypography variant="caption">
|
|
{logs.error}
|
|
</TerminalTypography>
|
|
</TerminalAlert>
|
|
) : logs?.logs && logs.logs.length > 0 ? (
|
|
<Box sx={{
|
|
maxHeight: '300px',
|
|
overflowY: 'auto',
|
|
overflowX: 'hidden',
|
|
'&::-webkit-scrollbar': {
|
|
width: '8px',
|
|
},
|
|
'&::-webkit-scrollbar-track': {
|
|
backgroundColor: 'rgba(0, 255, 0, 0.05)',
|
|
},
|
|
'&::-webkit-scrollbar-thumb': {
|
|
backgroundColor: terminalColors.primary + '80',
|
|
borderRadius: '4px',
|
|
'&:hover': {
|
|
backgroundColor: terminalColors.primary,
|
|
}
|
|
}
|
|
}}>
|
|
<Table size="small" sx={{
|
|
'& .MuiTableCell-root': {
|
|
color: terminalColors.primary,
|
|
borderColor: terminalColors.primary + '30',
|
|
fontSize: '0.7rem',
|
|
py: 0.5
|
|
}
|
|
}}>
|
|
<TableHead sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'rgba(0, 0, 0, 0.8)' }}>
|
|
<TableRow>
|
|
<TerminalTableCell>Date</TerminalTableCell>
|
|
<TerminalTableCell>Status</TerminalTableCell>
|
|
<TerminalTableCell>Result</TerminalTableCell>
|
|
<TerminalTableCell>Duration</TerminalTableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{logs.logs.map((log) => (
|
|
<React.Fragment key={log.id}>
|
|
<TerminalTableRow
|
|
onMouseEnter={() => setHoveredLogId(log.id)}
|
|
onMouseLeave={() => setHoveredLogId(null)}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
|
}
|
|
}}
|
|
>
|
|
<TerminalTableCell>
|
|
<TerminalTypography variant="caption" fontSize="0.65rem">
|
|
{formatDate(log.execution_date)}
|
|
</TerminalTypography>
|
|
</TerminalTableCell>
|
|
<TerminalTableCell>
|
|
{getLogStatusChip(log.status)}
|
|
</TerminalTableCell>
|
|
<TerminalTableCell>
|
|
<TerminalTypography variant="caption" fontSize="0.65rem" sx={{
|
|
fontFamily: 'monospace',
|
|
color: terminalColors.info,
|
|
maxWidth: '200px',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap'
|
|
}}>
|
|
{formatLogResult(log.result_data)}
|
|
</TerminalTypography>
|
|
</TerminalTableCell>
|
|
<TerminalTableCell>
|
|
<TerminalTypography variant="caption" fontSize="0.65rem">
|
|
{log.execution_time_ms ? `${log.execution_time_ms}ms` : 'N/A'}
|
|
</TerminalTypography>
|
|
</TerminalTableCell>
|
|
</TerminalTableRow>
|
|
{hoveredLogId === log.id && (
|
|
<TableRow>
|
|
<TableCell colSpan={4} sx={{ py: 1, backgroundColor: 'rgba(0, 255, 0, 0.08)', borderLeft: `3px solid ${terminalColors.primary}` }}>
|
|
<Box pl={2}>
|
|
<TerminalTypography variant="caption" fontWeight="bold" mb={0.5} display="block">
|
|
Full Details:
|
|
</TerminalTypography>
|
|
{log.error_message && (
|
|
<Box mb={1}>
|
|
<TerminalTypography variant="caption" fontWeight="bold" color={terminalColors.error} display="block" mb={0.5}>
|
|
Error:
|
|
</TerminalTypography>
|
|
<TerminalTypography variant="caption" fontSize="0.6rem" sx={{
|
|
fontFamily: 'monospace',
|
|
color: terminalColors.error,
|
|
wordBreak: 'break-word'
|
|
}}>
|
|
{log.error_message}
|
|
</TerminalTypography>
|
|
</Box>
|
|
)}
|
|
{log.result_data && (
|
|
<Box>
|
|
<TerminalTypography variant="caption" fontWeight="bold" color={terminalColors.info} display="block" mb={0.5}>
|
|
Result Data:
|
|
</TerminalTypography>
|
|
<TerminalTypography variant="caption" fontSize="0.6rem" sx={{
|
|
fontFamily: 'monospace',
|
|
color: terminalColors.info,
|
|
wordBreak: 'break-word',
|
|
whiteSpace: 'pre-wrap'
|
|
}}>
|
|
{typeof log.result_data === 'string' ? log.result_data : JSON.stringify(log.result_data, null, 2)}
|
|
</TerminalTypography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
{logs.logs.length >= 10 && (
|
|
<Box mt={1} textAlign="center">
|
|
<TerminalTypography variant="caption" color={terminalColors.textSecondary} sx={{ fontStyle: 'italic' }}>
|
|
Showing latest 10 logs. View all logs in OAuth Monitoring section.
|
|
</TerminalTypography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
) : (
|
|
<TerminalTypography variant="caption" color={terminalColors.textSecondary} sx={{ fontStyle: 'italic' }}>
|
|
No monitoring logs available yet. Logs will appear after the first scheduled check.
|
|
</TerminalTypography>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Existing connection status messages */}
|
|
{!task && platformStatus?.connected && (
|
|
<TerminalAlert severity="info">
|
|
<TerminalTypography variant="body2">
|
|
Connected but no monitoring task. Create one manually or wait for onboarding completion.
|
|
</TerminalTypography>
|
|
</TerminalAlert>
|
|
)}
|
|
{!platformStatus?.connected && (
|
|
<TerminalAlert severity="warning">
|
|
<TerminalTypography variant="body2">
|
|
Not connected. Connect in onboarding step 5.
|
|
</TerminalTypography>
|
|
</TerminalAlert>
|
|
)}
|
|
</Box>
|
|
</Collapse>
|
|
</TableCell>
|
|
</TableRow>
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</Box>
|
|
</TerminalPaper>
|
|
);
|
|
};
|
|
|
|
export default OAuthTokenStatus;
|
|
|