update for pc server

This commit is contained in:
2025-11-25 00:40:22 +01:00
parent 030cfcad9e
commit 0d1d388dca
6 changed files with 212 additions and 114 deletions

View File

@@ -1,4 +1,5 @@
from fastapi import FastAPI, Depends, HTTPException, status, WebSocket, WebSocketDisconnect, BackgroundTasks from fastapi import FastAPI, Depends, HTTPException, status, WebSocket, WebSocketDisconnect, BackgroundTasks
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List
@@ -33,17 +34,20 @@ def login_for_access_token(form_data: schemas.UserCreate, db: Session = Depends(
) )
return {"access_token": access_token, "token_type": "bearer"} return {"access_token": access_token, "token_type": "bearer"}
class PasswordResetRequest(BaseModel):
username: str
new_password: str
@app.post("/auth/reset-password") @app.post("/auth/reset-password")
def reset_password( def reset_password(
username: str, request: PasswordResetRequest,
new_password: str,
current_user: models.User = Depends(auth.get_current_admin_user), current_user: models.User = Depends(auth.get_current_admin_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
user = crud.get_user_by_username(db, username) user = crud.get_user_by_username(db, request.username)
if not user: if not user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
user.hashed_password = auth.get_password_hash(new_password) user.hashed_password = auth.get_password_hash(request.new_password)
db.commit() db.commit()
return {"message": "Password reset successfully"} return {"message": "Password reset successfully"}

View File

@@ -0,0 +1,34 @@
import sys
import os
# Add the parent directory to sys.path to import app modules
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import database, crud, schemas, auth, models
def reset_admin():
db = database.SessionLocal()
try:
username = "admin"
password = "admin123"
user = crud.get_user_by_username(db, username)
if user:
print(f"Updating password for {username}...")
user.hashed_password = auth.get_password_hash(password)
db.commit()
print("Password updated successfully.")
else:
print(f"Creating user {username}...")
user_in = schemas.UserCreate(username=username, password=password, role=models.UserRole.admin)
hashed_password = auth.get_password_hash(password)
crud.create_user(db, user_in, hashed_password)
print("User created successfully.")
except Exception as e:
print(f"Error: {e}")
finally:
db.close()
if __name__ == "__main__":
reset_admin()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import api from '../api'; import api from '../api';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { CheckCircle, XCircle, Clock, AlertCircle, Plus, LogOut } from 'lucide-react'; import { CheckCircle, XCircle, Clock, AlertCircle, Plus, LogOut, Search, Filter, Menu } from 'lucide-react';
import clsx from 'clsx'; import clsx from 'clsx';
interface Job { interface Job {
@@ -18,7 +18,7 @@ interface Job {
} }
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const { logout } = useAuth(); const { logout, user } = useAuth();
const [jobs, setJobs] = useState<Job[]>([]); const [jobs, setJobs] = useState<Job[]>([]);
const [selectedJob, setSelectedJob] = useState<Job | null>(null); const [selectedJob, setSelectedJob] = useState<Job | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -63,7 +63,7 @@ const Dashboard: React.FC = () => {
const handleAbort = async (jobId: number) => { const handleAbort = async (jobId: number) => {
try { try {
await api.post(`/jobs/${jobId}/abort`); await api.post(`/jobs/${jobId}/abort`);
fetchJobs(); // Refresh to ensure sync fetchJobs();
} catch (error) { } catch (error) {
console.error("Failed to abort job", error); console.error("Failed to abort job", error);
} }
@@ -71,120 +71,171 @@ const Dashboard: React.FC = () => {
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {
switch (status) { switch (status) {
case 'passed': return <CheckCircle className="text-green-500" />; case 'passed': return <CheckCircle className="text-green-500" size={18} />;
case 'failed': return <XCircle className="text-red-500" />; case 'failed': return <XCircle className="text-red-500" size={18} />;
case 'running': return <Clock className="text-orange-500 animate-pulse" />; case 'running': return <Clock className="text-orange-500 animate-pulse" size={18} />;
case 'aborted': return <AlertCircle className="text-red-900" />; case 'aborted': return <AlertCircle className="text-red-900" size={18} />;
default: return <Clock className="text-gray-400" />; default: return <Clock className="text-gray-400" size={18} />;
} }
}; };
return ( return (
<div className="flex h-screen bg-gray-100 overflow-hidden"> <div className="flex flex-col h-screen bg-secondary font-sans text-text">
{/* Sidebar */} {/* Header */}
<div className="w-1/3 bg-white border-r border-gray-200 flex flex-col"> <header className="bg-primary text-white shadow-md z-10">
<div className="p-4 border-b border-gray-200 flex justify-between items-center bg-primary text-white"> <div className="container mx-auto px-4 h-16 flex items-center justify-between">
<h1 className="font-bold text-lg">TestArena</h1> <div className="flex items-center gap-4">
<div className="flex gap-2"> <img src="/logo.png" alt="Logo" className="h-8 w-auto" />
<button onClick={() => navigate('/new-job')} className="p-1 hover:bg-secondary rounded" title="New Job"> <nav className="hidden md:flex gap-6 text-sm font-medium">
<Plus size={20} /> <a href="#" className="opacity-80 hover:opacity-100 border-b-2 border-transparent hover:border-white pb-1">SUBMIT</a>
</button> <a href="#" className="opacity-80 hover:opacity-100 border-b-2 border-transparent hover:border-white pb-1">SCHEDULE</a>
<button onClick={logout} className="p-1 hover:bg-secondary rounded" title="Logout"> <a href="#" className="opacity-100 border-b-2 border-white pb-1">DASHBOARD</a>
<LogOut size={20} /> <a href="#" className="opacity-80 hover:opacity-100 border-b-2 border-transparent hover:border-white pb-1">RESULTS</a>
</nav>
</div>
<div className="flex items-center gap-4">
<span className="text-sm opacity-90">Welcome, {user?.username}</span>
<button onClick={logout} className="p-2 hover:bg-blue-800 rounded-full transition-colors" title="Logout">
<LogOut size={18} />
</button> </button>
</div> </div>
</div> </div>
<div className="flex-1 overflow-y-auto"> </header>
{jobs.map(job => (
<div {/* Sub-header / Filters */}
key={job.id} <div className="bg-white border-b border-gray-200 p-4 shadow-sm">
onClick={() => setSelectedJob(job)} <div className="container mx-auto flex flex-wrap gap-4 items-center">
className={clsx( <div className="flex items-center gap-2 text-primary font-semibold">
"p-4 border-b border-gray-100 cursor-pointer hover:bg-gray-50 flex items-center justify-between", <Filter size={16} /> FILTERS
selectedJob?.id === job.id && "bg-blue-50 border-l-4 border-l-accent" </div>
)} <div className="flex gap-2">
> <input type="text" placeholder="Submitter" className="border border-gray-300 rounded px-3 py-1 text-sm focus:outline-none focus:border-accent" />
<div> <input type="text" placeholder="Source" className="border border-gray-300 rounded px-3 py-1 text-sm focus:outline-none focus:border-accent" />
<div className="font-medium">#{job.id} {job.branch_name}</div> <input type="text" placeholder="Task ID" className="border border-gray-300 rounded px-3 py-1 text-sm focus:outline-none focus:border-accent" />
<div className="text-xs text-gray-500">{new Date(job.created_at).toLocaleString()}</div> </div>
</div> <div className="ml-auto">
<div>{getStatusIcon(job.status)}</div> <button onClick={() => navigate('/new-job')} className="bg-accent hover:bg-accent-hover text-white px-4 py-1.5 rounded text-sm font-medium flex items-center gap-2 transition-colors">
</div> <Plus size={16} /> New Request
))} </button>
</div>
</div> </div>
</div> </div>
{/* Main Content */} {/* Main Content */}
<div className="flex-1 p-8 overflow-y-auto"> <div className="flex-1 container mx-auto p-4 overflow-hidden flex gap-4">
{selectedJob ? (
<div className="bg-white rounded-lg shadow-sm p-6">
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold">Job #{selectedJob.id} Details</h2>
{selectedJob.status === 'running' && (
<button
onClick={() => handleAbort(selectedJob.id)}
className="bg-red-100 text-red-700 px-4 py-2 rounded hover:bg-red-200"
>
Abort Job
</button>
)}
</div>
<div className="grid grid-cols-2 gap-6 mb-6"> {/* Left Panel: Job List */}
<div className="bg-gray-50 p-4 rounded"> <div className="w-1/3 bg-white rounded shadow-sm border border-gray-200 flex flex-col">
<div className="text-sm text-gray-500">Status</div> <div className="p-3 bg-primary text-white font-semibold text-sm flex items-center gap-2">
<div className="flex items-center gap-2 font-medium capitalize"> <Menu size={16} /> TEST REQUESTS
{getStatusIcon(selectedJob.status)} </div>
{selectedJob.status} <div className="flex-1 overflow-y-auto">
<table className="w-full text-sm text-left">
<thead className="bg-gray-50 text-text-muted font-medium border-b border-gray-200">
<tr>
<th className="p-3">ID</th>
<th className="p-3">STATUS</th>
<th className="p-3">BRANCH</th>
</tr>
</thead>
<tbody>
{jobs.map(job => (
<tr
key={job.id}
onClick={() => setSelectedJob(job)}
className={clsx(
"cursor-pointer border-b border-gray-100 hover:bg-blue-50 transition-colors",
selectedJob?.id === job.id && "bg-blue-50 border-l-4 border-l-accent"
)}
>
<td className="p-3 font-medium text-gray-700">#{job.id}</td>
<td className="p-3">{getStatusIcon(job.status)}</td>
<td className="p-3 text-gray-600 truncate max-w-[150px]">{job.branch_name}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Right Panel: Job Details */}
<div className="flex-1 bg-white rounded shadow-sm border border-gray-200 flex flex-col">
<div className="p-3 bg-primary text-white font-semibold text-sm flex items-center gap-2">
<AlertCircle size={16} /> TEST INFO
</div>
{selectedJob ? (
<div className="p-6 overflow-y-auto">
<div className="flex justify-between items-start mb-6 border-b border-gray-100 pb-4">
<div>
<h2 className="text-xl font-bold text-gray-800">Test Request #{selectedJob.id}</h2>
<p className="text-sm text-text-muted mt-1">Submitted on {new Date(selectedJob.created_at).toLocaleString()}</p>
</div>
<div className="flex gap-2">
{selectedJob.status === 'running' && (
<button
onClick={() => handleAbort(selectedJob.id)}
className="bg-red-100 text-red-700 px-4 py-2 rounded text-sm font-medium hover:bg-red-200 transition-colors"
>
Abort Job
</button>
)}
{selectedJob.result_path && (
<a
href={selectedJob.result_path}
target="_blank"
rel="noopener noreferrer"
className="bg-green-600 text-white px-4 py-2 rounded text-sm font-medium hover:bg-green-700 transition-colors flex items-center gap-2"
>
View Results
</a>
)}
</div> </div>
</div> </div>
<div className="bg-gray-50 p-4 rounded">
<div className="text-sm text-gray-500">Duration</div>
<div className="font-medium">{selectedJob.duration || '-'}</div>
</div>
<div className="bg-gray-50 p-4 rounded">
<div className="text-sm text-gray-500">Branch</div>
<div className="font-medium">{selectedJob.branch_name}</div>
</div>
<div className="bg-gray-50 p-4 rounded">
<div className="text-sm text-gray-500">Environment</div>
<div className="font-medium">{selectedJob.environment}</div>
</div>
<div className="bg-gray-50 p-4 rounded">
<div className="text-sm text-gray-500">Test Mode</div>
<div className="font-medium">{selectedJob.test_mode}</div>
</div>
</div>
<div className="mb-6"> <div className="grid grid-cols-2 gap-x-8 gap-y-4 text-sm">
<h3 className="font-bold mb-2">Selected Scenarios</h3> <div className="grid grid-cols-3 gap-4">
<div className="flex flex-wrap gap-2"> <span className="font-semibold text-gray-600">Status:</span>
{selectedJob.scenarios.map(s => ( <span className="col-span-2 flex items-center gap-2 capitalize font-medium">
<span key={s} className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm"> {getStatusIcon(selectedJob.status)} {selectedJob.status}
{s}
</span> </span>
))} </div>
<div className="grid grid-cols-3 gap-4">
<span className="font-semibold text-gray-600">Duration:</span>
<span className="col-span-2">{selectedJob.duration || '-'}</span>
</div>
<div className="grid grid-cols-3 gap-4">
<span className="font-semibold text-gray-600">Branch:</span>
<span className="col-span-2 font-mono text-gray-700 bg-gray-50 px-2 py-0.5 rounded inline-block w-fit">{selectedJob.branch_name}</span>
</div>
<div className="grid grid-cols-3 gap-4">
<span className="font-semibold text-gray-600">Environment:</span>
<span className="col-span-2">{selectedJob.environment}</span>
</div>
<div className="grid grid-cols-3 gap-4">
<span className="font-semibold text-gray-600">Test Mode:</span>
<span className="col-span-2">{selectedJob.test_mode}</span>
</div>
</div>
<div className="mt-8">
<h3 className="font-bold text-gray-800 mb-3 border-b border-gray-200 pb-2">Selected Scenarios</h3>
<div className="flex flex-wrap gap-2">
{selectedJob.scenarios.map(s => (
<span key={s} className="bg-blue-50 text-blue-700 border border-blue-100 px-3 py-1 rounded-full text-xs font-medium">
{s}
</span>
))}
</div>
</div> </div>
</div> </div>
) : (
<div className="flex-1 flex items-center justify-center text-gray-400 text-sm">
Select a job from the list to view details
</div>
)}
</div>
{selectedJob.result_path && (
<div className="mt-6">
<a
href={selectedJob.result_path}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
>
View HTML Report
</a>
</div>
)}
</div>
) : (
<div className="flex items-center justify-center h-full text-gray-400">
Select a job to view details
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -27,40 +27,45 @@ const Login: React.FC = () => {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-100"> <div className="min-h-screen flex items-center justify-center bg-secondary">
<div className="bg-white p-8 rounded-lg shadow-md w-96"> <div className="bg-white p-8 rounded shadow-sm border border-gray-200 w-96">
<h2 className="text-2xl font-bold mb-6 text-center text-primary">ASF TestArena</h2> <div className="flex justify-center mb-6">
{error && <div className="bg-red-100 text-red-700 p-2 rounded mb-4 text-sm">{error}</div>} <img src="/logo.png" alt="Logo" className="h-12 w-auto" />
</div>
<h2 className="text-xl font-bold mb-6 text-center text-primary">Sign In</h2>
{error && <div className="bg-red-50 text-red-700 p-2 rounded mb-4 text-sm border border-red-100 text-center">{error}</div>}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Username</label> <label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
<div className="relative"> <div className="relative">
<User className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" /> <User className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<input <input
type="text" type="text"
className="pl-10 w-full border border-gray-300 rounded-md p-2 focus:ring-accent focus:border-accent outline-none" className="pl-9 w-full border border-gray-300 rounded p-2 text-sm focus:ring-1 focus:ring-accent focus:border-accent outline-none transition-all"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
required required
placeholder="Enter your username"
/> />
</div> </div>
</div> </div>
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label> <label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
<div className="relative"> <div className="relative">
<Lock className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" /> <Lock className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<input <input
type="password" type="password"
className="pl-10 w-full border border-gray-300 rounded-md p-2 focus:ring-accent focus:border-accent outline-none" className="pl-9 w-full border border-gray-300 rounded p-2 text-sm focus:ring-1 focus:ring-accent focus:border-accent outline-none transition-all"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
placeholder="Enter your password"
/> />
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
className="w-full bg-accent text-white py-2 rounded-md hover:bg-blue-600 transition-colors" className="w-full bg-accent text-white py-2 rounded font-medium hover:bg-accent-hover transition-colors shadow-sm"
> >
Login Login
</button> </button>

View File

@@ -7,9 +7,13 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
primary: '#0f172a', // Slate 900 primary: '#1e40af', // Deep Blue (Header)
secondary: '#334155', // Slate 700 secondary: '#f3f4f6', // Light Gray (Background)
accent: '#3b82f6', // Blue 500 accent: '#3b82f6', // Bright Blue (Buttons/Highlights)
'accent-hover': '#2563eb',
surface: '#ffffff', // White (Cards)
text: '#1f2937', // Dark Gray (Text)
'text-muted': '#6b7280', // Light Gray (Subtext)
} }
}, },
}, },