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 { UserTodoPanel } from "./UserTodoPanel";
|
||||||
import { ProjectServerPanel } from "./ProjectServerPanel";
|
import { ProjectServerPanel } from "./ProjectServerPanel";
|
||||||
import { ClaudeConfigModal } from "./ClaudeConfigModal";
|
import { ClaudeConfigModal } from "./ClaudeConfigModal";
|
||||||
|
import { ProjectDevVariablesPanel } from "./ProjectDevVariablesPanel";
|
||||||
|
|
||||||
// ── 蓝图状态徽章 ──────────────────────────────────────────────────────────────
|
// ── 蓝图状态徽章 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
const [showBlueprintPrompt, setShowBlueprintPrompt] = useState(false);
|
const [showBlueprintPrompt, setShowBlueprintPrompt] = useState(false);
|
||||||
const [showMentor, setShowMentor] = useState(false);
|
const [showMentor, setShowMentor] = useState(false);
|
||||||
const [showClaudeConfig, setShowClaudeConfig] = useState(false);
|
const [showClaudeConfig, setShowClaudeConfig] = useState(false);
|
||||||
|
const [showDevVariables, setShowDevVariables] = useState(false);
|
||||||
const [showTodos, setShowTodos] = useState(false);
|
const [showTodos, setShowTodos] = useState(false);
|
||||||
const [showServers, setShowServers] = useState(false);
|
const [showServers, setShowServers] = useState(false);
|
||||||
const [showBranchMenu, setShowBranchMenu] = 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">
|
<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: "Claude 自主性", action: () => setShowClaudeConfig(true) },
|
||||||
|
{ icon: "🔑", label: "开发变量", action: () => setShowDevVariables(true) },
|
||||||
{ icon: "📝", label: showTodos ? "收起备忘" : "个人备忘", action: () => setShowTodos((v) => !v) },
|
{ icon: "📝", label: showTodos ? "收起备忘" : "个人备忘", action: () => setShowTodos((v) => !v) },
|
||||||
{ icon: "🖥️", label: showServers ? "收起服务器" : "关联服务器", action: () => setShowServers((v) => !v) },
|
{ icon: "🖥️", label: showServers ? "收起服务器" : "关联服务器", action: () => setShowServers((v) => !v) },
|
||||||
{ icon: "🚀", label: "CI/CD 控制台", action: () => setShowCicd(true) },
|
{ 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} />}
|
{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) =>
|
export const revealApiKey = (id: string) =>
|
||||||
invoke<string>("reveal_api_key", { id });
|
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 ──────────────────────────────────────────────
|
// ── SSH Config & VS Code Remote ──────────────────────────────────────────────
|
||||||
|
|
||||||
export interface SshConfigStatus {
|
export interface SshConfigStatus {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user