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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,44 @@
import {ColumnConfig} from '@common/datatable/column-config';
import {Trans} from '@common/i18n/trans';
import React from 'react';
import {ErrorLogItem} from '@common/admin/logging/error/error-log-item';
import {InfoIcon} from '@common/icons/material/Info';
import {ErrorIcon} from '@common/icons/material/Error';
import clsx from 'clsx';
import {FormattedRelativeTime} from '@common/i18n/formatted-relative-time';
export const ErrorLogDatatableColumns: ColumnConfig<ErrorLogItem>[] = [
{
key: 'message',
visibleInMode: 'all',
width: 'flex-3 min-w-200',
header: () => <Trans message="Message" />,
body: item => item.message,
},
{
key: 'datetime',
header: () => <Trans message="Date" />,
body: item => <FormattedRelativeTime date={item.datetime} />,
},
{
key: 'severity',
header: () => <Trans message="Severity" />,
body: item => {
return (
<span
className={clsx(
'flex items-center gap-6 text-xs capitalize',
item.level === 'error' ? 'text-danger' : 'text-primary',
)}
>
{item.level === 'error' ? (
<ErrorIcon size="sm" />
) : (
<InfoIcon size="sm" />
)}
{item.level}
</span>
);
},
},
];

View File

@@ -0,0 +1,161 @@
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, {Fragment, useEffect, useRef, useState} from 'react';
import bugFixingImage from '@common/admin/logging/error/bug-fixing.svg';
import {DataTableAddItemButton} from '@common/datatable/data-table-add-item-button';
import {DownloadIcon} from '@common/icons/material/Download';
import {ErrorLogDatatableColumns} from '@common/admin/logging/error/error-log-datatable-columns';
import {closeDialog, openDialog} from '@common/ui/overlays/store/dialog-store';
import {ErrorLogEntryDialog} from '@common/admin/logging/error/error-log-entry-dialog';
import {useDataTable} from '@common/datatable/page/data-table-context';
import {Select} from '@common/ui/forms/select/select';
import {Item} from '@common/ui/forms/listbox/item';
import {Skeleton} from '@common/ui/skeleton/skeleton';
import {ErrorLogItem} from '@common/admin/logging/error/error-log-item';
import {Button} from '@common/ui/buttons/button';
import {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';
import {useDeleteErrorLog} from '@common/admin/logging/error/use-delete-error-log';
import {FormattedBytes} from '@common/uploads/formatted-bytes';
interface ErrorLogFile {
name: string;
identifier: string;
size: number;
}
export function ErrorLogDatatable() {
return (
<DataTablePage
padding="pt-12 md:pt-24"
endpoint="logs/error"
title={<Trans message="Error log" />}
onRowAction={item => {
openDialog(ErrorLogEntryDialog, {error: item});
}}
columns={ErrorLogDatatableColumns}
actions={<Actions />}
enableSelection={false}
emptyStateMessage={
<DataTableEmptyStateMessage
image={bugFixingImage}
title={<Trans message="No errors have been logged yet" />}
filteringTitle={<Trans message="No matching error log entries" />}
/>
}
/>
);
}
function Actions() {
const {query, setParams} = useDataTable<
ErrorLogItem,
{files: ErrorLogFile[]; selectedFile?: string}
>();
const setOnce = useRef(false);
const [selectedFile, setSelectedFile] = useState<string | null>(null);
// set initial selected file once files are loaded
useEffect(() => {
if (query.data?.files?.length && !setOnce.current) {
setOnce.current = true;
const firstFile = query.data.files[0].identifier;
setSelectedFile(query.data.files[0].identifier);
// prevent unnecessary http call
if (firstFile !== query.data.selectedFile) {
setParams({file: query.data.files[0].identifier});
}
}
}, [query.data, setParams, setOnce]);
return (
<Fragment>
<FileSelector
files={query.data?.files ?? null}
selectedFile={selectedFile}
onSelected={file => {
setSelectedFile(file.identifier);
setParams({file: file.identifier});
}}
/>
<Button
variant="outline"
color="danger"
disabled={!selectedFile}
onClick={() =>
openDialog(ConfirmDeleteDialog, {identifier: selectedFile})
}
>
<Trans message="Delete" />
</Button>
{selectedFile && (
<DataTableAddItemButton
elementType="a"
download={
query.data?.files.find(f => f.identifier === selectedFile)?.name
}
href={`api/v1/logs/error/${selectedFile}/download`}
icon={<DownloadIcon />}
>
<Trans message="Download log" />
</DataTableAddItemButton>
)}
</Fragment>
);
}
interface FileSelectorProps {
files: ErrorLogFile[] | null;
selectedFile: string | null;
onSelected: (file: ErrorLogFile) => void;
}
function FileSelector({files, selectedFile, onSelected}: FileSelectorProps) {
// files have not loaded yet, show skeleton
if (!files) {
return <Skeleton variant="rect" className="max-w-[210px]" />;
}
// no error logs yet, hide select completely
if (!files.length) {
return null;
}
return (
<Select
selectionMode="single"
selectedValue={selectedFile}
size="sm"
minWidth="min-w-[210px]"
>
{files?.map(file => (
<Item
key={file.identifier}
value={file.identifier}
onSelected={() => onSelected(file)}
>
{file.name} (<FormattedBytes bytes={file.size} />)
</Item>
))}
</Select>
);
}
interface ConfirmDeleteDialogProps {
identifier: string;
}
function ConfirmDeleteDialog({identifier}: ConfirmDeleteDialogProps) {
const deleteLog = useDeleteErrorLog();
return (
<ConfirmationDialog
title={<Trans message="Delete log file" />}
body={<Trans message="Are you sure you want to delete this log file?" />}
confirm={<Trans message="Delete" />}
onConfirm={() =>
deleteLog.mutate({identifier}, {onSuccess: () => closeDialog()})
}
isLoading={deleteLog.isPending}
isDanger
/>
);
}

View File

@@ -0,0 +1,50 @@
import {Dialog} from '@common/ui/overlays/dialog/dialog';
import {DialogHeader} from '@common/ui/overlays/dialog/dialog-header';
import {Trans} from '@common/i18n/trans';
import {ErrorLogItem} from '@common/admin/logging/error/error-log-item';
import {DialogBody} from '@common/ui/overlays/dialog/dialog-body';
import {Button} from '@common/ui/buttons/button';
interface Props {
error: ErrorLogItem;
}
export function ErrorLogEntryDialog({error}: Props) {
return (
<Dialog size="fullscreen">
<DialogHeader
showDivider
padding="px-24 py-10"
actions={
<Button
variant="outline"
size="xs"
onClick={() => downloadLogItem(error)}
>
<Trans message="Download" />
</Button>
}
>
<Trans message="Error details" />
</DialogHeader>
<DialogBody>
<pre className="whitespace-pre-wrap break-words text-xs leading-5">
{error.exception}
</pre>
</DialogBody>
</Dialog>
);
}
function downloadLogItem(item: ErrorLogItem) {
const el = document.createElement('a');
el.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(item.exception),
);
el.setAttribute('download', `error-${item.id}.log`);
el.style.display = 'none';
document.body.appendChild(el);
el.click();
document.body.removeChild(el);
}

View File

@@ -0,0 +1,8 @@
export interface ErrorLogItem {
id: number;
index: number;
level: string;
datetime: string;
message: string;
exception: string;
}

View File

@@ -0,0 +1,29 @@
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {toast} from '@common/ui/toast/toast';
import {useTrans} from '@common/i18n/use-trans';
import {message} from '@common/i18n/message';
import {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';
interface Payload {
identifier: string;
}
export function useDeleteErrorLog() {
const {trans} = useTrans();
return useMutation({
mutationFn: (payload: Payload) => deleteLogFile(payload),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey('logs/error'),
});
toast(trans(message('Log file deleted')));
},
onError: err => showHttpErrorToast(err),
});
}
function deleteLogFile({identifier}: Payload) {
return apiClient.delete(`logs/error/${identifier}`).then(r => r.data);
}