first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="561" height="493" viewBox="0 0 561 493" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M876.03027,689.45c-.98047,1.37-1.97021,2.73-2.95019,4.08A16.82838,16.82838,0,0,1,863.5,696.5h-527a16.90383,16.90383,0,0,1-9.21-2.72c-.91016-1.2-1.81006-2.41-2.72022-3.62006l.91016-.5L592.27,541.78a16.01919,16.01919,0,0,1,15.47021-.02L875.12988,688.95Z" transform="translate(-319.5 -203.5)" fill="rgb(var(--be-primary))"/><path d="M863.5,378.5,632.28169,244.96964a64.023,64.023,0,0,0-63.98147-.03153L336.5,378.5a17.0241,17.0241,0,0,0-17,17v284a17.01984,17.01984,0,0,0,17,17h527a17.02879,17.02879,0,0,0,17-17v-284A17.02408,17.02408,0,0,0,863.5,378.5Zm15,301a15.03649,15.03649,0,0,1-15,15h-527a15.02706,15.02706,0,0,1-15-15v-284a15.01828,15.01828,0,0,1,15-15L568.30022,246.93811a64.023,64.023,0,0,1,63.98147.03153L863.5,380.5a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-319.5 -203.5)" fill="#3f3d56"/><path d="M600.2998,539.18018a15.36345,15.36345,0,0,1-5.116-.8584l-.30249-.10694-.06128-.67236c-.18848.09277-.37866.18164-.56909.26563l-.20118.08837-.20141-.08886c-.42139-.18506-.83985-.39453-1.24365-.62207L408.5,433.73242V222.5A18.5208,18.5208,0,0,1,427,204H773a18.5208,18.5208,0,0,1,18.5,18.5V434.00244l-.25488.14356-183.25,103.04A15.75694,15.75694,0,0,1,600.2998,539.18018Z" transform="translate(-319.5 -203.5)" fill="#fff"/><path d="M600.2998,539.68018a15.85649,15.85649,0,0,1-5.282-.88672l-.60547-.21338-.02588-.28565-.33691.14795-.40234-.17676c-.43653-.19189-.86963-.40869-1.28784-.64453L408,434.02539V222.5a19.02154,19.02154,0,0,1,19-19H773a19.02162,19.02162,0,0,1,19,19V434.29492L608.24,537.62158A16.2527,16.2527,0,0,1,600.2998,539.68018Zm-4.01342-2.57666a14.49247,14.49247,0,0,0,10.97436-1.22559L790,433.125V222.5a17.01917,17.01917,0,0,0-17-17H427a17.01909,17.01909,0,0,0-17,17V432.85449l11.98962,6.7334,171.35047,96.29053q.34973.197.71.3706.36035-.17358.70923-.37011l1.34668-.75879Z" transform="translate(-319.5 -203.5)" fill="#3f3d56"/><path d="M876.06982,385.88,803.5,426.68,791,433.71,607.75,536.75a15.24213,15.24213,0,0,1-7.4502,1.93,14.91079,14.91079,0,0,1-4.9497-.83,12.05366,12.05366,0,0,1-1.3003-.5q-.61449-.27-1.1997-.6L421.5,440.46,409,433.44l-84.91992-47.72a1.011,1.011,0,0,1-.37988-1.37.99933.99933,0,0,1,1.35986-.38L409,431.14l12.5,7.02L593.83008,535a13.07441,13.07441,0,0,0,1.77978.83c.26026.1.53028.19.8003.27A13.26424,13.26424,0,0,0,606.77,535L791,431.42l12.5-7.03,71.58984-40.25a.99849.99849,0,1,1,.98,1.74Z" transform="translate(-319.5 -203.5)" fill="#3f3d56"/><path d="M483.5748,269.5h-28a8,8,0,0,1,0-16h28a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="rgb(var(--be-primary))"/><path d="M516.5748,296.5h-61a8,8,0,0,1,0-16h61a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="#e6e6e6"/><path d="M687,368.5H514a8,8,0,0,1,0-16H687a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="rgb(var(--be-primary))"/><path d="M703,399.5H497a8,8,0,0,1,0-16H703a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="#e6e6e6"/><path d="M703,429.5H497a8,8,0,0,1,0-16H703a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="#e6e6e6"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,117 @@
import {ColumnConfig} from '@common/datatable/column-config';
import {Trans} from '@common/i18n/trans';
import {NameWithAvatar} from '@common/datatable/column-templates/name-with-avatar';
import {Tooltip} from '@common/ui/tooltip/tooltip';
import {IconButton} from '@common/ui/buttons/icon-button';
import React, {ReactNode} from 'react';
import {useRerunScheduledCommand} from '@common/admin/logging/schedule/use-rerurun-scheduled-command';
import {OutgoingEmailLogItem} from '@common/admin/logging/outgoing-email/outgoing-email-log-item';
import {FormattedRelativeTime} from '@common/i18n/formatted-relative-time';
import {Chip, ChipProps} from '@common/ui/forms/input-field/chip-field/chip';
import {VisibilityIcon} from '@common/icons/material/Visibility';
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
import {OutgoingEmailLogEntryDialog} from '@common/admin/logging/outgoing-email/outgoing-email-log-entry-dialog';
export const OutgoingEmailLogDatatableColumns: ColumnConfig<OutgoingEmailLogItem>[] =
[
{
key: 'message_id',
allowsSorting: true,
visibleInMode: 'all',
width: 'flex-3 min-w-200',
header: () => <Trans message="Subject" />,
body: item => (
<NameWithAvatar label={item.subject} description={item.message_id} />
),
},
{
key: 'status',
allowsSorting: true,
header: () => <Trans message="Status" />,
body: item => {
switch (item.status) {
case 'sent':
return (
<StatusChip color="positive">
<Trans message="Sent" />
</StatusChip>
);
case 'not-sent':
return (
<StatusChip color={undefined}>
<Trans message="Not sent" />
</StatusChip>
);
case 'error':
return (
<StatusChip color="danger">
<Trans message="Error" />
</StatusChip>
);
}
},
},
{
key: 'from',
allowsSorting: true,
header: () => <Trans message="From" />,
body: item => item.from,
},
{
key: 'to',
allowsSorting: true,
header: () => <Trans message="To" />,
body: item => item.to,
},
{
key: 'created_at',
allowsSorting: true,
header: () => <Trans message="Date" />,
body: item => <FormattedRelativeTime date={item.created_at} />,
},
{
key: 'actions',
header: () => <Trans message="Actions" />,
hideHeader: true,
align: 'end',
width: 'w-42 flex-shrink-0',
visibleInMode: 'all',
body: item => <PreviewEmailButton item={item} />,
},
];
interface PreviewButtonProps {
item: OutgoingEmailLogItem;
}
function PreviewEmailButton({item}: PreviewButtonProps) {
const rerunCommand = useRerunScheduledCommand();
return (
<DialogTrigger type="modal">
<Tooltip label={<Trans message="Preview" />}>
<IconButton
size="md"
className="text-muted"
disabled={rerunCommand.isPending}
onClick={() => {
rerunCommand.mutate({id: item.id});
}}
>
<VisibilityIcon />
</IconButton>
</Tooltip>
<OutgoingEmailLogEntryDialog logItemId={item.id} />
</DialogTrigger>
);
}
interface StatusChipProps {
color: ChipProps['color'];
children: ReactNode;
}
function StatusChip({color, children}: StatusChipProps) {
return (
<Chip color={color} size="xs" className="w-max min-w-50 text-center">
{children}
</Chip>
);
}

