mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-02 04:39:14 -05:00
Merge remote-tracking branch 'overseerr/develop' into develop
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import SearchInput from '@app/components/Layout/SearchInput';
|
||||
import Sidebar from '@app/components/Layout/Sidebar';
|
||||
import UserDropdown from '@app/components/Layout/UserDropdown';
|
||||
import PullToRefresh from '@app/components/PullToRefresh';
|
||||
import type { AvailableLocale } from '@app/context/LanguageContext';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
@@ -58,6 +59,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
|
||||
|
||||
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">
|
||||
<PullToRefresh />
|
||||
<div
|
||||
className={`searchbar fixed left-0 right-0 top-0 z-10 flex flex-shrink-0 bg-opacity-80 transition duration-300 ${
|
||||
isScrolled ? 'bg-gray-700' : 'bg-transparent'
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ServerIcon, ViewListIcon } from '@heroicons/react/outline';
|
||||
import { CheckCircleIcon, DocumentRemoveIcon } from '@heroicons/react/solid';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfaces';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
|
||||
36
src/components/PullToRefresh/index.tsx
Normal file
36
src/components/PullToRefresh/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import Router from 'next/router';
|
||||
import PR from 'pulltorefreshjs';
|
||||
import { useEffect } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
const PullToRefresh: React.FC = () => {
|
||||
useEffect(() => {
|
||||
PR.init({
|
||||
mainElement: '#pull-to-refresh',
|
||||
onRefresh() {
|
||||
Router.reload();
|
||||
},
|
||||
iconArrow: ReactDOMServer.renderToString(
|
||||
<RefreshIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
|
||||
),
|
||||
iconRefreshing: ReactDOMServer.renderToString(
|
||||
<RefreshIcon
|
||||
className="z-50 m-auto h-9 w-9 animate-spin rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700"
|
||||
style={{ animationDirection: 'reverse' }}
|
||||
/>
|
||||
),
|
||||
instructionsPullToRefresh: ReactDOMServer.renderToString(<div />),
|
||||
instructionsReleaseToRefresh: ReactDOMServer.renderToString(<div />),
|
||||
instructionsRefreshing: ReactDOMServer.renderToString(<div />),
|
||||
distReload: 55,
|
||||
});
|
||||
return () => {
|
||||
PR.destroyAll();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div id="pull-to-refresh"></div>;
|
||||
};
|
||||
|
||||
export default PullToRefresh;
|
||||
@@ -5,6 +5,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import Table from '@app/components/Common/Table';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { formatBytes } from '@app/utils/numberHelpers';
|
||||
@@ -15,7 +16,8 @@ import { MediaServerType } from '@server/constants/server';
|
||||
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces';
|
||||
import type { JobId } from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import { Fragment, useState } from 'react';
|
||||
import cronstrue from 'cronstrue/i18n';
|
||||
import { Fragment, useReducer, useState } from 'react';
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
@@ -59,7 +61,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
editJobSchedule: 'Modify Job',
|
||||
jobScheduleEditSaved: 'Job edited successfully!',
|
||||
jobScheduleEditFailed: 'Something went wrong while saving the job.',
|
||||
editJobSchedulePrompt: 'Frequency',
|
||||
editJobScheduleCurrent: 'Current Frequency',
|
||||
editJobSchedulePrompt: 'New Frequency',
|
||||
editJobScheduleSelectorHours:
|
||||
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
|
||||
editJobScheduleSelectorMinutes:
|
||||
@@ -71,12 +74,56 @@ interface Job {
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
interval: 'short' | 'long' | 'fixed';
|
||||
cronSchedule: string;
|
||||
nextExecutionTime: string;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
type JobModalState = {
|
||||
isOpen?: boolean;
|
||||
job?: Job;
|
||||
scheduleHours: number;
|
||||
scheduleMinutes: number;
|
||||
};
|
||||
|
||||
type JobModalAction =
|
||||
| { type: 'set'; hours?: number; minutes?: number }
|
||||
| {
|
||||
type: 'close';
|
||||
}
|
||||
| { type: 'open'; job?: Job };
|
||||
|
||||
const jobModalReducer = (
|
||||
state: JobModalState,
|
||||
action: JobModalAction
|
||||
): JobModalState => {
|
||||
switch (action.type) {
|
||||
case 'close':
|
||||
return {
|
||||
...state,
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
case 'open':
|
||||
return {
|
||||
isOpen: true,
|
||||
job: action.job,
|
||||
scheduleHours: 1,
|
||||
scheduleMinutes: 5,
|
||||
};
|
||||
|
||||
case 'set':
|
||||
return {
|
||||
...state,
|
||||
scheduleHours: action.hours ?? state.scheduleHours,
|
||||
scheduleMinutes: action.minutes ?? state.scheduleMinutes,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const SettingsJobs = () => {
|
||||
const intl = useIntl();
|
||||
const { locale } = useLocale();
|
||||
const { addToast } = useToasts();
|
||||
const {
|
||||
data,
|
||||
@@ -92,15 +139,12 @@ const SettingsJobs = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const [jobEditModal, setJobEditModal] = useState<{
|
||||
isOpen: boolean;
|
||||
job?: Job;
|
||||
}>({
|
||||
const [jobModalState, dispatch] = useReducer(jobModalReducer, {
|
||||
isOpen: false,
|
||||
scheduleHours: 1,
|
||||
scheduleMinutes: 5,
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [jobScheduleMinutes, setJobScheduleMinutes] = useState(5);
|
||||
const [jobScheduleHours, setJobScheduleHours] = useState(1);
|
||||
const settings = useSettings();
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -151,10 +195,10 @@ const SettingsJobs = () => {
|
||||
const jobScheduleCron = ['0', '0', '*', '*', '*', '*'];
|
||||
|
||||
try {
|
||||
if (jobEditModal.job?.interval === 'short') {
|
||||
jobScheduleCron[1] = `*/${jobScheduleMinutes}`;
|
||||
} else if (jobEditModal.job?.interval === 'long') {
|
||||
jobScheduleCron[2] = `*/${jobScheduleHours}`;
|
||||
if (jobModalState.job?.interval === 'short') {
|
||||
jobScheduleCron[1] = `*/${jobModalState.scheduleMinutes}`;
|
||||
} else if (jobModalState.job?.interval === 'long') {
|
||||
jobScheduleCron[2] = `*/${jobModalState.scheduleHours}`;
|
||||
} else {
|
||||
// jobs with interval: fixed should not be editable
|
||||
throw new Error();
|
||||
@@ -162,16 +206,18 @@ const SettingsJobs = () => {
|
||||
|
||||
setIsSaving(true);
|
||||
await axios.post(
|
||||
`/api/v1/settings/jobs/${jobEditModal.job?.id}/schedule`,
|
||||
`/api/v1/settings/jobs/${jobModalState.job.id}/schedule`,
|
||||
{
|
||||
schedule: jobScheduleCron.join(' '),
|
||||
}
|
||||
);
|
||||
|
||||
addToast(intl.formatMessage(messages.jobScheduleEditSaved), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
setJobEditModal({ isOpen: false });
|
||||
|
||||
dispatch({ type: 'close' });
|
||||
revalidate();
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.jobScheduleEditFailed), {
|
||||
@@ -199,7 +245,7 @@ const SettingsJobs = () => {
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={jobEditModal.isOpen}
|
||||
show={jobModalState.isOpen}
|
||||
>
|
||||
<Modal
|
||||
title={intl.formatMessage(messages.editJobSchedule)}
|
||||
@@ -208,24 +254,43 @@ const SettingsJobs = () => {
|
||||
? intl.formatMessage(globalMessages.saving)
|
||||
: intl.formatMessage(globalMessages.save)
|
||||
}
|
||||
onCancel={() => setJobEditModal({ isOpen: false })}
|
||||
onCancel={() => dispatch({ type: 'close' })}
|
||||
okDisabled={isSaving}
|
||||
onOk={() => scheduleJob()}
|
||||
>
|
||||
<div className="section">
|
||||
<form>
|
||||
<div className="form-row pb-6">
|
||||
<form className="mb-6">
|
||||
<div className="form-row">
|
||||
<label className="text-label">
|
||||
{intl.formatMessage(messages.editJobScheduleCurrent)}
|
||||
</label>
|
||||
<div className="form-input-area mt-2 mb-1">
|
||||
<div>
|
||||
{jobModalState.job &&
|
||||
cronstrue.toString(jobModalState.job.cronSchedule, {
|
||||
locale,
|
||||
})}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{jobModalState.job?.cronSchedule}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="jobSchedule" className="text-label">
|
||||
{intl.formatMessage(messages.editJobSchedulePrompt)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
{jobEditModal.job?.interval === 'short' ? (
|
||||
{jobModalState.job?.interval === 'short' ? (
|
||||
<select
|
||||
name="jobScheduleMinutes"
|
||||
className="inline"
|
||||
value={jobScheduleMinutes}
|
||||
value={jobModalState.scheduleMinutes}
|
||||
onChange={(e) =>
|
||||
setJobScheduleMinutes(Number(e.target.value))
|
||||
dispatch({
|
||||
type: 'set',
|
||||
minutes: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
>
|
||||
{[5, 10, 15, 20, 30, 60].map((v) => (
|
||||
@@ -243,9 +308,12 @@ const SettingsJobs = () => {
|
||||
<select
|
||||
name="jobScheduleHours"
|
||||
className="inline"
|
||||
value={jobScheduleHours}
|
||||
value={jobModalState.scheduleHours}
|
||||
onChange={(e) =>
|
||||
setJobScheduleHours(Number(e.target.value))
|
||||
dispatch({
|
||||
type: 'set',
|
||||
hours: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
>
|
||||
{[1, 2, 3, 4, 6, 8, 12, 24, 48, 72].map((v) => (
|
||||
@@ -324,9 +392,7 @@ const SettingsJobs = () => {
|
||||
<Button
|
||||
className="mr-2"
|
||||
buttonType="warning"
|
||||
onClick={() =>
|
||||
setJobEditModal({ isOpen: true, job: job })
|
||||
}
|
||||
onClick={() => dispatch({ type: 'open', job })}
|
||||
>
|
||||
<PencilIcon />
|
||||
<span>{intl.formatMessage(globalMessages.edit)}</span>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Modal from '@app/components/Common/Modal';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import Table from '@app/components/Common/Table';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import useDebouncedState from '@app/hooks/useDebouncedState';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
FilterIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
SearchIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import type {
|
||||
LogMessage,
|
||||
@@ -59,6 +61,8 @@ const SettingsLogs = () => {
|
||||
const { addToast } = useToasts();
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>('debug');
|
||||
const [currentPageSize, setCurrentPageSize] = useState(25);
|
||||
const [searchFilter, debouncedSearchFilter, setSearchFilter] =
|
||||
useDebouncedState('');
|
||||
const [refreshInterval, setRefreshInterval] = useState(5000);
|
||||
const [activeLog, setActiveLog] = useState<{
|
||||
isOpen: boolean;
|
||||
@@ -76,7 +80,9 @@ const SettingsLogs = () => {
|
||||
const { data, error } = useSWR<LogsResultsResponse>(
|
||||
`/api/v1/settings/logs?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&filter=${currentFilter}`,
|
||||
}&filter=${currentFilter}${
|
||||
debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''
|
||||
}`,
|
||||
{
|
||||
refreshInterval: refreshInterval,
|
||||
revalidateOnFocus: false,
|
||||
@@ -118,15 +124,13 @@ const SettingsLogs = () => {
|
||||
});
|
||||
};
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
// check if there's no data and no errors in the table
|
||||
// so as to show a spinner inside the table and not refresh the whole component
|
||||
if (!data && error) {
|
||||
return <Error statusCode={500} />;
|
||||
}
|
||||
|
||||
const hasNextPage = data.pageInfo.pages > pageIndex + 1;
|
||||
const hasNextPage = data?.pageInfo.pages ?? 0 > pageIndex + 1;
|
||||
const hasPrevPage = pageIndex > 0;
|
||||
|
||||
return (
|
||||
@@ -245,10 +249,21 @@ const SettingsLogs = () => {
|
||||
appDataPath: appData ? appData.appDataPath : '/app/config',
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-grow flex-row sm:flex-grow-0 sm:justify-end">
|
||||
<div className="mt-2 flex flex-grow flex-col sm:flex-grow-0 sm:flex-row sm:justify-end">
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 md:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<SearchIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="rounded-r-only"
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value as string)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2 flex flex-1 flex-row justify-between sm:mb-0 sm:flex-none">
|
||||
<Button
|
||||
className="mr-2 w-full flex-grow"
|
||||
className="mr-2 flex flex-grow"
|
||||
buttonType={refreshInterval ? 'default' : 'primary'}
|
||||
onClick={() => toggleLogs()}
|
||||
>
|
||||
@@ -259,34 +274,34 @@ const SettingsLogs = () => {
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-2 flex flex-1 sm:mb-0 sm:flex-none">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<FilterIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<select
|
||||
id="filter"
|
||||
name="filter"
|
||||
onChange={(e) => {
|
||||
setCurrentFilter(e.target.value as Filter);
|
||||
router.push(router.pathname);
|
||||
}}
|
||||
value={currentFilter}
|
||||
className="rounded-r-only"
|
||||
>
|
||||
<option value="debug">
|
||||
{intl.formatMessage(messages.filterDebug)}
|
||||
</option>
|
||||
<option value="info">
|
||||
{intl.formatMessage(messages.filterInfo)}
|
||||
</option>
|
||||
<option value="warn">
|
||||
{intl.formatMessage(messages.filterWarn)}
|
||||
</option>
|
||||
<option value="error">
|
||||
{intl.formatMessage(messages.filterError)}
|
||||
</option>
|
||||
</select>
|
||||
<div className="flex flex-grow">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<FilterIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<select
|
||||
id="filter"
|
||||
name="filter"
|
||||
onChange={(e) => {
|
||||
setCurrentFilter(e.target.value as Filter);
|
||||
router.push(router.pathname);
|
||||
}}
|
||||
value={currentFilter}
|
||||
className="rounded-r-only"
|
||||
>
|
||||
<option value="debug">
|
||||
{intl.formatMessage(messages.filterDebug)}
|
||||
</option>
|
||||
<option value="info">
|
||||
{intl.formatMessage(messages.filterInfo)}
|
||||
</option>
|
||||
<option value="warn">
|
||||
{intl.formatMessage(messages.filterWarn)}
|
||||
</option>
|
||||
<option value="error">
|
||||
{intl.formatMessage(messages.filterError)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
@@ -300,73 +315,81 @@ const SettingsLogs = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{data.results.map((row: LogMessage, index: number) => {
|
||||
return (
|
||||
<tr key={`log-list-${index}`}>
|
||||
<Table.TD className="text-gray-300">
|
||||
{intl.formatDate(row.timestamp, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
})}
|
||||
</Table.TD>
|
||||
<Table.TD className="text-gray-300">
|
||||
<Badge
|
||||
badgeType={
|
||||
row.level === 'error'
|
||||
? 'danger'
|
||||
: row.level === 'warn'
|
||||
? 'warning'
|
||||
: row.level === 'info'
|
||||
? 'success'
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{row.level.toUpperCase()}
|
||||
</Badge>
|
||||
</Table.TD>
|
||||
<Table.TD className="text-gray-300">
|
||||
{row.label ?? ''}
|
||||
</Table.TD>
|
||||
<Table.TD className="text-gray-300">{row.message}</Table.TD>
|
||||
<Table.TD className="-m-1 flex flex-wrap items-center justify-end">
|
||||
{row.data && (
|
||||
{!data ? (
|
||||
<tr>
|
||||
<Table.TD colSpan={5} noPadding>
|
||||
<LoadingSpinner />
|
||||
</Table.TD>
|
||||
</tr>
|
||||
) : (
|
||||
data.results.map((row: LogMessage, index: number) => {
|
||||
return (
|
||||
<tr key={`log-list-${index}`}>
|
||||
<Table.TD className="text-gray-300">
|
||||
{intl.formatDate(row.timestamp, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
})}
|
||||
</Table.TD>
|
||||
<Table.TD className="text-gray-300">
|
||||
<Badge
|
||||
badgeType={
|
||||
row.level === 'error'
|
||||
? 'danger'
|
||||
: row.level === 'warn'
|
||||
? 'warning'
|
||||
: row.level === 'info'
|
||||
? 'success'
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{row.level.toUpperCase()}
|
||||
</Badge>
|
||||
</Table.TD>
|
||||
<Table.TD className="text-gray-300">
|
||||
{row.label ?? ''}
|
||||
</Table.TD>
|
||||
<Table.TD className="text-gray-300">{row.message}</Table.TD>
|
||||
<Table.TD className="-m-1 flex flex-wrap items-center justify-end">
|
||||
{row.data && (
|
||||
<Tooltip
|
||||
content={intl.formatMessage(messages.viewdetails)}
|
||||
>
|
||||
<Button
|
||||
buttonSize="sm"
|
||||
buttonType="primary"
|
||||
onClick={() =>
|
||||
setActiveLog({ log: row, isOpen: true })
|
||||
}
|
||||
className="m-1"
|
||||
>
|
||||
<DocumentSearchIcon className="icon-md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
content={intl.formatMessage(messages.viewdetails)}
|
||||
content={intl.formatMessage(messages.copyToClipboard)}
|
||||
>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() =>
|
||||
setActiveLog({ log: row, isOpen: true })
|
||||
}
|
||||
onClick={() => copyLogString(row)}
|
||||
className="m-1"
|
||||
>
|
||||
<DocumentSearchIcon className="icon-md" />
|
||||
<ClipboardCopyIcon className="icon-md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
content={intl.formatMessage(messages.copyToClipboard)}
|
||||
>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => copyLogString(row)}
|
||||
className="m-1"
|
||||
>
|
||||
<ClipboardCopyIcon className="icon-md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Table.TD>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</Table.TD>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{data.results.length === 0 && (
|
||||
{data?.results.length === 0 && (
|
||||
<tr className="relative h-24 p-2 text-white">
|
||||
<Table.TD colSpan={5} noPadding>
|
||||
<div className="flex w-screen flex-col items-center justify-center p-6 md:w-full">
|
||||
@@ -396,15 +419,15 @@ const SettingsLogs = () => {
|
||||
>
|
||||
<div className="hidden lg:flex lg:flex-1">
|
||||
<p className="text-sm">
|
||||
{data.results.length > 0 &&
|
||||
{(data?.results.length ?? 0) > 0 &&
|
||||
intl.formatMessage(globalMessages.showingresults, {
|
||||
from: pageIndex * currentPageSize + 1,
|
||||
to:
|
||||
data.results.length < currentPageSize
|
||||
data?.results.length ?? 0 < currentPageSize
|
||||
? pageIndex * currentPageSize +
|
||||
data.results.length
|
||||
(data?.results.length ?? 0)
|
||||
: (pageIndex + 1) * currentPageSize,
|
||||
total: data.pageInfo.results,
|
||||
total: data?.pageInfo.results ?? 0,
|
||||
strong: (msg: React.ReactNode) => (
|
||||
<span className="font-medium">{msg}</span>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user