refactor: style css -> tailwindcss
This commit is contained in:
+69
-90
@@ -1,4 +1,36 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss/theme.css";
|
||||
@import "tailwindcss/utilities.css";
|
||||
|
||||
@theme {
|
||||
--font-sans: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
|
||||
--color-brand: #1978d4;
|
||||
--color-brand-strong: #0e5fb9;
|
||||
--color-brand-soft: #edf6ff;
|
||||
|
||||
--color-success: #18b979;
|
||||
--color-success-strong: #0f7d55;
|
||||
--color-success-soft: #e8f7f1;
|
||||
|
||||
--color-danger: #d75b5b;
|
||||
--color-danger-strong: #a94f4f;
|
||||
--color-danger-soft: #fff6f6;
|
||||
|
||||
--color-warning: #e6a33c;
|
||||
--color-warning-soft: #fff0ee;
|
||||
|
||||
--color-ink: #27394d;
|
||||
--color-ink-muted: #5d7186;
|
||||
--color-ink-subtle: #8b99a8;
|
||||
|
||||
--color-line: #dce4ec;
|
||||
--color-line-soft: #edf1f5;
|
||||
|
||||
--color-bg: #f3f6f9;
|
||||
--color-bg-panel: #ffffff;
|
||||
--color-bg-canvas: #f9fbfd;
|
||||
--color-bg-log: #17212b;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
@@ -16,97 +48,44 @@
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
color: #1f354b;
|
||||
background: #f3f6f9;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@layer base {
|
||||
:root {
|
||||
font-family: var(--font-sans);
|
||||
color: #1f354b;
|
||||
background: #f3f6f9;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 1024px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; min-width: 1024px; min-height: 100%; }
|
||||
button, input, select, textarea { font: inherit; }
|
||||
@layer utilities {
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.auth-loading,
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.auth-loading {
|
||||
gap: 12px;
|
||||
color: #61758a;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: #d9e1e9;
|
||||
}
|
||||
|
||||
.auth-loading > span {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #bfd5e9;
|
||||
border-top-color: #1677ff;
|
||||
border-radius: 50%;
|
||||
animation: auth-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes auth-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.login-page {
|
||||
padding: 48px;
|
||||
background:
|
||||
radial-gradient(circle at 20% 15%, rgba(22, 119, 255, 0.12), transparent 34%),
|
||||
linear-gradient(145deg, #eef5fb, #f8fafc 55%, #edf3f8);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 420px;
|
||||
padding: 42px;
|
||||
border: 1px solid #dbe6ef;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 22px 60px rgba(37, 63, 88, 0.14);
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 13px;
|
||||
color: white;
|
||||
background: linear-gradient(145deg, #1177e8, #25a1f2);
|
||||
}
|
||||
|
||||
.login-kicker {
|
||||
margin: 26px 0 8px;
|
||||
color: #1677ff;
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.login-card h1 { margin: 0; font-size: 27px; }
|
||||
.login-description { margin: 10px 0 28px; color: #728398; }
|
||||
.login-card form { display: grid; gap: 18px; }
|
||||
.login-card label { display: grid; gap: 8px; color: #465b70; font-size: 13px; }
|
||||
.login-card input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #cad8e5;
|
||||
border-radius: 9px;
|
||||
outline: none;
|
||||
color: #18334f;
|
||||
background: white;
|
||||
}
|
||||
.login-card input:focus { border-color: #1677ff; box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.12); }
|
||||
.login-card button {
|
||||
min-height: 44px;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
color: white;
|
||||
background: #1677ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login-card button:disabled { opacity: 0.65; cursor: wait; }
|
||||
.login-error { margin: -4px 0 0; color: #d4380d; font-size: 13px; }
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: #c4d0dc;
|
||||
}
|
||||
}
|
||||
@@ -58,24 +58,27 @@ function ArtifactListImpl({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="schedule-panel schedule-artifacts-panel">
|
||||
<div className="schedule-panel__title">
|
||||
<div>
|
||||
<strong>稳定版本脚本</strong>
|
||||
<span className="schedule-panel__count">{artifacts.length}</span>
|
||||
<section className="flex min-h-0 flex-col overflow-hidden">
|
||||
<div className="flex min-h-[48px] items-center justify-between px-3">
|
||||
<div className="flex items-center gap-[7px]">
|
||||
<strong className="text-[14px] text-[#223247]">稳定版本脚本</strong>
|
||||
<span className="inline-grid h-[20px] min-w-[20px] place-items-center rounded-[10px] bg-[#eef3f8] px-[5px] text-[10px] text-[#718195]">
|
||||
{artifacts.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<label className="schedule-search">
|
||||
<label className="mx-[10px] mb-[7px] flex min-h-[35px] items-center gap-[7px] rounded-[6px] border border-[#dce5ed] bg-white px-[9px] text-[#95a3b2] focus-within:border-[#8db9e8] focus-within:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]">
|
||||
<Icon name="search" size={15} />
|
||||
<input
|
||||
className="min-w-0 flex-1 border-0 bg-transparent text-[12px] text-[#33475d] outline-none placeholder:text-[#a6b2be]"
|
||||
value={keyword}
|
||||
placeholder="搜索稳定版本"
|
||||
onChange={(event) => onKeywordChange(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<div className="artifact-list">
|
||||
<div className="scrollbar-thin min-h-0 overflow-auto px-2 pb-[9px]">
|
||||
{groups.length === 0 ? (
|
||||
<p className="schedule-empty">
|
||||
<p className="my-[18px_10px] text-center text-[10px] leading-[1.6] text-[#9aa6b3]">
|
||||
暂无稳定版本,请先在"构建脚本"中发布
|
||||
</p>
|
||||
) : (
|
||||
@@ -84,33 +87,50 @@ function ArtifactListImpl({
|
||||
const scriptType = items[0].script_type;
|
||||
const iconName = scriptType === "notebook" ? "notebook" : "python";
|
||||
return (
|
||||
<div key={scriptName} className="artifact-group">
|
||||
<div
|
||||
key={scriptName}
|
||||
className="mb-1.5 overflow-hidden rounded-[8px] border border-line-soft bg-white transition-[border-color,box-shadow] duration-[160ms] hover:border-[#e1e8ef]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="artifact-group__header"
|
||||
className="flex min-h-[42px] w-full cursor-pointer select-none items-center justify-between gap-2 border-0 bg-transparent px-[7px] py-[7px] text-left text-inherit hover:bg-[#f8fafc] min-[1281px]:px-2"
|
||||
aria-expanded={isExpanded}
|
||||
onClick={() => toggle(scriptName)}
|
||||
>
|
||||
<span className="artifact-group__main">
|
||||
<span className={`artifact-group__chevron${isExpanded ? " is-open" : ""}`}>
|
||||
<span className="flex min-w-0 items-center gap-[7px]">
|
||||
<span
|
||||
data-state={isExpanded ? "open" : "closed"}
|
||||
className="h-[14px] w-[14px] flex-none text-[#9aa7b5] transition-transform duration-[180ms] data-[state=open]:rotate-90"
|
||||
>
|
||||
<Icon name="chevron" size={14} />
|
||||
</span>
|
||||
<span className={`artifact-group__icon artifact-group__icon--${scriptType}`}>
|
||||
<span
|
||||
className={`grid h-[27px] w-[27px] flex-none place-items-center rounded-[6px] ${
|
||||
scriptType === "notebook"
|
||||
? "bg-[#fff0ee] text-[#dc5c52]"
|
||||
: "bg-[#eaf3ff] text-[#326fc1]"
|
||||
}`}
|
||||
>
|
||||
<Icon name={iconName} size={14} />
|
||||
</span>
|
||||
<span className="artifact-group__name" title={scriptName}>
|
||||
<span
|
||||
className="max-w-[125px] min-w-0 truncate text-[11px] font-semibold text-[#33465a] min-[1281px]:max-w-none"
|
||||
title={scriptName}
|
||||
>
|
||||
{scriptName}
|
||||
</span>
|
||||
</span>
|
||||
{items.length > 1 && (
|
||||
<span className="artifact-group__count">{items.length}</span>
|
||||
<span className="inline-flex flex-none items-center rounded-full border border-[#e5eaf0] bg-[#f5f7fa] px-1.5 py-[2px] text-[9px] leading-[1.3] whitespace-nowrap text-[#8794a3]">
|
||||
{items.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="artifact-versions">
|
||||
<div className="flex flex-col gap-[5px] border-t border-line-soft bg-[#f8fafc] p-[7px_7px_7px_27px] min-[1281px]:pl-[29px]">
|
||||
{items.map((artifact) => (
|
||||
<div
|
||||
className="artifact-version"
|
||||
className="group flex min-h-[34px] cursor-grab select-none items-center justify-between gap-[7px] rounded-[6px] border border-[#e4e9ee] bg-white p-1.5 transition-[border-color,background-color] duration-[140ms] hover:border-[#a8c8e9] hover:bg-[#fbfdff] active:cursor-grabbing min-[1281px]:px-[7px]"
|
||||
draggable
|
||||
key={artifact.versions_id}
|
||||
onDragStart={(event) => {
|
||||
@@ -124,18 +144,18 @@ function ArtifactListImpl({
|
||||
onContextMenu={(event) => onContextMenu(event, artifact)}
|
||||
onDoubleClick={() => onDoubleClick(artifact)}
|
||||
>
|
||||
<span className="artifact-version__main">
|
||||
<span className="artifact-version__handle">
|
||||
<span className="flex min-w-0 items-center gap-[5px]">
|
||||
<span className="h-[13px] w-[13px] flex-none text-[#c4cdd6] group-hover:text-[#7e8d9d]">
|
||||
<Icon name="grip-vertical" size={13} />
|
||||
</span>
|
||||
<span className="artifact-version__name">
|
||||
<span className="flex-none text-[10px] font-semibold text-[#526477]">
|
||||
{artifact.version_label}
|
||||
</span>
|
||||
<span className="artifact-version__commit">
|
||||
<span className="min-w-0 truncate font-mono text-[8px] text-[#9aa5b1]">
|
||||
{shortHash(artifact.content_hash)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="artifact-version__action">
|
||||
<span className="grid h-[18px] w-[18px] flex-none place-items-center rounded-[4px] text-[#c1cad3] group-hover:bg-[#eef6ff] group-hover:text-[#287ac5]">
|
||||
<Icon name="external" size={12} />
|
||||
</span>
|
||||
</div>
|
||||
@@ -147,9 +167,9 @@ function ArtifactListImpl({
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<p className="artifact-tip artifact-tip--footer">
|
||||
<p className="mx-[10px] mb-[9px] mt-[3px] flex items-center justify-center gap-1 rounded-[6px] border border-line-soft bg-[#f8fafc] p-[7px_8px] text-center text-[9px] leading-[1.45] text-[#8795a4]">
|
||||
<span>按住</span>
|
||||
<span className="artifact-tip__grip">
|
||||
<span className="inline-flex items-center justify-center text-[#9eabb8]">
|
||||
<Icon name="grip-vertical" size={12} />
|
||||
</span>
|
||||
<span>拖拽卡片到画布即可绑定节点</span>
|
||||
@@ -158,4 +178,4 @@ function ArtifactListImpl({
|
||||
);
|
||||
}
|
||||
|
||||
export const ArtifactList = memo(ArtifactListImpl);
|
||||
export const ArtifactList = memo(ArtifactListImpl);
|
||||
|
||||
@@ -19,33 +19,48 @@ export function NodeInspector({
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="schedule-inspector">
|
||||
<section className="node-inspector-title">
|
||||
<span className={`artifact-card__icon artifact-card__icon--${node.version.script_type}`}>
|
||||
<div className="flex min-h-full flex-col">
|
||||
<section className="flex flex-row items-center gap-[10px] border-b border-[#e7ecf1] p-[14px]">
|
||||
<span
|
||||
className={`grid h-[31px] w-[31px] flex-none place-items-center rounded-[6px] ${
|
||||
node.version.script_type === "notebook"
|
||||
? "bg-[#fff0ee] text-[#e05e53]"
|
||||
: "bg-[#eaf3ff] text-[#326fc1]"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
name={node.version.script_type === "notebook" ? "notebook" : "python"}
|
||||
size={17}
|
||||
/>
|
||||
</span>
|
||||
<div>
|
||||
<small>节点配置</small>
|
||||
<strong>{node.node_key}</strong>
|
||||
<div className="flex min-w-0 flex-col gap-[3px]">
|
||||
<small className="text-[9px] text-[#96a2af]">节点配置</small>
|
||||
<strong className="truncate font-mono text-[11px] text-[#30465d]">
|
||||
{node.node_key}
|
||||
</strong>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>稳定版本</h3>
|
||||
<div className="version-readonly">
|
||||
<strong>{node.version.script_name}</strong>
|
||||
<span>{node.version.version_label}</span>
|
||||
<small>versions_id: {node.versions_id}</small>
|
||||
<small>SHA-256: {shortHash(node.version.content_hash)}</small>
|
||||
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
|
||||
<h3 className="mb-[2px] text-[13px] text-[#273b50]">稳定版本</h3>
|
||||
<div className="flex flex-col gap-[5px] rounded-[6px] border border-[#dae6f2] bg-[#f6faff] p-[9px]">
|
||||
<strong className="text-[11px] text-[#35516e]">{node.version.script_name}</strong>
|
||||
<span className="w-fit rounded-[8px] bg-[#e3f0fd] px-1.5 py-[2px] text-[9px] font-bold text-[#176ec5]">
|
||||
{node.version.version_label}
|
||||
</span>
|
||||
<small className="truncate font-mono text-[8px] text-[#8190a0]">
|
||||
versions_id: {node.versions_id}
|
||||
</small>
|
||||
<small className="truncate font-mono text-[8px] text-[#8190a0]">
|
||||
SHA-256: {shortHash(node.version.content_hash)}
|
||||
</small>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>节点信息</h3>
|
||||
<label>
|
||||
<span>节点名称</span>
|
||||
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
|
||||
<h3 className="mb-[2px] text-[13px] text-[#273b50]">节点信息</h3>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">节点名称</span>
|
||||
<input
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.nodeName}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -53,12 +68,13 @@ export function NodeInspector({
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<div className="inspector-grid">
|
||||
<label>
|
||||
<span>超时(秒)</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">超时(秒)</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.timeoutSeconds}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -66,12 +82,13 @@ export function NodeInspector({
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>重试次数</span>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">重试次数</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.retryCount}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -80,12 +97,13 @@ export function NodeInspector({
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="inspector-grid">
|
||||
<label>
|
||||
<span>重试间隔(秒)</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">重试间隔(秒)</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.retryIntervalSec}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -93,9 +111,10 @@ export function NodeInspector({
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Python 版本</span>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">Python 版本</span>
|
||||
<select
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.pythonVersion}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -111,12 +130,12 @@ export function NodeInspector({
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>运行参数</h3>
|
||||
<label>
|
||||
<span>arguments_json</span>
|
||||
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
|
||||
<h3 className="mb-[2px] text-[13px] text-[#273b50]">运行参数</h3>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">arguments_json</span>
|
||||
<textarea
|
||||
className="json-editor"
|
||||
className="w-full resize-y rounded-[5px] border border-line p-[7px_8px] font-mono text-[10px] leading-[1.5] !bg-[#f8fafc] !text-[#37516a] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
rows={5}
|
||||
spellCheck={false}
|
||||
value={form.argumentsJson}
|
||||
@@ -126,10 +145,10 @@ export function NodeInspector({
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>env_refs_json</span>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">env_refs_json</span>
|
||||
<textarea
|
||||
className="json-editor"
|
||||
className="w-full resize-y rounded-[5px] border border-line p-[7px_8px] font-mono text-[10px] leading-[1.5] !bg-[#f8fafc] !text-[#37516a] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
rows={4}
|
||||
spellCheck={false}
|
||||
value={form.envRefsJson}
|
||||
@@ -140,9 +159,9 @@ export function NodeInspector({
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<div className="node-inspector-actions">
|
||||
<div className="mt-auto grid grid-cols-[1fr_90px] gap-[7px] p-[14px]">
|
||||
<button
|
||||
className="inspector-primary-button"
|
||||
className="flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-brand bg-brand text-[11px] text-white [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={onSave}
|
||||
@@ -150,7 +169,7 @@ export function NodeInspector({
|
||||
保存节点
|
||||
</button>
|
||||
<button
|
||||
className="inspector-danger-button"
|
||||
className="flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-[#efc6c6] bg-[#fff7f7] text-[11px] text-[#bd4f4f] [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={onDelete}
|
||||
|
||||
@@ -216,13 +216,16 @@ export function RunHistory({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="schedule-run-history">
|
||||
<header>
|
||||
<div>
|
||||
<strong>运行记录</strong>
|
||||
<span>{runs.length}</span>
|
||||
<section className="flex min-h-0 flex-col border-t border-[#e0e7ee] bg-[#fbfcfe]">
|
||||
<header className="flex min-h-[44px] items-center justify-between border-b border-[#e8edf2] bg-white px-3">
|
||||
<div className="flex items-center gap-[7px]">
|
||||
<strong className="text-[12px] text-[#2b4056]">运行记录</strong>
|
||||
<span className="inline-grid h-[18px] min-w-[18px] place-items-center rounded-[9px] bg-[#edf2f7] text-[9px] text-[#71849a]">
|
||||
{runs.length}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="grid h-[27px] w-[27px] cursor-pointer place-items-center rounded-[5px] border border-[#dce5ee] bg-white text-[#66809a] disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
type="button"
|
||||
aria-label="刷新运行记录"
|
||||
disabled={disabled || loading}
|
||||
@@ -231,34 +234,50 @@ export function RunHistory({
|
||||
<Icon name="refresh" size={13} />
|
||||
</button>
|
||||
</header>
|
||||
<div className="schedule-run-list">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-[7px] overflow-auto p-[9px]">
|
||||
{loading && runs.length === 0 ? (
|
||||
<p>正在加载运行记录…</p>
|
||||
<p className="my-auto p-3 text-center text-[10px] leading-[1.6] text-[#929fac]">
|
||||
正在加载运行记录…
|
||||
</p>
|
||||
) : runs.length === 0 ? (
|
||||
<p>点击右上角“立即运行”后,这里会显示状态和耗时。</p>
|
||||
<p className="my-auto p-3 text-center text-[10px] leading-[1.6] text-[#929fac]">
|
||||
点击右上角“立即运行”后,这里会显示状态和耗时。
|
||||
</p>
|
||||
) : (
|
||||
runs.map((run) => {
|
||||
const expanded = expandedRunId === run.run_id;
|
||||
const detail = runDetails[run.run_id];
|
||||
return (
|
||||
<article
|
||||
className={`schedule-run-card${expanded ? " is-expanded" : ""}`}
|
||||
data-state={expanded ? "expanded" : "collapsed"}
|
||||
className="flex-none overflow-hidden rounded-[6px] border border-[#e1e8ef] bg-white data-[state=expanded]:border-[#b9d6f1] data-[state=expanded]:shadow-[0_3px_10px_rgb(38_117_185_/_8%)]"
|
||||
key={run.run_id}
|
||||
>
|
||||
<div
|
||||
className="schedule-run-summary"
|
||||
className="grid w-full items-center gap-[7px] border-0 bg-white p-2 text-left text-inherit [grid-template-columns:8px_minmax(0,1fr)_auto_auto] hover:bg-[#f7fbff]"
|
||||
>
|
||||
<span className={`run-status-dot is-${run.run_status}`} />
|
||||
<span className="schedule-run-summary-copy">
|
||||
<strong>{RUN_STATUS_LABELS[run.run_status]}</strong>
|
||||
<small>
|
||||
<span
|
||||
data-state={run.run_status}
|
||||
className="mt-[3px] size-[7px] rounded-full bg-slate-400 data-[state=queued]:bg-warning data-[state=running]:bg-blue-500 data-[state=succeeded]:bg-emerald-500 data-[state=failed]:bg-red-500 data-[state=cancelled]:bg-red-500 data-[state=timed_out]:bg-red-500 data-[state=skipped]:bg-slate-400"
|
||||
/>
|
||||
<span className="flex min-w-0 flex-col gap-[2px]">
|
||||
<strong className="text-[10px] text-[#425970]">
|
||||
{RUN_STATUS_LABELS[run.run_status]}
|
||||
</strong>
|
||||
<small className="text-[8px] text-ink-subtle">
|
||||
{formatTime(run.queued_at)} · {formatDuration(run.duration_ms)}
|
||||
</small>
|
||||
{run.error_message && <span>{run.error_message}</span>}
|
||||
{run.error_message && (
|
||||
<span className="truncate text-[8px] leading-[1.4] text-[#b65353]">
|
||||
{run.error_message}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<code title={run.run_id}>{run.run_id.slice(-8)}</code>
|
||||
<code className="font-mono text-[8px] text-[#8b9bad]" title={run.run_id}>
|
||||
{run.run_id.slice(-8)}
|
||||
</code>
|
||||
<button
|
||||
className="schedule-run-result-button"
|
||||
className="min-h-[27px] cursor-pointer whitespace-nowrap rounded-[5px] border border-[#bcd4e9] bg-[#f5faff] px-[9px] text-[9px] text-[#2374ba] [font-weight:650] hover:border-[#7eafe0] hover:bg-[#eaf5ff]"
|
||||
type="button"
|
||||
aria-haspopup="dialog"
|
||||
onClick={() => toggleRun(run.run_id)}
|
||||
@@ -270,25 +289,29 @@ export function RunHistory({
|
||||
{expanded && (
|
||||
<>
|
||||
<button
|
||||
className="schedule-run-result-backdrop"
|
||||
className="fixed inset-0 z-[40] cursor-default border-0 bg-[rgb(10_27_45_/_46%)]"
|
||||
type="button"
|
||||
aria-label="关闭运行结果"
|
||||
onClick={() => toggleRun(run.run_id)}
|
||||
/>
|
||||
<section
|
||||
className="schedule-run-result-modal"
|
||||
className="fixed left-1/2 top-1/2 z-[41] grid max-h-[min(760px,calc(100vh-64px))] w-[min(780px,calc(100vw-48px))] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-[12px] border border-[#d8e2eb] bg-white shadow-[0_24px_80px_rgb(8_27_48_/_30%)] [grid-template-rows:auto_minmax(0,1fr)]"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="调度运行结果"
|
||||
>
|
||||
<header className="schedule-run-result-modal__header">
|
||||
<div>
|
||||
<span>调度运行结果</span>
|
||||
<strong>{RUN_STATUS_LABELS[run.run_status]}</strong>
|
||||
<code>{run.run_id}</code>
|
||||
<header className="flex items-center justify-between gap-[18px] border-b border-[#e3eaf0] bg-white p-[17px_20px]">
|
||||
<div className="grid min-w-0 items-center gap-[5px_10px] [grid-template-columns:auto_auto]">
|
||||
<span className="text-[17px] font-bold text-[#1f344a]">调度运行结果</span>
|
||||
<strong className="justify-self-start rounded-[11px] bg-success-soft px-2 py-[3px] text-[10px] text-[#17805a]">
|
||||
{RUN_STATUS_LABELS[run.run_status]}
|
||||
</strong>
|
||||
<code className="col-span-2 truncate font-mono text-[10px] text-[#8291a1]">
|
||||
{run.run_id}
|
||||
</code>
|
||||
</div>
|
||||
<button
|
||||
className="icon-button"
|
||||
className="grid h-[34px] w-[34px] cursor-pointer place-items-center rounded-[7px] border border-[#dfe6ed] bg-white hover:border-[#b9c9da] hover:bg-[#f7faff]"
|
||||
type="button"
|
||||
aria-label="关闭运行结果"
|
||||
onClick={() => toggleRun(run.run_id)}
|
||||
@@ -296,49 +319,90 @@ export function RunHistory({
|
||||
<Icon name="close" size={16} />
|
||||
</button>
|
||||
</header>
|
||||
<div className="schedule-run-detail">
|
||||
<div className="min-h-0 overflow-auto bg-[#f8fbfe] p-[18px]">
|
||||
{detailLoadingId === run.run_id && !detail ? (
|
||||
<p className="run-detail-message">正在加载运行详情…</p>
|
||||
<p className="m-0 p-[8px_4px] text-center text-[9px] leading-[1.5] text-[#8796a6]">
|
||||
正在加载运行详情…
|
||||
</p>
|
||||
) : detailErrors[run.run_id] ? (
|
||||
<p className="run-detail-message is-error">
|
||||
<p
|
||||
data-state="error"
|
||||
className="m-0 p-[8px_4px] text-center text-[9px] leading-[1.5] text-[#8796a6] data-[state=error]:text-[#bf5656]"
|
||||
>
|
||||
{detailErrors[run.run_id]}
|
||||
</p>
|
||||
) : detail ? (
|
||||
<>
|
||||
<dl className="schedule-run-meta">
|
||||
<div><dt>触发方式</dt><dd>{TRIGGER_TYPE_LABELS[detail.trigger_type]}</dd></div>
|
||||
<div><dt>工作流版本</dt><dd>v{detail.workflow_version}</dd></div>
|
||||
<div><dt>开始时间</dt><dd>{formatTime(detail.started_at)}</dd></div>
|
||||
<div><dt>完成时间</dt><dd>{formatTime(detail.finished_at)}</dd></div>
|
||||
<dl className="mb-[14px] grid gap-[10px_18px] [grid-template-columns:repeat(4,minmax(0,1fr))]">
|
||||
<div className="min-w-0">
|
||||
<dt className="mb-1 text-[10px] text-[#95a2af]">触发方式</dt>
|
||||
<dd className="m-0 truncate text-[12px] text-[#41566c]">
|
||||
{TRIGGER_TYPE_LABELS[detail.trigger_type]}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<dt className="mb-1 text-[10px] text-[#95a2af]">工作流版本</dt>
|
||||
<dd className="m-0 truncate text-[12px] text-[#41566c]">
|
||||
v{detail.workflow_version}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<dt className="mb-1 text-[10px] text-[#95a2af]">开始时间</dt>
|
||||
<dd className="m-0 truncate text-[12px] text-[#41566c]">
|
||||
{formatTime(detail.started_at)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<dt className="mb-1 text-[10px] text-[#95a2af]">完成时间</dt>
|
||||
<dd className="m-0 truncate text-[12px] text-[#41566c]">
|
||||
{formatTime(detail.finished_at)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{(detail.error_code || detail.error_message) && (
|
||||
<div className="schedule-run-error">
|
||||
{detail.error_code && <code>{detail.error_code}</code>}
|
||||
{detail.error_message && <p>{detail.error_message}</p>}
|
||||
<div className="mb-2 rounded-[5px] border border-[#f0cece] bg-danger-soft p-[7px] text-danger-strong">
|
||||
{detail.error_code && <code className="text-[8px] font-bold">{detail.error_code}</code>}
|
||||
{detail.error_message && (
|
||||
<p className="mt-[3px] text-[8px] leading-[1.45]">{detail.error_message}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="schedule-node-run-list">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{detail.node_runs.length === 0 ? (
|
||||
<p className="run-detail-message">节点尚未开始执行</p>
|
||||
<p className="m-0 p-[8px_4px] text-center text-[9px] leading-[1.5] text-[#8796a6]">
|
||||
节点尚未开始执行
|
||||
</p>
|
||||
) : detail.node_runs.map((nodeRun) => {
|
||||
const node = nodes.find((item) => item.node_id === nodeRun.node_id);
|
||||
const logState = logStates[nodeRun.node_run_id];
|
||||
const logOpen = openLogNodeRunIds.has(nodeRun.node_run_id);
|
||||
return (
|
||||
<article className="schedule-node-run" key={nodeRun.node_run_id}>
|
||||
<header>
|
||||
<span className={`run-status-dot is-${nodeRun.node_status}`} />
|
||||
<strong>{node?.node_name ?? nodeRun.node_id.slice(-8)}</strong>
|
||||
<span>{NODE_STATUS_LABELS[nodeRun.node_status]}</span>
|
||||
<article className="rounded-[7px] border border-[#dde7f0] bg-white p-3" key={nodeRun.node_run_id}>
|
||||
<header className="grid items-start gap-1.5 [grid-template-columns:8px_minmax(0,1fr)_auto]">
|
||||
<span
|
||||
data-state={nodeRun.node_status}
|
||||
className="mt-[3px] size-[7px] rounded-full bg-slate-400 data-[state=queued]:bg-warning data-[state=running]:bg-blue-500 data-[state=succeeded]:bg-emerald-500 data-[state=failed]:bg-red-500 data-[state=cancelled]:bg-red-500 data-[state=timed_out]:bg-red-500 data-[state=skipped]:bg-slate-400"
|
||||
/>
|
||||
<strong className="truncate text-[13px] text-[#40566c]">
|
||||
{node?.node_name ?? nodeRun.node_id.slice(-8)}
|
||||
</strong>
|
||||
<span className="text-[10px] text-[#71869a]">
|
||||
{NODE_STATUS_LABELS[nodeRun.node_status]}
|
||||
</span>
|
||||
</header>
|
||||
<div className="schedule-node-run-meta">
|
||||
<div className="ml-[14px] mt-[5px] flex flex-wrap gap-[3px_8px] text-[10px] text-[#8a98a7]">
|
||||
<span>第 {nodeRun.attempt_no} 次</span>
|
||||
<span>{formatDuration(nodeRun.duration_ms)}</span>
|
||||
<span>退出码 {nodeRun.exit_code ?? "—"}</span>
|
||||
</div>
|
||||
{nodeRun.message && <p>{nodeRun.message}</p>}
|
||||
<div className="schedule-node-run-actions">
|
||||
{nodeRun.message && (
|
||||
<p className="ml-[14px] mt-[5px] text-[10px] leading-[1.45] text-[#6c7e90]">
|
||||
{nodeRun.message}
|
||||
</p>
|
||||
)}
|
||||
<div className="ml-[14px] mt-[7px] flex gap-1.5">
|
||||
<button
|
||||
className="cursor-pointer rounded-[4px] border border-[#cbddeb] bg-[#f5faff] p-[6px_10px] text-[10px] text-[#2878bd] disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
type="button"
|
||||
disabled={!nodeRun.logs_object_id || logState?.loading}
|
||||
onClick={() => void toggleLog(run.run_id, nodeRun.node_run_id)}
|
||||
@@ -346,6 +410,7 @@ export function RunHistory({
|
||||
{logState?.loading ? "读取中…" : logOpen ? "收起日志" : "查看日志"}
|
||||
</button>
|
||||
<button
|
||||
className="cursor-pointer rounded-[4px] border border-[#cbddeb] bg-[#f5faff] p-[6px_10px] text-[10px] text-[#2878bd] disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
type="button"
|
||||
disabled={
|
||||
!nodeRun.logs_object_id
|
||||
@@ -358,6 +423,7 @@ export function RunHistory({
|
||||
: "下载日志"}
|
||||
</button>
|
||||
<button
|
||||
className="cursor-pointer rounded-[4px] border border-[#cbddeb] bg-[#f5faff] p-[6px_10px] text-[10px] text-[#2878bd] disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
type="button"
|
||||
disabled={
|
||||
!nodeRun.result_object_id
|
||||
@@ -371,24 +437,28 @@ export function RunHistory({
|
||||
</button>
|
||||
</div>
|
||||
{artifactErrors[nodeRun.node_run_id] && (
|
||||
<p className="schedule-artifact-error">
|
||||
<p className="!text-[#b65353]">
|
||||
{artifactErrors[nodeRun.node_run_id]}
|
||||
</p>
|
||||
)}
|
||||
{logOpen && (
|
||||
<div className="schedule-node-log">
|
||||
<div className="ml-[14px] mt-[7px] overflow-hidden rounded-[4px] border border-slate-700 bg-bg-log">
|
||||
{logState?.fileName && (
|
||||
<header>
|
||||
<header className="flex items-center justify-between border-b border-slate-700 bg-bg-log p-[5px_7px] text-[10px] text-slate-300">
|
||||
<span>{logState.fileName}</span>
|
||||
<small>
|
||||
<small className="text-[#8292a1]">
|
||||
{formatSize(new TextEncoder().encode(logState.content ?? "").length)}
|
||||
</small>
|
||||
</header>
|
||||
)}
|
||||
{logState?.error ? (
|
||||
<p>{logState.error}</p>
|
||||
<p className="m-0 max-h-[260px] overflow-auto p-[11px] font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-words text-slate-200">
|
||||
{logState.error}
|
||||
</p>
|
||||
) : (
|
||||
<pre>{logState?.content ?? "正在读取日志…"}</pre>
|
||||
<pre className="m-0 max-h-[260px] overflow-auto p-[11px] font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-words text-slate-200">
|
||||
{logState?.content ?? "正在读取日志…"}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,15 +8,19 @@ export function ScheduleCanvasHeader({
|
||||
onCancelLink: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="schedule-canvas-title">
|
||||
<div>
|
||||
<strong>流程画布</strong>
|
||||
<span>
|
||||
<div className="flex min-h-[53px] items-center justify-between border-b border-[#e4eaf0] px-3">
|
||||
<div className="flex min-w-0 flex-col gap-[3px]">
|
||||
<strong className="text-[14px] text-ink">流程画布</strong>
|
||||
<span className="hidden truncate text-[10px] text-[#93a0ae] 2xl:block">
|
||||
拖入稳定版本;点击节点右侧圆点,再点击目标左侧圆点完成连线
|
||||
</span>
|
||||
</div>
|
||||
{linkSourceId && (
|
||||
<button type="button" onClick={onCancelLink}>
|
||||
<button
|
||||
className="inline-flex min-h-[28px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-[#dce5ee] bg-white px-3 text-[12px] font-semibold text-[#b25a4f] hover:border-[#aac6e5] hover:bg-[#f6faff] hover:text-[#126aca]"
|
||||
type="button"
|
||||
onClick={onCancelLink}
|
||||
>
|
||||
取消连线
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -32,20 +32,23 @@ export function ScheduleInspector({
|
||||
}) {
|
||||
if (!schedule) {
|
||||
return (
|
||||
<div className="schedule-inspector-empty">
|
||||
<div className="flex min-h-full flex-col items-center justify-center p-[30px] text-center text-[#9aa8b6] [&>svg]:mb-[10px] [&>svg]:text-[#93b4d6]">
|
||||
<Icon name="settings" size={28} />
|
||||
<strong>调度属性</strong>
|
||||
<p>选中调度方案后可配置触发方式、Cron 和执行策略。</p>
|
||||
<strong className="text-[14px] text-[#516a82]">调度属性</strong>
|
||||
<p className="text-[11px] leading-[1.6]">
|
||||
选中调度方案后可配置触发方式、Cron 和执行策略。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="schedule-inspector">
|
||||
<section>
|
||||
<h3>基本信息</h3>
|
||||
<label>
|
||||
<span>调度名称</span>
|
||||
<div className="flex min-h-full flex-col">
|
||||
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
|
||||
<h3 className="mb-[2px] text-[13px] text-[#273b50]">基本信息</h3>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">调度名称</span>
|
||||
<input
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.scheduleName}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -53,9 +56,10 @@ export function ScheduleInspector({
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>说明</span>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">说明</span>
|
||||
<textarea
|
||||
className="w-full resize-y rounded-[5px] border border-line bg-white p-[7px_8px] text-[11px] leading-[1.5] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
rows={2}
|
||||
value={form.description}
|
||||
onChange={(event) => onChange({
|
||||
@@ -64,13 +68,14 @@ export function ScheduleInspector({
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label className="schedule-switch-row">
|
||||
<span>
|
||||
<b>启用调度</b>
|
||||
<small>DAG 有效后才能启用</small>
|
||||
<label className="flex flex-row items-center justify-between rounded-[6px] bg-[#f5f8fb] p-[8px_9px]">
|
||||
<span className="flex flex-col gap-[2px]">
|
||||
<b className="text-[11px] text-[#45596f]">启用调度</b>
|
||||
<small className="text-[9px] font-normal text-[#94a1ae]">DAG 有效后才能启用</small>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-[18px] w-[34px] accent-brand"
|
||||
checked={form.enabled}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -80,11 +85,12 @@ export function ScheduleInspector({
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>触发器</h3>
|
||||
<label>
|
||||
<span>触发方式</span>
|
||||
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
|
||||
<h3 className="mb-[2px] text-[13px] text-[#273b50]">触发器</h3>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">触发方式</span>
|
||||
<select
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.triggerType}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -99,9 +105,10 @@ export function ScheduleInspector({
|
||||
</label>
|
||||
{form.triggerType === "cron" && (
|
||||
<>
|
||||
<label>
|
||||
<span>Cron(分 时 日 月 周)</span>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">Cron(分 时 日 月 周)</span>
|
||||
<input
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.cronExpression}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -109,9 +116,10 @@ export function ScheduleInspector({
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>时区</span>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">时区</span>
|
||||
<input
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.timezone}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -120,7 +128,7 @@ export function ScheduleInspector({
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="inspector-secondary-button"
|
||||
className="flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-[#bcd2e9] bg-[#f7fbff] text-[11px] text-[#2871bb] [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={onPreview}
|
||||
@@ -128,7 +136,7 @@ export function ScheduleInspector({
|
||||
预览未来 5 次
|
||||
</button>
|
||||
{cronResult && (
|
||||
<ol className="cron-preview-list">
|
||||
<ol className="m-0 list-decimal rounded-[5px] bg-[#f5f8fb] p-[8px_8px_8px_28px] font-mono text-[9px] leading-[1.7] text-ink-muted">
|
||||
{cronResult.occurrences.map((item) => (
|
||||
<li key={item.utc_time}>
|
||||
{new Date(item.local_time).toLocaleString("zh-CN", {
|
||||
@@ -142,15 +150,16 @@ export function ScheduleInspector({
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>执行策略</h3>
|
||||
<div className="inspector-grid">
|
||||
<label>
|
||||
<span>最大并发</span>
|
||||
<section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
|
||||
<h3 className="mb-[2px] text-[13px] text-[#273b50]">执行策略</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">最大并发</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="64"
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.maxConcurrency}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -158,9 +167,10 @@ export function ScheduleInspector({
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>失败策略</span>
|
||||
<label className="flex flex-col gap-[5px]">
|
||||
<span className="text-[10px] font-semibold text-[#758497]">失败策略</span>
|
||||
<select
|
||||
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-[#81b2e7] focus:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]"
|
||||
value={form.failurePolicy}
|
||||
onChange={(event) => onChange({
|
||||
...form,
|
||||
@@ -172,14 +182,14 @@ export function ScheduleInspector({
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="schedule-next-run">
|
||||
<div className="flex items-center justify-between rounded-[5px] bg-[#f5f8fb] p-[8px_9px] text-[10px] text-ink-subtle">
|
||||
<span>下一次执行</span>
|
||||
<strong>{formatTime(schedule.next_run_at)}</strong>
|
||||
<strong className="text-[10px] text-[#425970]">{formatTime(schedule.next_run_at)}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button
|
||||
className="inspector-primary-button"
|
||||
className="m-[14px] flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-brand bg-brand text-[11px] text-white [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={onSave}
|
||||
|
||||
@@ -23,49 +23,63 @@ export function ScheduleList({
|
||||
: schedules;
|
||||
|
||||
return (
|
||||
<section className="schedule-panel schedule-list-panel">
|
||||
<section className="flex min-h-0 flex-col overflow-hidden border-b border-[#e3e9ef]">
|
||||
<div
|
||||
className="schedule-panel__title"
|
||||
className="flex min-h-[48px] items-center justify-between px-3"
|
||||
onContextMenu={(event) => onContextMenu(event, null)}
|
||||
>
|
||||
<div><strong>调度方案</strong><span>{schedules.length}</span></div>
|
||||
<div className="flex items-center gap-[7px]">
|
||||
<strong className="text-[14px] text-[#223247]">调度方案</strong>
|
||||
<span className="inline-grid h-[20px] min-w-[20px] place-items-center rounded-[10px] bg-[#eef3f8] px-[5px] text-[10px] text-[#718195]">
|
||||
{schedules.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<label className="schedule-search">
|
||||
<label className="mx-[10px] mb-[7px] flex min-h-[35px] items-center gap-[7px] rounded-[6px] border border-[#dce5ed] bg-white px-[9px] text-[#95a3b2] focus-within:border-[#8db9e8] focus-within:shadow-[0_0_0_3px_rgb(38_124_216_/_8%)]">
|
||||
<Icon name="search" size={15} />
|
||||
<input
|
||||
className="min-w-0 flex-1 border-0 bg-transparent text-[12px] text-[#33475d] outline-none placeholder:text-[#a6b2be]"
|
||||
value={keyword}
|
||||
placeholder="搜索调度名称"
|
||||
onChange={(event) => onKeywordChange(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
className="schedule-list"
|
||||
className="min-h-0 overflow-auto px-2 pb-[9px]"
|
||||
onContextMenu={(event) => onContextMenu(event, null)}
|
||||
>
|
||||
{loading ? (
|
||||
<p className="schedule-empty">正在加载调度方案…</p>
|
||||
<p className="my-[18px_10px] text-center text-[11px] leading-[1.6] text-[#95a2b0]">
|
||||
正在加载调度方案…
|
||||
</p>
|
||||
) : filtered.length ? (
|
||||
filtered.map((item) => (
|
||||
<button
|
||||
className={`schedule-card${
|
||||
selectedScheduleId === item.schedule_id ? " schedule-card--active" : ""
|
||||
}`}
|
||||
className={`relative mb-1.5 flex min-h-[62px] w-full cursor-pointer items-center gap-[9px] rounded-[6px] border border-[#e1e8ef] bg-white px-[10px] py-2 text-left text-[#526276] hover:border-[#b9cfe7] hover:bg-[#f8fbff] data-[state=active]:border-[#89b8ec] data-[state=active]:bg-brand-soft data-[state=active]:text-[#1c64b1] data-[state=active]:shadow-[inset_3px_0_#247fdc]`}
|
||||
data-state={selectedScheduleId === item.schedule_id ? "active" : "idle"}
|
||||
key={item.schedule_id}
|
||||
type="button"
|
||||
onClick={() => onSelect(item.schedule_id)}
|
||||
onContextMenu={(event) => onContextMenu(event, item)}
|
||||
>
|
||||
<span className={`schedule-status${item.enabled ? " is-enabled" : ""}`} />
|
||||
<span>
|
||||
<strong>{item.schedule_name}</strong>
|
||||
<small>
|
||||
<span
|
||||
data-state={item.enabled ? "enabled" : "disabled"}
|
||||
className="h-2 w-2 flex-none rounded-full bg-[#aeb9c5] shadow-[0_0_0_3px_rgb(142_156_171_/_11%)] data-[state=enabled]:bg-success data-[state=enabled]:shadow-[0_0_0_3px_rgb(24_185_121_/_12%)]"
|
||||
/>
|
||||
<span className="flex min-w-0 flex-col gap-1">
|
||||
<strong className="max-w-[205px] truncate text-[12px] text-[#283b50]">
|
||||
{item.schedule_name}
|
||||
</strong>
|
||||
<small className="text-[10px] text-[#8d9aab]">
|
||||
{item.node_count} 个节点 · {item.enabled ? "已启用" : "未启用"}
|
||||
</small>
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p className="schedule-empty">暂无调度方案</p>
|
||||
<p className="my-[18px_10px] text-center text-[11px] leading-[1.6] text-[#95a2b0]">
|
||||
暂无调度方案
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
} from "./state/schedulesStore";
|
||||
import { useSchedulesStore } from "./state/schedulesStore";
|
||||
import { edgePath, nodeToForm, scheduleToForm } from "./utils";
|
||||
import "../../styles/schedule.css";
|
||||
|
||||
type Notice = {
|
||||
tone: "success" | "error" | "info";
|
||||
@@ -78,7 +77,7 @@ function RunNowButton({
|
||||
const positionDraftCount = useSchedulesStore((s) => s.positionDraftCount);
|
||||
return (
|
||||
<button
|
||||
className="schedule-action--run"
|
||||
className="inline-flex min-h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-emerald-600 bg-emerald-600 px-2 text-[12px] font-semibold text-white enabled:hover:border-emerald-700 enabled:hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
|
||||
type="button"
|
||||
disabled={
|
||||
!schedule
|
||||
@@ -139,7 +138,7 @@ const ScheduleEdge = memo(function ScheduleEdge({
|
||||
if (!path) return null;
|
||||
return (
|
||||
<g
|
||||
className={selectedEdgeId === edge.edge_id ? "is-selected" : ""}
|
||||
data-state={selectedEdgeId === edge.edge_id ? "selected" : "idle"}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
useSchedulesStore.getState().setSelectedEdgeId(edge.edge_id);
|
||||
@@ -150,9 +149,16 @@ const ScheduleEdge = memo(function ScheduleEdge({
|
||||
edge,
|
||||
})}
|
||||
>
|
||||
<path className="schedule-edge-hit" d={path} />
|
||||
<path
|
||||
className="schedule-edge-line"
|
||||
className="schedule-edge-hit fill-none stroke-transparent [stroke-width:14] cursor-pointer [pointer-events:stroke]"
|
||||
d={path}
|
||||
/>
|
||||
<path
|
||||
className={`schedule-edge-line fill-none ${
|
||||
selectedEdgeId === edge.edge_id
|
||||
? "stroke-[#df8142] [stroke-width:3.4]"
|
||||
: "stroke-[#4e92db] [stroke-width:2.2]"
|
||||
}`}
|
||||
d={path}
|
||||
markerEnd="url(#schedule-arrow)"
|
||||
/>
|
||||
@@ -338,23 +344,28 @@ export default function SchedulePage({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="schedule-page">
|
||||
<header className="schedule-toolbar">
|
||||
<div>
|
||||
<strong>图形化调度</strong>
|
||||
<span>
|
||||
<section className="relative flex min-h-0 flex-1 flex-col gap-[10px] overflow-hidden p-[10px]">
|
||||
<header className="flex min-h-[56px] items-center justify-between rounded-[7px] border border-line bg-white px-[14px] shadow-[0_2px_7px_rgb(25_46_73_/_3%)]">
|
||||
<div className="flex items-baseline gap-[10px]">
|
||||
<strong className="text-[16px] text-[#1c2d42]">图形化调度</strong>
|
||||
<span className="hidden text-[11px] text-[#8b99a9] 2xl:inline">
|
||||
{schedule
|
||||
? `版本 ${schedule.workflow_version} · ${schedule.dag_validation.valid ? "DAG 有效" : "待校验"}`
|
||||
: "创建调度后开始编排"}
|
||||
<PositionDraftBadge />
|
||||
</span>
|
||||
</div>
|
||||
<div className="schedule-toolbar__actions">
|
||||
<button type="button" onClick={() => useSchedulesStore.getState().openCreateDialog()}>
|
||||
<Icon name="plus" size={15} />新建
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="inline-flex h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-[#dce5ee] bg-white px-2 text-[12px] font-semibold text-[#526477] enabled:hover:border-[#aac6e5] enabled:hover:bg-[#f6faff] enabled:hover:text-[#126aca] disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
|
||||
type="button"
|
||||
onClick={() => useSchedulesStore.getState().openCreateDialog()}
|
||||
>
|
||||
<Icon name="plus" size={14} />
|
||||
<span>新建</span>
|
||||
</button>
|
||||
<button
|
||||
className="schedule-action--primary"
|
||||
className="inline-flex min-h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-brand bg-brand px-2 text-[12px] font-semibold text-white enabled:hover:border-brand-strong enabled:hover:bg-brand-strong disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
|
||||
type="button"
|
||||
disabled={!schedule || Boolean(busy)}
|
||||
onClick={() => useSchedulesStore.getState().saveSchedule()}
|
||||
@@ -362,6 +373,7 @@ export default function SchedulePage({
|
||||
<Icon name="check" size={15} />保存配置
|
||||
</button>
|
||||
<button
|
||||
className="inline-flex min-h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-[#dce5ee] bg-white px-2 text-[12px] font-semibold text-[#526477] enabled:hover:border-[#aac6e5] enabled:hover:bg-[#f6faff] enabled:hover:text-[#126aca] disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
|
||||
type="button"
|
||||
disabled={!schedule || Boolean(busy)}
|
||||
onClick={() => useSchedulesStore.getState().checkDag()}
|
||||
@@ -370,7 +382,7 @@ export default function SchedulePage({
|
||||
</button>
|
||||
<RunNowButton schedule={schedule} busy={busy} />
|
||||
<button
|
||||
className="schedule-action--danger"
|
||||
className="inline-flex min-h-[32px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-red-200 bg-white px-2 text-[12px] font-semibold text-red-700 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50 min-[1281px]:px-3"
|
||||
type="button"
|
||||
disabled={!schedule || Boolean(busy)}
|
||||
onClick={() => useSchedulesStore.getState().removeSchedule(schedule ?? undefined)}
|
||||
@@ -380,8 +392,8 @@ export default function SchedulePage({
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="schedule-workbench">
|
||||
<aside className="schedule-left">
|
||||
<div className="grid min-h-0 flex-1 gap-[10px] [grid-template-columns:225px_minmax(360px,1fr)_270px] min-[1281px]:[grid-template-columns:250px_minmax(380px,1fr)_292px] 2xl:[grid-template-columns:278px_minmax(420px,1fr)_316px]">
|
||||
<aside className="grid min-h-0 overflow-hidden rounded-[7px] border border-line bg-white shadow-[0_2px_7px_rgb(25_46_73_/_3%)] [grid-template-rows:minmax(210px,42%)_minmax(260px,58%)]">
|
||||
<ScheduleList
|
||||
schedules={filteredSchedules}
|
||||
selectedScheduleId={schedule?.schedule_id ?? null}
|
||||
@@ -401,14 +413,21 @@ export default function SchedulePage({
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<main className="schedule-center">
|
||||
<main className="flex min-h-0 flex-col overflow-hidden rounded-[7px] border border-line bg-white shadow-[0_2px_7px_rgb(25_46_73_/_3%)]">
|
||||
<ScheduleCanvasHeader
|
||||
linkSourceId={linkSourceId}
|
||||
onCancelLink={() => setLinkSourceId(null)}
|
||||
/>
|
||||
<div
|
||||
className={`schedule-canvas${linkSourceId ? " is-linking" : ""}`}
|
||||
className="relative min-h-0 flex-1 overflow-auto bg-bg-canvas data-[state=linking]:cursor-crosshair"
|
||||
data-state={linkSourceId ? "linking" : "idle"}
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(#e7edf3 1px, transparent 1px), linear-gradient(90deg, #e7edf3 1px, transparent 1px), linear-gradient(#f1f4f7 1px, transparent 1px), linear-gradient(90deg, #f1f4f7 1px, transparent 1px)",
|
||||
backgroundSize: "80px 80px, 80px 80px, 16px 16px, 16px 16px",
|
||||
backgroundPosition: "-1px -1px",
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
@@ -427,12 +446,12 @@ export default function SchedulePage({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="schedule-canvas__surface"
|
||||
className="relative"
|
||||
style={{ width: CANVAS_WIDTH, height: CANVAS_HEIGHT }}
|
||||
>
|
||||
{schedule && (
|
||||
<svg
|
||||
className="schedule-edges"
|
||||
className="absolute inset-0 z-[1] h-full w-full overflow-visible"
|
||||
viewBox={`0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`}
|
||||
aria-label="调度连线"
|
||||
>
|
||||
@@ -461,9 +480,8 @@ export default function SchedulePage({
|
||||
|
||||
{schedule?.nodes.map((node) => (
|
||||
<div
|
||||
className={`schedule-node${
|
||||
selectedNodeId === node.node_id ? " is-selected" : ""
|
||||
}`}
|
||||
className={`schedule-node absolute z-[2] flex cursor-grab touch-none select-none flex-col rounded-[8px] border border-[#91b7de] bg-white p-[11px_12px_9px] text-[#34516f] shadow-[0_7px_18px_rgb(36_75_117_/_11%)] active:cursor-grabbing data-[state=selected]:border-brand data-[state=selected]:shadow-[0_0_0_3px_rgb(25_120_212_/_11%),0_8px_22px_rgb(36_75_117_/_13%)]`}
|
||||
data-state={selectedNodeId === node.node_id ? "selected" : "idle"}
|
||||
key={node.node_id}
|
||||
style={{
|
||||
left: node.position_x,
|
||||
@@ -481,7 +499,7 @@ export default function SchedulePage({
|
||||
})}
|
||||
>
|
||||
<button
|
||||
className="schedule-node__port schedule-node__port--in"
|
||||
className="absolute left-[-8px] top-[calc(50%-7px)] z-[4] h-[14px] w-[14px] cursor-crosshair rounded-full border-2 border-[#4e92db] bg-white p-0 hover:border-[#176ec5] hover:bg-[#4e92db] hover:shadow-[0_0_0_4px_rgb(45_124_207_/_16%)]"
|
||||
type="button"
|
||||
aria-label={`连接到${node.node_name}`}
|
||||
title={linkSourceId ? "点击完成连线" : "输入端口"}
|
||||
@@ -491,15 +509,24 @@ export default function SchedulePage({
|
||||
void useSchedulesStore.getState().connectTo(node.node_id);
|
||||
}}
|
||||
/>
|
||||
<div className="schedule-node__heading">
|
||||
<span className={`schedule-node__type schedule-node__type--${node.version.script_type}`}>
|
||||
<div className="flex items-center gap-[7px]">
|
||||
<span
|
||||
className={`grid h-[25px] w-[25px] flex-none place-items-center rounded-[5px] ${
|
||||
node.version.script_type === "notebook"
|
||||
? "bg-[#fff0ef] text-[#da5c52]"
|
||||
: "bg-[#eaf3ff] text-[#316fbf]"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
name={node.version.script_type === "notebook" ? "notebook" : "python"}
|
||||
size={14}
|
||||
/>
|
||||
</span>
|
||||
<strong>{node.node_name}</strong>
|
||||
<strong className="min-w-0 flex-1 truncate text-[12px] text-[#243b53]">
|
||||
{node.node_name}
|
||||
</strong>
|
||||
<button
|
||||
className="grid h-[23px] w-[23px] cursor-pointer place-items-center rounded-[4px] border-0 bg-transparent p-0 text-[#8b99a7] hover:bg-[#edf5fd] hover:text-brand"
|
||||
type="button"
|
||||
aria-label="删除节点"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
@@ -512,15 +539,18 @@ export default function SchedulePage({
|
||||
<Icon name="settings" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<p>{node.version.script_name}</p>
|
||||
<footer>
|
||||
<span>{node.node_key}</span>
|
||||
<b>{node.version.version_label}</b>
|
||||
<p className="mt-2 mb-1.5 truncate text-[10px] text-[#7e8d9d]">
|
||||
{node.version.script_name}
|
||||
</p>
|
||||
<footer className="flex items-center justify-between border-t border-line-soft pt-1.5">
|
||||
<span className="max-w-[145px] truncate font-mono text-[9px] text-[#8d9baa]">
|
||||
{node.node_key}
|
||||
</span>
|
||||
<b className="text-[10px] text-brand">{node.version.version_label}</b>
|
||||
</footer>
|
||||
<button
|
||||
className={`schedule-node__port schedule-node__port--out${
|
||||
linkSourceId === node.node_id ? " is-active" : ""
|
||||
}`}
|
||||
className="absolute right-[-8px] top-[calc(50%-7px)] z-[4] h-[14px] w-[14px] cursor-crosshair rounded-full border-2 border-[#4e92db] bg-white p-0 hover:border-[#176ec5] hover:bg-[#4e92db] hover:shadow-[0_0_0_4px_rgb(45_124_207_/_16%)] data-[state=active]:border-[#176ec5] data-[state=active]:bg-[#4e92db] data-[state=active]:shadow-[0_0_0_4px_rgb(45_124_207_/_16%)]"
|
||||
data-state={linkSourceId === node.node_id ? "active" : "idle"}
|
||||
type="button"
|
||||
aria-label={`从${node.node_name}开始连线`}
|
||||
title="输出端口"
|
||||
@@ -540,39 +570,50 @@ export default function SchedulePage({
|
||||
))}
|
||||
|
||||
{!schedule ? (
|
||||
<div className="schedule-canvas-empty">
|
||||
<div className="sticky left-1/2 top-[46%] z-0 mx-auto mt-[190px] flex w-[350px] flex-col items-center text-center text-[#9aa8b6] [&>svg]:mb-3 [&>svg]:text-[#8cb4de]">
|
||||
<Icon name="schedule" size={34} />
|
||||
<strong>还没有选中调度方案</strong>
|
||||
<p>点击"新建"创建一个调度,然后拖入稳定版本脚本。</p>
|
||||
<button type="button" onClick={() => useSchedulesStore.getState().openCreateDialog()}>
|
||||
<strong className="text-[14px] text-[#567089]">还没有选中调度方案</strong>
|
||||
<p className="my-[7px_13px] text-[11px]">点击"新建"创建一个调度,然后拖入稳定版本脚本。</p>
|
||||
<button
|
||||
className="inline-flex min-h-[32px] cursor-pointer items-center gap-1 rounded-[6px] border border-[#2580da] bg-[#2580da] px-3 text-[11px] text-white"
|
||||
type="button"
|
||||
onClick={() => useSchedulesStore.getState().openCreateDialog()}
|
||||
>
|
||||
<Icon name="plus" size={15} />新建调度
|
||||
</button>
|
||||
</div>
|
||||
) : schedule.nodes.length === 0 ? (
|
||||
<div className="schedule-canvas-empty">
|
||||
<div className="sticky left-1/2 top-[46%] z-0 mx-auto mt-[190px] flex w-[350px] flex-col items-center text-center text-[#9aa8b6] [&>svg]:mb-3 [&>svg]:text-[#8cb4de]">
|
||||
<Icon name="release" size={34} />
|
||||
<strong>从稳定版本开始编排</strong>
|
||||
<p>把左侧脚本卡片拖到这里,或双击卡片快速加入。</p>
|
||||
<strong className="text-[14px] text-[#567089]">从稳定版本开始编排</strong>
|
||||
<p className="my-[7px_13px] text-[11px]">把左侧脚本卡片拖到这里,或双击卡片快速加入。</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<footer className="schedule-canvas-status">
|
||||
<span className={schedule?.dag_validation.valid ? "is-valid" : ""}>
|
||||
<footer className="flex min-h-[35px] items-center gap-[14px] border-t border-[#e4eaf0] bg-white px-3 text-[10px] text-[#8b99a9]">
|
||||
<span
|
||||
data-state={schedule?.dag_validation.valid ? "valid" : "invalid"}
|
||||
className="text-[#b17a48] data-[state=valid]:text-[#16875b]"
|
||||
>
|
||||
{schedule?.dag_validation.valid ? "✓ DAG 有效" : "○ DAG 待校验"}
|
||||
</span>
|
||||
<span>{schedule?.nodes.length ?? 0} 个节点</span>
|
||||
<span>{schedule?.edges.length ?? 0} 条连线</span>
|
||||
{selectedEdge && (
|
||||
<button type="button" onClick={() => useSchedulesStore.getState().removeEdge(selectedEdge)}>
|
||||
<button
|
||||
className="ml-auto inline-flex min-h-[24px] cursor-pointer items-center justify-center gap-[5px] rounded-[6px] border border-[#dce5ee] bg-white px-2 text-[10px] font-semibold text-[#bd504b] hover:border-[#aac6e5] hover:bg-[#f6faff] hover:text-[#126aca]"
|
||||
type="button"
|
||||
onClick={() => useSchedulesStore.getState().removeEdge(selectedEdge)}
|
||||
>
|
||||
删除选中连线
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<aside className="schedule-right">
|
||||
<div className="schedule-inspector-scroll">
|
||||
<aside className="grid min-h-0 overflow-hidden rounded-[7px] border border-line bg-white shadow-[0_2px_7px_rgb(25_46_73_/_3%)] [grid-template-rows:repeat(2,minmax(0,1fr))]">
|
||||
<div className="min-h-0 overflow-auto">
|
||||
{selectedNode ? (
|
||||
<NodeInspector
|
||||
node={selectedNode}
|
||||
@@ -606,14 +647,14 @@ export default function SchedulePage({
|
||||
|
||||
{contextMenu && (
|
||||
<div
|
||||
className="schedule-context-menu"
|
||||
className="fixed z-[60] min-w-[168px] rounded-[8px] border border-[#d9e3ec] bg-white p-[5px] shadow-[0_14px_36px_rgb(14_35_57_/_22%)]"
|
||||
role="menu"
|
||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
{contextMenu.kind === "schedule-list" && (
|
||||
<button
|
||||
className="schedule-context-menu__action"
|
||||
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#173a5e] hover:bg-[#eef6ff]"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().openCreateDialog()}
|
||||
@@ -625,7 +666,7 @@ export default function SchedulePage({
|
||||
{contextMenu.kind === "schedule" && (
|
||||
<>
|
||||
<button
|
||||
className="schedule-context-menu__action"
|
||||
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#173a5e] hover:bg-[#eef6ff]"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().openCreateDialog()}
|
||||
@@ -634,7 +675,7 @@ export default function SchedulePage({
|
||||
新建调度方案
|
||||
</button>
|
||||
<button
|
||||
className="schedule-context-menu__action"
|
||||
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#173a5e] hover:bg-[#eef6ff]"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().renameSchedule(contextMenu.schedule)}
|
||||
@@ -643,6 +684,7 @@ export default function SchedulePage({
|
||||
改名
|
||||
</button>
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().removeSchedule(contextMenu.schedule)}
|
||||
@@ -655,6 +697,7 @@ export default function SchedulePage({
|
||||
{contextMenu.kind === "artifact" && null}
|
||||
{contextMenu.kind === "node" && (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().removeNode(contextMenu.node)}
|
||||
@@ -665,6 +708,7 @@ export default function SchedulePage({
|
||||
)}
|
||||
{contextMenu.kind === "edge" && (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center gap-[9px] rounded-[5px] border-0 bg-transparent p-[9px_10px] text-left text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().removeEdge(contextMenu.edge)}
|
||||
@@ -690,7 +734,7 @@ export default function SchedulePage({
|
||||
<h2 id="create-schedule-title">新建调度方案</h2>
|
||||
</div>
|
||||
<button
|
||||
className="icon-button"
|
||||
className="grid h-[34px] w-[34px] cursor-pointer place-items-center rounded-[7px] border border-[#dfe6ed] bg-white hover:border-[#b9c9da] hover:bg-[#f7faff]"
|
||||
type="button"
|
||||
aria-label="关闭"
|
||||
disabled={Boolean(busy)}
|
||||
@@ -737,8 +781,11 @@ export default function SchedulePage({
|
||||
)}
|
||||
|
||||
{busy && (
|
||||
<div className="schedule-busy" aria-live="polite">
|
||||
<span />
|
||||
<div
|
||||
className="absolute bottom-[22px] right-[22px] z-[20] flex min-h-[38px] items-center gap-2 rounded-[7px] border border-[#c9dcee] bg-white px-[13px] text-[11px] text-[#4e657d] shadow-[0_8px_25px_rgb(28_56_86_/_15%)]"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span className="h-[15px] w-[15px] rounded-full border-2 border-[#b9d4ef] border-t-brand" />
|
||||
{busy === "run-now" ? "正在创建运行…" : "正在同步调度配置…"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user