feat(admin/models): delete an added model with inline confirm
Backend: AiModelRepository.delete() + DELETE /api/admin/models/{id} (owner-only,
audited) in the RPC-served admin router; session commits per request.
Frontend: api.adminDeleteModel() (WebSocket RPC DELETE) and a per-row حذف
action with a two-step inline confirm (تأیید حذف / انصراف) that removes the
row from state on success. tsc clean, py_compile clean.
This commit is contained in:
@@ -281,6 +281,14 @@ class AiModelRepository:
|
||||
await self.session.flush()
|
||||
return _model_to_domain(row)
|
||||
|
||||
async def delete(self, model_id: str) -> bool:
|
||||
row = await self.session.get(AiModelRow, model_id)
|
||||
if row is None:
|
||||
return False
|
||||
await self.session.delete(row)
|
||||
await self.session.flush()
|
||||
return True
|
||||
|
||||
async def pick_for_mode(self, mode: RoutingMode) -> AiModel | None:
|
||||
"""Choose a model for a routing mode.
|
||||
|
||||
|
||||
@@ -215,6 +215,28 @@ async def create_model(
|
||||
return _model_out(model)
|
||||
|
||||
|
||||
@router.delete("/models/{model_id}", status_code=status.HTTP_200_OK)
|
||||
async def delete_model(
|
||||
model_id: str,
|
||||
request: Request,
|
||||
owner: User = Depends(require_platform_owner),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict:
|
||||
repo = AiModelRepository(session)
|
||||
existing = await repo.get(model_id)
|
||||
if existing is None:
|
||||
raise NotFoundError("مدلی با این شناسه یافت نشد.")
|
||||
await repo.delete(model_id)
|
||||
ip, ua = client_meta(request)
|
||||
await record_audit(
|
||||
session, action=AuditAction.MODEL_CHANGE, actor_user_id=owner.id,
|
||||
target_type="model", target_id=model_id,
|
||||
metadata={"internal_name": existing.internal_name, "deleted": True},
|
||||
ip=ip, user_agent=ua,
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routing policies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -120,6 +120,9 @@ export default function AdminModelsPage() {
|
||||
* list updated without hunting for it (the list sits below the form). */
|
||||
const [justAddedId, setJustAddedId] = React.useState<string | null>(null);
|
||||
const listRef = React.useRef<HTMLDivElement | null>(null);
|
||||
/** Inline delete-confirmation: which row is armed, and which is in flight. */
|
||||
const [confirmId, setConfirmId] = React.useState<string | null>(null);
|
||||
const [deletingId, setDeletingId] = React.useState<string | null>(null);
|
||||
|
||||
const load = React.useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -272,6 +275,19 @@ export default function AdminModelsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (model: AdminModel) => {
|
||||
setDeletingId(model.id);
|
||||
try {
|
||||
await api.adminDeleteModel(model.id);
|
||||
setModels((current) => current.filter((m) => m.id !== model.id));
|
||||
setConfirmId(null);
|
||||
} catch (err) {
|
||||
setFormError(err instanceof ApiError ? err.message : "حذف مدل ناموفق بود.");
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedFamily = groupedModels.find((g) => g.base === modelId);
|
||||
const efforts: ReasoningEffort[] = selectedFamily
|
||||
? (selectedFamily.efforts as ReasoningEffort[])
|
||||
@@ -443,6 +459,7 @@ export default function AdminModelsPage() {
|
||||
<th className="px-2 py-2 text-right font-medium">ورودی</th>
|
||||
<th className="px-2 py-2 text-right font-medium">خروجی</th>
|
||||
<th className="px-2 py-2 text-right font-medium">وضعیت</th>
|
||||
<th className="px-2 py-2 text-right font-medium">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="anim-stagger">
|
||||
@@ -487,6 +504,40 @@ export default function AdminModelsPage() {
|
||||
{model.is_active ? "فعال" : "غیرفعال"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{confirmId === model.id ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="danger"
|
||||
loading={deletingId === model.id}
|
||||
onClick={() => void onDelete(model)}
|
||||
>
|
||||
تأیید حذف
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={deletingId === model.id}
|
||||
onClick={() => setConfirmId(null)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-danger hover:bg-danger/10"
|
||||
onClick={() => setConfirmId(model.id)}
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -518,6 +518,10 @@ export const api = {
|
||||
method: "POST",
|
||||
body,
|
||||
}),
|
||||
adminDeleteModel: (modelId: string) =>
|
||||
apiRequest<void>(`/api/admin/models/${encodeURIComponent(modelId)}`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
adminRoutingPolicies: () =>
|
||||
apiRequest<import("@agy/contracts").AdminRoutingPolicy[]>("/api/admin/routing-policies"),
|
||||
adminCreateRoutingPolicy: (body: {
|
||||
|
||||
Reference in New Issue
Block a user