301 lines
9.7 KiB
TypeScript
301 lines
9.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Button,
|
|
useTheme,
|
|
useMediaQuery,
|
|
CircularProgress,
|
|
} from '@mui/material';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { DeviceMessage, Logout } from 'iconsax-react';
|
|
import { CardContainer } from '@/components/CardContainer';
|
|
import { PageWrapper } from '../components/PageWrapper';
|
|
import { Icon } from '@rkheftan/harmony-ui';
|
|
import { fetchProfile, deleteSessions } from '../api/settingsApi';
|
|
import { type ApiSession } from '../types/settingsApiType';
|
|
import { useApi } from '@/hooks/useApi';
|
|
import { formatDate } from '@/utils/formatSessionDate';
|
|
import { type Device } from '../types/settingsType';
|
|
import { useToast } from '@rkheftan/harmony-ui';
|
|
|
|
export function ActiveDevicesPage() {
|
|
const { t, i18n } = useTranslation('setting');
|
|
const [devices, setDevices] = useState<Device[]>([]);
|
|
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
|
|
const theme = useTheme();
|
|
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
|
const showToast = useToast();
|
|
|
|
const {
|
|
data: profileData,
|
|
loading: isLoading,
|
|
error: fetchError,
|
|
} = useApi(fetchProfile, { immediate: true });
|
|
|
|
const {
|
|
data: terminateData,
|
|
loading: isTerminating,
|
|
execute: executeTerminateAll,
|
|
} = useApi(deleteSessions);
|
|
|
|
useEffect(() => {
|
|
if (profileData?.success && profileData.activeSessions) {
|
|
const { sessions, currentKey } = profileData.activeSessions;
|
|
const formattedDevices = sessions.map((session: ApiSession) => ({
|
|
id: session.key,
|
|
timeAndDate: formatDate(session.created, i18n.language, t),
|
|
deviceModel: `${session.deviceOs} ${session.deviceName}`,
|
|
ip: session.ipAddress,
|
|
current: session.key === currentKey,
|
|
}));
|
|
|
|
const sortedDevices = formattedDevices.sort((a, b) => {
|
|
if (a.current && !b.current) return -1;
|
|
if (!a.current && b.current) return 1;
|
|
return 0;
|
|
});
|
|
|
|
setDevices(sortedDevices);
|
|
}
|
|
}, [profileData, i18n.language, t]);
|
|
|
|
useEffect(() => {
|
|
if (terminateData?.success) {
|
|
setDevices((prev) => prev.filter((d) => d.current));
|
|
}
|
|
}, [terminateData]);
|
|
|
|
const handleDeleteDevice = async (id: string) => {
|
|
if (loadingDeleteIds.includes(id)) return;
|
|
setLoadingDeleteIds((prev) => [...prev, id]);
|
|
try {
|
|
const { data } = await deleteSessions({ keys: [id] });
|
|
if (data.success) {
|
|
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
|
|
showToast({ message: t('active.successDelete'), severity: 'success' });
|
|
} else {
|
|
showToast({
|
|
message: data.message || t('active.deleteFailed'),
|
|
severity: 'error',
|
|
});
|
|
}
|
|
} catch (error: unknown) {
|
|
showToast({
|
|
message: t('active.deleteFailed'),
|
|
severity: 'error',
|
|
});
|
|
} finally {
|
|
setLoadingDeleteIds((prev) =>
|
|
prev.filter((loadingId) => loadingId !== id),
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleTerminateAllOtherSessions = () => {
|
|
const otherSessionKeys = devices.filter((d) => !d.current).map((d) => d.id);
|
|
if (otherSessionKeys.length > 0) {
|
|
executeTerminateAll({ keys: otherSessionKeys });
|
|
}
|
|
};
|
|
|
|
const getErrorMessage = (error: unknown): string | null => {
|
|
if (!error) return null;
|
|
if (error instanceof Error) return error.message;
|
|
return String(error);
|
|
};
|
|
|
|
return (
|
|
<PageWrapper>
|
|
<CardContainer
|
|
title={t('active.activeDevices')}
|
|
subtitle={t('active.activeDevicesCaption')}
|
|
action={
|
|
<Button
|
|
size="medium"
|
|
variant="outlined"
|
|
onClick={handleTerminateAllOtherSessions}
|
|
sx={{
|
|
borderRadius: 1,
|
|
borderColor: 'error.main',
|
|
color: 'error.main',
|
|
textTransform: 'none',
|
|
flexShrink: 0,
|
|
flexGrow: 0,
|
|
width: 'auto',
|
|
}}
|
|
disabled={isLoading || isTerminating}
|
|
>
|
|
{isTerminating
|
|
? t('active.deleting')
|
|
: t('active.deleteDevicesButton')}
|
|
</Button>
|
|
}
|
|
>
|
|
{isLoading ? (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
p: 4,
|
|
minHeight: '200px',
|
|
}}
|
|
>
|
|
<CircularProgress />
|
|
</Box>
|
|
) : fetchError ? (
|
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
|
<Typography color="error.main">
|
|
{getErrorMessage(fetchError)}
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Box
|
|
sx={{
|
|
px: { xs: 2, sm: 3, md: 4 },
|
|
py: 2,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
{devices.map((device) => (
|
|
<React.Fragment key={device.id}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: { xs: 'column', sm: 'row' },
|
|
alignItems: { xs: 'flex-start', sm: 'center' },
|
|
minHeight: 50,
|
|
py: 1.5,
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
flexBasis: { xs: '100%', sm: 'auto' },
|
|
mb: { xs: 1, sm: 0 },
|
|
minWidth: { sm: '138px' },
|
|
order: { xs: 1, sm: 1 },
|
|
}}
|
|
>
|
|
{device.timeAndDate}
|
|
</Typography>
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
width: '100%',
|
|
order: { xs: 2, sm: 2 },
|
|
}}
|
|
>
|
|
<Icon
|
|
Component={DeviceMessage}
|
|
size="medium"
|
|
color="primary.main"
|
|
/>
|
|
<Typography variant="body2" noWrap>
|
|
{device.deviceModel}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
flexBasis: { xs: '100%', sm: 'auto' },
|
|
mb: { xs: 1, sm: 0 },
|
|
minWidth: { sm: '138px' },
|
|
order: { xs: 3, sm: 3 },
|
|
}}
|
|
>
|
|
{device.ip}
|
|
</Typography>
|
|
|
|
<Box
|
|
sx={{
|
|
flexBasis: { xs: '100%', sm: 'auto' },
|
|
mb: { xs: 1, sm: 0 },
|
|
minWidth: { sm: '138px' },
|
|
order: { xs: 4, sm: 4 },
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
{device.current && (
|
|
<Button
|
|
variant="outlined"
|
|
size="medium"
|
|
sx={{
|
|
borderRadius: 12.5,
|
|
border: '1px solid',
|
|
borderColor: 'success.main',
|
|
whiteSpace: 'nowrap',
|
|
color: 'success.main',
|
|
textTransform: 'none',
|
|
maxWidth: '125px',
|
|
'&.Mui-disabled': {
|
|
color: 'success.main',
|
|
borderColor: 'success.main',
|
|
},
|
|
}}
|
|
disabled
|
|
>
|
|
{t('active.currentDevice')}
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
|
|
<Box
|
|
sx={{
|
|
flexBasis: { xs: '100%', sm: 'auto' },
|
|
mb: { xs: 1, sm: 0 },
|
|
textAlign: { xs: 'left', sm: 'center' },
|
|
minWidth: { sm: '138px' },
|
|
order: { xs: 5, sm: 5 },
|
|
}}
|
|
>
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
startIcon={<Icon Component={Logout} size="small" />}
|
|
disabled={
|
|
device.current || loadingDeleteIds.includes(device.id)
|
|
}
|
|
onClick={() => handleDeleteDevice(device.id)}
|
|
sx={{
|
|
color: 'error.main',
|
|
borderRadius: 1,
|
|
borderColor: 'error.main',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
whiteSpace: 'nowrap',
|
|
textTransform: 'none',
|
|
'& .MuiButton-startIcon': {
|
|
marginRight: 0.5,
|
|
marginLeft: 0,
|
|
},
|
|
}}
|
|
>
|
|
{loadingDeleteIds.includes(device.id) ? (
|
|
<CircularProgress size={20} color="error" />
|
|
) : (
|
|
t('active.deleteDevice')
|
|
)}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
{isXsup && (
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }} />
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</Box>
|
|
)}
|
|
</CardContainer>
|
|
</PageWrapper>
|
|
);
|
|
}
|