mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: add Log Stream and Trace List components to dashboard with configuration options
This commit is contained in:
@@ -4,12 +4,16 @@ import DashboardChartComponentType from "Common/Types/Dashboard/DashboardCompone
|
||||
import DashboardValueComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardValueComponent";
|
||||
import DashboardTableComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardTableComponent";
|
||||
import DashboardGaugeComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardGaugeComponent";
|
||||
import DashboardLogStreamComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardLogStreamComponent";
|
||||
import DashboardTraceListComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardTraceListComponent";
|
||||
import DashboardBaseComponent from "Common/Types/Dashboard/DashboardComponents/DashboardBaseComponent";
|
||||
import DashboardChartComponent from "./DashboardChartComponent";
|
||||
import DashboardValueComponent from "./DashboardValueComponent";
|
||||
import DashboardTextComponent from "./DashboardTextComponent";
|
||||
import DashboardTableComponent from "./DashboardTableComponent";
|
||||
import DashboardGaugeComponent from "./DashboardGaugeComponent";
|
||||
import DashboardLogStreamComponent from "./DashboardLogStreamComponent";
|
||||
import DashboardTraceListComponent from "./DashboardTraceListComponent";
|
||||
import DefaultDashboardSize, {
|
||||
GetDashboardComponentHeightInDashboardUnits,
|
||||
GetDashboardComponentWidthInDashboardUnits,
|
||||
@@ -439,6 +443,22 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
|
||||
component={component as DashboardGaugeComponentType}
|
||||
/>
|
||||
)}
|
||||
{component.componentType === DashboardComponentType.LogStream && (
|
||||
<DashboardLogStreamComponent
|
||||
{...props}
|
||||
isEditMode={props.isEditMode}
|
||||
isSelected={props.isSelected}
|
||||
component={component as DashboardLogStreamComponentType}
|
||||
/>
|
||||
)}
|
||||
{component.componentType === DashboardComponentType.TraceList && (
|
||||
<DashboardTraceListComponent
|
||||
{...props}
|
||||
isEditMode={props.isEditMode}
|
||||
isSelected={props.isSelected}
|
||||
component={component as DashboardTraceListComponentType}
|
||||
/>
|
||||
)}
|
||||
|
||||
{getResizeWidthElement()}
|
||||
{getResizeHeightElement()}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from "react";
|
||||
import DashboardLogStreamComponent from "Common/Types/Dashboard/DashboardComponents/DashboardLogStreamComponent";
|
||||
import { DashboardBaseComponentProps } from "./DashboardBaseComponent";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AnalyticsModelAPI, {
|
||||
ListResult,
|
||||
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
|
||||
import Log from "Common/Models/AnalyticsModels/Log";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import Icon from "Common/UI/Components/Icon/Icon";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { RangeStartAndEndDateTimeUtil } from "Common/Types/Time/RangeStartAndEndDateTime";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
|
||||
export interface ComponentProps extends DashboardBaseComponentProps {
|
||||
component: DashboardLogStreamComponent;
|
||||
}
|
||||
|
||||
type SeverityColor = {
|
||||
dot: string;
|
||||
text: string;
|
||||
bg: string;
|
||||
};
|
||||
|
||||
const getSeverityColor: (severity: string) => SeverityColor = (
|
||||
severity: string,
|
||||
): SeverityColor => {
|
||||
const lower: string = severity.toLowerCase();
|
||||
if (lower === "fatal") {
|
||||
return { dot: "bg-purple-500", text: "text-purple-700", bg: "bg-purple-50" };
|
||||
}
|
||||
if (lower === "error") {
|
||||
return { dot: "bg-red-500", text: "text-red-700", bg: "bg-red-50" };
|
||||
}
|
||||
if (lower === "warning") {
|
||||
return { dot: "bg-yellow-500", text: "text-yellow-700", bg: "bg-yellow-50" };
|
||||
}
|
||||
if (lower === "information") {
|
||||
return { dot: "bg-blue-500", text: "text-blue-700", bg: "bg-blue-50" };
|
||||
}
|
||||
if (lower === "debug") {
|
||||
return { dot: "bg-gray-400", text: "text-gray-600", bg: "bg-gray-50" };
|
||||
}
|
||||
if (lower === "trace") {
|
||||
return { dot: "bg-gray-300", text: "text-gray-500", bg: "bg-gray-50" };
|
||||
}
|
||||
return { dot: "bg-gray-300", text: "text-gray-500", bg: "bg-gray-50" };
|
||||
};
|
||||
|
||||
const DashboardLogStreamComponentElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [logs, setLogs] = React.useState<Array<Log>>([]);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
const maxRows: number = props.component.arguments.maxRows || 50;
|
||||
|
||||
const fetchLogs: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
const startAndEndDate: InBetween<Date> =
|
||||
RangeStartAndEndDateTimeUtil.getStartAndEndDate(
|
||||
props.dashboardStartAndEndDate,
|
||||
);
|
||||
|
||||
if (!startAndEndDate.startValue || !startAndEndDate.endValue) {
|
||||
setIsLoading(false);
|
||||
setError("Please select a valid start and end date.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const query: Query<Log> = {
|
||||
time: new InBetween<Date>(
|
||||
startAndEndDate.startValue,
|
||||
startAndEndDate.endValue,
|
||||
),
|
||||
} as Query<Log>;
|
||||
|
||||
// Add severity filter if set
|
||||
if (
|
||||
props.component.arguments.severityFilter &&
|
||||
props.component.arguments.severityFilter !== ""
|
||||
) {
|
||||
(query as Record<string, unknown>)["severityText"] =
|
||||
props.component.arguments.severityFilter;
|
||||
}
|
||||
|
||||
// Add body contains filter if set
|
||||
if (
|
||||
props.component.arguments.bodyContains &&
|
||||
props.component.arguments.bodyContains.trim() !== ""
|
||||
) {
|
||||
(query as Record<string, unknown>)["body"] =
|
||||
props.component.arguments.bodyContains.trim();
|
||||
}
|
||||
|
||||
const listResult: ListResult<Log> =
|
||||
await AnalyticsModelAPI.getList<Log>({
|
||||
modelType: Log,
|
||||
query: query,
|
||||
limit: maxRows,
|
||||
skip: 0,
|
||||
select: {
|
||||
time: true,
|
||||
severityText: true,
|
||||
body: true,
|
||||
serviceId: true,
|
||||
traceId: true,
|
||||
spanId: true,
|
||||
},
|
||||
sort: {
|
||||
time: SortOrder.Descending,
|
||||
},
|
||||
requestOptions: {},
|
||||
});
|
||||
|
||||
setLogs(listResult.data);
|
||||
setError("");
|
||||
} catch (err: unknown) {
|
||||
setError(API.getFriendlyErrorMessage(err as Error));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [props.dashboardStartAndEndDate, props.refreshTick]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [
|
||||
props.component.arguments.severityFilter,
|
||||
props.component.arguments.bodyContains,
|
||||
props.component.arguments.maxRows,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex flex-col animate-pulse">
|
||||
<div className="h-3 w-24 bg-gray-100 rounded mb-3"></div>
|
||||
<div className="flex-1 space-y-2">
|
||||
{Array.from({ length: 6 }).map((_: unknown, i: number) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex gap-2 items-center"
|
||||
style={{ opacity: 1 - i * 0.12 }}
|
||||
>
|
||||
<div className="w-1.5 h-1.5 bg-gray-200 rounded-full"></div>
|
||||
<div className="h-3 w-16 bg-gray-100 rounded"></div>
|
||||
<div
|
||||
className="h-3 bg-gray-50 rounded flex-1"
|
||||
style={{ maxWidth: `${40 + Math.random() * 50}%` }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-50 flex items-center justify-center">
|
||||
<div className="h-5 w-5 text-gray-300">
|
||||
<Icon icon={IconProp.List} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 text-center max-w-48">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto flex flex-col">
|
||||
{props.component.arguments.title && (
|
||||
<div className="flex items-center justify-between mb-2 px-1">
|
||||
<span className="text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
{props.component.arguments.title}
|
||||
</span>
|
||||
<span className="text-xs text-gray-300 tabular-nums">
|
||||
{logs.length} entries
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-auto rounded-md border border-gray-100">
|
||||
<div className="divide-y divide-gray-50">
|
||||
{logs.map((log: Log, index: number) => {
|
||||
const severity: string =
|
||||
(log.severityText as string) || "Unspecified";
|
||||
const colors: SeverityColor = getSeverityColor(severity);
|
||||
const body: string = (log.body as string) || "";
|
||||
const time: Date | undefined = log.time
|
||||
? OneUptimeDate.fromString(log.time as string)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-2 px-3 py-1.5 hover:bg-gray-50/50 transition-colors duration-100 group"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 shrink-0 mt-0.5">
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full ${colors.dot}`}
|
||||
></div>
|
||||
<span
|
||||
className={`text-xs font-medium ${colors.text} ${colors.bg} px-1 py-0.5 rounded w-12 text-center`}
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
{severity.substring(0, 4).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
{time && (
|
||||
<span
|
||||
className="text-xs text-gray-400 shrink-0 tabular-nums"
|
||||
style={{ fontSize: "11px" }}
|
||||
>
|
||||
{OneUptimeDate.getDateAsLocalFormattedString(time, true)}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="text-xs text-gray-600 truncate flex-1 font-mono"
|
||||
style={{ fontSize: "11px" }}
|
||||
>
|
||||
{body}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{logs.length === 0 && (
|
||||
<div className="px-4 py-8 text-center text-gray-400 text-sm">
|
||||
No logs found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardLogStreamComponentElement;
|
||||
@@ -0,0 +1,294 @@
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from "react";
|
||||
import DashboardTraceListComponent from "Common/Types/Dashboard/DashboardComponents/DashboardTraceListComponent";
|
||||
import { DashboardBaseComponentProps } from "./DashboardBaseComponent";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AnalyticsModelAPI, {
|
||||
ListResult,
|
||||
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
|
||||
import Span, { SpanStatus } from "Common/Models/AnalyticsModels/Span";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import Icon from "Common/UI/Components/Icon/Icon";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { RangeStartAndEndDateTimeUtil } from "Common/Types/Time/RangeStartAndEndDateTime";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
|
||||
export interface ComponentProps extends DashboardBaseComponentProps {
|
||||
component: DashboardTraceListComponent;
|
||||
}
|
||||
|
||||
type StatusStyle = {
|
||||
label: string;
|
||||
textClass: string;
|
||||
bgClass: string;
|
||||
};
|
||||
|
||||
const getStatusStyle: (statusCode: number) => StatusStyle = (
|
||||
statusCode: number,
|
||||
): StatusStyle => {
|
||||
if (statusCode === SpanStatus.Error) {
|
||||
return {
|
||||
label: "Error",
|
||||
textClass: "text-red-700",
|
||||
bgClass: "bg-red-50 border-red-100",
|
||||
};
|
||||
}
|
||||
if (statusCode === SpanStatus.Ok) {
|
||||
return {
|
||||
label: "Ok",
|
||||
textClass: "text-green-700",
|
||||
bgClass: "bg-green-50 border-green-100",
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: "Unset",
|
||||
textClass: "text-gray-500",
|
||||
bgClass: "bg-gray-50 border-gray-100",
|
||||
};
|
||||
};
|
||||
|
||||
const formatDuration: (durationNano: number) => string = (
|
||||
durationNano: number,
|
||||
): string => {
|
||||
if (durationNano < 1000) {
|
||||
return `${durationNano}ns`;
|
||||
}
|
||||
const durationMicro: number = durationNano / 1000;
|
||||
if (durationMicro < 1000) {
|
||||
return `${Math.round(durationMicro)}µs`;
|
||||
}
|
||||
const durationMs: number = durationMicro / 1000;
|
||||
if (durationMs < 1000) {
|
||||
return `${Math.round(durationMs * 10) / 10}ms`;
|
||||
}
|
||||
const durationS: number = durationMs / 1000;
|
||||
return `${Math.round(durationS * 100) / 100}s`;
|
||||
};
|
||||
|
||||
const DashboardTraceListComponentElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [spans, setSpans] = React.useState<Array<Span>>([]);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
const maxRows: number = props.component.arguments.maxRows || 50;
|
||||
|
||||
const fetchTraces: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
const startAndEndDate: InBetween<Date> =
|
||||
RangeStartAndEndDateTimeUtil.getStartAndEndDate(
|
||||
props.dashboardStartAndEndDate,
|
||||
);
|
||||
|
||||
if (!startAndEndDate.startValue || !startAndEndDate.endValue) {
|
||||
setIsLoading(false);
|
||||
setError("Please select a valid start and end date.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const query: Query<Span> = {
|
||||
startTime: new InBetween<Date>(
|
||||
startAndEndDate.startValue,
|
||||
startAndEndDate.endValue,
|
||||
),
|
||||
} as Query<Span>;
|
||||
|
||||
// Add status filter if set
|
||||
if (
|
||||
props.component.arguments.statusFilter &&
|
||||
props.component.arguments.statusFilter !== ""
|
||||
) {
|
||||
(query as Record<string, unknown>)["statusCode"] = parseInt(
|
||||
props.component.arguments.statusFilter,
|
||||
);
|
||||
}
|
||||
|
||||
const listResult: ListResult<Span> =
|
||||
await AnalyticsModelAPI.getList<Span>({
|
||||
modelType: Span,
|
||||
query: query,
|
||||
limit: maxRows,
|
||||
skip: 0,
|
||||
select: {
|
||||
startTime: true,
|
||||
name: true,
|
||||
statusCode: true,
|
||||
durationUnixNano: true,
|
||||
traceId: true,
|
||||
spanId: true,
|
||||
kind: true,
|
||||
serviceId: true,
|
||||
},
|
||||
sort: {
|
||||
startTime: SortOrder.Descending,
|
||||
},
|
||||
requestOptions: {},
|
||||
});
|
||||
|
||||
setSpans(listResult.data);
|
||||
setError("");
|
||||
} catch (err: unknown) {
|
||||
setError(API.getFriendlyErrorMessage(err as Error));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTraces();
|
||||
}, [props.dashboardStartAndEndDate, props.refreshTick]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTraces();
|
||||
}, [
|
||||
props.component.arguments.statusFilter,
|
||||
props.component.arguments.maxRows,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex flex-col animate-pulse">
|
||||
<div className="h-3 w-24 bg-gray-100 rounded mb-3"></div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="h-3 w-32 bg-gray-100 rounded"></div>
|
||||
<div className="h-3 w-16 bg-gray-100 rounded"></div>
|
||||
<div className="h-3 w-12 bg-gray-100 rounded ml-auto"></div>
|
||||
</div>
|
||||
{Array.from({ length: 5 }).map((_: unknown, i: number) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex gap-4"
|
||||
style={{ opacity: 1 - i * 0.15 }}
|
||||
>
|
||||
<div className="h-3 w-28 bg-gray-50 rounded"></div>
|
||||
<div className="h-3 w-14 bg-gray-50 rounded"></div>
|
||||
<div className="h-3 w-10 bg-gray-50 rounded ml-auto"></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-50 flex items-center justify-center">
|
||||
<div className="h-5 w-5 text-gray-300">
|
||||
<Icon icon={IconProp.Activity} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 text-center max-w-48">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto flex flex-col">
|
||||
{props.component.arguments.title && (
|
||||
<div className="flex items-center justify-between mb-2 px-1">
|
||||
<span className="text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
{props.component.arguments.title}
|
||||
</span>
|
||||
<span className="text-xs text-gray-300 tabular-nums">
|
||||
{spans.length} traces
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-auto rounded-md border border-gray-100">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-gray-400 uppercase bg-gray-50/80 sticky top-0 border-b border-gray-100">
|
||||
<tr>
|
||||
<th
|
||||
className="px-3 py-2.5 font-medium tracking-wider"
|
||||
style={{ width: "35%" }}
|
||||
>
|
||||
Span Name
|
||||
</th>
|
||||
<th
|
||||
className="px-3 py-2.5 font-medium tracking-wider"
|
||||
style={{ width: "20%" }}
|
||||
>
|
||||
Duration
|
||||
</th>
|
||||
<th
|
||||
className="px-3 py-2.5 font-medium tracking-wider"
|
||||
style={{ width: "15%" }}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
className="px-3 py-2.5 font-medium tracking-wider"
|
||||
style={{ width: "30%" }}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{spans.map((span: Span, index: number) => {
|
||||
const statusCode: number =
|
||||
(span.statusCode as number) || SpanStatus.Unset;
|
||||
const statusStyle: StatusStyle = getStatusStyle(statusCode);
|
||||
const durationNano: number =
|
||||
(span.durationUnixNano as number) || 0;
|
||||
const startTime: Date | undefined = span.startTime
|
||||
? OneUptimeDate.fromString(span.startTime as string)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
className="hover:bg-gray-50/50 transition-colors duration-100 group"
|
||||
>
|
||||
<td className="px-3 py-2 text-xs text-gray-700 font-mono truncate">
|
||||
{(span.name as string) || "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-gray-600 tabular-nums font-medium">
|
||||
{formatDuration(durationNano)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<span
|
||||
className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium border ${statusStyle.textClass} ${statusStyle.bgClass}`}
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-gray-500 tabular-nums">
|
||||
{startTime
|
||||
? OneUptimeDate.getDateAsLocalFormattedString(
|
||||
startTime,
|
||||
true,
|
||||
)
|
||||
: "—"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{spans.length === 0 && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="px-4 py-8 text-center text-gray-400 text-sm"
|
||||
>
|
||||
No traces found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardTraceListComponentElement;
|
||||
@@ -21,6 +21,8 @@ import DashboardValueComponentUtil from "Common/Utils/Dashboard/Components/Dashb
|
||||
import DashboardTextComponentUtil from "Common/Utils/Dashboard/Components/DashboardTextComponent";
|
||||
import DashboardTableComponentUtil from "Common/Utils/Dashboard/Components/DashboardTableComponent";
|
||||
import DashboardGaugeComponentUtil from "Common/Utils/Dashboard/Components/DashboardGaugeComponent";
|
||||
import DashboardLogStreamComponentUtil from "Common/Utils/Dashboard/Components/DashboardLogStreamComponent";
|
||||
import DashboardTraceListComponentUtil from "Common/Utils/Dashboard/Components/DashboardTraceListComponent";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Dashboard from "Common/Models/DatabaseModels/Dashboard";
|
||||
@@ -378,6 +380,16 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
|
||||
newComponent = DashboardGaugeComponentUtil.getDefaultComponent();
|
||||
}
|
||||
|
||||
if (componentType === DashboardComponentType.LogStream) {
|
||||
newComponent =
|
||||
DashboardLogStreamComponentUtil.getDefaultComponent();
|
||||
}
|
||||
|
||||
if (componentType === DashboardComponentType.TraceList) {
|
||||
newComponent =
|
||||
DashboardTraceListComponentUtil.getDefaultComponent();
|
||||
}
|
||||
|
||||
if (!newComponent) {
|
||||
throw new BadDataException(
|
||||
`Unknown component type: ${componentType}`,
|
||||
|
||||
@@ -135,6 +135,24 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
|
||||
props.onAddComponentClick(DashboardComponentType.Gauge);
|
||||
}}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
text={"Log Stream"}
|
||||
key={"add-log-stream"}
|
||||
onClick={() => {
|
||||
props.onAddComponentClick(
|
||||
DashboardComponentType.LogStream,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
text={"Trace List"}
|
||||
key={"add-trace-list"}
|
||||
onClick={() => {
|
||||
props.onAddComponentClick(
|
||||
DashboardComponentType.TraceList,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</MoreMenu>
|
||||
|
||||
<div className="w-px h-6 bg-gray-200 mx-1"></div>
|
||||
|
||||
@@ -4,6 +4,8 @@ enum DashboardComponentType {
|
||||
Text = `Text`,
|
||||
Table = `Table`,
|
||||
Gauge = `Gauge`,
|
||||
LogStream = `LogStream`,
|
||||
TraceList = `TraceList`,
|
||||
}
|
||||
|
||||
export default DashboardComponentType;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import ObjectID from "../../ObjectID";
|
||||
import DashboardComponentType from "../DashboardComponentType";
|
||||
import BaseComponent from "./DashboardBaseComponent";
|
||||
|
||||
export default interface DashboardLogStreamComponent extends BaseComponent {
|
||||
componentType: DashboardComponentType.LogStream;
|
||||
componentId: ObjectID;
|
||||
arguments: {
|
||||
title?: string | undefined;
|
||||
severityFilter?: string | undefined;
|
||||
bodyContains?: string | undefined;
|
||||
maxRows?: number | undefined;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import ObjectID from "../../ObjectID";
|
||||
import DashboardComponentType from "../DashboardComponentType";
|
||||
import BaseComponent from "./DashboardBaseComponent";
|
||||
|
||||
export default interface DashboardTraceListComponent extends BaseComponent {
|
||||
componentType: DashboardComponentType.TraceList;
|
||||
componentId: ObjectID;
|
||||
arguments: {
|
||||
title?: string | undefined;
|
||||
statusFilter?: string | undefined;
|
||||
maxRows?: number | undefined;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import DashboardLogStreamComponent from "../../../Types/Dashboard/DashboardComponents/DashboardLogStreamComponent";
|
||||
import { ObjectType } from "../../../Types/JSON";
|
||||
import ObjectID from "../../../Types/ObjectID";
|
||||
import DashboardBaseComponentUtil from "./DashboardBaseComponent";
|
||||
import {
|
||||
ComponentArgument,
|
||||
ComponentInputType,
|
||||
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
|
||||
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
|
||||
|
||||
export default class DashboardLogStreamComponentUtil extends DashboardBaseComponentUtil {
|
||||
public static override getDefaultComponent(): DashboardLogStreamComponent {
|
||||
return {
|
||||
_type: ObjectType.DashboardComponent,
|
||||
componentType: DashboardComponentType.LogStream,
|
||||
widthInDashboardUnits: 6,
|
||||
heightInDashboardUnits: 4,
|
||||
topInDashboardUnits: 0,
|
||||
leftInDashboardUnits: 0,
|
||||
componentId: ObjectID.generate(),
|
||||
minHeightInDashboardUnits: 3,
|
||||
minWidthInDashboardUnits: 6,
|
||||
arguments: {
|
||||
maxRows: 50,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static override getComponentConfigArguments(): Array<
|
||||
ComponentArgument<DashboardLogStreamComponent>
|
||||
> {
|
||||
const componentArguments: Array<
|
||||
ComponentArgument<DashboardLogStreamComponent>
|
||||
> = [];
|
||||
|
||||
componentArguments.push({
|
||||
name: "Title",
|
||||
description: "The title of the log stream widget",
|
||||
required: false,
|
||||
type: ComponentInputType.Text,
|
||||
id: "title",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Severity Filter",
|
||||
description: "Filter logs by severity level",
|
||||
required: false,
|
||||
type: ComponentInputType.Dropdown,
|
||||
id: "severityFilter",
|
||||
dropdownOptions: [
|
||||
{ label: "All", value: "" },
|
||||
{ label: "Trace", value: "Trace" },
|
||||
{ label: "Debug", value: "Debug" },
|
||||
{ label: "Information", value: "Information" },
|
||||
{ label: "Warning", value: "Warning" },
|
||||
{ label: "Error", value: "Error" },
|
||||
{ label: "Fatal", value: "Fatal" },
|
||||
],
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Body Contains",
|
||||
description: "Filter logs where the body contains this text",
|
||||
required: false,
|
||||
type: ComponentInputType.Text,
|
||||
id: "bodyContains",
|
||||
placeholder: "Search text...",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Max Rows",
|
||||
description: "Maximum number of log entries to display",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "maxRows",
|
||||
placeholder: "50",
|
||||
});
|
||||
|
||||
return componentArguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import DashboardTraceListComponent from "../../../Types/Dashboard/DashboardComponents/DashboardTraceListComponent";
|
||||
import { ObjectType } from "../../../Types/JSON";
|
||||
import ObjectID from "../../../Types/ObjectID";
|
||||
import DashboardBaseComponentUtil from "./DashboardBaseComponent";
|
||||
import {
|
||||
ComponentArgument,
|
||||
ComponentInputType,
|
||||
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
|
||||
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
|
||||
|
||||
export default class DashboardTraceListComponentUtil extends DashboardBaseComponentUtil {
|
||||
public static override getDefaultComponent(): DashboardTraceListComponent {
|
||||
return {
|
||||
_type: ObjectType.DashboardComponent,
|
||||
componentType: DashboardComponentType.TraceList,
|
||||
widthInDashboardUnits: 6,
|
||||
heightInDashboardUnits: 4,
|
||||
topInDashboardUnits: 0,
|
||||
leftInDashboardUnits: 0,
|
||||
componentId: ObjectID.generate(),
|
||||
minHeightInDashboardUnits: 3,
|
||||
minWidthInDashboardUnits: 6,
|
||||
arguments: {
|
||||
maxRows: 50,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static override getComponentConfigArguments(): Array<
|
||||
ComponentArgument<DashboardTraceListComponent>
|
||||
> {
|
||||
const componentArguments: Array<
|
||||
ComponentArgument<DashboardTraceListComponent>
|
||||
> = [];
|
||||
|
||||
componentArguments.push({
|
||||
name: "Title",
|
||||
description: "The title of the trace list widget",
|
||||
required: false,
|
||||
type: ComponentInputType.Text,
|
||||
id: "title",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Status Filter",
|
||||
description: "Filter traces by status",
|
||||
required: false,
|
||||
type: ComponentInputType.Dropdown,
|
||||
id: "statusFilter",
|
||||
dropdownOptions: [
|
||||
{ label: "All", value: "" },
|
||||
{ label: "Ok", value: "1" },
|
||||
{ label: "Error", value: "2" },
|
||||
{ label: "Unset", value: "0" },
|
||||
],
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Max Rows",
|
||||
description: "Maximum number of traces to display",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "maxRows",
|
||||
placeholder: "50",
|
||||
});
|
||||
|
||||
return componentArguments;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentT
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
import DashboardChartComponentUtil from "./DashboardChartComponent";
|
||||
import DashboardGaugeComponentUtil from "./DashboardGaugeComponent";
|
||||
import DashboardLogStreamComponentUtil from "./DashboardLogStreamComponent";
|
||||
import DashboardTableComponentUtil from "./DashboardTableComponent";
|
||||
import DashboardTextComponentUtil from "./DashboardTextComponent";
|
||||
import DashboardTraceListComponentUtil from "./DashboardTraceListComponent";
|
||||
import DashboardValueComponentUtil from "./DashboardValueComponent";
|
||||
|
||||
export default class DashboardComponentsUtil {
|
||||
@@ -42,6 +44,18 @@ export default class DashboardComponentsUtil {
|
||||
>;
|
||||
}
|
||||
|
||||
if (dashboardComponentType === DashboardComponentType.LogStream) {
|
||||
return DashboardLogStreamComponentUtil.getComponentConfigArguments() as Array<
|
||||
ComponentArgument<DashboardBaseComponent>
|
||||
>;
|
||||
}
|
||||
|
||||
if (dashboardComponentType === DashboardComponentType.TraceList) {
|
||||
return DashboardTraceListComponentUtil.getComponentConfigArguments() as Array<
|
||||
ComponentArgument<DashboardBaseComponent>
|
||||
>;
|
||||
}
|
||||
|
||||
throw new BadDataException(
|
||||
`Unknown dashboard component type: ${dashboardComponentType}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user