refactor: style css -> tailwindcss

This commit is contained in:
tao.chen
2026-08-24 18:34:43 +08:00
parent 9ce0b106fa
commit 6b7701852f
9 changed files with 475 additions and 2142 deletions
+55 -76
View File
@@ -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-face {
font-family: "Inter"; font-family: "Inter";
@@ -16,97 +48,44 @@
font-display: swap; font-display: swap;
} }
@layer base {
:root { :root {
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; font-family: var(--font-sans);
color: #1f354b; color: #1f354b;
background: #f3f6f9; background: #f3f6f9;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
* { box-sizing: border-box; } html,
html, body { margin: 0; min-width: 1024px; min-height: 100%; } body {
button, input, select, textarea { font: inherit; } margin: 0;
min-width: 1024px;
.auth-loading, min-height: 100%;
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
} }
.auth-loading { *,
gap: 12px; *::before,
color: #61758a; *::after {
box-sizing: border-box;
}
} }
.auth-loading > span { @layer utilities {
width: 20px; .scrollbar-thin::-webkit-scrollbar {
height: 20px; width: 5px;
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); } } .scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
.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 { .scrollbar-thin::-webkit-scrollbar-thumb {
width: 420px; border-radius: 999px;
padding: 42px; background: #d9e1e9;
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 { .scrollbar-thin::-webkit-scrollbar-thumb:hover {
width: 46px; background: #c4d0dc;
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; }
@@ -58,24 +58,27 @@ function ArtifactListImpl({
} }
return ( return (
<section className="schedule-panel schedule-artifacts-panel"> <section className="flex min-h-0 flex-col overflow-hidden">
<div className="schedule-panel__title"> <div className="flex min-h-[48px] items-center justify-between px-3">
<div> <div className="flex items-center gap-[7px]">
<strong></strong> <strong className="text-[14px] text-[#223247]"></strong>
<span className="schedule-panel__count">{artifacts.length}</span> <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>
</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} /> <Icon name="search" size={15} />
<input <input
className="min-w-0 flex-1 border-0 bg-transparent text-[12px] text-[#33475d] outline-none placeholder:text-[#a6b2be]"
value={keyword} value={keyword}
placeholder="搜索稳定版本" placeholder="搜索稳定版本"
onChange={(event) => onKeywordChange(event.target.value)} onChange={(event) => onKeywordChange(event.target.value)}
/> />
</label> </label>
<div className="artifact-list"> <div className="scrollbar-thin min-h-0 overflow-auto px-2 pb-[9px]">
{groups.length === 0 ? ( {groups.length === 0 ? (
<p className="schedule-empty"> <p className="my-[18px_10px] text-center text-[10px] leading-[1.6] text-[#9aa6b3]">
"构建脚本" "构建脚本"
</p> </p>
) : ( ) : (
@@ -84,33 +87,50 @@ function ArtifactListImpl({
const scriptType = items[0].script_type; const scriptType = items[0].script_type;
const iconName = scriptType === "notebook" ? "notebook" : "python"; const iconName = scriptType === "notebook" ? "notebook" : "python";
return ( 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 <button
type="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} aria-expanded={isExpanded}
onClick={() => toggle(scriptName)} onClick={() => toggle(scriptName)}
> >
<span className="artifact-group__main"> <span className="flex min-w-0 items-center gap-[7px]">
<span className={`artifact-group__chevron${isExpanded ? " is-open" : ""}`}> <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} /> <Icon name="chevron" size={14} />
</span> </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} /> <Icon name={iconName} size={14} />
</span> </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} {scriptName}
</span> </span>
</span> </span>
{items.length > 1 && ( {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> </button>
{isExpanded && ( {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) => ( {items.map((artifact) => (
<div <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 draggable
key={artifact.versions_id} key={artifact.versions_id}
onDragStart={(event) => { onDragStart={(event) => {
@@ -124,18 +144,18 @@ function ArtifactListImpl({
onContextMenu={(event) => onContextMenu(event, artifact)} onContextMenu={(event) => onContextMenu(event, artifact)}
onDoubleClick={() => onDoubleClick(artifact)} onDoubleClick={() => onDoubleClick(artifact)}
> >
<span className="artifact-version__main"> <span className="flex min-w-0 items-center gap-[5px]">
<span className="artifact-version__handle"> <span className="h-[13px] w-[13px] flex-none text-[#c4cdd6] group-hover:text-[#7e8d9d]">
<Icon name="grip-vertical" size={13} /> <Icon name="grip-vertical" size={13} />
</span> </span>
<span className="artifact-version__name"> <span className="flex-none text-[10px] font-semibold text-[#526477]">
{artifact.version_label} {artifact.version_label}
</span> </span>
<span className="artifact-version__commit"> <span className="min-w-0 truncate font-mono text-[8px] text-[#9aa5b1]">
{shortHash(artifact.content_hash)} {shortHash(artifact.content_hash)}
</span> </span>
</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} /> <Icon name="external" size={12} />
</span> </span>
</div> </div>
@@ -147,9 +167,9 @@ function ArtifactListImpl({
}) })
)} )}
</div> </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></span>
<span className="artifact-tip__grip"> <span className="inline-flex items-center justify-center text-[#9eabb8]">
<Icon name="grip-vertical" size={12} /> <Icon name="grip-vertical" size={12} />
</span> </span>
<span></span> <span></span>
@@ -19,33 +19,48 @@ export function NodeInspector({
onDelete: () => void; onDelete: () => void;
}) { }) {
return ( return (
<div className="schedule-inspector"> <div className="flex min-h-full flex-col">
<section className="node-inspector-title"> <section className="flex flex-row items-center gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<span className={`artifact-card__icon artifact-card__icon--${node.version.script_type}`}> <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 <Icon
name={node.version.script_type === "notebook" ? "notebook" : "python"} name={node.version.script_type === "notebook" ? "notebook" : "python"}
size={17} size={17}
/> />
</span> </span>
<div> <div className="flex min-w-0 flex-col gap-[3px]">
<small></small> <small className="text-[9px] text-[#96a2af]"></small>
<strong>{node.node_key}</strong> <strong className="truncate font-mono text-[11px] text-[#30465d]">
{node.node_key}
</strong>
</div> </div>
</section> </section>
<section> <section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3></h3> <h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<div className="version-readonly"> <div className="flex flex-col gap-[5px] rounded-[6px] border border-[#dae6f2] bg-[#f6faff] p-[9px]">
<strong>{node.version.script_name}</strong> <strong className="text-[11px] text-[#35516e]">{node.version.script_name}</strong>
<span>{node.version.version_label}</span> <span className="w-fit rounded-[8px] bg-[#e3f0fd] px-1.5 py-[2px] text-[9px] font-bold text-[#176ec5]">
<small>versions_id: {node.versions_id}</small> {node.version.version_label}
<small>SHA-256: {shortHash(node.version.content_hash)}</small> </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> </div>
</section> </section>
<section> <section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3></h3> <h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<input <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} value={form.nodeName}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -53,12 +68,13 @@ export function NodeInspector({
})} })}
/> />
</label> </label>
<div className="inspector-grid"> <div className="grid grid-cols-2 gap-2">
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<input <input
type="number" type="number"
min="1" 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} value={form.timeoutSeconds}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -66,12 +82,13 @@ export function NodeInspector({
})} })}
/> />
</label> </label>
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<input <input
type="number" type="number"
min="0" min="0"
max="10" 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} value={form.retryCount}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -80,12 +97,13 @@ export function NodeInspector({
/> />
</label> </label>
</div> </div>
<div className="inspector-grid"> <div className="grid grid-cols-2 gap-2">
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<input <input
type="number" type="number"
min="0" 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} value={form.retryIntervalSec}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -93,9 +111,10 @@ export function NodeInspector({
})} })}
/> />
</label> </label>
<label> <label className="flex flex-col gap-[5px]">
<span>Python </span> <span className="text-[10px] font-semibold text-[#758497]">Python </span>
<select <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} value={form.pythonVersion}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -111,12 +130,12 @@ export function NodeInspector({
</label> </label>
</div> </div>
</section> </section>
<section> <section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3></h3> <h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<label> <label className="flex flex-col gap-[5px]">
<span>arguments_json</span> <span className="text-[10px] font-semibold text-[#758497]">arguments_json</span>
<textarea <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} rows={5}
spellCheck={false} spellCheck={false}
value={form.argumentsJson} value={form.argumentsJson}
@@ -126,10 +145,10 @@ export function NodeInspector({
})} })}
/> />
</label> </label>
<label> <label className="flex flex-col gap-[5px]">
<span>env_refs_json</span> <span className="text-[10px] font-semibold text-[#758497]">env_refs_json</span>
<textarea <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} rows={4}
spellCheck={false} spellCheck={false}
value={form.envRefsJson} value={form.envRefsJson}
@@ -140,9 +159,9 @@ export function NodeInspector({
/> />
</label> </label>
</section> </section>
<div className="node-inspector-actions"> <div className="mt-auto grid grid-cols-[1fr_90px] gap-[7px] p-[14px]">
<button <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" type="button"
disabled={busy} disabled={busy}
onClick={onSave} onClick={onSave}
@@ -150,7 +169,7 @@ export function NodeInspector({
</button> </button>
<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" type="button"
disabled={busy} disabled={busy}
onClick={onDelete} onClick={onDelete}
+122 -52
View File
@@ -216,13 +216,16 @@ export function RunHistory({
}; };
return ( return (
<section className="schedule-run-history"> <section className="flex min-h-0 flex-col border-t border-[#e0e7ee] bg-[#fbfcfe]">
<header> <header className="flex min-h-[44px] items-center justify-between border-b border-[#e8edf2] bg-white px-3">
<div> <div className="flex items-center gap-[7px]">
<strong></strong> <strong className="text-[12px] text-[#2b4056]"></strong>
<span>{runs.length}</span> <span className="inline-grid h-[18px] min-w-[18px] place-items-center rounded-[9px] bg-[#edf2f7] text-[9px] text-[#71849a]">
{runs.length}
</span>
</div> </div>
<button <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" type="button"
aria-label="刷新运行记录" aria-label="刷新运行记录"
disabled={disabled || loading} disabled={disabled || loading}
@@ -231,34 +234,50 @@ export function RunHistory({
<Icon name="refresh" size={13} /> <Icon name="refresh" size={13} />
</button> </button>
</header> </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 ? ( {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 ? ( ) : runs.length === 0 ? (
<p></p> <p className="my-auto p-3 text-center text-[10px] leading-[1.6] text-[#929fac]">
</p>
) : ( ) : (
runs.map((run) => { runs.map((run) => {
const expanded = expandedRunId === run.run_id; const expanded = expandedRunId === run.run_id;
const detail = runDetails[run.run_id]; const detail = runDetails[run.run_id];
return ( return (
<article <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} key={run.run_id}
> >
<div <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
<span className="schedule-run-summary-copy"> data-state={run.run_status}
<strong>{RUN_STATUS_LABELS[run.run_status]}</strong> 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"
<small> />
<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)} {formatTime(run.queued_at)} · {formatDuration(run.duration_ms)}
</small> </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> )}
</span>
<code className="font-mono text-[8px] text-[#8b9bad]" title={run.run_id}>
{run.run_id.slice(-8)}
</code>
<button <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" type="button"
aria-haspopup="dialog" aria-haspopup="dialog"
onClick={() => toggleRun(run.run_id)} onClick={() => toggleRun(run.run_id)}
@@ -270,25 +289,29 @@ export function RunHistory({
{expanded && ( {expanded && (
<> <>
<button <button
className="schedule-run-result-backdrop" className="fixed inset-0 z-[40] cursor-default border-0 bg-[rgb(10_27_45_/_46%)]"
type="button" type="button"
aria-label="关闭运行结果" aria-label="关闭运行结果"
onClick={() => toggleRun(run.run_id)} onClick={() => toggleRun(run.run_id)}
/> />
<section <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" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="调度运行结果" aria-label="调度运行结果"
> >
<header className="schedule-run-result-modal__header"> <header className="flex items-center justify-between gap-[18px] border-b border-[#e3eaf0] bg-white p-[17px_20px]">
<div> <div className="grid min-w-0 items-center gap-[5px_10px] [grid-template-columns:auto_auto]">
<span></span> <span className="text-[17px] font-bold text-[#1f344a]"></span>
<strong>{RUN_STATUS_LABELS[run.run_status]}</strong> <strong className="justify-self-start rounded-[11px] bg-success-soft px-2 py-[3px] text-[10px] text-[#17805a]">
<code>{run.run_id}</code> {RUN_STATUS_LABELS[run.run_status]}
</strong>
<code className="col-span-2 truncate font-mono text-[10px] text-[#8291a1]">
{run.run_id}
</code>
</div> </div>
<button <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" type="button"
aria-label="关闭运行结果" aria-label="关闭运行结果"
onClick={() => toggleRun(run.run_id)} onClick={() => toggleRun(run.run_id)}
@@ -296,49 +319,90 @@ export function RunHistory({
<Icon name="close" size={16} /> <Icon name="close" size={16} />
</button> </button>
</header> </header>
<div className="schedule-run-detail"> <div className="min-h-0 overflow-auto bg-[#f8fbfe] p-[18px]">
{detailLoadingId === run.run_id && !detail ? ( {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] ? ( ) : 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]} {detailErrors[run.run_id]}
</p> </p>
) : detail ? ( ) : detail ? (
<> <>
<dl className="schedule-run-meta"> <dl className="mb-[14px] grid gap-[10px_18px] [grid-template-columns:repeat(4,minmax(0,1fr))]">
<div><dt></dt><dd>{TRIGGER_TYPE_LABELS[detail.trigger_type]}</dd></div> <div className="min-w-0">
<div><dt></dt><dd>v{detail.workflow_version}</dd></div> <dt className="mb-1 text-[10px] text-[#95a2af]"></dt>
<div><dt></dt><dd>{formatTime(detail.started_at)}</dd></div> <dd className="m-0 truncate text-[12px] text-[#41566c]">
<div><dt></dt><dd>{formatTime(detail.finished_at)}</dd></div> {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> </dl>
{(detail.error_code || detail.error_message) && ( {(detail.error_code || detail.error_message) && (
<div className="schedule-run-error"> <div className="mb-2 rounded-[5px] border border-[#f0cece] bg-danger-soft p-[7px] text-danger-strong">
{detail.error_code && <code>{detail.error_code}</code>} {detail.error_code && <code className="text-[8px] font-bold">{detail.error_code}</code>}
{detail.error_message && <p>{detail.error_message}</p>} {detail.error_message && (
<p className="mt-[3px] text-[8px] leading-[1.45]">{detail.error_message}</p>
)}
</div> </div>
)} )}
<div className="schedule-node-run-list"> <div className="flex flex-col gap-2.5">
{detail.node_runs.length === 0 ? ( {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) => { ) : detail.node_runs.map((nodeRun) => {
const node = nodes.find((item) => item.node_id === nodeRun.node_id); const node = nodes.find((item) => item.node_id === nodeRun.node_id);
const logState = logStates[nodeRun.node_run_id]; const logState = logStates[nodeRun.node_run_id];
const logOpen = openLogNodeRunIds.has(nodeRun.node_run_id); const logOpen = openLogNodeRunIds.has(nodeRun.node_run_id);
return ( return (
<article className="schedule-node-run" key={nodeRun.node_run_id}> <article className="rounded-[7px] border border-[#dde7f0] bg-white p-3" key={nodeRun.node_run_id}>
<header> <header className="grid items-start gap-1.5 [grid-template-columns:8px_minmax(0,1fr)_auto]">
<span className={`run-status-dot is-${nodeRun.node_status}`} /> <span
<strong>{node?.node_name ?? nodeRun.node_id.slice(-8)}</strong> data-state={nodeRun.node_status}
<span>{NODE_STATUS_LABELS[nodeRun.node_status]}</span> 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> </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> {nodeRun.attempt_no} </span>
<span>{formatDuration(nodeRun.duration_ms)}</span> <span>{formatDuration(nodeRun.duration_ms)}</span>
<span>退 {nodeRun.exit_code ?? "—"}</span> <span>退 {nodeRun.exit_code ?? "—"}</span>
</div> </div>
{nodeRun.message && <p>{nodeRun.message}</p>} {nodeRun.message && (
<div className="schedule-node-run-actions"> <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 <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" type="button"
disabled={!nodeRun.logs_object_id || logState?.loading} disabled={!nodeRun.logs_object_id || logState?.loading}
onClick={() => void toggleLog(run.run_id, nodeRun.node_run_id)} onClick={() => void toggleLog(run.run_id, nodeRun.node_run_id)}
@@ -346,6 +410,7 @@ export function RunHistory({
{logState?.loading ? "读取中…" : logOpen ? "收起日志" : "查看日志"} {logState?.loading ? "读取中…" : logOpen ? "收起日志" : "查看日志"}
</button> </button>
<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" type="button"
disabled={ disabled={
!nodeRun.logs_object_id !nodeRun.logs_object_id
@@ -358,6 +423,7 @@ export function RunHistory({
: "下载日志"} : "下载日志"}
</button> </button>
<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" type="button"
disabled={ disabled={
!nodeRun.result_object_id !nodeRun.result_object_id
@@ -371,24 +437,28 @@ export function RunHistory({
</button> </button>
</div> </div>
{artifactErrors[nodeRun.node_run_id] && ( {artifactErrors[nodeRun.node_run_id] && (
<p className="schedule-artifact-error"> <p className="!text-[#b65353]">
{artifactErrors[nodeRun.node_run_id]} {artifactErrors[nodeRun.node_run_id]}
</p> </p>
)} )}
{logOpen && ( {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 && ( {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> <span>{logState.fileName}</span>
<small> <small className="text-[#8292a1]">
{formatSize(new TextEncoder().encode(logState.content ?? "").length)} {formatSize(new TextEncoder().encode(logState.content ?? "").length)}
</small> </small>
</header> </header>
)} )}
{logState?.error ? ( {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> </div>
)} )}
@@ -8,15 +8,19 @@ export function ScheduleCanvasHeader({
onCancelLink: () => void; onCancelLink: () => void;
}) { }) {
return ( return (
<div className="schedule-canvas-title"> <div className="flex min-h-[53px] items-center justify-between border-b border-[#e4eaf0] px-3">
<div> <div className="flex min-w-0 flex-col gap-[3px]">
<strong></strong> <strong className="text-[14px] text-ink"></strong>
<span> <span className="hidden truncate text-[10px] text-[#93a0ae] 2xl:block">
线 线
</span> </span>
</div> </div>
{linkSourceId && ( {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> </button>
)} )}
@@ -32,20 +32,23 @@ export function ScheduleInspector({
}) { }) {
if (!schedule) { if (!schedule) {
return ( 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} /> <Icon name="settings" size={28} />
<strong></strong> <strong className="text-[14px] text-[#516a82]"></strong>
<p>Cron </p> <p className="text-[11px] leading-[1.6]">
Cron
</p>
</div> </div>
); );
} }
return ( return (
<div className="schedule-inspector"> <div className="flex min-h-full flex-col">
<section> <section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3></h3> <h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<input <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} value={form.scheduleName}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -53,9 +56,10 @@ export function ScheduleInspector({
})} })}
/> />
</label> </label>
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<textarea <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} rows={2}
value={form.description} value={form.description}
onChange={(event) => onChange({ onChange={(event) => onChange({
@@ -64,13 +68,14 @@ export function ScheduleInspector({
})} })}
/> />
</label> </label>
<label className="schedule-switch-row"> <label className="flex flex-row items-center justify-between rounded-[6px] bg-[#f5f8fb] p-[8px_9px]">
<span> <span className="flex flex-col gap-[2px]">
<b></b> <b className="text-[11px] text-[#45596f]"></b>
<small>DAG </small> <small className="text-[9px] font-normal text-[#94a1ae]">DAG </small>
</span> </span>
<input <input
type="checkbox" type="checkbox"
className="h-[18px] w-[34px] accent-brand"
checked={form.enabled} checked={form.enabled}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -80,11 +85,12 @@ export function ScheduleInspector({
</label> </label>
</section> </section>
<section> <section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3></h3> <h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<select <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} value={form.triggerType}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -99,9 +105,10 @@ export function ScheduleInspector({
</label> </label>
{form.triggerType === "cron" && ( {form.triggerType === "cron" && (
<> <>
<label> <label className="flex flex-col gap-[5px]">
<span>Cron </span> <span className="text-[10px] font-semibold text-[#758497]">Cron </span>
<input <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} value={form.cronExpression}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -109,9 +116,10 @@ export function ScheduleInspector({
})} })}
/> />
</label> </label>
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<input <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} value={form.timezone}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -120,7 +128,7 @@ export function ScheduleInspector({
/> />
</label> </label>
<button <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" type="button"
disabled={busy} disabled={busy}
onClick={onPreview} onClick={onPreview}
@@ -128,7 +136,7 @@ export function ScheduleInspector({
5 5
</button> </button>
{cronResult && ( {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) => ( {cronResult.occurrences.map((item) => (
<li key={item.utc_time}> <li key={item.utc_time}>
{new Date(item.local_time).toLocaleString("zh-CN", { {new Date(item.local_time).toLocaleString("zh-CN", {
@@ -142,15 +150,16 @@ export function ScheduleInspector({
)} )}
</section> </section>
<section> <section className="flex flex-col gap-[10px] border-b border-[#e7ecf1] p-[14px]">
<h3></h3> <h3 className="mb-[2px] text-[13px] text-[#273b50]"></h3>
<div className="inspector-grid"> <div className="grid grid-cols-2 gap-2">
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<input <input
type="number" type="number"
min="1" min="1"
max="64" 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} value={form.maxConcurrency}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -158,9 +167,10 @@ export function ScheduleInspector({
})} })}
/> />
</label> </label>
<label> <label className="flex flex-col gap-[5px]">
<span></span> <span className="text-[10px] font-semibold text-[#758497]"></span>
<select <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} value={form.failurePolicy}
onChange={(event) => onChange({ onChange={(event) => onChange({
...form, ...form,
@@ -172,14 +182,14 @@ export function ScheduleInspector({
</select> </select>
</label> </label>
</div> </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> <span></span>
<strong>{formatTime(schedule.next_run_at)}</strong> <strong className="text-[10px] text-[#425970]">{formatTime(schedule.next_run_at)}</strong>
</div> </div>
</section> </section>
<button <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" type="button"
disabled={busy} disabled={busy}
onClick={onSave} onClick={onSave}
@@ -23,49 +23,63 @@ export function ScheduleList({
: schedules; : schedules;
return ( return (
<section className="schedule-panel schedule-list-panel"> <section className="flex min-h-0 flex-col overflow-hidden border-b border-[#e3e9ef]">
<div <div
className="schedule-panel__title" className="flex min-h-[48px] items-center justify-between px-3"
onContextMenu={(event) => onContextMenu(event, null)} 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"> </div>
<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} /> <Icon name="search" size={15} />
<input <input
className="min-w-0 flex-1 border-0 bg-transparent text-[12px] text-[#33475d] outline-none placeholder:text-[#a6b2be]"
value={keyword} value={keyword}
placeholder="搜索调度名称" placeholder="搜索调度名称"
onChange={(event) => onKeywordChange(event.target.value)} onChange={(event) => onKeywordChange(event.target.value)}
/> />
</label> </label>
<div <div
className="schedule-list" className="min-h-0 overflow-auto px-2 pb-[9px]"
onContextMenu={(event) => onContextMenu(event, null)} onContextMenu={(event) => onContextMenu(event, null)}
> >
{loading ? ( {loading ? (
<p className="schedule-empty"></p> <p className="my-[18px_10px] text-center text-[11px] leading-[1.6] text-[#95a2b0]">
</p>
) : filtered.length ? ( ) : filtered.length ? (
filtered.map((item) => ( filtered.map((item) => (
<button <button
className={`schedule-card${ 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]`}
selectedScheduleId === item.schedule_id ? " schedule-card--active" : "" data-state={selectedScheduleId === item.schedule_id ? "active" : "idle"}
}`}
key={item.schedule_id} key={item.schedule_id}
type="button" type="button"
onClick={() => onSelect(item.schedule_id)} onClick={() => onSelect(item.schedule_id)}
onContextMenu={(event) => onContextMenu(event, item)} onContextMenu={(event) => onContextMenu(event, item)}
> >
<span className={`schedule-status${item.enabled ? " is-enabled" : ""}`} /> <span
<span> data-state={item.enabled ? "enabled" : "disabled"}
<strong>{item.schedule_name}</strong> 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%)]"
<small> />
<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 ? "已启用" : "未启用"} {item.node_count} · {item.enabled ? "已启用" : "未启用"}
</small> </small>
</span> </span>
</button> </button>
)) ))
) : ( ) : (
<p className="schedule-empty"></p> <p className="my-[18px_10px] text-center text-[11px] leading-[1.6] text-[#95a2b0]">
</p>
)} )}
</div> </div>
</section> </section>
+101 -54
View File
@@ -33,7 +33,6 @@ import {
} from "./state/schedulesStore"; } from "./state/schedulesStore";
import { useSchedulesStore } from "./state/schedulesStore"; import { useSchedulesStore } from "./state/schedulesStore";
import { edgePath, nodeToForm, scheduleToForm } from "./utils"; import { edgePath, nodeToForm, scheduleToForm } from "./utils";
import "../../styles/schedule.css";
type Notice = { type Notice = {
tone: "success" | "error" | "info"; tone: "success" | "error" | "info";
@@ -78,7 +77,7 @@ function RunNowButton({
const positionDraftCount = useSchedulesStore((s) => s.positionDraftCount); const positionDraftCount = useSchedulesStore((s) => s.positionDraftCount);
return ( return (
<button <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" type="button"
disabled={ disabled={
!schedule !schedule
@@ -139,7 +138,7 @@ const ScheduleEdge = memo(function ScheduleEdge({
if (!path) return null; if (!path) return null;
return ( return (
<g <g
className={selectedEdgeId === edge.edge_id ? "is-selected" : ""} data-state={selectedEdgeId === edge.edge_id ? "selected" : "idle"}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
useSchedulesStore.getState().setSelectedEdgeId(edge.edge_id); useSchedulesStore.getState().setSelectedEdgeId(edge.edge_id);
@@ -150,9 +149,16 @@ const ScheduleEdge = memo(function ScheduleEdge({
edge, edge,
})} })}
> >
<path className="schedule-edge-hit" d={path} />
<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} d={path}
markerEnd="url(#schedule-arrow)" markerEnd="url(#schedule-arrow)"
/> />
@@ -338,23 +344,28 @@ export default function SchedulePage({
); );
return ( return (
<section className="schedule-page"> <section className="relative flex min-h-0 flex-1 flex-col gap-[10px] overflow-hidden p-[10px]">
<header className="schedule-toolbar"> <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> <div className="flex items-baseline gap-[10px]">
<strong></strong> <strong className="text-[16px] text-[#1c2d42]"></strong>
<span> <span className="hidden text-[11px] text-[#8b99a9] 2xl:inline">
{schedule {schedule
? `版本 ${schedule.workflow_version} · ${schedule.dag_validation.valid ? "DAG 有效" : "待校验"}` ? `版本 ${schedule.workflow_version} · ${schedule.dag_validation.valid ? "DAG 有效" : "待校验"}`
: "创建调度后开始编排"} : "创建调度后开始编排"}
<PositionDraftBadge /> <PositionDraftBadge />
</span> </span>
</div> </div>
<div className="schedule-toolbar__actions"> <div className="flex items-center gap-2">
<button type="button" onClick={() => useSchedulesStore.getState().openCreateDialog()}> <button
<Icon name="plus" size={15} /> 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>
<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" type="button"
disabled={!schedule || Boolean(busy)} disabled={!schedule || Boolean(busy)}
onClick={() => useSchedulesStore.getState().saveSchedule()} onClick={() => useSchedulesStore.getState().saveSchedule()}
@@ -362,6 +373,7 @@ export default function SchedulePage({
<Icon name="check" size={15} /> <Icon name="check" size={15} />
</button> </button>
<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" type="button"
disabled={!schedule || Boolean(busy)} disabled={!schedule || Boolean(busy)}
onClick={() => useSchedulesStore.getState().checkDag()} onClick={() => useSchedulesStore.getState().checkDag()}
@@ -370,7 +382,7 @@ export default function SchedulePage({
</button> </button>
<RunNowButton schedule={schedule} busy={busy} /> <RunNowButton schedule={schedule} busy={busy} />
<button <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" type="button"
disabled={!schedule || Boolean(busy)} disabled={!schedule || Boolean(busy)}
onClick={() => useSchedulesStore.getState().removeSchedule(schedule ?? undefined)} onClick={() => useSchedulesStore.getState().removeSchedule(schedule ?? undefined)}
@@ -380,8 +392,8 @@ export default function SchedulePage({
</div> </div>
</header> </header>
<div className="schedule-workbench"> <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="schedule-left"> <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 <ScheduleList
schedules={filteredSchedules} schedules={filteredSchedules}
selectedScheduleId={schedule?.schedule_id ?? null} selectedScheduleId={schedule?.schedule_id ?? null}
@@ -401,14 +413,21 @@ export default function SchedulePage({
/> />
</aside> </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 <ScheduleCanvasHeader
linkSourceId={linkSourceId} linkSourceId={linkSourceId}
onCancelLink={() => setLinkSourceId(null)} onCancelLink={() => setLinkSourceId(null)}
/> />
<div <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} 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) => { onDragOver={(event) => {
event.preventDefault(); event.preventDefault();
event.dataTransfer.dropEffect = "copy"; event.dataTransfer.dropEffect = "copy";
@@ -427,12 +446,12 @@ export default function SchedulePage({
}} }}
> >
<div <div
className="schedule-canvas__surface" className="relative"
style={{ width: CANVAS_WIDTH, height: CANVAS_HEIGHT }} style={{ width: CANVAS_WIDTH, height: CANVAS_HEIGHT }}
> >
{schedule && ( {schedule && (
<svg <svg
className="schedule-edges" className="absolute inset-0 z-[1] h-full w-full overflow-visible"
viewBox={`0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`} viewBox={`0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`}
aria-label="调度连线" aria-label="调度连线"
> >
@@ -461,9 +480,8 @@ export default function SchedulePage({
{schedule?.nodes.map((node) => ( {schedule?.nodes.map((node) => (
<div <div
className={`schedule-node${ 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%)]`}
selectedNodeId === node.node_id ? " is-selected" : "" data-state={selectedNodeId === node.node_id ? "selected" : "idle"}
}`}
key={node.node_id} key={node.node_id}
style={{ style={{
left: node.position_x, left: node.position_x,
@@ -481,7 +499,7 @@ export default function SchedulePage({
})} })}
> >
<button <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" type="button"
aria-label={`连接到${node.node_name}`} aria-label={`连接到${node.node_name}`}
title={linkSourceId ? "点击完成连线" : "输入端口"} title={linkSourceId ? "点击完成连线" : "输入端口"}
@@ -491,15 +509,24 @@ export default function SchedulePage({
void useSchedulesStore.getState().connectTo(node.node_id); void useSchedulesStore.getState().connectTo(node.node_id);
}} }}
/> />
<div className="schedule-node__heading"> <div className="flex items-center gap-[7px]">
<span className={`schedule-node__type schedule-node__type--${node.version.script_type}`}> <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 <Icon
name={node.version.script_type === "notebook" ? "notebook" : "python"} name={node.version.script_type === "notebook" ? "notebook" : "python"}
size={14} size={14}
/> />
</span> </span>
<strong>{node.node_name}</strong> <strong className="min-w-0 flex-1 truncate text-[12px] text-[#243b53]">
{node.node_name}
</strong>
<button <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" type="button"
aria-label="删除节点" aria-label="删除节点"
onPointerDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}
@@ -512,15 +539,18 @@ export default function SchedulePage({
<Icon name="settings" size={14} /> <Icon name="settings" size={14} />
</button> </button>
</div> </div>
<p>{node.version.script_name}</p> <p className="mt-2 mb-1.5 truncate text-[10px] text-[#7e8d9d]">
<footer> {node.version.script_name}
<span>{node.node_key}</span> </p>
<b>{node.version.version_label}</b> <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> </footer>
<button <button
className={`schedule-node__port schedule-node__port--out${ 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%)]"
linkSourceId === node.node_id ? " is-active" : "" data-state={linkSourceId === node.node_id ? "active" : "idle"}
}`}
type="button" type="button"
aria-label={`${node.node_name}开始连线`} aria-label={`${node.node_name}开始连线`}
title="输出端口" title="输出端口"
@@ -540,39 +570,50 @@ export default function SchedulePage({
))} ))}
{!schedule ? ( {!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} /> <Icon name="schedule" size={34} />
<strong></strong> <strong className="text-[14px] text-[#567089]"></strong>
<p>"新建"</p> <p className="my-[7px_13px] text-[11px]">"新建"</p>
<button type="button" onClick={() => useSchedulesStore.getState().openCreateDialog()}> <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} /> <Icon name="plus" size={15} />
</button> </button>
</div> </div>
) : schedule.nodes.length === 0 ? ( ) : 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} /> <Icon name="release" size={34} />
<strong></strong> <strong className="text-[14px] text-[#567089]"></strong>
<p></p> <p className="my-[7px_13px] text-[11px]"></p>
</div> </div>
) : null} ) : null}
</div> </div>
</div> </div>
<footer className="schedule-canvas-status"> <footer className="flex min-h-[35px] items-center gap-[14px] border-t border-[#e4eaf0] bg-white px-3 text-[10px] text-[#8b99a9]">
<span className={schedule?.dag_validation.valid ? "is-valid" : ""}> <span
data-state={schedule?.dag_validation.valid ? "valid" : "invalid"}
className="text-[#b17a48] data-[state=valid]:text-[#16875b]"
>
{schedule?.dag_validation.valid ? "✓ DAG 有效" : "○ DAG 待校验"} {schedule?.dag_validation.valid ? "✓ DAG 有效" : "○ DAG 待校验"}
</span> </span>
<span>{schedule?.nodes.length ?? 0} </span> <span>{schedule?.nodes.length ?? 0} </span>
<span>{schedule?.edges.length ?? 0} 线</span> <span>{schedule?.edges.length ?? 0} 线</span>
{selectedEdge && ( {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> </button>
)} )}
</footer> </footer>
</main> </main>
<aside className="schedule-right"> <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="schedule-inspector-scroll"> <div className="min-h-0 overflow-auto">
{selectedNode ? ( {selectedNode ? (
<NodeInspector <NodeInspector
node={selectedNode} node={selectedNode}
@@ -606,14 +647,14 @@ export default function SchedulePage({
{contextMenu && ( {contextMenu && (
<div <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" role="menu"
style={{ left: contextMenu.x, top: contextMenu.y }} style={{ left: contextMenu.x, top: contextMenu.y }}
onPointerDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}
> >
{contextMenu.kind === "schedule-list" && ( {contextMenu.kind === "schedule-list" && (
<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" type="button"
role="menuitem" role="menuitem"
onClick={() => useSchedulesStore.getState().openCreateDialog()} onClick={() => useSchedulesStore.getState().openCreateDialog()}
@@ -625,7 +666,7 @@ export default function SchedulePage({
{contextMenu.kind === "schedule" && ( {contextMenu.kind === "schedule" && (
<> <>
<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" type="button"
role="menuitem" role="menuitem"
onClick={() => useSchedulesStore.getState().openCreateDialog()} onClick={() => useSchedulesStore.getState().openCreateDialog()}
@@ -634,7 +675,7 @@ export default function SchedulePage({
</button> </button>
<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" type="button"
role="menuitem" role="menuitem"
onClick={() => useSchedulesStore.getState().renameSchedule(contextMenu.schedule)} onClick={() => useSchedulesStore.getState().renameSchedule(contextMenu.schedule)}
@@ -643,6 +684,7 @@ export default function SchedulePage({
</button> </button>
<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" type="button"
role="menuitem" role="menuitem"
onClick={() => useSchedulesStore.getState().removeSchedule(contextMenu.schedule)} onClick={() => useSchedulesStore.getState().removeSchedule(contextMenu.schedule)}
@@ -655,6 +697,7 @@ export default function SchedulePage({
{contextMenu.kind === "artifact" && null} {contextMenu.kind === "artifact" && null}
{contextMenu.kind === "node" && ( {contextMenu.kind === "node" && (
<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" type="button"
role="menuitem" role="menuitem"
onClick={() => useSchedulesStore.getState().removeNode(contextMenu.node)} onClick={() => useSchedulesStore.getState().removeNode(contextMenu.node)}
@@ -665,6 +708,7 @@ export default function SchedulePage({
)} )}
{contextMenu.kind === "edge" && ( {contextMenu.kind === "edge" && (
<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" type="button"
role="menuitem" role="menuitem"
onClick={() => useSchedulesStore.getState().removeEdge(contextMenu.edge)} onClick={() => useSchedulesStore.getState().removeEdge(contextMenu.edge)}
@@ -690,7 +734,7 @@ export default function SchedulePage({
<h2 id="create-schedule-title"></h2> <h2 id="create-schedule-title"></h2>
</div> </div>
<button <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" type="button"
aria-label="关闭" aria-label="关闭"
disabled={Boolean(busy)} disabled={Boolean(busy)}
@@ -737,8 +781,11 @@ export default function SchedulePage({
)} )}
{busy && ( {busy && (
<div className="schedule-busy" aria-live="polite"> <div
<span /> 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" ? "正在创建运行…" : "正在同步调度配置…"} {busy === "run-now" ? "正在创建运行…" : "正在同步调度配置…"}
</div> </div>
)} )}
File diff suppressed because it is too large Load Diff