Jobs dashboard additions.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Form, UploadFile, File
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from typing import List, Optional, Union, Tuple, Dict, Any
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
@@ -26,11 +26,13 @@ class JobSummary(BaseModel):
|
||||
finished_at: Optional[datetime] = None
|
||||
status: str # JobStatus.name
|
||||
return_code: int
|
||||
env: Dict[str, str] = {}
|
||||
|
||||
class JobDetail(JobSummary):
|
||||
output: str # base64-encoded
|
||||
errors: str # base64-encoded
|
||||
artifacts: List[Tuple[str, str]] # list of (filename, base64_data)
|
||||
env: Dict[str, str] = {}
|
||||
|
||||
from typing import Dict
|
||||
|
||||
@@ -64,7 +66,8 @@ async def list_user_jobs(
|
||||
started_at=j.started_at,
|
||||
finished_at=j.finished_at,
|
||||
status=j.status.name,
|
||||
return_code=j.return_code
|
||||
return_code=j.return_code,
|
||||
env=j.env
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
@@ -104,7 +107,8 @@ async def get_job_detail(
|
||||
return_code=job.return_code,
|
||||
output=job_dict["output"],
|
||||
errors=job_dict["errors"],
|
||||
artifacts=job_dict["artifacts"]
|
||||
artifacts=job_dict["artifacts"],
|
||||
env=job_dict.get("env", {})
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
@@ -201,3 +205,57 @@ async def create_job(
|
||||
except Exception as e:
|
||||
logging.error(f"Job creation failed for {username}: {e}\n{format_exc()}")
|
||||
raise HTTPException(status_code=500, detail="Failed to queue job")
|
||||
|
||||
@dashboard_router.get("/jobs/new", response_class=HTMLResponse)
|
||||
async def new_job_form(
|
||||
request: Request,
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
return templates.TemplateResponse(
|
||||
"job_new.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user.username
|
||||
}
|
||||
)
|
||||
|
||||
@dashboard_router.post("/jobs/new")
|
||||
async def create_job_from_form(
|
||||
db: DbDependency,
|
||||
request: Request,
|
||||
cmd: str = Form(...),
|
||||
env_keys: List[str] = Form(default=[]),
|
||||
env_values: List[str] = Form(default=[]),
|
||||
files: List[UploadFile] = File(default=[]),
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
# Build env dict from parallel lists
|
||||
env = {}
|
||||
for k, v in zip(env_keys, env_values):
|
||||
if k.strip():
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
# Build files dict for API (filename → base64)
|
||||
files_dict = {}
|
||||
for upload in files:
|
||||
if upload.filename:
|
||||
content = await upload.read()
|
||||
files_dict[upload.filename] = base64.b64encode(content).decode('ascii')
|
||||
|
||||
# Prepare payload for the existing API
|
||||
payload = {
|
||||
"cmd": [part.strip() for part in cmd.split() if part.strip()], # split on whitespace, like shell
|
||||
"env": env if env else None,
|
||||
"files": files_dict if files_dict else None
|
||||
}
|
||||
|
||||
# Call the API internally
|
||||
from packetserver.http.routers.jobs import create_job as api_create_job
|
||||
response = await api_create_job(
|
||||
payload=JobCreate(**{k: v for k, v in payload.items() if v is not None}),
|
||||
db=db,
|
||||
current_user=current_user
|
||||
)
|
||||
|
||||
# Redirect to the new job detail page
|
||||
return RedirectResponse(url=f"/jobs/{response.id}", status_code=303)
|
||||
@@ -90,6 +90,18 @@
|
||||
<li class="list-group-item"><strong>Started:</strong> {% if job.started_at %}{{ job.started_at.strftime('%H:%M:%S') }}{% else %}-{% endif %}</li>
|
||||
<li class="list-group-item"><strong>Finished:</strong> {% if job.finished_at %}{{ job.finished_at.strftime('%H:%M:%S') }}{% else %}-{% endif %}</li>
|
||||
<li class="list-group-item"><strong>Return Code:</strong> {% if job.return_code is not none %}{{ job.return_code }}{% else %}-{% endif %}</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Environment Variables:</strong>
|
||||
{% if job.env %}
|
||||
<ul class="list-unstyled mt-2 mb-0">
|
||||
{% for key, value in job.env.items() %}
|
||||
<li><code>{{ key }}={{ value }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
54
packetserver/http/templates/job_new.html
Normal file
54
packetserver/http/templates/job_new.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New Job - {{ current_user }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Queue New Job</h2>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Command</label>
|
||||
<textarea name="cmd" class="form-control" rows="3" placeholder="e.g. python script.py --input data.txt" required></textarea>
|
||||
<div class="form-text">Space-separated command and arguments (like a shell command).</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Environment Variables</label>
|
||||
<div id="env-fields">
|
||||
<div class="row mb-2 env-row">
|
||||
<div class="col"><input type="text" name="env_keys" class="form-control" placeholder="KEY"></div>
|
||||
<div class="col"><input type="text" name="env_values" class="form-control" placeholder="value"></div>
|
||||
<div class="col-auto"><button type="button" class="btn btn-outline-danger btn-sm" onclick="this.parentElement.parentElement.remove()">Remove</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="addEnvRow()">+ Add Env Var</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Files (scripts, config, data)</label>
|
||||
<input type="file" name="files" class="form-control" multiple>
|
||||
<div class="form-text">Uploaded files will be available in the container's working directory.</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary me-2">Queue Job</button>
|
||||
<a href="/jobs" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addEnvRow() {
|
||||
const container = document.getElementById('env-fields');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row mb-2 env-row';
|
||||
row.innerHTML = `
|
||||
<div class="col"><input type="text" name="env_keys" class="form-control" placeholder="KEY"></div>
|
||||
<div class="col"><input type="text" name="env_values" class="form-control" placeholder="value"></div>
|
||||
<div class="col-auto"><button type="button" class="btn btn-outline-danger btn-sm" onclick="this.parentElement.parentElement.remove()">Remove</button></div>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -3,7 +3,13 @@
|
||||
{% block title %}My Jobs - {{ current_user }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">My Jobs</h2>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">My Jobs</h2>
|
||||
<a href="/jobs/new" class="btn btn-success">
|
||||
<i class="bi bi-plus-lg"></i> New Job
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if jobs %}
|
||||
<table class="table table-striped table-hover">
|
||||
|
||||
Reference in New Issue
Block a user