Add tool detail view to admin pending page
- Click tool name to view full details in modal - Shows description, arguments, steps (prompt/code), and README - Approve/Reject buttons in detail modal - New API endpoint GET /api/v1/admin/tools/<id> returns full tool config Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
706accc008
commit
3914ca74b3
|
|
@ -2459,6 +2459,59 @@ def create_app() -> Flask:
|
|||
"meta": paginate(page, per_page, total),
|
||||
})
|
||||
|
||||
@app.route("/api/v1/admin/tools/<int:tool_id>", methods=["GET"])
|
||||
@require_moderator
|
||||
def admin_get_tool(tool_id: int) -> Response:
|
||||
"""Get full tool details for admin review."""
|
||||
tool = query_one(
|
||||
g.db,
|
||||
"""
|
||||
SELECT t.*, p.display_name as publisher_name
|
||||
FROM tools t
|
||||
JOIN publishers p ON t.publisher_id = p.id
|
||||
WHERE t.id = ?
|
||||
""",
|
||||
[tool_id],
|
||||
)
|
||||
if not tool:
|
||||
return error_response("TOOL_NOT_FOUND", "Tool not found", 404)
|
||||
|
||||
# Parse config YAML to extract steps
|
||||
config = {}
|
||||
if tool["config_yaml"]:
|
||||
try:
|
||||
config = yaml.safe_load(tool["config_yaml"]) or {}
|
||||
except yaml.YAMLError:
|
||||
pass
|
||||
|
||||
# Parse scrutiny report
|
||||
scrutiny_report = None
|
||||
if tool["scrutiny_report"]:
|
||||
try:
|
||||
scrutiny_report = json.loads(tool["scrutiny_report"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
"data": {
|
||||
"id": tool["id"],
|
||||
"owner": tool["owner"],
|
||||
"name": tool["name"],
|
||||
"version": tool["version"],
|
||||
"description": tool["description"],
|
||||
"category": tool["category"],
|
||||
"tags": tool["tags"],
|
||||
"published_at": tool["published_at"],
|
||||
"publisher_name": tool["publisher_name"],
|
||||
"visibility": tool["visibility"],
|
||||
"moderation_status": tool["moderation_status"],
|
||||
"scrutiny_status": tool["scrutiny_status"],
|
||||
"scrutiny_report": scrutiny_report,
|
||||
"config": config,
|
||||
"readme": tool["readme"] or "",
|
||||
}
|
||||
})
|
||||
|
||||
@app.route("/api/v1/admin/tools/<int:tool_id>/approve", methods=["POST"])
|
||||
@require_moderator
|
||||
def admin_approve_tool(tool_id: int) -> Response:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@
|
|||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ tool.owner }}/{{ tool.name }}</div>
|
||||
<button onclick="viewTool({{ tool.id }})" class="text-sm font-medium text-indigo-600 hover:text-indigo-800 hover:underline">
|
||||
{{ tool.owner }}/{{ tool.name }}
|
||||
</button>
|
||||
<div class="text-sm text-gray-500">v{{ tool.version }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,6 +139,51 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Tool Detail Modal -->
|
||||
<div id="detail-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-10 mx-auto p-6 border max-w-4xl shadow-lg rounded-md bg-white mb-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 id="detail-title" class="text-lg font-medium text-gray-900">Tool Details</h3>
|
||||
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="detail-loading" class="py-8 text-center text-gray-500">Loading...</div>
|
||||
<div id="detail-content" class="hidden">
|
||||
<!-- Description -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">Description</h4>
|
||||
<p id="detail-description" class="text-sm text-gray-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Arguments -->
|
||||
<div id="detail-args-section" class="mb-6">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">Arguments</h4>
|
||||
<div id="detail-args" class="bg-gray-50 rounded-md p-3 text-sm font-mono overflow-x-auto"></div>
|
||||
</div>
|
||||
|
||||
<!-- Steps -->
|
||||
<div id="detail-steps-section" class="mb-6">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">Steps</h4>
|
||||
<div id="detail-steps" class="space-y-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- README -->
|
||||
<div id="detail-readme-section" class="mb-6">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">README</h4>
|
||||
<div id="detail-readme" class="bg-gray-50 rounded-md p-4 text-sm prose prose-sm max-w-none overflow-auto max-h-64"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-3 border-t pt-4">
|
||||
<button onclick="closeDetailModal()" class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-500">Close</button>
|
||||
<button id="detail-approve-btn" class="px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-md hover:bg-green-700">Approve</button>
|
||||
<button id="detail-reject-btn" class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-md hover:bg-red-700">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reject Modal -->
|
||||
<div id="reject-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
|
|
@ -153,12 +200,114 @@
|
|||
|
||||
<script>
|
||||
let currentToolId = null;
|
||||
let currentDetailToolId = null;
|
||||
|
||||
function toggleFindings(toolId) {
|
||||
const el = document.getElementById(`findings-${toolId}`);
|
||||
el.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
async function viewTool(toolId) {
|
||||
currentDetailToolId = toolId;
|
||||
document.getElementById('detail-modal').classList.remove('hidden');
|
||||
document.getElementById('detail-loading').classList.remove('hidden');
|
||||
document.getElementById('detail-content').classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/admin/tools/${toolId}`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer {{ session.get("auth_token") }}'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load tool details');
|
||||
}
|
||||
const result = await response.json();
|
||||
const tool = result.data;
|
||||
|
||||
// Update title
|
||||
document.getElementById('detail-title').textContent = `${tool.owner}/${tool.name} v${tool.version}`;
|
||||
|
||||
// Description
|
||||
document.getElementById('detail-description').textContent = tool.description || 'No description';
|
||||
|
||||
// Arguments
|
||||
const argsSection = document.getElementById('detail-args-section');
|
||||
const argsDiv = document.getElementById('detail-args');
|
||||
if (tool.config.arguments && tool.config.arguments.length > 0) {
|
||||
argsSection.classList.remove('hidden');
|
||||
argsDiv.innerHTML = tool.config.arguments.map(arg => {
|
||||
const required = arg.required ? '<span class="text-red-600">*</span>' : '';
|
||||
return `<div class="mb-2"><span class="text-indigo-600">${arg.flag || arg.variable}</span>${required} - ${arg.description || 'No description'}</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
argsSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Steps
|
||||
const stepsSection = document.getElementById('detail-steps-section');
|
||||
const stepsDiv = document.getElementById('detail-steps');
|
||||
if (tool.config.steps && tool.config.steps.length > 0) {
|
||||
stepsSection.classList.remove('hidden');
|
||||
stepsDiv.innerHTML = tool.config.steps.map((step, idx) => {
|
||||
let content = '';
|
||||
if (step.type === 'prompt') {
|
||||
content = `<div class="text-xs text-gray-500 mb-1">Prompt Step</div>
|
||||
<pre class="bg-white p-2 rounded border text-xs overflow-x-auto whitespace-pre-wrap">${escapeHtml(step.content || '')}</pre>`;
|
||||
} else if (step.type === 'code') {
|
||||
content = `<div class="text-xs text-gray-500 mb-1">Code Step${step.output_var ? ` → ${step.output_var}` : ''}</div>
|
||||
<pre class="bg-gray-900 text-green-400 p-2 rounded text-xs overflow-x-auto"><code>${escapeHtml(step.code || '')}</code></pre>`;
|
||||
} else {
|
||||
content = `<div class="text-xs text-gray-500">${step.type} step</div>`;
|
||||
}
|
||||
return `<div class="bg-gray-50 p-3 rounded-md">
|
||||
<div class="text-xs font-medium text-gray-700 mb-2">Step ${idx + 1}</div>
|
||||
${content}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
stepsSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
// README
|
||||
const readmeSection = document.getElementById('detail-readme-section');
|
||||
const readmeDiv = document.getElementById('detail-readme');
|
||||
if (tool.readme) {
|
||||
readmeSection.classList.remove('hidden');
|
||||
readmeDiv.innerHTML = `<pre class="whitespace-pre-wrap text-xs">${escapeHtml(tool.readme)}</pre>`;
|
||||
} else {
|
||||
readmeSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Wire up approve/reject buttons
|
||||
document.getElementById('detail-approve-btn').onclick = () => {
|
||||
closeDetailModal();
|
||||
approveTool(toolId);
|
||||
};
|
||||
document.getElementById('detail-reject-btn').onclick = () => {
|
||||
closeDetailModal();
|
||||
rejectTool(toolId);
|
||||
};
|
||||
|
||||
document.getElementById('detail-loading').classList.add('hidden');
|
||||
document.getElementById('detail-content').classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading tool:', error);
|
||||
document.getElementById('detail-loading').textContent = 'Error loading tool details';
|
||||
}
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detail-modal').classList.add('hidden');
|
||||
currentDetailToolId = null;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function approveTool(toolId) {
|
||||
if (!confirm('Approve this tool?')) return;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue