Files
worldquant-alpha-system/frontend/src/settings/ScopePicker.tsx
T

223 lines
6.9 KiB
TypeScript
Raw Normal View History

import { useEffect, useId, useState } from "react";
import { Banner, Button } from "@douyinfe/semi-ui-19";
import { api } from "../api";
import { ResearchSelect } from "../research/ResearchSelect";
import type { Scope } from "../preparations/types";
import {
changeScope,
scopeFields,
scopeLabels,
scopeValues,
validScope,
type ScopeOption,
} from "./model";
import "./style.css";
/** Edit a scope transactionally. Supplied options bypass network reads (fixed simulation metadata). */
export function ScopePicker({
value,
onChange,
local = false,
active = true,
revision = "",
options,
readOnly = false,
disabled = false,
onValidityChange,
}: {
value: Scope;
onChange: (scope: Scope) => void;
local?: boolean;
active?: boolean;
revision?: string;
options?: ScopeOption[];
readOnly?: boolean;
disabled?: boolean;
onValidityChange?: (valid: boolean) => void;
}) {
const [open, setOpen] = useState(false);
const [draft, setDraft] = useState<Partial<Scope>>(value);
const [rows, setRows] = useState<ScopeOption[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [retry, setRetry] = useState(0);
const id = useId();
const choices = options ?? rows;
useEffect(() => {
if (!active || options || readOnly) return;
const c = new AbortController();
setRows([]);
setLoading(true);
setError("");
api<{ instrument_options: ScopeOption[] }>(
local ? "/catalog/local-scopes" : "/catalog/scopes",
{ signal: c.signal },
)
.then((result) => {
if (!c.signal.aborted) setRows(result.instrument_options);
})
.catch((e) => {
if (!c.signal.aborted) setError(e.message);
})
.finally(() => {
if (!c.signal.aborted) setLoading(false);
});
return () => c.abort();
}, [active, local, revision, retry, options, readOnly]);
useEffect(() => {
setOpen(false);
}, [
active,
local,
revision,
readOnly,
value.instrument_type,
value.region,
value.delay,
value.universe,
]);
const valid = validScope(choices, value) && !loading && !error;
useEffect(() => {
onValidityChange?.(valid && !open);
}, [valid, open, onValidityChange]);
const complete =
value.instrument_type &&
value.region &&
value.universe &&
Number.isFinite(value.delay);
return (
<section className="scope-picker" aria-label="组合选择">
<div className="scope-picker-summary">
<div>
<span className="settings-label">组合</span>
<span>
{complete
? `${value.instrument_type} / ${value.region} / ${value.universe} / Delay ${value.delay}`
: "请选择组合"}
</span>
{readOnly && <small>来自固定数据准备</small>}
</div>
{!readOnly && (
<Button
disabled={disabled || !active}
aria-expanded={open}
aria-controls={id}
onClick={() => {
setDraft(value);
setOpen(!open);
}}
>
{complete ? "更改组合" : "选择组合"}
</Button>
)}
</div>
{!readOnly &&
!options &&
!open &&
(loading ? (
<small className="settings-hint">正在加载组合选项…</small>
) : error ? (
<Banner
type="warning"
description={
<>
{error}{" "}
<Button onClick={() => setRetry((n) => n + 1)}>
重试范围选项
</Button>
</>
}
/>
) : !choices.length ? (
<small className="settings-hint">
暂无可用组合
{local ? ",请先同步本地数据" : ",请检查平台连接或重试范围选项"}
</small>
) : !valid ? (
<small className="settings-error">当前组合不可用,请重新选择</small>
) : null)}
{open && (
<div id={id} className="scope-picker-panel">
<div className="settings-grid">
{scopeFields.map((key) => {
const values = scopeValues(choices, draft, key);
const current = draft[key];
const invalid =
current !== undefined &&
current !== "" &&
!values.includes(current);
return (
<div className="settings-field" key={key}>
<span>{scopeLabels[key]}</span>
<ResearchSelect
label={scopeLabels[key]}
value={current}
loading={loading}
disabled={disabled || loading || !values.length}
filter={values.length > 6}
placeholder={`请选择 ${scopeLabels[key]}`}
validateStatus={invalid ? "error" : "default"}
optionList={values.map((v) => ({
value: v,
label: key === "delay" ? `Delay ${v}` : String(v),
}))}
onChange={(v) =>
setDraft(
changeScope(
choices,
draft,
key,
key === "delay" ? Number(v) : String(v),
),
)
}
/>
{invalid && (
<small className="settings-error">
原值 {String(current)} 不可用,请重新选择
</small>
)}
</div>
);
})}
</div>
{error ? (
<Banner type="warning" description={error} />
) : !loading && !choices.length ? (
<p>
暂无可用组合
{local ? ",请先同步本地数据" : ",请同步选项或检查平台连接"}
</p>
) : (
<p className="settings-hint">
更改上级选项后,请补齐失效的下级选项。应用后生效。
</p>
)}
<div className="settings-actions">
{!options && (
<Button loading={loading} onClick={() => setRetry((n) => n + 1)}>
重试范围选项
</Button>
)}
<Button onClick={() => setOpen(false)}>取消</Button>
<Button
theme="solid"
disabled={
disabled || loading || !!error || !validScope(choices, draft)
}
onClick={() => {
if (validScope(choices, draft)) {
onChange(draft);
setOpen(false);
}
}}
>
应用组合
</Button>
</div>
</div>
)}
</section>
);
}