feat(resource/dev-variables): add per-project human filling panel
Expose status-only Tauri types and explicit save/revoke actions without caching entered values in React Query. Feature-Confirmed: true Rules-Applied: R01,R12,R13
This commit is contained in:
parent
2b65c18193
commit
b1c6eaebac
@ -12,6 +12,7 @@ import { MentorPanel } from "../mentor/MentorPanel";
|
||||
import { UserTodoPanel } from "./UserTodoPanel";
|
||||
import { ProjectServerPanel } from "./ProjectServerPanel";
|
||||
import { ClaudeConfigModal } from "./ClaudeConfigModal";
|
||||
import { ProjectDevVariablesPanel } from "./ProjectDevVariablesPanel";
|
||||
|
||||
// ── 蓝图状态徽章 ──────────────────────────────────────────────────────────────
|
||||
|
||||
@ -66,6 +67,7 @@ export function ProjectCard({ project: p }: Props) {
|
||||
const [showBlueprintPrompt, setShowBlueprintPrompt] = useState(false);
|
||||
const [showMentor, setShowMentor] = useState(false);
|
||||
const [showClaudeConfig, setShowClaudeConfig] = useState(false);
|
||||
const [showDevVariables, setShowDevVariables] = useState(false);
|
||||
const [showTodos, setShowTodos] = useState(false);
|
||||
const [showServers, setShowServers] = useState(false);
|
||||
const [showBranchMenu, setShowBranchMenu] = useState(false);
|
||||
@ -613,6 +615,7 @@ export function ProjectCard({ project: p }: Props) {
|
||||
<div className="absolute right-0 bottom-full mb-1 w-40 bg-white border border-gray-200 rounded-lg shadow-lg z-50 py-1 text-xs">
|
||||
{([
|
||||
{ icon: "🤖", label: "Claude 自主性", action: () => setShowClaudeConfig(true) },
|
||||
{ icon: "🔑", label: "开发变量", action: () => setShowDevVariables(true) },
|
||||
{ icon: "📝", label: showTodos ? "收起备忘" : "个人备忘", action: () => setShowTodos((v) => !v) },
|
||||
{ icon: "🖥️", label: showServers ? "收起服务器" : "关联服务器", action: () => setShowServers((v) => !v) },
|
||||
{ icon: "🚀", label: "CI/CD 控制台", action: () => setShowCicd(true) },
|
||||
@ -705,6 +708,14 @@ export function ProjectCard({ project: p }: Props) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDevVariables && (
|
||||
<ProjectDevVariablesPanel
|
||||
projectId={p.id}
|
||||
projectName={p.name}
|
||||
onClose={() => setShowDevVariables(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 个人备忘 */}
|
||||
{showTodos && <UserTodoPanel projectId={p.id} />}
|
||||
|
||||
|
||||
119
src/components/dashboard/ProjectDevVariablesPanel.tsx
Normal file
119
src/components/dashboard/ProjectDevVariablesPanel.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
listProjectDevVariables,
|
||||
saveProjectDevVariable,
|
||||
revokeProjectDevVariable,
|
||||
} from "../../lib/commands";
|
||||
import { useUIStore } from "../../store/ui";
|
||||
import { Dialog } from "../ui/Dialog";
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const showToast = useUIStore((state) => state.showToast);
|
||||
const [editingName, setEditingName] = useState<string | null>(null);
|
||||
const [draftValue, setDraftValue] = 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;
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveProjectDevVariable(projectId, name, draftValue);
|
||||
setDraftValue("");
|
||||
setEditingName(null);
|
||||
queryClient.invalidateQueries({ queryKey: ["projectDevVariables", projectId] });
|
||||
showToast("开发变量已保存");
|
||||
} catch (cause) {
|
||||
showToast(`保存失败:${cause}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
const revokeMutation = useMutation({
|
||||
mutationFn: (name: string) => revokeProjectDevVariable(projectId, name),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["projectDevVariables", projectId] });
|
||||
showToast("开发变量已撤销");
|
||||
},
|
||||
onError: (cause) => showToast(`撤销失败:${cause}`),
|
||||
});
|
||||
|
||||
return (
|
||||
<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 单项读取,
|
||||
不会写入子项目文件;请勿存放生产密码。
|
||||
</p>
|
||||
{isPending && <p className="text-gray-500">正在读取需求…</p>}
|
||||
{error && <p className="text-red-600">读取失败:{String(error)}</p>}
|
||||
{!isPending && !error && variables.length === 0 && (
|
||||
<p className="text-gray-500">暂无变量需求。Agent 登记需要的名称和用途后,会显示在这里等待填写。</p>
|
||||
)}
|
||||
{variables.map((item) => (
|
||||
<div key={item.name} className="border border-gray-200 rounded-lg p-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="font-semibold text-gray-800 break-all flex-1">{item.name}</code>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${item.configured ? "bg-green-50 text-green-700" : "bg-amber-50 text-amber-700"}`}>
|
||||
{item.configured ? "已配置" : "待填写"}
|
||||
</span>
|
||||
</div>
|
||||
<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);
|
||||
}}
|
||||
className="flex flex-wrap gap-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>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex gap-3 text-xs">
|
||||
<button onClick={() => { setDraftValue(""); setEditingName(item.name); }}
|
||||
className="text-blue-600 hover:underline">{item.configured ? "修改" : "填写"}</button>
|
||||
{item.configured && (
|
||||
<button
|
||||
disabled={revokeMutation.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm(`撤销 ${item.name}?Agent 将无法再读取该值。`)) {
|
||||
revokeMutation.mutate(item.name);
|
||||
}
|
||||
}}
|
||||
className="text-red-600 hover:underline disabled:opacity-50"
|
||||
>撤销</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -1405,6 +1405,25 @@ export const deleteApiKey = (id: string) =>
|
||||
export const revealApiKey = (id: string) =>
|
||||
invoke<string>("reveal_api_key", { id });
|
||||
|
||||
// ── Project development variables ──────────────────────────────────────────
|
||||
|
||||
export interface ProjectDevVariableStatus {
|
||||
project_id: string;
|
||||
name: string;
|
||||
purpose: string;
|
||||
configured: boolean;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export const listProjectDevVariables = (projectId: string) =>
|
||||
invoke<ProjectDevVariableStatus[]>("list_project_dev_variables", { projectId });
|
||||
|
||||
export const saveProjectDevVariable = (projectId: string, name: string, value: string) =>
|
||||
invoke<void>("save_project_dev_variable", { projectId, name, value });
|
||||
|
||||
export const revokeProjectDevVariable = (projectId: string, name: string) =>
|
||||
invoke<void>("revoke_project_dev_variable", { projectId, name });
|
||||
|
||||
// ── SSH Config & VS Code Remote ──────────────────────────────────────────────
|
||||
|
||||
export interface SshConfigStatus {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user