@oneuptime/common 8.0.5480 → 8.0.5488
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/UI/Components/LogsViewer/LogsViewer.tsx +331 -367
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +343 -0
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +74 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +109 -0
- package/UI/Components/LogsViewer/components/LogsTable.tsx +270 -0
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +51 -0
- package/UI/Components/LogsViewer/components/SeverityBadge.tsx +28 -0
- package/UI/Components/LogsViewer/components/severityTheme.ts +69 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +211 -201
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +151 -0
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +40 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +49 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +130 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +20 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/SeverityBadge.js +13 -0
- package/build/dist/UI/Components/LogsViewer/components/SeverityBadge.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js +54 -0
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js.map +1 -0
- package/package.json +1 -1
- package/UI/Components/LogsViewer/LogItem.tsx +0 -503
- package/build/dist/UI/Components/LogsViewer/LogItem.js +0 -221
- package/build/dist/UI/Components/LogsViewer/LogItem.js.map +0 -1
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement, useMemo } from "react";
|
|
2
|
+
import Log from "../../../../Models/AnalyticsModels/Log";
|
|
3
|
+
import TelemetryService from "../../../../Models/DatabaseModels/TelemetryService";
|
|
4
|
+
import Dictionary from "../../../../Types/Dictionary";
|
|
5
|
+
import Route from "../../../../Types/API/Route";
|
|
6
|
+
import URL from "../../../../Types/API/URL";
|
|
7
|
+
import CopyTextButton from "../../CopyTextButton/CopyTextButton";
|
|
8
|
+
import Icon from "../../Icon/Icon";
|
|
9
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
10
|
+
import Link from "../../Link/Link";
|
|
11
|
+
import OneUptimeDate from "../../../../Types/Date";
|
|
12
|
+
import JSONFunctions from "../../../../Types/JSONFunctions";
|
|
13
|
+
import SeverityBadge from "./SeverityBadge";
|
|
14
|
+
|
|
15
|
+
export interface LogDetailsPanelProps {
|
|
16
|
+
log: Log;
|
|
17
|
+
serviceMap: Dictionary<TelemetryService>;
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
getTraceRoute?:
|
|
20
|
+
| ((traceId: string, log: Log) => Route | URL | undefined)
|
|
21
|
+
| undefined;
|
|
22
|
+
getSpanRoute?:
|
|
23
|
+
| ((spanId: string, log: Log) => Route | URL | undefined)
|
|
24
|
+
| undefined;
|
|
25
|
+
variant?: "floating" | "embedded";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PreparedBody {
|
|
29
|
+
isJson: boolean;
|
|
30
|
+
pretty: string;
|
|
31
|
+
compact: string;
|
|
32
|
+
raw: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const prepareBody: (body: string | undefined) => PreparedBody = (
|
|
36
|
+
body: string | undefined,
|
|
37
|
+
): PreparedBody => {
|
|
38
|
+
if (!body) {
|
|
39
|
+
return {
|
|
40
|
+
isJson: false,
|
|
41
|
+
pretty: "",
|
|
42
|
+
compact: "",
|
|
43
|
+
raw: "",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const parsed: any = JSON.parse(body);
|
|
49
|
+
const pretty: string = JSON.stringify(parsed, null, 2);
|
|
50
|
+
const compact: string = JSON.stringify(parsed);
|
|
51
|
+
return {
|
|
52
|
+
isJson: true,
|
|
53
|
+
pretty,
|
|
54
|
+
compact,
|
|
55
|
+
raw: body,
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return {
|
|
59
|
+
isJson: false,
|
|
60
|
+
pretty: body,
|
|
61
|
+
compact: body,
|
|
62
|
+
raw: body,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
68
|
+
props: LogDetailsPanelProps,
|
|
69
|
+
): ReactElement => {
|
|
70
|
+
const variant: "floating" | "embedded" = props.variant || "floating";
|
|
71
|
+
const serviceId: string = props.log.serviceId?.toString() || "";
|
|
72
|
+
const service: TelemetryService | undefined = props.serviceMap[serviceId];
|
|
73
|
+
const serviceName: string = service?.name || serviceId || "Unknown service";
|
|
74
|
+
const serviceColor: string =
|
|
75
|
+
(service?.serviceColor && service?.serviceColor.toString()) || "#64748b";
|
|
76
|
+
|
|
77
|
+
const bodyDetails: PreparedBody = useMemo(() => {
|
|
78
|
+
return prepareBody(props.log.body?.toString());
|
|
79
|
+
}, [props.log.body]);
|
|
80
|
+
|
|
81
|
+
const prettyAttributes: string | null = useMemo(() => {
|
|
82
|
+
if (!props.log.attributes) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const normalized: Record<string, unknown> = JSONFunctions.unflattenObject(
|
|
88
|
+
props.log.attributes || {},
|
|
89
|
+
);
|
|
90
|
+
return JSON.stringify(normalized, null, 2);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}, [props.log.attributes]);
|
|
95
|
+
|
|
96
|
+
const traceId: string = props.log.traceId?.toString() || "";
|
|
97
|
+
const spanId: string = props.log.spanId?.toString() || "";
|
|
98
|
+
|
|
99
|
+
const traceRoute: Route | URL | undefined = useMemo(() => {
|
|
100
|
+
if (!traceId || !props.getTraceRoute) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return props.getTraceRoute(traceId, props.log);
|
|
105
|
+
}, [traceId, props]);
|
|
106
|
+
|
|
107
|
+
const spanRoute: Route | URL | undefined = useMemo(() => {
|
|
108
|
+
if (!spanId) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (props.getSpanRoute) {
|
|
113
|
+
return props.getSpanRoute(spanId, props.log);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (props.getTraceRoute && traceId) {
|
|
117
|
+
const baseRoute: Route | URL | undefined = props.getTraceRoute(
|
|
118
|
+
traceId,
|
|
119
|
+
props.log,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (!baseRoute) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (baseRoute instanceof Route) {
|
|
127
|
+
const nextRoute: Route = new Route(baseRoute.toString());
|
|
128
|
+
nextRoute.addQueryParams({ spanId });
|
|
129
|
+
return nextRoute;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const nextUrl: URL = URL.fromURL(baseRoute);
|
|
133
|
+
nextUrl.addQueryParam("spanId", spanId);
|
|
134
|
+
return nextUrl;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return undefined;
|
|
138
|
+
}, [spanId, props, traceId]);
|
|
139
|
+
|
|
140
|
+
const containerClassName: string =
|
|
141
|
+
variant === "embedded"
|
|
142
|
+
? "rounded-xl border border-slate-900 bg-slate-950 p-5 shadow-inner shadow-slate-950/40"
|
|
143
|
+
: "rounded-xl border border-slate-800 bg-slate-950/90 p-5 shadow-sm ring-1 ring-inset ring-transparent";
|
|
144
|
+
|
|
145
|
+
const headerBorderClass: string =
|
|
146
|
+
variant === "embedded" ? "border-slate-900" : "border-slate-800";
|
|
147
|
+
|
|
148
|
+
const surfaceCardClass: string =
|
|
149
|
+
variant === "embedded"
|
|
150
|
+
? "border-slate-900 bg-slate-950/70"
|
|
151
|
+
: "border-slate-800 bg-slate-950/80";
|
|
152
|
+
|
|
153
|
+
const smallBadgeClass: string =
|
|
154
|
+
"inline-flex items-center gap-1 rounded-full border border-slate-800 bg-slate-900 px-2 py-1 text-[11px] font-mono uppercase tracking-wide text-slate-300";
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className={containerClassName}>
|
|
158
|
+
<div
|
|
159
|
+
className={`flex flex-col gap-4 border-b ${headerBorderClass} pb-4 lg:flex-row lg:items-start lg:justify-between`}
|
|
160
|
+
>
|
|
161
|
+
<div className="flex flex-1 items-start gap-3">
|
|
162
|
+
<span
|
|
163
|
+
className="mt-1 h-3 w-3 flex-none rounded-full border border-slate-700"
|
|
164
|
+
style={{ backgroundColor: serviceColor }}
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
/>
|
|
167
|
+
<div className="space-y-3">
|
|
168
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
169
|
+
<h3 className="text-lg font-semibold text-slate-50">
|
|
170
|
+
{serviceName}
|
|
171
|
+
</h3>
|
|
172
|
+
<SeverityBadge severity={props.log.severityText} />
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
175
|
+
{props.log.time && (
|
|
176
|
+
<span className={smallBadgeClass}>
|
|
177
|
+
<Icon icon={IconProp.Clock} className="h-3 w-3" />
|
|
178
|
+
{OneUptimeDate.getDateAsUserFriendlyFormattedString(
|
|
179
|
+
props.log.time,
|
|
180
|
+
)}
|
|
181
|
+
</span>
|
|
182
|
+
)}
|
|
183
|
+
{traceId && (
|
|
184
|
+
<span className={smallBadgeClass}>
|
|
185
|
+
<Icon icon={IconProp.Logs} className="h-3 w-3" />
|
|
186
|
+
Trace
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
{spanId && (
|
|
190
|
+
<span className={smallBadgeClass}>
|
|
191
|
+
<Icon icon={IconProp.Terminal} className="h-3 w-3" />
|
|
192
|
+
Span
|
|
193
|
+
</span>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{props.onClose && (
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
onClick={props.onClose}
|
|
203
|
+
className="flex h-9 w-9 items-center justify-center rounded-full border border-slate-800 bg-slate-900 text-slate-400 transition-colors hover:border-slate-700 hover:text-slate-100"
|
|
204
|
+
title="Close details"
|
|
205
|
+
>
|
|
206
|
+
<Icon icon={IconProp.Close} className="h-4 w-4" />
|
|
207
|
+
</button>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div className="mt-4 space-y-5 text-sm text-slate-200">
|
|
212
|
+
<section className="space-y-3">
|
|
213
|
+
<header className="flex items-center justify-between text-[11px] uppercase tracking-wide text-slate-500">
|
|
214
|
+
<span>Log Body</span>
|
|
215
|
+
<CopyTextButton
|
|
216
|
+
textToBeCopied={bodyDetails.raw}
|
|
217
|
+
size="xs"
|
|
218
|
+
variant="ghost"
|
|
219
|
+
iconOnly={false}
|
|
220
|
+
title="Copy log body"
|
|
221
|
+
/>
|
|
222
|
+
</header>
|
|
223
|
+
|
|
224
|
+
<div className={`rounded-lg border ${surfaceCardClass} p-4`}>
|
|
225
|
+
{bodyDetails.isJson ? (
|
|
226
|
+
<pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-slate-100">
|
|
227
|
+
{bodyDetails.pretty}
|
|
228
|
+
</pre>
|
|
229
|
+
) : (
|
|
230
|
+
<p className="whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-slate-100">
|
|
231
|
+
{bodyDetails.pretty || "—"}
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</section>
|
|
236
|
+
|
|
237
|
+
{(traceId || spanId) && (
|
|
238
|
+
<section className="grid gap-4 md:grid-cols-2">
|
|
239
|
+
{traceId && (
|
|
240
|
+
<div className={`rounded-lg border ${surfaceCardClass} p-4`}>
|
|
241
|
+
<div className="mb-2 flex items-center justify-between text-[11px] uppercase tracking-wide text-slate-500">
|
|
242
|
+
<span>Trace ID</span>
|
|
243
|
+
<CopyTextButton
|
|
244
|
+
textToBeCopied={traceId}
|
|
245
|
+
size="xs"
|
|
246
|
+
variant="ghost"
|
|
247
|
+
iconOnly={true}
|
|
248
|
+
title="Copy trace id"
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
<div className="flex items-center justify-between gap-2">
|
|
252
|
+
{traceRoute ? (
|
|
253
|
+
<Link
|
|
254
|
+
to={traceRoute}
|
|
255
|
+
className="max-w-full truncate font-mono text-xs text-indigo-200 hover:text-indigo-100"
|
|
256
|
+
title={`View trace ${traceId}`}
|
|
257
|
+
>
|
|
258
|
+
{traceId}
|
|
259
|
+
</Link>
|
|
260
|
+
) : (
|
|
261
|
+
<span
|
|
262
|
+
className="max-w-full truncate font-mono text-xs text-slate-200"
|
|
263
|
+
title={traceId}
|
|
264
|
+
>
|
|
265
|
+
{traceId}
|
|
266
|
+
</span>
|
|
267
|
+
)}
|
|
268
|
+
{traceRoute && (
|
|
269
|
+
<Icon
|
|
270
|
+
icon={IconProp.ExternalLink}
|
|
271
|
+
className="h-4 w-4 flex-none text-indigo-300"
|
|
272
|
+
/>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{spanId && (
|
|
279
|
+
<div className={`rounded-lg border ${surfaceCardClass} p-4`}>
|
|
280
|
+
<div className="mb-2 flex items-center justify-between text-[11px] uppercase tracking-wide text-slate-500">
|
|
281
|
+
<span>Span ID</span>
|
|
282
|
+
<CopyTextButton
|
|
283
|
+
textToBeCopied={spanId}
|
|
284
|
+
size="xs"
|
|
285
|
+
variant="ghost"
|
|
286
|
+
iconOnly={true}
|
|
287
|
+
title="Copy span id"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="flex items-center justify-between gap-2">
|
|
291
|
+
{spanRoute ? (
|
|
292
|
+
<Link
|
|
293
|
+
to={spanRoute}
|
|
294
|
+
className="max-w-full truncate font-mono text-xs text-indigo-200 hover:text-indigo-100"
|
|
295
|
+
title={`View span ${spanId}`}
|
|
296
|
+
>
|
|
297
|
+
{spanId}
|
|
298
|
+
</Link>
|
|
299
|
+
) : (
|
|
300
|
+
<span
|
|
301
|
+
className="max-w-full truncate font-mono text-xs text-slate-200"
|
|
302
|
+
title={spanId}
|
|
303
|
+
>
|
|
304
|
+
{spanId}
|
|
305
|
+
</span>
|
|
306
|
+
)}
|
|
307
|
+
{spanRoute && (
|
|
308
|
+
<Icon
|
|
309
|
+
icon={IconProp.ExternalLink}
|
|
310
|
+
className="h-4 w-4 flex-none text-indigo-300"
|
|
311
|
+
/>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</section>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{prettyAttributes && (
|
|
320
|
+
<section className="space-y-3">
|
|
321
|
+
<header className="flex items-center justify-between text-[11px] uppercase tracking-wide text-slate-500">
|
|
322
|
+
<span>Attributes</span>
|
|
323
|
+
<CopyTextButton
|
|
324
|
+
textToBeCopied={prettyAttributes}
|
|
325
|
+
size="xs"
|
|
326
|
+
variant="ghost"
|
|
327
|
+
iconOnly={false}
|
|
328
|
+
title="Copy attributes"
|
|
329
|
+
/>
|
|
330
|
+
</header>
|
|
331
|
+
<div className={`rounded-lg border ${surfaceCardClass} p-4`}>
|
|
332
|
+
<pre className="max-h-72 overflow-auto whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-slate-100">
|
|
333
|
+
{prettyAttributes}
|
|
334
|
+
</pre>
|
|
335
|
+
</div>
|
|
336
|
+
</section>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export default LogDetailsPanel;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement, ReactNode } from "react";
|
|
2
|
+
import Card from "../../Card/Card";
|
|
3
|
+
import FiltersForm from "../../Filters/FiltersForm";
|
|
4
|
+
import FieldType from "../../Types/FieldType";
|
|
5
|
+
import DropdownUtil from "../../../Utils/Dropdown";
|
|
6
|
+
import LogSeverity from "../../../../Types/Log/LogSeverity";
|
|
7
|
+
import Query from "../../../../Types/BaseDatabase/Query";
|
|
8
|
+
import Log from "../../../../Models/AnalyticsModels/Log";
|
|
9
|
+
|
|
10
|
+
export interface LogsFilterCardProps {
|
|
11
|
+
filterData: Query<Log>;
|
|
12
|
+
onFilterChanged: (filterData: Query<Log>) => void;
|
|
13
|
+
onAdvancedFiltersToggle: (show: boolean) => void;
|
|
14
|
+
isFilterLoading: boolean;
|
|
15
|
+
filterError?: string | undefined;
|
|
16
|
+
onFilterRefreshClick?: (() => void) | undefined;
|
|
17
|
+
logAttributes: Array<string>;
|
|
18
|
+
toolbar: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LogsFilterCard: FunctionComponent<LogsFilterCardProps> = (
|
|
22
|
+
props: LogsFilterCardProps,
|
|
23
|
+
): ReactElement => {
|
|
24
|
+
return (
|
|
25
|
+
<Card>
|
|
26
|
+
<div className="-mt-8">
|
|
27
|
+
<FiltersForm<Log>
|
|
28
|
+
id="logs-filter"
|
|
29
|
+
showFilter={true}
|
|
30
|
+
filterData={props.filterData}
|
|
31
|
+
onFilterChanged={props.onFilterChanged}
|
|
32
|
+
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
|
|
33
|
+
isFilterLoading={props.isFilterLoading}
|
|
34
|
+
filterError={props.filterError}
|
|
35
|
+
onFilterRefreshClick={props.onFilterRefreshClick}
|
|
36
|
+
filters={[
|
|
37
|
+
{
|
|
38
|
+
key: "body",
|
|
39
|
+
type: FieldType.Text,
|
|
40
|
+
title: "Search Log",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "severityText",
|
|
44
|
+
filterDropdownOptions:
|
|
45
|
+
DropdownUtil.getDropdownOptionsFromEnum(LogSeverity),
|
|
46
|
+
type: FieldType.Dropdown,
|
|
47
|
+
title: "Log Severity",
|
|
48
|
+
isAdvancedFilter: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "time",
|
|
52
|
+
type: FieldType.DateTime,
|
|
53
|
+
title: "Start and End Date",
|
|
54
|
+
isAdvancedFilter: true,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "attributes",
|
|
58
|
+
type: FieldType.JSON,
|
|
59
|
+
title: "Filter by Attributes",
|
|
60
|
+
jsonKeys: props.logAttributes,
|
|
61
|
+
isAdvancedFilter: true,
|
|
62
|
+
},
|
|
63
|
+
]}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="-mx-6 -mb-6 border-t border-slate-200 bg-white/60 px-6 py-3 backdrop-blur">
|
|
68
|
+
{props.toolbar}
|
|
69
|
+
</div>
|
|
70
|
+
</Card>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default LogsFilterCard;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export interface LogsPaginationProps {
|
|
4
|
+
currentPage: number;
|
|
5
|
+
totalItems: number;
|
|
6
|
+
pageSize: number;
|
|
7
|
+
pageSizeOptions: Array<number>;
|
|
8
|
+
onPageChange: (page: number) => void;
|
|
9
|
+
onPageSizeChange: (size: number) => void;
|
|
10
|
+
isDisabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LogsPagination: FunctionComponent<LogsPaginationProps> = (
|
|
14
|
+
props: LogsPaginationProps,
|
|
15
|
+
): ReactElement => {
|
|
16
|
+
const totalPages: number = Math.max(
|
|
17
|
+
1,
|
|
18
|
+
Math.ceil(
|
|
19
|
+
props.totalItems === 0
|
|
20
|
+
? 1
|
|
21
|
+
: props.totalItems / Math.max(props.pageSize, 1),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const safeCurrentPage: number = Math.min(props.currentPage, totalPages);
|
|
26
|
+
|
|
27
|
+
const firstItemIndex: number =
|
|
28
|
+
props.totalItems === 0 ? 0 : (safeCurrentPage - 1) * props.pageSize + 1;
|
|
29
|
+
const lastItemIndex: number =
|
|
30
|
+
props.totalItems === 0
|
|
31
|
+
? 0
|
|
32
|
+
: Math.min(props.totalItems, safeCurrentPage * props.pageSize);
|
|
33
|
+
|
|
34
|
+
const disablePrev: boolean =
|
|
35
|
+
props.isDisabled || props.totalItems === 0 || safeCurrentPage <= 1;
|
|
36
|
+
const disableNext: boolean =
|
|
37
|
+
props.isDisabled || props.totalItems === 0 || safeCurrentPage >= totalPages;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex flex-col gap-3 border-t border-slate-800 bg-slate-950/60 px-4 py-3 text-xs text-slate-400 md:flex-row md:items-center md:justify-between">
|
|
41
|
+
<div>
|
|
42
|
+
{props.totalItems === 0 ? (
|
|
43
|
+
<span>No results to display.</span>
|
|
44
|
+
) : (
|
|
45
|
+
<span>
|
|
46
|
+
Showing {firstItemIndex.toLocaleString()}-
|
|
47
|
+
{lastItemIndex.toLocaleString()} of{" "}
|
|
48
|
+
{props.totalItems.toLocaleString()}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
54
|
+
<label className="flex items-center gap-2 text-slate-500">
|
|
55
|
+
<span className="uppercase tracking-wide text-[10px]">Rows</span>
|
|
56
|
+
<select
|
|
57
|
+
className="rounded-md border border-slate-700 bg-slate-900/80 px-2 py-1 text-xs text-slate-200 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
58
|
+
value={props.pageSize}
|
|
59
|
+
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
60
|
+
const size: number = Number(event.target.value) || props.pageSize;
|
|
61
|
+
props.onPageSizeChange(size);
|
|
62
|
+
}}
|
|
63
|
+
disabled={props.isDisabled}
|
|
64
|
+
>
|
|
65
|
+
{props.pageSizeOptions.map((option: number) => {
|
|
66
|
+
return (
|
|
67
|
+
<option key={option} value={option}>
|
|
68
|
+
{option}
|
|
69
|
+
</option>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</select>
|
|
73
|
+
</label>
|
|
74
|
+
|
|
75
|
+
<div className="inline-flex items-center gap-1 rounded-full border border-slate-800 bg-slate-900/70 p-0.5">
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
className="rounded-full px-3 py-1 text-xs font-medium text-slate-300 transition-colors hover:bg-slate-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-40"
|
|
79
|
+
onClick={() => {
|
|
80
|
+
if (!disablePrev) {
|
|
81
|
+
props.onPageChange(Math.max(1, safeCurrentPage - 1));
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
disabled={disablePrev}
|
|
85
|
+
>
|
|
86
|
+
Previous
|
|
87
|
+
</button>
|
|
88
|
+
<span className="px-3 text-[11px] uppercase tracking-wide text-slate-500">
|
|
89
|
+
Page {safeCurrentPage} / {totalPages}
|
|
90
|
+
</span>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
className="rounded-full px-3 py-1 text-xs font-medium text-slate-300 transition-colors hover:bg-slate-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-40"
|
|
94
|
+
onClick={() => {
|
|
95
|
+
if (!disableNext) {
|
|
96
|
+
props.onPageChange(Math.min(totalPages, safeCurrentPage + 1));
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
disabled={disableNext}
|
|
100
|
+
>
|
|
101
|
+
Next
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default LogsPagination;
|