View File

@@ -0,0 +1,40 @@
import {
BackendFilter,
FilterControlType,
FilterOperator,
} from '@common/datatable/filters/backend-filter';
import {message} from '@common/i18n/message';
import {createdAtFilter} from '@common/datatable/filters/timestamp-filters';
export const OutgoingEmailLogDatatableFilters: BackendFilter[] = [
{
key: 'status',
label: message('Status'),
description: message('Status of the outgoing email'),
defaultOperator: FilterOperator.eq,
control: {
type: FilterControlType.Select,
defaultValue: '01',
options: [
{
key: '01',
label: message('Not sent'),
value: 'no-sent',
},
{
key: '02',
label: message('Sent'),
value: 'sent',
},
{
key: '03',
label: message('Error'),
value: 'error',
},
],
},
},
createdAtFilter({
description: message('Date email send was attempted'),
}),
];

View File

@@ -0,0 +1,43 @@
import {DataTablePage} from '@common/datatable/page/data-table-page';
import {Trans} from '@common/i18n/trans';
import {DataTableEmptyStateMessage} from '@common/datatable/page/data-table-emty-state-message';
import React from 'react';
import openedImage from '@common/admin/logging/outgoing-email/opened.svg';
import {DataTableAddItemButton} from '@common/datatable/data-table-add-item-button';
import {DownloadIcon} from '@common/icons/material/Download';
import {OutgoingEmailLogDatatableColumns} from '@common/admin/logging/outgoing-email/outgoing-email-log-datatable-columns';
import {OutgoingEmailLogDatatableFilters} from '@common/admin/logging/outgoing-email/outgoing-email-log-datatable-filters';
export function OutgoingEmailLogDatatable() {
return (
<DataTablePage
padding="pt-12 md:pt-24"
endpoint="logs/outgoing-email"
title={<Trans message="Outgoing email" />}
columns={OutgoingEmailLogDatatableColumns}
filters={OutgoingEmailLogDatatableFilters}
actions={<Actions />}
enableSelection={false}
emptyStateMessage={
<DataTableEmptyStateMessage
image={openedImage}
title={<Trans message="No outgoing emails have been logged yet" />}
filteringTitle={<Trans message="No matching emails" />}
/>
}
/>
);
}
function Actions() {
return (
<DataTableAddItemButton
elementType="a"
href="api/v1/logs/outgoing-email/download"
download
icon={<DownloadIcon />}
>
<Trans message="Download log" />
</DataTableAddItemButton>
);
}

