Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app.admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^4.39.2",
"chart.js": "^4.4.9",
"react": "^18.2.0",
"react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-icons": "^5.5.0",
"react-router-dom": "^6.30.1",
"recharts": "^2.15.3",
Expand Down
217 changes: 217 additions & 0 deletions app.admin/src/features/admin/components/EtherealCredentials.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import {
Alert,
Button,
Card,
CardContent,
Flex,
Loading,
Typography,
} from '@pairflix/components';
import React, { useState } from 'react';
import styled from 'styled-components';

// Styled components
const CredentialsContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing.md};
`;

const CredentialItem = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: ${({ theme }) => theme.spacing.sm};
background-color: ${({ theme }) => theme.colors.background.secondary};
border-radius: ${({ theme }) => theme.borderRadius.sm};
margin-bottom: ${({ theme }) => theme.spacing.xs};
`;

const CredentialLabel = styled.span`
font-weight: 600;
color: ${({ theme }) => theme.colors.text.primary};
`;

const CredentialValue = styled.span`
font-family: monospace;
color: ${({ theme }) => theme.colors.text.secondary};
background-color: ${({ theme }) => theme.colors.background.tertiary};
padding: ${({ theme }) => theme.spacing.xs};
border-radius: ${({ theme }) => theme.borderRadius.sm};
border: 1px solid ${({ theme }) => theme.colors.border.light};
`;

const LoginButton = styled(Button)`
margin-top: ${({ theme }) => theme.spacing.sm};
`;

interface EtherealCredentials {
user: string;
pass: string;
previewUrl: string;
}

interface EtherealCredentialsResponse {
message: string;
credentials: EtherealCredentials | null;
}

const EtherealCredentials: React.FC = () => {
const [credentials, setCredentials] = useState<EtherealCredentials | null>(
null
);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isVisible, setIsVisible] = useState(false);

const fetchCredentials = async () => {
setLoading(true);
setError(null);

try {
const response = await fetch('/api/email/ethereal-credentials', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch credentials');
}

const data: EtherealCredentialsResponse = await response.json();
setCredentials(data.credentials);
setIsVisible(true);
} catch (err) {
setError(
err instanceof Error ? err.message : 'Failed to fetch credentials'
);
} finally {
setLoading(false);
}
};

const copyToClipboard = async (text: string, label: string) => {
try {
await navigator.clipboard.writeText(text);
// Could add a toast notification here
console.log(`${label} copied to clipboard`);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};

const openEtherealLogin = () => {
if (credentials?.previewUrl) {
window.open(credentials.previewUrl, '_blank');
}
};

// Check if we're in development environment
const isDevelopment =
process.env.NODE_ENV === 'development' ||
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';

if (!isDevelopment) {
return null; // Don't show in production
}

return (
<CredentialsContainer>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
๐Ÿงช Development Email Testing
</Typography>
<Typography variant="body2" gutterBottom>
Access your test email server credentials for development and
testing.
</Typography>

{error && (
<Alert variant="error" onClose={() => setError(null)}>
{error}
</Alert>
)}

{!isVisible && !loading && (
<Button onClick={fetchCredentials} variant="secondary">
Show Ethereal Mail Credentials
</Button>
)}

{loading && <Loading message="Fetching credentials..." />}

{credentials && isVisible && (
<>
<CredentialItem>
<CredentialLabel>Username:</CredentialLabel>
<Flex gap="sm" alignItems="center">
<CredentialValue>{credentials.user}</CredentialValue>
<Button
onClick={() =>
copyToClipboard(credentials.user, 'Username')
}
>
Copy
</Button>
</Flex>
</CredentialItem>

<CredentialItem>
<CredentialLabel>Password:</CredentialLabel>
<Flex gap="sm" alignItems="center">
<CredentialValue>{credentials.pass}</CredentialValue>
<Button
onClick={() =>
copyToClipboard(credentials.pass, 'Password')
}
>
Copy
</Button>
</Flex>
</CredentialItem>

<CredentialItem>
<CredentialLabel>Web Interface:</CredentialLabel>
<Flex gap="sm" alignItems="center">
<CredentialValue>{credentials.previewUrl}</CredentialValue>
<Button
onClick={() =>
copyToClipboard(credentials.previewUrl, 'Preview URL')
}
>
Copy
</Button>
</Flex>
</CredentialItem>

<LoginButton onClick={openEtherealLogin} variant="primary">
๐Ÿ”— Open Ethereal Mail Login
</LoginButton>

<Typography
variant="caption"
style={{ marginTop: '8px', display: 'block' }}
>
Use these credentials to log into the Ethereal Email web
interface and view test emails sent by the application.
</Typography>
</>
)}

{!credentials && isVisible && !loading && (
<Alert variant="info">
Email service not initialized yet. Try sending an email first to
generate credentials.
</Alert>
)}
</CardContent>
</Card>
</CredentialsContainer>
);
};

