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