update for pc server
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, WebSocket, WebSocketDisconnect, BackgroundTasks
|
||||
from pydantic import BaseModel
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.orm import Session
|
||||
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"}
|
||||
|
||||
class PasswordResetRequest(BaseModel):
|
||||
username: str
|
||||
new_password: str
|
||||
|
||||
@app.post("/auth/reset-password")
|
||||
def reset_password(
|
||||
username: str,
|
||||
new_password: str,
|
||||
request: PasswordResetRequest,
|
||||
current_user: models.User = Depends(auth.get_current_admin_user),
|
||||
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:
|
||||
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()
|
||||
return {"message": "Password reset successfully"}
|
||||
|
||||
|
||||
34
asf-cloud-server/testarena/backend/scripts/reset_admin.py
Normal file
34
asf-cloud-server/testarena/backend/scripts/reset_admin.py
Normal 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()
|
||||
BIN
asf-cloud-server/testarena/frontend/public/logo.png
Normal file
BIN
asf-cloud-server/testarena/frontend/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import api from '../api';
|
||||
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';
|
||||
|
||||
interface Job {
|
||||
@@ -18,7 +18,7 @@ interface Job {
|
||||
}
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { logout } = useAuth();
|
||||
const { logout, user } = useAuth();
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||
const navigate = useNavigate();
|
||||
@@ -63,7 +63,7 @@ const Dashboard: React.FC = () => {
|
||||
const handleAbort = async (jobId: number) => {
|
||||
try {
|
||||
await api.post(`/jobs/${jobId}/abort`);
|
||||
fetchJobs(); // Refresh to ensure sync
|
||||
fetchJobs();
|
||||
} catch (error) {
|
||||
console.error("Failed to abort job", error);
|
||||
}
|
||||
@@ -71,121 +71,172 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'passed': return <CheckCircle className="text-green-500" />;
|
||||
case 'failed': return <XCircle className="text-red-500" />;
|
||||
case 'running': return <Clock className="text-orange-500 animate-pulse" />;
|
||||
case 'aborted': return <AlertCircle className="text-red-900" />;
|
||||
default: return <Clock className="text-gray-400" />;
|
||||
case 'passed': return <CheckCircle className="text-green-500" size={18} />;
|
||||
case 'failed': return <XCircle className="text-red-500" size={18} />;
|
||||
case 'running': return <Clock className="text-orange-500 animate-pulse" size={18} />;
|
||||
case 'aborted': return <AlertCircle className="text-red-900" size={18} />;
|
||||
default: return <Clock className="text-gray-400" size={18} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100 overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<div className="w-1/3 bg-white border-r border-gray-200 flex flex-col">
|
||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center bg-primary text-white">
|
||||
<h1 className="font-bold text-lg">TestArena</h1>
|
||||
<div className="flex flex-col h-screen bg-secondary font-sans text-text">
|
||||
{/* Header */}
|
||||
<header className="bg-primary text-white shadow-md z-10">
|
||||
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<img src="/logo.png" alt="Logo" className="h-8 w-auto" />
|
||||
<nav className="hidden md:flex gap-6 text-sm font-medium">
|
||||
<a href="#" className="opacity-80 hover:opacity-100 border-b-2 border-transparent hover:border-white pb-1">SUBMIT</a>
|
||||
<a href="#" className="opacity-80 hover:opacity-100 border-b-2 border-transparent hover:border-white pb-1">SCHEDULE</a>
|
||||
<a href="#" className="opacity-100 border-b-2 border-white pb-1">DASHBOARD</a>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Sub-header / Filters */}
|
||||
<div className="bg-white border-b border-gray-200 p-4 shadow-sm">
|
||||
<div className="container mx-auto flex flex-wrap gap-4 items-center">
|
||||
<div className="flex items-center gap-2 text-primary font-semibold">
|
||||
<Filter size={16} /> FILTERS
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => navigate('/new-job')} className="p-1 hover:bg-secondary rounded" title="New Job">
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
<button onClick={logout} className="p-1 hover:bg-secondary rounded" title="Logout">
|
||||
<LogOut size={20} />
|
||||
<input type="text" placeholder="Submitter" className="border border-gray-300 rounded px-3 py-1 text-sm focus:outline-none focus:border-accent" />
|
||||
<input type="text" placeholder="Source" className="border border-gray-300 rounded px-3 py-1 text-sm focus:outline-none focus:border-accent" />
|
||||
<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>
|
||||
<div className="ml-auto">
|
||||
<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">
|
||||
<Plus size={16} /> New Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{jobs.map(job => (
|
||||
<div
|
||||
key={job.id}
|
||||
onClick={() => setSelectedJob(job)}
|
||||
className={clsx(
|
||||
"p-4 border-b border-gray-100 cursor-pointer hover:bg-gray-50 flex items-center justify-between",
|
||||
selectedJob?.id === job.id && "bg-blue-50 border-l-4 border-l-accent"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">#{job.id} {job.branch_name}</div>
|
||||
<div className="text-xs text-gray-500">{new Date(job.created_at).toLocaleString()}</div>
|
||||
</div>
|
||||
<div>{getStatusIcon(job.status)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
|
||||
{/* Left Panel: Job List */}
|
||||
<div className="w-1/3 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">
|
||||
<Menu size={16} /> TEST REQUESTS
|
||||
</div>
|
||||
<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="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>
|
||||
<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 hover:bg-red-200"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
<div className="bg-gray-50 p-4 rounded">
|
||||
<div className="text-sm text-gray-500">Status</div>
|
||||
<div className="flex items-center gap-2 font-medium capitalize">
|
||||
{getStatusIcon(selectedJob.status)}
|
||||
{selectedJob.status}
|
||||
</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>
|
||||
{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 className="mb-6">
|
||||
<h3 className="font-bold mb-2">Selected Scenarios</h3>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4 text-sm">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<span className="font-semibold text-gray-600">Status:</span>
|
||||
<span className="col-span-2 flex items-center gap-2 capitalize font-medium">
|
||||
{getStatusIcon(selectedJob.status)} {selectedJob.status}
|
||||
</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-100 text-blue-800 px-2 py-1 rounded text-sm">
|
||||
<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>
|
||||
|
||||
{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 className="flex-1 flex items-center justify-center text-gray-400 text-sm">
|
||||
Select a job from the list to view details
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,40 +27,45 @@ const Login: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="bg-white p-8 rounded-lg shadow-md w-96">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center text-primary">ASF TestArena</h2>
|
||||
{error && <div className="bg-red-100 text-red-700 p-2 rounded mb-4 text-sm">{error}</div>}
|
||||
<div className="min-h-screen flex items-center justify-center bg-secondary">
|
||||
<div className="bg-white p-8 rounded shadow-sm border border-gray-200 w-96">
|
||||
<div className="flex justify-center mb-6">
|
||||
<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}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
||||
<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
|
||||
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}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
||||
<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
|
||||
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}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -7,9 +7,13 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#0f172a', // Slate 900
|
||||
secondary: '#334155', // Slate 700
|
||||
accent: '#3b82f6', // Blue 500
|
||||
primary: '#1e40af', // Deep Blue (Header)
|
||||
secondary: '#f3f4f6', // Light Gray (Background)
|
||||
accent: '#3b82f6', // Bright Blue (Buttons/Highlights)
|
||||
'accent-hover': '#2563eb',
|
||||
surface: '#ffffff', // White (Cards)
|
||||
text: '#1f2937', // Dark Gray (Text)
|
||||
'text-muted': '#6b7280', // Light Gray (Subtext)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user