export default EtherealCredentials;
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface DashboardMetrics {
};
content: {
watchlistEntries: number;
matches: number;
groups: number;
};
system: {
status?: 'healthy' | 'unhealthy';
Expand Down Expand Up @@ -85,7 +85,7 @@ const AdminDashboardContent: React.FC = () => {
},
content: {
watchlistEntries: metricsData.watchlistEntries,
matches: metricsData.totalMatches,
groups: metricsData.totalGroups,
},
// Add placeholder system data for the system health card
system: {
Expand Down Expand Up @@ -130,7 +130,7 @@ const AdminDashboardContent: React.FC = () => {
},
content: {
watchlistEntries: metricsData.watchlistEntries,
matches: metricsData.totalMatches,
groups: metricsData.totalGroups,
},
// Add placeholder system data for the system health card
system: {
Expand Down Expand Up @@ -194,7 +194,7 @@ const AdminDashboardContent: React.FC = () => {
<>
<StatsOverview
metrics={metrics}
cards={['users', 'activeUsers', 'content', 'matches']}
cards={['users', 'activeUsers', 'content', 'groups']}
/>

<SectionTitle>Quick Actions</SectionTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ const SystemMonitoringContent: React.FC = () => {
current: data.database?.activeUsers || 0,
peak24h: data.database?.totalUsers || 0,
total24h:
data.database?.contentStats?.matches ||
data.database?.contentStats?.groups ||
Math.round(data.database?.totalUsers * 1.5) ||
0,
},
Expand Down
17 changes: 8 additions & 9 deletions app.admin/src/features/admin/components/shared/StatsOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
FaChartLine,
FaClock,
FaExclamationTriangle,
FaHeart,
FaHeartbeat,
FaList,
FaMemory,
Expand Down Expand Up @@ -115,7 +114,7 @@ export type StatsCardType =
| 'users'
| 'activeUsers'
| 'content'
| 'matches'
| 'groups'
| 'activity'
| 'errors'
| 'systemHealth'
Expand All @@ -130,7 +129,7 @@ interface MetricsType {
};
content?: {
watchlistEntries: number;
matches: number;
groups: number;
};
activity?: {
last24Hours: number;
Expand Down Expand Up @@ -184,8 +183,8 @@ const MetricsCard: React.FC<MetricsCardProps> = ({ type, metrics, icon }) => {
return <FaUserCheck />;
case 'content':
return <FaList />;
case 'matches':
return <FaHeart />;
case 'groups':
return <FaUsers />;
case 'activity':
return <FaChartLine />;
case 'errors':
Expand Down Expand Up @@ -255,16 +254,16 @@ const MetricsCard: React.FC<MetricsCardProps> = ({ type, metrics, icon }) => {
</StatsCard>
);

case 'matches':
case 'groups':
if (!metrics.content) return null;
return (
<StatsCard>
<CardContent>
<Flex alignItems="center">
{icon && <StatIcon>{renderIcon(type)}</StatIcon>}
<div>
<StatValue>{formatNumber(metrics.content.matches)}</StatValue>
<StatLabel>Total Matches</StatLabel>
<StatValue>{formatNumber(metrics.content.groups)}</StatValue>
<StatLabel>Total Groups</StatLabel>
</div>
</Flex>
</CardContent>
Expand Down Expand Up @@ -404,7 +403,7 @@ interface StatsOverviewProps {
// Main component for displaying stats in a grid
export const StatsOverview: React.FC<StatsOverviewProps> = ({
metrics,
cards = ['users', 'activeUsers', 'content', 'matches'],
cards = ['users', 'activeUsers', 'content', 'groups'],
columns = 4,
}) => {
if (!isValidMetrics(metrics)) return null;
Expand Down
Loading
Loading