mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat: diagnostics logs & crons
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
import { useMount } from "ahooks";
|
||||
import { Avatar, Select, type SelectProps, Typography, theme } from "antd";
|
||||
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
@@ -16,7 +16,7 @@ const AccessSelect = ({ onFilter, ...props }: AccessTypeSelectProps) => {
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses"]));
|
||||
useAsyncEffect(() => fetchAccesses(false), []);
|
||||
useMount(() => fetchAccesses(false));
|
||||
|
||||
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: AccessModel }>>([]);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -65,13 +65,13 @@ const AccessConfigFormFieldsProviderSSH = ({ disabled }: { disabled?: boolean })
|
||||
|
||||
<Show when={fieldAuthMethod === AUTH_METHOD_PASSWORD}>
|
||||
<Form.Item name={[parentNamePath, "password"]} initialValue={initialValues.password} label={t("access.form.ssh_password.label")} rules={[formRule]}>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Show when={fieldAuthMethod === AUTH_METHOD_KEY}>
|
||||
<Form.Item name={[parentNamePath, "key"]} initialValue={initialValues.key} label={t("access.form.ssh_key.label")} rules={[formRule]}>
|
||||
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
|
||||
<TextFileInput autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncEffect, useMount, useSize } from "ahooks";
|
||||
import { useMount, useSize } from "ahooks";
|
||||
import { Avatar, Card, Checkbox, Empty, Flex, Input, type InputRef, Tabs, Tooltip, Typography } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
@@ -23,7 +23,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, gap = "middle",
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { accesses, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "fetchAccesses"]));
|
||||
useAsyncEffect(() => fetchAccesses(false), []);
|
||||
useMount(() => fetchAccesses(false));
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperSize = useSize(wrapperRef);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { EditorState, FlowLayoutDefault } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { IconBrowserShare, IconCheck, IconDots, IconDownload, IconSettings2, IconTransferOut } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Alert, App, Button, Card, Divider, Dropdown, Empty, Skeleton, Spin, Table, type TableProps, Tooltip, Typography, theme } from "antd";
|
||||
import { Alert, App, Button, Card, Divider, Dropdown, Empty, Skeleton, Table, type TableProps, Tooltip, Typography, theme } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
@@ -197,7 +197,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
const [showTimestamp, setShowTimestamp] = useState(true);
|
||||
const [showWhitespace, setShowWhitespace] = useState(true);
|
||||
|
||||
const renderRecord = (record: Log) => {
|
||||
const renderLogRecord = (record: Log) => {
|
||||
let message = <>{record.message}</>;
|
||||
if (record.data != null && Object.keys(record.data).length > 0) {
|
||||
message = (
|
||||
@@ -215,18 +215,19 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2" style={{ wordBreak: "break-word" }}>
|
||||
{showTimestamp ? <div className="whitespace-nowrap text-stone-400">[{dayjs(record.timestamp).format("YYYY-MM-DD HH:mm:ss")}]</div> : <></>}
|
||||
{showTimestamp ? <div className="font-mono text-stone-400">[{dayjs(record.timestamp).format("YYYY-MM-DD HH:mm:ss")}]</div> : <></>}
|
||||
<div
|
||||
className={mergeCls(
|
||||
"flex-1",
|
||||
"font-mono",
|
||||
{ ["whitespace-pre-line"]: !showWhitespace },
|
||||
record.level < WorkflowLogLevel.Info
|
||||
? "text-stone-400"
|
||||
: record.level < WorkflowLogLevel.Warn
|
||||
? ""
|
||||
: record.level < WorkflowLogLevel.Error
|
||||
? "text-warning"
|
||||
: "text-error",
|
||||
{ ["whitespace-pre-line"]: !showWhitespace }
|
||||
: "text-error"
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
@@ -308,22 +309,15 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
<Divider className="my-0 bg-stone-800" />
|
||||
|
||||
<div className="min-h-8 px-4 py-2">
|
||||
<Show
|
||||
when={!loading || listData.length > 0}
|
||||
fallback={
|
||||
<Spin spinning>
|
||||
<Skeleton />
|
||||
</Spin>
|
||||
}
|
||||
>
|
||||
<Show when={!loading || listData.length > 0} fallback={<Skeleton />}>
|
||||
{listData.map((group) => {
|
||||
return (
|
||||
<div className="mb-3 text-stone-200">
|
||||
<div className="mb-3">
|
||||
<div className="truncate text-xs leading-loose">
|
||||
<span className="font-mono text-stone-400">{`#${group.id}\u00A0`}</span>
|
||||
<span>{group.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1 text-xs">{group.records.map((record) => renderRecord(record))}</div>
|
||||
<div className="flex flex-col text-xs leading-relaxed">{group.records.map((record) => renderLogRecord(record))}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getI18n, useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import { type FlowNodeEntity, getNodeForm } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { IconChevronRight, IconCircleMinus, IconPlus } from "@tabler/icons-react";
|
||||
import { useAsyncEffect, useControllableValue } from "ahooks";
|
||||
import { useControllableValue, useMount } from "ahooks";
|
||||
import { type AnchorProps, AutoComplete, Button, Divider, Flex, Form, type FormInstance, Input, InputNumber, Select, Switch, Typography } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
@@ -456,7 +456,7 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
|
||||
const InternalEmailInput = memo(
|
||||
({ disabled, placeholder, ...props }: { disabled?: boolean; placeholder?: string; value?: string; onChange?: (value: string) => void }) => {
|
||||
const { emails, fetchEmails, removeEmail } = useContactEmailsStore();
|
||||
useAsyncEffect(() => fetchEmails(false), []);
|
||||
useMount(() => fetchEmails(false));
|
||||
|
||||
const [value, setValue] = useControllableValue<string>(props, {
|
||||
valuePropName: "value",
|
||||
|
||||
@@ -46,5 +46,11 @@
|
||||
"settings.persistence.form.expired_certificates_max_days.label": "Max days retention of expired certificates",
|
||||
"settings.persistence.form.expired_certificates_max_days.placeholder": "Please enter the maximum retention days of expired certificates",
|
||||
"settings.persistence.form.expired_certificates_max_days.unit": "days",
|
||||
"settings.persistence.form.expired_certificates_max_days.help": "Set to <b>0</emb> to disable cleanup expired certificates."
|
||||
"settings.persistence.form.expired_certificates_max_days.help": "Set to <b>0</emb> to disable cleanup expired certificates.",
|
||||
|
||||
"settings.diagnostics.tab": "Diagnostics",
|
||||
"settings.diagnostics.logs.title": "System logs",
|
||||
"settings.diagnostics.crons.title": "CRON jobs",
|
||||
"settings.diagnostics.crons.job.next_trigger_time": "Expected next execution time: ",
|
||||
"settings.diagnostics.workflow_dispatcher.title": "Workflow dispatcher"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,11 @@
|
||||
"settings.persistence.form.expired_certificates_max_days.label": "证书过期后保留天数",
|
||||
"settings.persistence.form.expired_certificates_max_days.placeholder": "请输入过期证书保留天数",
|
||||
"settings.persistence.form.expired_certificates_max_days.unit": "天",
|
||||
"settings.persistence.form.expired_certificates_max_days.help": "设置为 <b>0</b> 表示永久保留,不会自动清理。"
|
||||
"settings.persistence.form.expired_certificates_max_days.help": "设置为 <b>0</b> 表示永久保留,不会自动清理。",
|
||||
|
||||
"settings.diagnostics.tab": "系统诊断",
|
||||
"settings.diagnostics.logs.title": "系统日志",
|
||||
"settings.diagnostics.crons.title": "后台任务",
|
||||
"settings.diagnostics.crons.job.next_trigger_time": "预计下次运行时间:",
|
||||
"settings.diagnostics.workflow_dispatcher.title": "工作流调度器"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { IconCirclePlus, IconCopy, IconDots, IconEdit, IconFingerprint, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { useMount, useRequest } from "ahooks";
|
||||
import { App, Avatar, Button, Dropdown, Input, Skeleton, Table, type TableProps, Tabs, Typography, theme } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
@@ -35,6 +35,16 @@ const AccessList = () => {
|
||||
const { accesses, loadedAtOnce, fetchAccesses, deleteAccess } = useAccessesStore(
|
||||
useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses", "deleteAccess"])
|
||||
);
|
||||
useMount(() => {
|
||||
fetchAccesses().catch((err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
});
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
@@ -187,17 +197,6 @@ const AccessList = () => {
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccesses().catch((err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { loading, run: refreshData } = useRequest(
|
||||
() => {
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
|
||||
@@ -217,7 +217,7 @@ const CertificateList = () => {
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
error: loadError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
@@ -413,10 +413,10 @@ const CertificateList = () => {
|
||||
<Empty
|
||||
className="py-24"
|
||||
title={t("certificate.nodata.title")}
|
||||
description={loadedError ? getErrMsg(loadedError) : t("certificate.nodata.description")}
|
||||
description={loadError ? getErrMsg(loadError) : t("certificate.nodata.description")}
|
||||
icon={<IconCertificate size={24} />}
|
||||
extra={
|
||||
loadedError ? (
|
||||
loadError ? (
|
||||
<Button icon={<IconReload size="1.25em" />} type="primary" onClick={handleReloadClick}>
|
||||
{t("common.button.reload")}
|
||||
</Button>
|
||||
|
||||
@@ -324,7 +324,7 @@ const WorkflowRunHistoryTable = ({ className, style }: { className?: string; sty
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
error: loadError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
@@ -379,10 +379,10 @@ const WorkflowRunHistoryTable = ({ className, style }: { className?: string; sty
|
||||
<Empty
|
||||
className="py-24"
|
||||
title={t("common.text.nodata")}
|
||||
description={loadedError ? getErrMsg(loadedError) : t("dashboard.latest_workflow_runs.nodata.description")}
|
||||
description={loadError ? getErrMsg(loadError) : t("dashboard.latest_workflow_runs.nodata.description")}
|
||||
icon={<IconHistory size={24} />}
|
||||
extra={
|
||||
loadedError ? (
|
||||
loadError ? (
|
||||
<Button icon={<IconReload size="1.25em" />} type="primary" onClick={handleReloadClick}>
|
||||
{t("common.button.reload")}
|
||||
</Button>
|
||||
|
||||
@@ -15,6 +15,7 @@ const Settings = () => {
|
||||
["appearance", "settings.appearance.tab", <IconPalette size="1em" />],
|
||||
["ssl-provider", "settings.sslprovider.tab", <IconPlugConnected size="1em" />],
|
||||
["persistence", "settings.persistence.tab", <IconDatabaseCog size="1em" />],
|
||||
["diagnostics", "settings.diagnostics.tab", <IconDatabaseCog size="1em" />],
|
||||
] satisfies [string, string, React.ReactElement][];
|
||||
const [menuKey, setMenuKey] = useState<string>(() => location.pathname.split("/")[2]);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -23,7 +23,7 @@ const SettingsAppearance = () => {
|
||||
<Divider />
|
||||
|
||||
<h2>{t("settings.appearance.pagination.title")}</h2>
|
||||
<SettingsPaginationLanguage className="md:max-w-160" />
|
||||
<SettingsAppearancePagination className="md:max-w-160" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -100,7 +100,7 @@ const SettingsAppearanceLanguage = ({ className, style }: { className?: string;
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsPaginationLanguage = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {
|
||||
const SettingsAppearancePagination = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { appSettings: globalAppSettings, setAppSettings: setGlobalAppSettings } = useAppSettings();
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconReload } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Button, Divider, Empty, List, Pagination, Tooltip, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { listCronJobs, listLogs } from "@/repository/system";
|
||||
import { getNextCronExecutions } from "@/utils/cron";
|
||||
import { mergeCls } from "@/utils/css";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const SettingsDiagnostics = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{t("settings.diagnostics.logs.title")}</h2>
|
||||
<SettingsDiagnosticsLogs />
|
||||
|
||||
<Divider />
|
||||
|
||||
<h2>{t("settings.diagnostics.crons.title")}</h2>
|
||||
<SettingsDiagnosticsCrons />
|
||||
|
||||
<Divider />
|
||||
|
||||
<h2>{t("settings.diagnostics.workflow_dispatcher.title")}</h2>
|
||||
<SettingsDiagnosticsWorkflowDispatcher className="md:max-w-160" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsDiagnosticsLogs = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
|
||||
type Log = Awaited<ReturnType<typeof listLogs>>["items"][number];
|
||||
const [listData, setListData] = useState<Log[]>([]);
|
||||
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
return listLogs({ page: page, perPage: pageSize });
|
||||
},
|
||||
{
|
||||
refreshDeps: [page, pageSize],
|
||||
onSuccess: (res) => {
|
||||
if (page === 1) {
|
||||
setListData([]);
|
||||
}
|
||||
|
||||
setListData((prev) => [...prev, ...res.items]);
|
||||
setHasMore(res.items.length >= pageSize);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const renderLogRecord = (record: Log) => {
|
||||
let message = <>{record.message}</>;
|
||||
if (record.data != null && Object.keys(record.data).length > 0) {
|
||||
message = (
|
||||
<details>
|
||||
<summary>{record.message}</summary>
|
||||
{Object.entries(record.data).map(([key, value]) => (
|
||||
<div key={key} className="flex space-x-2" style={{ wordBreak: "break-word" }}>
|
||||
<div>{key}:</div>
|
||||
<div>{JSON.stringify(value)}</div>
|
||||
</div>
|
||||
))}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
enum LogLevel {
|
||||
Info = 0,
|
||||
Warn = 4,
|
||||
Error = 8,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
<div className="font-mono text-stone-400">[{dayjs(record.created).format("YYYY-MM-DD HH:mm:ss")}]</div>
|
||||
<div
|
||||
className={mergeCls(
|
||||
"flex-1",
|
||||
"font-mono",
|
||||
+record.level < LogLevel.Info
|
||||
? "text-stone-400"
|
||||
: +record.level < LogLevel.Warn
|
||||
? ""
|
||||
: +record.level < LogLevel.Error
|
||||
? "text-warning"
|
||||
: "text-error"
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleReloadClick = () => {
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const handleLoadMoreClick = () => {
|
||||
setPage((prev) => prev + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className="size-full overflow-hidden rounded-md bg-black text-stone-200">
|
||||
<div className="relative">
|
||||
<Show when={loading}>
|
||||
<div className="absolute top-4 right-8">
|
||||
<Button className="pointer-none" loading>
|
||||
Loading ...
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={listData.length === 0}>
|
||||
<Empty description={loadError ? getErrMsg(loadError) : t("common.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE}>
|
||||
{loadError && (
|
||||
<Button icon={<IconReload size="1.25em" />} type="primary" onClick={handleReloadClick}>
|
||||
{t("common.button.reload")}
|
||||
</Button>
|
||||
)}
|
||||
</Empty>
|
||||
</Show>
|
||||
<Show when={listData.length > 0}>
|
||||
<div className="min-h-16 px-4 py-2">
|
||||
<div className="flex w-full flex-col overflow-hidden">
|
||||
{listData.map((record) => {
|
||||
return (
|
||||
<div key={record.id} className="text-xs leading-relaxed">
|
||||
{renderLogRecord(record)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasMore && (
|
||||
<a onClick={handleLoadMoreClick}>
|
||||
<span className="text-xs">Load more</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsDiagnosticsCrons = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
type CronJob = Awaited<ReturnType<typeof listCronJobs>>["items"][number];
|
||||
const [listData, setListData] = useState<CronJob[]>([]);
|
||||
const [listTotal, setListTotal] = useState(0);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
return listCronJobs().then((res) => {
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
return {
|
||||
items: res.items.slice(startIndex, endIndex),
|
||||
totalItems: res.items.length,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [page, pageSize],
|
||||
onSuccess: (res) => {
|
||||
setListData(res.items);
|
||||
setListTotal(res.totalItems);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const handleReloadClick = () => {
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const handlePaginationChange = (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<List<CronJob>
|
||||
bordered
|
||||
dataSource={listData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<Empty description={loadError ? getErrMsg(loadError) : t("common.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE}>
|
||||
{loadError && (
|
||||
<Button icon={<IconReload size="1.25em" />} type="primary" onClick={handleReloadClick}>
|
||||
{t("common.button.reload")}
|
||||
</Button>
|
||||
)}
|
||||
</Empty>
|
||||
),
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
renderItem={(record) => (
|
||||
<List.Item>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
{t("settings.diagnostics.crons.job.next_trigger_time")}
|
||||
<br />
|
||||
{dayjs(getNextCronExecutions(record.cron)[0]).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</>
|
||||
}
|
||||
mouseEnterDelay={1}
|
||||
placement="topRight"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-4 overflow-hidden">
|
||||
<div className="flex-1 truncate">
|
||||
<Typography.Text>{record.id}</Typography.Text>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Typography.Text type="secondary">{record.cron}</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<Show when={page > 1 || listTotal > pageSize}>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Pagination current={page} pageSize={pageSize} size="small" total={listTotal} onChange={handlePaginationChange} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsDiagnosticsWorkflowDispatcher = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div>TODO ...</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsDiagnostics;
|
||||
@@ -194,7 +194,7 @@ const WorkflowDetailRuns = () => {
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
error: loadError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
@@ -363,7 +363,7 @@ const WorkflowDetailRuns = () => {
|
||||
<Empty
|
||||
className="py-24"
|
||||
title={t("common.text.nodata")}
|
||||
description={loadedError ? getErrMsg(loadedError) : t("workflow_run.nodata.description")}
|
||||
description={loadError ? getErrMsg(loadError) : t("workflow_run.nodata.description")}
|
||||
icon={<IconHistory size={24} />}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -229,7 +229,7 @@ const WorkflowList = () => {
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
error: loadError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
@@ -491,10 +491,10 @@ const WorkflowList = () => {
|
||||
<Empty
|
||||
className="py-24"
|
||||
title={t("workflow.nodata.title")}
|
||||
description={loadedError ? getErrMsg(loadedError) : t("workflow.nodata.description")}
|
||||
description={loadError ? getErrMsg(loadError) : t("workflow.nodata.description")}
|
||||
icon={<IconHierarchy3 size={24} />}
|
||||
extra={
|
||||
loadedError ? (
|
||||
loadError ? (
|
||||
<Button icon={<IconReload size="1.25em" />} type="primary" onClick={handleReloadClick}>
|
||||
{t("common.button.reload")}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
export const listCronJobs = () => {
|
||||
return getPocketBase()
|
||||
.crons.getFullList({
|
||||
requestKey: null,
|
||||
})
|
||||
.then((res) => {
|
||||
const jobs = res
|
||||
.filter((job) => !job.id.startsWith("__pb"))
|
||||
.map((job) => {
|
||||
return {
|
||||
id: job.id,
|
||||
cron: job.expression,
|
||||
};
|
||||
});
|
||||
return {
|
||||
items: jobs,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export type ListLogsRequest = {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const listLogs = (request: ListLogsRequest) => {
|
||||
const page = request.page || 1;
|
||||
const perPage = request.perPage || 10;
|
||||
|
||||
return getPocketBase()
|
||||
.logs.getList(page, perPage, {
|
||||
filter: 'data.type!="request"',
|
||||
sort: "-@rowid",
|
||||
skipTotal: true,
|
||||
requestKey: null,
|
||||
})
|
||||
.then((res) => {
|
||||
return {
|
||||
items: res.items,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import Login from "@/pages/login/Login";
|
||||
import Settings from "@/pages/settings/Settings";
|
||||
import SettingsAccount from "@/pages/settings/SettingsAccount";
|
||||
import SettingsAppearance from "@/pages/settings/SettingsAppearance";
|
||||
import SettingsDiagnostics from "@/pages/settings/SettingsDiagnostics";
|
||||
import SettingsPersistence from "@/pages/settings/SettingsPersistence";
|
||||
import SettingsSSLProvider from "@/pages/settings/SettingsSSLProvider";
|
||||
import WorkflowDetail from "@/pages/workflows/WorkflowDetail";
|
||||
@@ -82,6 +83,10 @@ export const router = createHashRouter([
|
||||
path: "/settings/persistence",
|
||||
element: <SettingsPersistence />,
|
||||
},
|
||||
{
|
||||
path: "/settings/diagnostics",
|
||||
element: <SettingsDiagnostics />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user