1
common/resources/client/admin/logging/error/bug-fixing.svg
Executable file
1
common/resources/client/admin/logging/error/bug-fixing.svg
Executable file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 40 KiB |
44
common/resources/client/admin/logging/error/error-log-datatable-columns.tsx
Executable file
44
common/resources/client/admin/logging/error/error-log-datatable-columns.tsx
Executable 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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
161
common/resources/client/admin/logging/error/error-log-datatable.tsx
Executable file
161
common/resources/client/admin/logging/error/error-log-datatable.tsx
Executable 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
|
||||
/>
|
||||
);
|
||||
}
|
||||
50
common/resources/client/admin/logging/error/error-log-entry-dialog.tsx
Executable file
50
common/resources/client/admin/logging/error/error-log-entry-dialog.tsx
Executable 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);
|
||||
}
|
||||
8
common/resources/client/admin/logging/error/error-log-item.tsx
Executable file
8
common/resources/client/admin/logging/error/error-log-item.tsx
Executable file
@@ -0,0 +1,8 @@
|
||||
export interface ErrorLogItem {
|
||||
id: number;
|
||||
index: number;
|
||||
level: string;
|
||||
datetime: string;
|
||||
message: string;
|
||||
exception: string;
|
||||
}
|
||||
29
common/resources/client/admin/logging/error/use-delete-error-log.ts
Executable file
29
common/resources/client/admin/logging/error/use-delete-error-log.ts
Executable 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);
|
||||
}
|
||||
Reference in New Issue
Block a user