diff --git a/client/package-lock.json b/client/package-lock.json
index 145cf5045..84fc4c3a6 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -26,6 +26,7 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^7.0.0",
+ "pretty-bytes": "^7.1.0",
"pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
@@ -5265,6 +5266,18 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/pretty-bytes": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz",
+ "integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/pretty-ms": {
"version": "9.3.0",
"license": "MIT",
@@ -6737,7 +6750,6 @@
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"devOptional": true,
"license": "ISC",
- "peer": true,
"bin": {
"yaml": "bin.mjs"
},
diff --git a/client/package.json b/client/package.json
index caf4d0856..f0b34dbbc 100644
--- a/client/package.json
+++ b/client/package.json
@@ -34,6 +34,7 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^7.0.0",
+ "pretty-bytes": "^7.1.0",
"pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
diff --git a/client/src/Components/v2/design-elements/Gauge.tsx b/client/src/Components/v2/design-elements/Gauge.tsx
index 34c57487d..4fee5386c 100644
--- a/client/src/Components/v2/design-elements/Gauge.tsx
+++ b/client/src/Components/v2/design-elements/Gauge.tsx
@@ -1,3 +1,5 @@
+import { BaseChart } from "@/Components/v2/design-elements";
+import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
@@ -104,3 +106,50 @@ export const Gauge = ({
);
};
+
+export const DetailGauge = ({
+ title,
+ progress,
+ upperLabel,
+ upperValue,
+ lowerLabel,
+ lowerValue,
+}: {
+ title: string;
+ progress: number;
+ upperLabel?: string;
+ upperValue?: string | number;
+ lowerLabel?: string;
+ lowerValue?: string | number;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+ {upperLabel}
+ {upperValue}
+
+
+ {lowerLabel}
+ {lowerValue}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx
index aea8b40a0..35cd4512f 100644
--- a/client/src/Components/v2/design-elements/Table.tsx
+++ b/client/src/Components/v2/design-elements/Table.tsx
@@ -29,6 +29,7 @@ import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
import { useTranslation } from "react-i18next";
import { useState, Fragment, useEffect } from "react";
import { useTheme } from "@mui/material/styles";
+import type { SxProps, Theme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
export type Header = {
@@ -47,6 +48,7 @@ type DataTableProps =
renderExpandedContent?: (row: T) => React.ReactNode;
emptyViewText?: string;
emptyViewPositive?: boolean;
+ getRowSx?: (row: T) => SxProps;
};
export function DataTable<
@@ -64,6 +66,7 @@ export function DataTable<
renderExpandedContent,
emptyViewText,
emptyViewPositive,
+ getRowSx,
}: DataTableProps) {
const theme = useTheme();
const [expanded, setExpanded] = useState<(string | number) | null>(null);
@@ -204,6 +207,7 @@ export function DataTable<
{
if (expandableRows) handleExpand(row);
diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js
index 65aff99a8..014940a9a 100644
--- a/client/src/Features/UI/uiSlice.js
+++ b/client/src/Features/UI/uiSlice.js
@@ -19,6 +19,9 @@ const initialState = {
infrastructure: {
rowsPerPage: 5,
},
+ logs: {
+ rowsPerPage: 15,
+ },
sidebar: {
collapsed: false,
},
@@ -41,9 +44,10 @@ const uiSlice = createSlice({
},
setRowsPerPage: (state, action) => {
const { table, value } = action.payload;
- if (state[table]) {
- state[table].rowsPerPage = value;
+ if (!state[table]) {
+ state[table] = {};
}
+ state[table].rowsPerPage = value;
},
toggleSidebar: (state) => {
state.sidebar.collapsed = !state.sidebar.collapsed;
diff --git a/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx b/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx
index a72530db4..7e508a545 100644
--- a/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx
+++ b/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx
@@ -1,6 +1,5 @@
import Stack from "@mui/material/Stack";
-import Typography from "@mui/material/Typography";
-import { BaseChart, Gauge } from "@/Components/v2/design-elements";
+import { DetailGauge } from "@/Components/v2/design-elements";
import { useTranslation } from "react-i18next";
import { getGbs, getFrequency } from "@/Utils/InfraUtils";
@@ -8,53 +7,6 @@ import { useTheme } from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { CheckSnapshot } from "@/Types/Check";
-const InfraDetailGauge = ({
- title,
- progress,
- upperLabel,
- upperValue,
- lowerLabel,
- lowerValue,
-}: {
- title: string;
- progress: number;
- upperLabel?: string;
- upperValue?: string | number;
- lowerLabel?: string;
- lowerValue?: string | number;
-}) => {
- const theme = useTheme();
- return (
-
-
-
-
-
- {upperLabel}
- {upperValue}
-
-
- {lowerLabel}
- {lowerValue}
-
-
- );
-};
-
export const InfraDetailsGauges = ({
snapshot,
}: {
@@ -74,7 +26,7 @@ export const InfraDetailsGauges = ({
spacing={theme.spacing(8)}
alignItems={"center"}
>
-
-
{snapshot?.disk?.map((disk, idx) => {
return (
- {
- const theme = useTheme()
- return(
-
- {children}
-
- );
-};
-
-const InfrastructureStyleGauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => {
- const theme = useTheme();
-
- const MetricRow = ({ label, value }) => (
-
- {label}
-
- {value}
-
-
- );
-
- return(
-
-
-
-
-
- {heading}
-
-
-
-
- {metricTwo && valueTwo && (
-
- )}
-
-
-
- );
-};
-
-const Gauges = ({ diagnostics, isLoading }) => {
- const heapTotalSize = getPercentage(
- diagnostics?.v8HeapStats?.totalHeapSizeBytes,
- diagnostics?.v8HeapStats?.heapSizeLimitBytes
- );
-
- const heapUsedSize = getPercentage(
- diagnostics?.v8HeapStats?.usedHeapSizeBytes,
- diagnostics?.v8HeapStats?.heapSizeLimitBytes
- );
-
- const actualHeapUsed = getPercentage(
- diagnostics?.v8HeapStats?.usedHeapSizeBytes,
- diagnostics?.v8HeapStats?.totalHeapSizeBytes
- );
-
- const theme = useTheme();
- const { t } = useTranslation();
-
- return (
-
-
-
-
-
-
- );
-};
-
-Gauges.propTypes = {
- diagnostics: PropTypes.object,
- isLoading: PropTypes.bool,
-};
-
-InfrastructureStyleGauge.propTypes = {
- value: PropTypes.number,
- heading: PropTypes.string,
- metricOne: PropTypes.string,
- valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
- metricTwo: PropTypes.string,
- valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
-};
-
-BaseContainer.propTypes = {
- children: PropTypes.node.isRequired,
-};
-
-export default Gauges;
diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx
deleted file mode 100644
index 13ed63618..000000000
--- a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Typography from "@mui/material/Typography";
-import Card from "@mui/material/Card";
-import CardContent from "@mui/material/CardContent";
-import CircularProgress from "@mui/material/CircularProgress";
-
-import { useTheme } from "@emotion/react";
-import PropTypes from "prop-types";
-
-import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js";
-import { formatBytes } from "../../utils/utils.js";
-import { useTranslation } from "react-i18next";
-
-const StatsCard = ({ title, value, unit = "", isLoading }) => {
- const theme = useTheme();
- return (
-
- {isLoading ? (
-
-
-
- ) : (
-
-
- {title}
-
-
- {value} {unit}
-
-
- )}
-
- );
-};
-
-StatsCard.propTypes = {
- title: PropTypes.string,
- value: PropTypes.string,
- unit: PropTypes.string,
- isLoading: PropTypes.bool,
-};
-
-const Stats = ({ diagnostics, isLoading }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- return (
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-Stats.propTypes = {
- diagnostics: PropTypes.object,
- isLoading: PropTypes.bool,
-};
-
-export default Stats;
diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx
deleted file mode 100644
index 194e13821..000000000
--- a/client/src/Pages/Logs/Diagnostics/index.jsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Box from "@mui/material/Box";
-import Gauges from "./components/gauges/index.jsx";
-import Button from "@mui/material/Button";
-import StatBox from "@/Components/v1/StatBox/index.jsx";
-import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
-import { useTheme } from "@emotion/react";
-import { useTranslation } from "react-i18next";
-import { useFetchDiagnostics } from "../../../Hooks/logHooks.js";
-import { getHumanReadableDuration } from "../../../Utils/timeUtilsLegacy.js";
-import { formatBytes, getPercentage } from "./utils/utils.js";
-
-const Diagnostics = () => {
- // Local state
-
- // Hooks
- const theme = useTheme();
- const { t } = useTranslation();
- const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics();
- // Setup
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default Diagnostics;
diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/Logs/Diagnostics/utils/utils.js
deleted file mode 100644
index a3b75631f..000000000
--- a/client/src/Pages/Logs/Diagnostics/utils/utils.js
+++ /dev/null
@@ -1,16 +0,0 @@
-export const getPercentage = (value, total) => {
- if (!value || !total) return 0;
- return (value / total) * 100;
-};
-
-export const formatBytes = (bytes) => {
- if (!bytes) return "N/A";
-
- if (bytes === 0) return "0 Bytes";
-
- if (bytes >= 1024 * 1024 * 1024) {
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
- }
-
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
-};
diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx
deleted file mode 100644
index d0319a387..000000000
--- a/client/src/Pages/Logs/Logs/index.jsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Box from "@mui/material/Box";
-import Select from "@/Components/v1/Inputs/Select/index.jsx";
-import Typography from "@mui/material/Typography";
-import DataTable from "@/Components/v1/Table/index.jsx";
-import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
-
-import { useFetchLogs } from "../../../Hooks/logHooks.js";
-import { useTheme } from "@emotion/react";
-import { useState } from "react";
-import { useTranslation } from "react-i18next";
-
-const LevelBadge = ({ level }) => {
- const theme = useTheme();
-
- const levelColors = {
- info: theme.palette.success.main,
- warn: theme.palette.warning.main,
- error: theme.palette.error.main,
- debug: theme.palette.accent.main,
- };
-
- const color = levelColors[level] || theme.palette.primary.contrastText;
-
- return (
-
- {level}
-
- );
-};
-
-const formatTimestamp = (timestamp) => {
- if (!timestamp) return "-";
- const date = new Date(timestamp);
- return date.toLocaleString();
-};
-
-const Logs = () => {
- const [logLevel, setLogLevel] = useState("all");
- const [page, setPage] = useState(0);
- const [rowsPerPage, setRowsPerPage] = useState(15);
-
- const theme = useTheme();
- const { t } = useTranslation();
- const [logs, isLoading, error] = useFetchLogs();
-
- const LOG_LEVELS = [
- { _id: "all", name: t("logsPage.logLevelSelect.values.all") },
- { _id: "info", name: t("logsPage.logLevelSelect.values.info") },
- { _id: "warn", name: t("logsPage.logLevelSelect.values.warn") },
- { _id: "error", name: t("logsPage.logLevelSelect.values.error") },
- { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
- ];
-
- const headers = [
- {
- id: "timestamp",
- content: t("logsPage.table.timestamp"),
- render: (row) => (
-
- {formatTimestamp(row.timestamp)}
-
- ),
- },
- {
- id: "level",
- content: t("logsPage.table.level"),
- render: (row) => ,
- },
- {
- id: "service",
- content: t("logsPage.table.service"),
- render: (row) => (
- {row.service || "-"}
- ),
- },
- {
- id: "method",
- content: t("logsPage.table.method"),
- render: (row) => (
- {row.method || "-"}
- ),
- },
- {
- id: "message",
- content: t("logsPage.table.message"),
- render: (row) => (
-
- {row.message || "-"}
-
- ),
- },
- ];
-
- const filteredLogs = logs
- ?.filter((log) => {
- if (logLevel === "all") return true;
- return log.level === logLevel;
- })
- .reverse()
- .map((log, idx) => ({ ...log, id: idx }));
-
- const paginatedLogs = filteredLogs?.slice(
- page * rowsPerPage,
- page * rowsPerPage + rowsPerPage
- );
-
- const handleChangePage = (event, newPage) => {
- setPage(newPage);
- };
-
- const handleChangeRowsPerPage = (event) => {
- setRowsPerPage(parseInt(event.target.value, 10));
- setPage(0);
- };
-
- const handleLogLevelChange = (e) => {
- setLogLevel(e.target.value);
- setPage(0);
- };
-
- return (
-
-
- {t("logsPage.logLevelSelect.title")}
-
-
-
-
-
- {filteredLogs?.length > 0 && (
-
- )}
-
- );
-};
-
-export default Logs;
diff --git a/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx b/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx
deleted file mode 100644
index 49f6317bf..000000000
--- a/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Typography from "@mui/material/Typography";
-import DataTable from "@/Components/v1/Table/index.jsx";
-
-import { useNavigate } from "react-router-dom";
-import { useTheme } from "@emotion/react";
-import { TypeToPathMap } from "../../../../../Utils/monitorUtilsLegacy.js";
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-
-const FailedJobTable = ({ metrics = {} }) => {
- const { t } = useTranslation();
- const theme = useTheme();
- const jobsWithFailures = metrics?.jobsWithFailures;
- const navigate = useNavigate();
-
- const headers = [
- {
- id: "monitorId",
- content: t("queuePage.failedJobTable.monitorIdHeader"),
- render: (row) => {
- return row.monitorId;
- },
- },
- {
- id: "monitorUrl",
- content: t("queuePage.failedJobTable.monitorUrlHeader"),
- render: (row) => {
- return row.monitorUrl;
- },
- },
- {
- id: "failCount",
- content: t("queuePage.failedJobTable.failCountHeader"),
- render: (row) => {
- return row.failCount;
- },
- },
- {
- id: "failedAt",
- content: t("queuePage.failedJobTable.failedAtHeader"),
- render: (row) => {
- return row.failedAt;
- },
- },
- {
- id: "failReason",
- content: t("queuePage.failedJobTable.failReasonHeader"),
- render: (row) => {
- return row.failReason;
- },
- },
- ];
- return (
-
- {t("queuePage.failedJobTable.title")}
- {
- const path = TypeToPathMap[row.monitorType];
- navigate(`/${path}/${row.monitorId}`);
- },
- rowSX: {
- cursor: "pointer",
- "&:hover td": {
- backgroundColor: theme.palette.tertiary.main,
- transition: "background-color .3s ease",
- },
- },
- }}
- />
-
- );
-};
-
-FailedJobTable.propTypes = {
- metrics: PropTypes.object,
-};
-
-export default FailedJobTable;
diff --git a/client/src/Pages/Logs/Queue/components/JobTable/index.jsx b/client/src/Pages/Logs/Queue/components/JobTable/index.jsx
deleted file mode 100644
index 33636b6f5..000000000
--- a/client/src/Pages/Logs/Queue/components/JobTable/index.jsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import Stack from "@mui/material/Stack";
-import DataTable from "@/Components/v1/Table/index.jsx";
-import Typography from "@mui/material/Typography";
-// Utils
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import { useNavigate } from "react-router-dom";
-import { TypeToPathMap } from "../../../../../Utils/monitorUtilsLegacy.js";
-import { useTranslation } from "react-i18next";
-import { createHeaderFactory } from "@/Components/v1/Table/TableUtils.js";
-
-const JobTable = ({ jobs = [] }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- const navigate = useNavigate();
- const buildSx = (row) => {
- if (row.lockedAt) {
- return {
- color: `${theme.palette.success.main} !important`,
- };
- }
- if (!row.active) {
- return {
- color: `${theme.palette.warning.main} !important`,
- };
- }
-
- if (row.failCount > 0 && row.lastFailedAt >= row.lastFinishedAt) {
- return {
- color: `${theme.palette.error.main} !important`,
- };
- }
-
- return {};
- };
-
- const createHeader = createHeaderFactory(buildSx);
- const headersData = [
- {
- id: "id",
- content: t("queuePage.jobTable.idHeader"),
- render: (row) => row.monitorId,
- },
- {
- id: "url",
- content: t("queuePage.jobTable.urlHeader"),
- render: (row) => row.monitorUrl,
- },
- {
- id: "interval",
- content: t("queuePage.jobTable.intervalHeader"),
- render: (row) => `${row.monitorInterval} ms`,
- },
- {
- id: "type",
- content: t("queuePage.jobTable.typeHeader"),
- render: (row) => row.monitorType,
- },
- {
- id: "active",
- content: t("queuePage.jobTable.activeHeader"),
- render: (row) => row.active.toString(),
- },
- {
- id: "runCount",
- content: t("queuePage.jobTable.runCountHeader"),
- render: (row) => row.runCount,
- },
- {
- id: "failCount",
- content: t("queuePage.jobTable.failCountHeader"),
- render: (row) => row.failCount,
- },
- {
- id: "lastRun",
- content: t("queuePage.jobTable.lastRunHeader"),
- render: (row) => row.lastRunAt || "-",
- },
- {
- id: "lockedAt",
- content: t("queuePage.jobTable.lockedAtHeader"),
- render: (row) => row.lockedAt || "-",
- },
-
- {
- id: "lastFinish",
- content: t("queuePage.jobTable.lastFinishedAtHeader"),
- render: (row) => row.lastFinishedAt || "-",
- },
- {
- id: "lastRunTook",
- content: t("queuePage.jobTable.lastRunTookHeader"),
- render: (row) => {
- const value = row.lastRunTook ? row.lastRunTook + " ms" : "-";
- return value;
- },
- },
- ];
-
- const headers = headersData.map((header) => createHeader(header));
-
- return (
-
- {t("queuePage.jobTable.title")}
- {
- const path = TypeToPathMap[row.monitorType];
- navigate(`/${path}/${row.monitorId}`);
- },
- rowSX: {
- cursor: "pointer",
- "&:hover td": {
- backgroundColor: theme.palette.tertiary.main,
- transition: "background-color .3s ease",
- },
- },
- }}
- />
-
- );
-};
-
-JobTable.propTypes = {
- jobs: PropTypes.array,
-};
-
-export default JobTable;
diff --git a/client/src/Pages/Logs/Queue/components/Metrics/index.jsx b/client/src/Pages/Logs/Queue/components/Metrics/index.jsx
deleted file mode 100644
index c4f4389c3..000000000
--- a/client/src/Pages/Logs/Queue/components/Metrics/index.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import Stack from "@mui/material/Stack";
-import StatBox from "@/Components/v1/StatBox/index.jsx";
-import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
-import { useTranslation } from "react-i18next";
-import { useTheme } from "@emotion/react";
-
-const camelToTitle = (str) => {
- return str
- .replace(/([A-Z])/g, " $1")
- .toLowerCase()
- .replace(/^./, (m) => m.toUpperCase());
-};
-
-const Metrics = ({ metrics = {} }) => {
- const { t } = useTranslation();
- const theme = useTheme();
-
- const data = Object.keys(metrics)
- .filter((key) => key !== "jobsWithFailures")
- .map((key) => {
- return { key, title: camelToTitle(key), value: metrics[key] };
- });
-
- return (
-
- {data.map((metric) => {
- return (
-
- );
- })}
-
- );
-};
-export default Metrics;
diff --git a/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx b/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx
deleted file mode 100644
index d96f37a20..000000000
--- a/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Typography from "@mui/material/Typography";
-import DataTable from "@/Components/v1/Table/index.jsx";
-
-// Utils
-import { useTranslation } from "react-i18next";
-import { useTheme } from "@emotion/react";
-import PropTypes from "prop-types";
-
-const camelToTitle = (str) => {
- return str
- .replace(/([A-Z])/g, " $1")
- .toLowerCase()
- .replace(/^./, (m) => m.toUpperCase());
-};
-
-const Metrics = ({ metrics = {} }) => {
- const { t } = useTranslation();
- const theme = useTheme();
- const keys = Object.keys(metrics);
-
- const headers = [
- {
- id: "metric",
- content: t("queuePage.metricsTable.metricHeader"),
- render: (row) => {
- return {row.key};
- },
- },
- {
- id: "value",
- content: t("queuePage.metricsTable.valueHeader"),
- render: (row) => {
- return {row.value};
- },
- },
- ];
-
- const data = keys
- .filter((key) => key !== "jobsWithFailures")
- .map((key) => {
- return { key: camelToTitle(key), value: metrics[key] };
- });
-
- return (
-
- {t("queuePage.metricsTable.title")}
-
-
- );
-};
-
-Metrics.propTypes = {
- metrics: PropTypes.object,
-};
-
-export default Metrics;
diff --git a/client/src/Pages/Logs/Queue/index.jsx b/client/src/Pages/Logs/Queue/index.jsx
deleted file mode 100644
index b2d0dc685..000000000
--- a/client/src/Pages/Logs/Queue/index.jsx
+++ /dev/null
@@ -1,73 +0,0 @@
-// Components
-import Stack from "@mui/material/Stack";
-import JobTable from "./components/JobTable/index.jsx";
-import Metrics from "./components/Metrics/index.jsx";
-import FailedJobTable from "./components/FailedJobTable/index.jsx";
-import ButtonGroup from "@mui/material/ButtonGroup";
-import Button from "@mui/material/Button";
-import Typography from "@mui/material/Typography";
-import Divider from "@mui/material/Divider";
-
-// Utils
-import { useState } from "react";
-import { useFetchQueueData, useFlushQueue } from "../../../Hooks/logHooks.js";
-import { useTranslation } from "react-i18next";
-import { useTheme } from "@emotion/react";
-
-const QueueDetails = () => {
- // Local state
- const [trigger, setTrigger] = useState(false);
-
- // Hooks
- const { t } = useTranslation();
- const theme = useTheme();
- const [jobs, metrics, isLoading, error] = useFetchQueueData(trigger);
- const [flushQueue, isFlushing, flushError] = useFlushQueue();
-
- if (isLoading) return Loading...
;
- if (error || flushError) return Error: {error.message}
;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default QueueDetails;
diff --git a/client/src/Pages/Logs/TabDiagnostics.tsx b/client/src/Pages/Logs/TabDiagnostics.tsx
new file mode 100644
index 000000000..7ab84ba25
--- /dev/null
+++ b/client/src/Pages/Logs/TabDiagnostics.tsx
@@ -0,0 +1,24 @@
+import Stack from "@mui/material/Stack";
+import { Stats } from "@/Pages/Logs/components/Stats";
+import { StatGauges } from "@/Pages/Logs/components/StatGauges";
+
+import { useTheme } from "@mui/material";
+import { useGet } from "@/Hooks/UseApi";
+import type { Diagnostics } from "@/Types/Diagnostics";
+
+export const TabDiagnostics = () => {
+ const theme = useTheme();
+ const {
+ data: diagnostics,
+ isLoading: _isLoading,
+ error: _error,
+ refetch: _refetch,
+ } = useGet("/diagnostic/system", {}, { refreshInterval: 5000 });
+
+ return (
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Logs/TabLogs.tsx b/client/src/Pages/Logs/TabLogs.tsx
new file mode 100644
index 000000000..a44de73a4
--- /dev/null
+++ b/client/src/Pages/Logs/TabLogs.tsx
@@ -0,0 +1,63 @@
+import Stack from "@mui/material/Stack";
+import MenuItem from "@mui/material/MenuItem";
+import Typography from "@mui/material/Typography";
+import { useSelector } from "react-redux";
+import { Select } from "@/Components/v2/inputs";
+import { TableLogs } from "@/Pages/Logs/components/TableLogs";
+
+import { useTheme } from "@mui/material";
+import { useGet } from "@/Hooks/UseApi";
+import { useState } from "react";
+import type { Log, LogLevelOption } from "@/Types/Log";
+import { LOG_LEVEL_OPTIONS } from "@/Types/Log";
+import { t } from "i18next";
+import type { RootState } from "@/Types/state";
+
+export const TabLogs = () => {
+ const theme = useTheme();
+ const { data: logs } = useGet("/logs");
+ const [selectedLogLevel, setSelectedLogLevel] = useState("all");
+ const [page, setPage] = useState(0);
+ const rowsPerPage = useSelector(
+ (state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15
+ );
+
+ const filteredLogs = logs
+ ?.filter((log) => {
+ if (selectedLogLevel === "all") return true;
+ return log.level === selectedLogLevel;
+ })
+ .reverse()
+ .map((log, idx) => ({ ...log, id: idx }));
+
+ const paginatedLogs =
+ filteredLogs?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) ?? [];
+
+ return (
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx
new file mode 100644
index 000000000..be0013368
--- /dev/null
+++ b/client/src/Pages/Logs/TabQueue.tsx
@@ -0,0 +1,60 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { TableJobs, TableFailedJobs } from "./components/TableJobs";
+
+import { useTheme } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { useGet, usePost } from "@/Hooks/UseApi";
+import type { QueueData } from "@/Types/Queue";
+import { Metrics } from "@/Pages/Logs/components/Metrics";
+import { Button } from "@/Components/v2/inputs";
+
+export const TabQueue = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const {
+ data: queueData,
+ // isLoading,
+ // error,
+ refetch,
+ } = useGet("/queue/all-metrics", {}, { refreshInterval: 5000 });
+
+ const { post, loading: isFlushing } = usePost();
+
+ const jobs = queueData?.jobs ?? [];
+ const metrics = queueData?.metrics ?? null;
+
+ const handleFlushQueue = async () => {
+ await post("/queue/flush", {});
+ refetch();
+ };
+
+ return (
+
+
+
+ {t("pages.logs.jobQueue")}
+
+
+
+ {t("pages.logs.failedJobs")}
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Logs/components/Metrics.tsx b/client/src/Pages/Logs/components/Metrics.tsx
new file mode 100644
index 000000000..8f96ec5ac
--- /dev/null
+++ b/client/src/Pages/Logs/components/Metrics.tsx
@@ -0,0 +1,50 @@
+import Stack from "@mui/material/Stack";
+import { StatBox } from "@/Components/v2/design-elements";
+
+import { useTheme } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import type { QueueMetrics } from "@/Types/Queue";
+
+interface MetricsProps {
+ metrics: QueueMetrics | null;
+}
+
+export const Metrics = ({ metrics }: MetricsProps) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ if (!metrics) {
+ return null;
+ }
+
+ return (
+
+
+ 0 ? "success" : undefined}
+ title={t("pages.logs.metrics.activeJobs")}
+ subtitle={String(metrics.activeJobs)}
+ />
+ 0 ? "error" : undefined}
+ title={t("pages.logs.metrics.failingJobs")}
+ subtitle={String(metrics.failingJobs)}
+ />
+
+ 0 ? "error" : undefined}
+ title={t("pages.logs.metrics.totalFailures")}
+ subtitle={String(metrics.totalFailures)}
+ />
+
+ );
+};
diff --git a/client/src/Pages/Logs/components/StatGauges.tsx b/client/src/Pages/Logs/components/StatGauges.tsx
new file mode 100644
index 000000000..17137f114
--- /dev/null
+++ b/client/src/Pages/Logs/components/StatGauges.tsx
@@ -0,0 +1,84 @@
+import Stack from "@mui/material/Stack";
+import { DetailGauge } from "@/Components/v2/design-elements";
+
+import prettyBytes from "pretty-bytes";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@mui/material";
+import type { Diagnostics } from "@/Types/Diagnostics";
+
+interface StatGaugesProps {
+ diagnostics: Diagnostics | null;
+}
+
+const getPercentage = (value: number, total: number) => {
+ if (!value || !total) return 0;
+ return (value / total) * 100;
+};
+
+const formatPercentage = new Intl.NumberFormat("en-US", {
+ style: "percent",
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1,
+});
+
+export const StatGauges = ({ diagnostics }: StatGaugesProps) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ if (!diagnostics) {
+ return null;
+ }
+
+ const heapTotalSize = getPercentage(
+ diagnostics?.v8HeapStats?.totalHeapSizeBytes,
+ diagnostics?.v8HeapStats?.heapSizeLimitBytes
+ );
+
+ const heapUsedSize = getPercentage(
+ diagnostics?.v8HeapStats?.usedHeapSizeBytes,
+ diagnostics?.v8HeapStats?.heapSizeLimitBytes
+ );
+
+ const actualHeapUsed = getPercentage(
+ diagnostics?.v8HeapStats?.usedHeapSizeBytes,
+ diagnostics?.v8HeapStats?.totalHeapSizeBytes
+ );
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Logs/components/Stats.tsx b/client/src/Pages/Logs/components/Stats.tsx
new file mode 100644
index 000000000..429330873
--- /dev/null
+++ b/client/src/Pages/Logs/components/Stats.tsx
@@ -0,0 +1,51 @@
+import Stack from "@mui/material/Stack";
+import { StatBox } from "@/Components/v2/design-elements";
+
+import prettyBytes from "pretty-bytes";
+import prettyMilliseconds from "pretty-ms";
+import { useTheme } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import type { Diagnostics } from "@/Types/Diagnostics";
+
+interface StatsProps {
+ diagnostics: Diagnostics | null;
+}
+
+export const Stats = ({ diagnostics }: StatsProps) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ if (!diagnostics) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Logs/components/TableJobs.tsx b/client/src/Pages/Logs/components/TableJobs.tsx
new file mode 100644
index 000000000..e9608df9a
--- /dev/null
+++ b/client/src/Pages/Logs/components/TableJobs.tsx
@@ -0,0 +1,164 @@
+import { Table } from "@/Components/v2/design-elements";
+import { Typography, useTheme } from "@mui/material";
+import prettyMilliseconds from "pretty-ms";
+import { formatTimestamp } from "@/Utils/TimeUtils";
+
+import { useTranslation } from "react-i18next";
+import type { QueueJobFailure, QueueJobSummary, QueueMetrics } from "@/Types/Queue";
+import type { Header } from "@/Components/v2/design-elements";
+
+type QueueJobWithId = QueueJobSummary & { id: string | number };
+
+interface TableJobsProps {
+ jobs: QueueJobSummary[];
+}
+
+export const TableJobs = ({ jobs }: TableJobsProps) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ const jobsWithId: QueueJobWithId[] = jobs.map((job) => ({
+ ...job,
+ id: job.monitorId,
+ }));
+
+ const headers: Header[] = [
+ {
+ id: "id",
+ content: t("common.table.headers.monitorId"),
+ render: (row) => {row.monitorId},
+ },
+ {
+ id: "url",
+ content: t("common.table.headers.url"),
+ render: (row) => {row.monitorUrl},
+ },
+ {
+ id: "interval",
+ content: t("common.table.headers.interval"),
+ render: (row) => (
+ {prettyMilliseconds(row.monitorInterval ?? 0)}
+ ),
+ },
+ {
+ id: "type",
+ content: t("common.table.headers.type"),
+ render: (row) => {row.monitorType},
+ },
+ {
+ id: "active",
+ content: t("common.table.headers.active"),
+ render: (row) => {row.active.toString()},
+ },
+ {
+ id: "runCount",
+ content: t("pages.logs.table.headers.runCount"),
+ render: (row) => {row.runCount},
+ },
+ {
+ id: "failCount",
+ content: t("pages.logs.table.headers.failCount"),
+ render: (row) => {row.failCount},
+ },
+ {
+ id: "lastRun",
+ content: t("pages.logs.table.headers.lastRunAt"),
+ render: (row) => {formatTimestamp(row.lastRunAt)},
+ },
+ {
+ id: "lockedAt",
+ content: t("pages.logs.table.headers.lockedAt"),
+ render: (row) => {formatTimestamp(row.lockedAt)},
+ },
+
+ {
+ id: "lastFinish",
+ content: t("pages.logs.table.headers.lastFinishedAt"),
+ render: (row) => {formatTimestamp(row.lastFinishedAt)},
+ },
+ {
+ id: "lastRunTook",
+ content: t("pages.logs.table.headers.lastRunTook"),
+ render: (row) => {
+ const value = row.lastRunTook ? prettyMilliseconds(row.lastRunTook) : "-";
+ return {value};
+ },
+ },
+ ];
+
+ return (
+ ({
+ ...(row.lockedAt && {
+ "& td": { backgroundColor: theme.palette.success.light },
+ }),
+ })}
+ />
+ );
+};
+
+type QueueJobFailureWithId = QueueJobFailure & { id: string | number };
+
+interface TableFailedJobsProps {
+ metrics: QueueMetrics | null;
+}
+export const TableFailedJobs = ({ metrics }: TableFailedJobsProps) => {
+ const { t } = useTranslation();
+ if (!metrics) {
+ return null;
+ }
+
+ const jobFailuresWithId: QueueJobFailureWithId[] = metrics.jobsWithFailures.map(
+ (job) => ({
+ ...job,
+ id: job.monitorId,
+ })
+ );
+
+ const headers: Header[] = [
+ {
+ id: "monitorId",
+ content: t("common.table.headers.monitorId"),
+ render: (row) => {
+ return {row.monitorId};
+ },
+ },
+ {
+ id: "monitorUrl",
+ content: t("common.table.headers.url"),
+ render: (row) => {
+ return {row.monitorUrl};
+ },
+ },
+ {
+ id: "failCount",
+ content: t("pages.logs.table.headers.failCount"),
+ render: (row) => {
+ return {row.failCount};
+ },
+ },
+ {
+ id: "failedAt",
+ content: t("pages.logs.table.headers.lastFailedAt"),
+ render: (row) => {
+ return {formatTimestamp(row.failedAt)};
+ },
+ },
+ {
+ id: "failReason",
+ content: t("pages.logs.table.headers.failReason"),
+ render: (row) => {
+ return {row.failReason};
+ },
+ },
+ ];
+
+ return (
+
+ );
+};
diff --git a/client/src/Pages/Logs/components/TableLogs.tsx b/client/src/Pages/Logs/components/TableLogs.tsx
new file mode 100644
index 000000000..dace0db43
--- /dev/null
+++ b/client/src/Pages/Logs/components/TableLogs.tsx
@@ -0,0 +1,128 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { Table } from "@/Components/v2/design-elements";
+import { Pagination } from "@/Components/v2/design-elements/Table";
+import { useTheme } from "@mui/material";
+import type { Header } from "@/Components/v2/design-elements/Table";
+import type { Log, LogLevel } from "@/Types/Log";
+import { useTranslation } from "react-i18next";
+import { useSelector, useDispatch } from "react-redux";
+import type { RootState } from "@/Types/state";
+import { setRowsPerPage } from "@/Features/UI/uiSlice";
+import { formatTimestamp } from "@/Utils/TimeUtils";
+
+type LogWithId = Log & { id: number };
+
+interface TableLogsProps {
+ logs: LogWithId[];
+ logCount: number;
+ page: number;
+ setPage: (page: number) => void;
+}
+
+const LevelBadge = ({ level }: { level: LogLevel }) => {
+ const theme = useTheme();
+
+ const levelColors: Record = {
+ info: theme.palette.success.main,
+ warn: theme.palette.warning.main,
+ error: theme.palette.error.main,
+ debug: theme.palette.info.main,
+ };
+
+ const color = levelColors[level] || theme.palette.text.primary;
+
+ return (
+
+ {level}
+
+ );
+};
+
+export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const rowsPerPage = useSelector(
+ (state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15
+ );
+
+ const headers: Header[] = [
+ {
+ id: "timestamp",
+ content: t("pages.logs.table.headers.timestamp"),
+ render: (row) => (
+
+ {formatTimestamp(row.timestamp)}
+
+ ),
+ },
+ {
+ id: "level",
+ content: t("pages.logs.table.headers.level"),
+ render: (row) => ,
+ },
+ {
+ id: "service",
+ content: t("pages.logs.table.headers.service"),
+ render: (row) => {row.service || "-"},
+ },
+ {
+ id: "method",
+ content: t("pages.logs.table.headers.method"),
+ render: (row) => (
+ {row.method || "-"}
+ ),
+ },
+ {
+ id: "message",
+ content: t("common.table.headers.message"),
+ render: (row) => (
+
+ {row.message || "-"}
+
+ ),
+ },
+ ];
+
+ const handlePageChange = (
+ _e: React.MouseEvent | null,
+ newPage: number
+ ) => {
+ setPage(newPage);
+ };
+
+ const handleRowsPerPageChange = (
+ e: React.ChangeEvent
+ ) => {
+ dispatch(setRowsPerPage({ value: Number(e.target.value), table: "logs" }));
+ setPage(0);
+ };
+
+ return (
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/Logs/index.jsx
deleted file mode 100644
index e5bdf623d..000000000
--- a/client/src/Pages/Logs/index.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
-import Tabs from "@mui/material/Tabs";
-import Tab from "@mui/material/Tab";
-import Queue from "./Queue/index.jsx";
-import LogsComponent from "./Logs/index.jsx";
-import Diagnostics from "./Diagnostics/index.jsx";
-
-import { useTheme } from "@emotion/react";
-import { useTranslation } from "react-i18next";
-import { useState } from "react";
-
-const Logs = () => {
- const { t } = useTranslation();
- const theme = useTheme();
-
- // Local state
- const [value, setValue] = useState(0);
-
- // Handlers
- const handleChange = (event, newValue) => {
- setValue(newValue);
- };
-
- const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }];
- return (
-
-
-
-
-
-
-
- {value === 0 && }
- {value === 1 && }
- {value === 2 && }
-
- );
-};
-
-export default Logs;
diff --git a/client/src/Pages/Logs/index.tsx b/client/src/Pages/Logs/index.tsx
new file mode 100644
index 000000000..ab30b84eb
--- /dev/null
+++ b/client/src/Pages/Logs/index.tsx
@@ -0,0 +1,29 @@
+import { BasePage, Tabs, Tab } from "@/Components/v2/design-elements";
+import { TabLogs } from "@/Pages/Logs/TabLogs";
+import { TabQueue } from "@/Pages/Logs/TabQueue";
+import { TabDiagnostics } from "@/Pages/Logs/TabDiagnostics";
+
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+
+const LogsPage = () => {
+ const { t } = useTranslation();
+ const [activeTab, setActiveTab] = useState(2);
+ return (
+
+ setActiveTab(newValue)}
+ >
+
+
+
+
+ {activeTab === 0 && }
+ {activeTab === 1 && }
+ {activeTab === 2 && }
+
+ );
+};
+
+export default LogsPage;
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index d4a5619d6..22b5b446a 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -55,7 +55,7 @@ import ProtectedRoute from "../Components/v1/ProtectedRoute";
import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute";
import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport/index.jsx";
-import Logs from "../Pages/Logs/index.jsx";
+import Logs from "../Pages/Logs";
import CreateMonitor from "@/Pages/CreateMonitor";
@@ -350,7 +350,9 @@ const Routes = () => {
path="logs"
element={
-
+
+
+
}
/>
diff --git a/client/src/Types/Diagnostics.ts b/client/src/Types/Diagnostics.ts
new file mode 100644
index 000000000..fe2ea6f16
--- /dev/null
+++ b/client/src/Types/Diagnostics.ts
@@ -0,0 +1,33 @@
+export interface Diagnostics {
+ osStats: OsStats;
+ memoryUsage: MemoryUsage;
+ cpuUsage: CpuUsage;
+ v8HeapStats: V8HeapStats;
+ eventLoopDelayMs: number;
+ uptimeMs: number;
+}
+
+export interface OsStats {
+ freeMemoryBytes: number;
+ totalMemoryBytes: number;
+}
+
+export interface MemoryUsage {
+ rss: number;
+ heapTotal: number;
+ heapUsed: number;
+ external: number;
+ arrayBuffers: number;
+}
+
+export interface CpuUsage {
+ userUsageMs: number;
+ systemUsageMs: number;
+ usagePercentage: number;
+}
+
+export interface V8HeapStats {
+ totalHeapSizeBytes: number;
+ usedHeapSizeBytes: number;
+ heapSizeLimitBytes: number;
+}
diff --git a/client/src/Types/Log.ts b/client/src/Types/Log.ts
new file mode 100644
index 000000000..b2038e31f
--- /dev/null
+++ b/client/src/Types/Log.ts
@@ -0,0 +1,13 @@
+export const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
+export type LogLevel = (typeof LOG_LEVELS)[number];
+export const LOG_LEVEL_OPTIONS = ["all", ...LOG_LEVELS] as const;
+export type LogLevelOption = (typeof LOG_LEVEL_OPTIONS)[number];
+export interface Log {
+ message: string;
+ service?: string;
+ method?: string;
+ details?: Record;
+ stack?: string;
+ level: LogLevel;
+ timestamp: string;
+}
diff --git a/client/src/Types/Queue.ts b/client/src/Types/Queue.ts
new file mode 100644
index 000000000..77a770aeb
--- /dev/null
+++ b/client/src/Types/Queue.ts
@@ -0,0 +1,40 @@
+import type { MonitorType } from "./Monitor";
+
+export interface QueueJobFailure {
+ monitorId: string | number;
+ monitorUrl: string | null;
+ monitorType: MonitorType | null;
+ failedAt: number | null;
+ failCount: number;
+ failReason: string | null;
+}
+
+export interface QueueMetrics {
+ jobs: number;
+ activeJobs: number;
+ failingJobs: number;
+ jobsWithFailures: QueueJobFailure[];
+ totalRuns: number;
+ totalFailures: number;
+}
+
+export interface QueueJobSummary {
+ monitorId: string | number;
+ monitorUrl: string | null;
+ monitorType: MonitorType | null;
+ monitorInterval: number | null;
+ active: boolean;
+ lockedAt: number | null;
+ runCount: number;
+ failCount: number;
+ failReason: string | null;
+ lastRunAt: number | null;
+ lastFinishedAt: number | null;
+ lastRunTook: number | null;
+ lastFailedAt: number | null;
+}
+
+export interface QueueData {
+ jobs: QueueJobSummary[];
+ metrics: QueueMetrics;
+}
diff --git a/client/src/Types/state.ts b/client/src/Types/state.ts
index 137d67b66..1575fffb8 100644
--- a/client/src/Types/state.ts
+++ b/client/src/Types/state.ts
@@ -19,6 +19,9 @@ export interface UIState {
infrastructure: {
rowsPerPage: number;
};
+ logs: {
+ rowsPerPage: number;
+ };
sidebar: {
collapsed: boolean;
};
diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts
index a19a0bf8b..abd56ce44 100644
--- a/client/src/Utils/TimeUtils.ts
+++ b/client/src/Utils/TimeUtils.ts
@@ -50,3 +50,9 @@ export const tooltipDateFormatLookup = (range: string) => {
}
return format;
};
+
+export const formatTimestamp = (timestamp: string | number | null): string => {
+ if (!timestamp) return "-";
+ const date = new Date(timestamp);
+ return date.toLocaleString();
+};
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 644057425..4bd280dcf 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -199,7 +199,8 @@
"save": "Save",
"test": "Test",
"testNotifications": "Test notifications",
- "toggleTheme": "Toggles light & dark"
+ "toggleTheme": "Toggles light & dark",
+ "flushQueue": "Flush queue"
},
"charts": {
"labels": {
@@ -230,9 +231,13 @@
"dateTime": "Date & time",
"message": "Message",
"monitor": "Monitor",
+ "monitorId": "Monitor ID",
"name": "Name",
"status": "Status",
- "type": "Type"
+ "type": "Type",
+ "url": "Url",
+ "interval": "Interval",
+ "active": "Active"
}
},
"toasts": {
@@ -777,6 +782,63 @@
"title": "An infrastructure monitor is used to:"
}
},
+ "logs": {
+ "tabs": {
+ "diagnostics": "Diagnostics",
+ "logs": "Logs",
+ "queue": "Job queue"
+ },
+ "logLevelSelect": {
+ "label": "Log level"
+ },
+ "jobQueue": "Job queue",
+ "failedJobs": "Failed jobs",
+ "noLogs": "No logs found",
+ "metrics": {
+ "jobs": "Jobs",
+ "activeJobs": "Active jobs",
+ "failingJobs": "Failing jobs",
+ "totalRuns": "Total runs",
+ "totalFailures": "Total failures"
+ },
+ "diagnostics": {
+ "stats": {
+ "eventLoopDelay": "Event loop delay",
+ "uptime": "Uptime",
+ "usedHeapSize": "Used heap size",
+ "totalHeapSize": "Total heap size",
+ "osMemoryLimit": "OS memory limit"
+ },
+ "gauges": {
+ "heapAllocation": "Heap allocaton",
+ "heapUsage": "Heap usage",
+ "heapUtilization": "Heap utilization",
+ "instantCpuUsage": "Instant CPU usage",
+ "availableMemoryPercentage": "% of available memory",
+ "allocatedPercentage": "% allocated",
+ "usedSPercentage": "% of 1s used by CPU",
+ "total": "Total",
+ "used": "Used"
+ }
+ },
+ "table": {
+ "headers": {
+ "timestamp": "Timestamp",
+ "level": "Level",
+ "service": "Service",
+ "method": "Method",
+ "monitorId": "Monitor ID",
+ "runCount": "Run count",
+ "failCount": "Fail count",
+ "lastRunAt": "Last run at",
+ "lockedAt": "Locked at",
+ "lastFinishedAt": "Last finished at",
+ "lastRunTook": "Last run took",
+ "lastFailedAt": "Last failed at",
+ "failReason": "Fail reason"
+ }
+ }
+ },
"maintenanceWindow": {
"fallback": {
"actionButton": "Let's create your first maintenance window!",
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 3d8d66335..f381eb5ed 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig(({}) => {
- let version = "3.2.1";
+ let version = "3.3";
return {
base: "/",