View File

@@ -0,0 +1,62 @@
import {Dialog} from '@common/ui/overlays/dialog/dialog';
import {DialogHeader} from '@common/ui/overlays/dialog/dialog-header';
import {Trans} from '@common/i18n/trans';
import {DialogBody} from '@common/ui/overlays/dialog/dialog-body';
import {Button} from '@common/ui/buttons/button';
import {useOutgoingEmailLogItemWithMime} from '@common/admin/logging/outgoing-email/use-outgoing-email-log-item-with-mime';
import {ProgressCircle} from '@common/ui/progress/progress-circle';
import {downloadFileFromUrl} from '@common/uploads/utils/download-file-from-url';
import {useSettings} from '@common/core/settings/use-settings';
interface Props {
logItemId: number;
}
export function OutgoingEmailLogEntryDialog({logItemId}: Props) {
const {data} = useOutgoingEmailLogItemWithMime(logItemId);
const {base_url} = useSettings();
return (
<Dialog size="fullscreen">
<DialogHeader
showDivider
padding="px-24 py-10"
actions={
<Button
variant="outline"
size="xs"
disabled={!data}
type="button"
onClick={
data
? () =>
downloadFileFromUrl(
`${base_url}/api/v1/logs/outgoing-email/${logItemId}/download`,
)
: undefined
}
>
<Trans message="Download" />
</Button>
}
>
<Trans message="Email preview" />
</DialogHeader>
<DialogBody>
{data ? (
<iframe
srcDoc={data.logItem.parsed_message!.body.html}
className="h-max w-full border-none"
onLoad={e => {
const iframe = e.target as HTMLIFrameElement;
iframe.style.height =
iframe.contentWindow!.document.body.scrollHeight + 'px';
}}
/>
) : (
<div className="flex min-h-200 items-center justify-center">
<ProgressCircle isIndeterminate />
</div>
)}
</DialogBody>
</Dialog>
);
}

View File

@@ -0,0 +1,17 @@
export interface OutgoingEmailLogItem {
id: number;
message_id: string;
status: 'sent' | 'not-sent' | 'error';
from: string;
to: string;
subject: string;
parsed_message?: {
headers: Record<string, string>;
body: {
html: string;
text: string;
};
};
created_at: string;
updated_at: string;
}

View File

@@ -0,0 +1,19 @@
import {useQuery} from '@tanstack/react-query';
import {apiClient} from '@common/http/query-client';
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {OutgoingEmailLogItem} from '@common/admin/logging/outgoing-email/outgoing-email-log-item';
interface Response extends BackendResponse {
logItem: OutgoingEmailLogItem;
}
export function useOutgoingEmailLogItemWithMime(id: number) {
return useQuery({
queryKey: ['logs/outgoing-email', id],
queryFn: () => fetchLogItem(id),
});
}
function fetchLogItem(id: number) {
return apiClient.get<Response>(`logs/outgoing-email/${id}`).then(r => r.data);
}