feat(dev-variables): 项目开发变量与测试账户补给(V1–V11) #23
@ -405,6 +405,7 @@ pub fn run() {
|
||||
// project development variables (human filling; no reveal command)
|
||||
commands::dev_variables::list_project_dev_variables,
|
||||
commands::dev_variables::save_project_dev_variable,
|
||||
commands::dev_variables::save_project_test_account,
|
||||
commands::dev_variables::revoke_project_dev_variable,
|
||||
// host apps
|
||||
commands::host_apps::get_host_apps,
|
||||
|
||||
@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
listProjectDevVariables,
|
||||
saveProjectDevVariable,
|
||||
saveProjectTestAccount,
|
||||
revokeProjectDevVariable,
|
||||
} from "../../lib/commands";
|
||||
import { useUIStore } from "../../store/ui";
|
||||
@ -19,21 +20,32 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
|
||||
const showToast = useUIStore((state) => state.showToast);
|
||||
const [editingName, setEditingName] = useState<string | null>(null);
|
||||
const [draftValue, setDraftValue] = useState("");
|
||||
const [draftUsername, setDraftUsername] = useState("");
|
||||
const [draftPassword, setDraftPassword] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { data: variables = [], isPending, error } = useQuery({
|
||||
queryKey: ["projectDevVariables", projectId],
|
||||
queryFn: () => listProjectDevVariables(projectId),
|
||||
retry: false,
|
||||
});
|
||||
const handleSave = async (name: string) => {
|
||||
if (!draftValue || saving) return;
|
||||
const clearDraft = () => {
|
||||
setDraftValue("");
|
||||
setDraftUsername("");
|
||||
setDraftPassword("");
|
||||
};
|
||||
const handleSave = async (name: string, kind: "scalar" | "test_account") => {
|
||||
if (saving || (kind === "test_account" ? !draftUsername || !draftPassword : !draftValue)) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveProjectDevVariable(projectId, name, draftValue);
|
||||
setDraftValue("");
|
||||
if (kind === "test_account") {
|
||||
await saveProjectTestAccount(projectId, name, draftUsername, draftPassword);
|
||||
} else {
|
||||
await saveProjectDevVariable(projectId, name, draftValue);
|
||||
}
|
||||
clearDraft();
|
||||
setEditingName(null);
|
||||
queryClient.invalidateQueries({ queryKey: ["projectDevVariables", projectId] });
|
||||
showToast("开发变量已保存");
|
||||
showToast(kind === "test_account" ? "测试账户已保存" : "开发变量已保存");
|
||||
} catch (cause) {
|
||||
showToast(`保存失败:${cause}`);
|
||||
} finally {
|
||||
@ -53,7 +65,7 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
|
||||
<Dialog title={`开发变量 · ${projectName}`} onClose={onClose} size="md">
|
||||
<div className="overflow-y-auto p-5 space-y-4 text-sm">
|
||||
<p className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
仅填写非生产、低权限、可重置的开发值。已配置的值可被此项目的已认证 Agent 单项读取,
|
||||
仅填写非生产、低权限、可重置的开发值或测试账户。已配置的值可被此项目的已认证 Agent 单项读取,
|
||||
不会写入子项目文件;请勿存放生产密码。
|
||||
</p>
|
||||
{isPending && <p className="text-gray-500">正在读取需求…</p>}
|
||||
@ -69,34 +81,63 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
|
||||
{item.configured ? "已配置" : "待填写"}
|
||||
</span>
|
||||
</div>
|
||||
{item.kind === "test_account" && (
|
||||
<p className="text-xs text-gray-600">测试账户 · {item.service} · {item.environment}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 whitespace-pre-wrap">{item.purpose}</p>
|
||||
{editingName === item.name ? (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void handleSave(item.name);
|
||||
void handleSave(item.name, item.kind);
|
||||
}}
|
||||
className="flex flex-wrap gap-2"
|
||||
className="space-y-2"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
value={draftValue}
|
||||
onChange={(event) => setDraftValue(event.target.value)}
|
||||
placeholder="填写开发值"
|
||||
className="flex-1 min-w-44 border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||
/>
|
||||
<button type="submit" disabled={!draftValue || saving}
|
||||
className="px-3 py-1.5 bg-blue-600 text-white rounded disabled:opacity-50">
|
||||
{saving ? "保存中…" : "保存"}
|
||||
</button>
|
||||
<button type="button" onClick={() => { setDraftValue(""); setEditingName(null); }}
|
||||
className="px-2 py-1.5 text-gray-500">取消</button>
|
||||
{item.kind === "test_account" ? (
|
||||
<div className="grid gap-2">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
value={draftUsername}
|
||||
onChange={(event) => setDraftUsername(event.target.value)}
|
||||
placeholder="测试账号"
|
||||
aria-label={`${item.name} 测试账号`}
|
||||
className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
value={draftPassword}
|
||||
onChange={(event) => setDraftPassword(event.target.value)}
|
||||
placeholder="测试密码"
|
||||
aria-label={`${item.name} 测试密码`}
|
||||
className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
value={draftValue}
|
||||
onChange={(event) => setDraftValue(event.target.value)}
|
||||
placeholder="填写开发值"
|
||||
className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving || (item.kind === "test_account" ? !draftUsername || !draftPassword : !draftValue)}
|
||||
className="px-3 py-1.5 bg-blue-600 text-white rounded disabled:opacity-50">
|
||||
{saving ? "保存中…" : "保存"}
|
||||
</button>
|
||||
<button type="button" onClick={() => { clearDraft(); setEditingName(null); }}
|
||||
className="px-2 py-1.5 text-gray-500">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex gap-3 text-xs">
|
||||
<button onClick={() => { setDraftValue(""); setEditingName(item.name); }}
|
||||
<button onClick={() => { clearDraft(); setEditingName(item.name); }}
|
||||
className="text-blue-600 hover:underline">{item.configured ? "修改" : "填写"}</button>
|
||||
{item.configured && (
|
||||
<button
|
||||
|
||||
@ -1411,6 +1411,9 @@ export interface ProjectDevVariableStatus {
|
||||
project_id: string;
|
||||
name: string;
|
||||
purpose: string;
|
||||
kind: "scalar" | "test_account";
|
||||
service: string | null;
|
||||
environment: string | null;
|
||||
configured: boolean;
|
||||
updated_at: string;
|
||||
}
|
||||
@ -1421,6 +1424,13 @@ export const listProjectDevVariables = (projectId: string) =>
|
||||
export const saveProjectDevVariable = (projectId: string, name: string, value: string) =>
|
||||
invoke<void>("save_project_dev_variable", { projectId, name, value });
|
||||
|
||||
export const saveProjectTestAccount = (
|
||||
projectId: string,
|
||||
name: string,
|
||||
username: string,
|
||||
password: string,
|
||||
) => invoke<void>("save_project_test_account", { projectId, name, username, password });
|
||||
|
||||
export const revokeProjectDevVariable = (projectId: string, name: string) =>
|
||||
invoke<void>("revoke_project_dev_variable", { projectId, name });
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user