mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
Optimization Factory update (#2035)
Co-authored-by: Mohamed Ashraf <mohamedashrraf222@gmail.com> Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com> Co-authored-by: Kevin Turcios <106575910+KRRT7@users.noreply.github.com>
This commit is contained in:
parent
d4ddff44df
commit
8216f32d9e
5 changed files with 878 additions and 84 deletions
|
|
@ -252,7 +252,27 @@ FORK_REPO="codeflash-ai/${FORK_REPO_NAME}"
|
|||
echo "Checking if fork already exists: ${FORK_REPO}"
|
||||
if gh repo view "${FORK_REPO}" >/dev/null 2>&1; then
|
||||
echo "✅ Fork already exists: ${FORK_REPO}"
|
||||
echo "Using existing fork - no need to create a new one"
|
||||
echo "Updating fork from upstream before cloning..."
|
||||
|
||||
_stage "sync_fork_start" "\"fork_repo\":\"${FORK_REPO}\",\"upstream\":\"${GITHUB_REPO_URL}\""
|
||||
echo "Syncing fork ${FORK_REPO} with upstream ${GITHUB_REPO_URL}..."
|
||||
|
||||
# Sync the fork with its upstream repository
|
||||
# gh repo sync automatically syncs a fork with its upstream if the repo is a fork
|
||||
# This updates the fork on GitHub with the latest changes from the upstream repository
|
||||
# We use --force to perform a hard reset, ensuring the fork matches upstream exactly even if it has diverged
|
||||
if gh repo sync "${FORK_REPO}" --force 2>&1 | tee -a /tmp/sync_debug.log; then
|
||||
echo "✅ Successfully synced fork ${FORK_REPO} with upstream"
|
||||
_stage "sync_fork_complete" "\"fork_repo\":\"${FORK_REPO}\""
|
||||
else
|
||||
SYNC_EXIT_CODE=${PIPESTATUS[0]}
|
||||
echo "⚠️ Fork sync failed with exit code: $SYNC_EXIT_CODE"
|
||||
echo "Sync debug log:"
|
||||
cat /tmp/sync_debug.log 2>/dev/null || echo "No sync debug log available"
|
||||
echo "Note: If the fork doesn't have upstream configured, sync may fail."
|
||||
echo "This is non-fatal - continuing with clone anyway - fork may still be usable"
|
||||
_stage "sync_fork_warning" "\"fork_repo\":\"${FORK_REPO}\",\"exit_code\":$SYNC_EXIT_CODE"
|
||||
fi
|
||||
else
|
||||
echo "Fork does not exist, creating new fork..."
|
||||
|
||||
|
|
@ -838,6 +858,29 @@ fi
|
|||
|
||||
python -c "import pytest" 2>/dev/null || pip install pytest || true
|
||||
|
||||
# Ensure CodeFlash environment is properly set up before tests
|
||||
echo "Setting up CodeFlash environment..."
|
||||
if [ -n "${CODEFLASH_API_KEY:-}" ]; then
|
||||
export CODEFLASH_API_KEY="${CODEFLASH_API_KEY}"
|
||||
echo "✅ CodeFlash API key is set and exported"
|
||||
|
||||
# Verify CodeFlash is accessible
|
||||
if command -v codeflash >/dev/null 2>&1; then
|
||||
echo "✅ CodeFlash CLI is available"
|
||||
else
|
||||
echo "⚠️ CodeFlash CLI not found, installing..."
|
||||
pip install codeflash[asyncio] || pip install codeflash || true
|
||||
fi
|
||||
|
||||
# Test CodeFlash connectivity
|
||||
if command -v codeflash >/dev/null 2>&1; then
|
||||
echo "Testing CodeFlash connectivity..."
|
||||
timeout 10 codeflash --version 2>/dev/null && echo "✅ CodeFlash connectivity verified" || echo "⚠️ CodeFlash connectivity test failed"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ CODEFLASH_API_KEY not set - optimization may fail"
|
||||
fi
|
||||
|
||||
# Optional: preflight test run to detect missing modules (with timeout protection)
|
||||
if [ -d "${TESTS_ROOT_VALUE}" ]; then
|
||||
echo "Preflight test run to detect missing modules..."
|
||||
|
|
@ -1426,7 +1469,7 @@ EOF
|
|||
CLAUDE_CMD=""
|
||||
echo "Checking for Claude Code CLI availability..." | tee -a "$TEST_LOG_FILE" "$CLAUDE_LOG"
|
||||
|
||||
# Check for global claude command first
|
||||
# Check for global claude command first (should be pre-installed)
|
||||
echo "DEBUG: Checking for global claude command..." | tee -a "$TEST_LOG_FILE"
|
||||
if command -v claude >/dev/null 2>&1; then
|
||||
CLAUDE_CMD="claude"
|
||||
|
|
@ -2221,7 +2264,7 @@ echo "DEBUG: Exit code persistence completed" | tee -a "$TEST_LOG_FILE"
|
|||
cat > /tmp/claude_setup_round_${ROUND}.md << EOF
|
||||
# Repository Setup Assistant - Round $ROUND
|
||||
|
||||
You are continuing to fix repository setup issues. Previous attempts have been made but tests are still failing.
|
||||
You are fixing repository setup issues. Previous attempts have been made but tests are still failing.
|
||||
|
||||
## Current Situation
|
||||
- This is setup round $ROUND of maximum $SETUP_MAX_ROUNDS
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ def _read_csv() -> List[Dict[str, str]]:
|
|||
for row in reader:
|
||||
rows.append({
|
||||
"repo_url": row.get("repo_url", "").strip(),
|
||||
"auto_terminate": row.get("auto_terminate", "true").strip(),
|
||||
"auto_terminate": row.get("auto_terminate", "false").strip(),
|
||||
"analysis_config": row.get("analysis_config", "").strip(),
|
||||
})
|
||||
logger.debug(f"📊 Read {len(rows)} repositories from CSV")
|
||||
|
|
@ -305,7 +305,7 @@ def _write_csv(rows: List[Dict[str, str]]) -> None:
|
|||
for r in rows:
|
||||
writer.writerow({
|
||||
"repo_url": r["repo_url"].strip(),
|
||||
"auto_terminate": r.get("auto_terminate", "true").strip(),
|
||||
"auto_terminate": r.get("auto_terminate", "false").strip(),
|
||||
"analysis_config": r.get("analysis_config", "").strip(),
|
||||
})
|
||||
logger.info(f"💾 Saved {len(rows)} repositories to CSV")
|
||||
|
|
@ -469,7 +469,7 @@ echo "EC2 bootstrap complete for {JOB_NAME}" > /home/ubuntu/.bootstrap_done
|
|||
"BlockDeviceMappings": [
|
||||
{
|
||||
"DeviceName": root_device_name,
|
||||
"Ebs": {"VolumeSize": 100, "VolumeType": "gp3", "DeleteOnTermination": True},
|
||||
"Ebs": {"VolumeSize": 500, "VolumeType": "gp3", "DeleteOnTermination": True},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -923,7 +923,7 @@ def _start_completion_watcher(instance_id: str, public_ip: str, repo_url: str, l
|
|||
canonical_url = _canon_repo_url(repo_url)
|
||||
for row in rows:
|
||||
if _canon_repo_url(row["repo_url"]) == canonical_url:
|
||||
auto_terminate_enabled = row.get("auto_terminate", "true").lower() == "true"
|
||||
auto_terminate_enabled = row.get("auto_terminate", "false").lower() == "true"
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Could not check auto-termination setting for {repo_url}: {e}")
|
||||
|
|
@ -3539,16 +3539,27 @@ def get_enriched_repos() -> Any:
|
|||
"""
|
||||
GET /api/repos/enriched - Get repositories with analysis data enriched
|
||||
|
||||
Query Parameters:
|
||||
- page: Page number (default: 1)
|
||||
- page_size: Number of items per page (default: all)
|
||||
- filter_running: Filter for running optimizations only (default: false)
|
||||
|
||||
This endpoint:
|
||||
1. Loads basic repository data from CSV
|
||||
2. Enriches each repository with analysis data
|
||||
3. Returns complete repository information including module_root, tests_root from analysis
|
||||
4. Supports pagination and filtering
|
||||
|
||||
Returns: JSON with enriched repository data
|
||||
Returns: JSON with enriched repository data (paginated if requested)
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
log_request_start("GET /api/repos/enriched")
|
||||
# Get pagination and filter parameters
|
||||
page = request.args.get("page", type=int) or 1
|
||||
page_size = request.args.get("page_size", type=int) or None
|
||||
filter_running = request.args.get("filter_running", type=str, default="false").lower() == "true"
|
||||
|
||||
log_request_start("GET /api/repos/enriched", page=page, page_size=page_size, filter_running=filter_running)
|
||||
|
||||
try:
|
||||
# Load basic repository data
|
||||
|
|
@ -3584,6 +3595,7 @@ def get_enriched_repos() -> Any:
|
|||
# Get last EC2 ID and IP from jobs
|
||||
last_ec2_id = "None"
|
||||
last_ec2_ip = "None"
|
||||
is_running = False
|
||||
try:
|
||||
jobs_data = json.loads(JOBS_JSON.read_text(encoding="utf-8")) if JOBS_JSON.exists() else {}
|
||||
canonical_url = _canon_repo_url(repo_url)
|
||||
|
|
@ -3591,6 +3603,14 @@ def get_enriched_repos() -> Any:
|
|||
instance_id = jobs_data[canonical_url]
|
||||
last_ec2_id = instance_id
|
||||
|
||||
# Check if instance is running
|
||||
try:
|
||||
state = _describe_instance_state(instance_id)
|
||||
if state and state.lower() == "running":
|
||||
is_running = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get IP address for the instance
|
||||
try:
|
||||
logger.debug(f"Attempting to get public IP for instance {instance_id}")
|
||||
|
|
@ -3640,17 +3660,34 @@ def get_enriched_repos() -> Any:
|
|||
"analysis_config": row["analysis_config"],
|
||||
"last_ec2_id": last_ec2_id,
|
||||
"last_ec2_ip": last_ec2_ip,
|
||||
"has_analysis": bool(analysis)
|
||||
"has_analysis": bool(analysis),
|
||||
"is_running": is_running
|
||||
}
|
||||
|
||||
# Apply filter for running optimizations
|
||||
if filter_running and not is_running:
|
||||
continue
|
||||
|
||||
enriched_repos.append(enriched_repo)
|
||||
|
||||
# Apply pagination if page_size is provided
|
||||
total_count = len(enriched_repos)
|
||||
if page_size is not None:
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
enriched_repos = enriched_repos[start_idx:end_idx]
|
||||
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
log_request_success("GET /api/repos/enriched", duration_ms, repo_count=len(enriched_repos))
|
||||
log_request_success("GET /api/repos/enriched", duration_ms,
|
||||
page=page, page_size=page_size, filter_running=filter_running,
|
||||
total_count=total_count, returned_count=len(enriched_repos))
|
||||
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"repos": enriched_repos
|
||||
"repos": enriched_repos,
|
||||
"total_count": total_count,
|
||||
"page": page,
|
||||
"page_size": page_size or total_count
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -4151,6 +4188,34 @@ def get_custom_run_status(instance_id: str) -> Any:
|
|||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
log_request_error("GET /api/custom-run/status/<instance_id>", str(e), duration_ms, instance_id=instance_id)
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
@app.get("/api/ssh-config")
|
||||
def get_ssh_config() -> Any:
|
||||
"""
|
||||
GET /api/ssh-config - Get SSH configuration for connecting to EC2 instances
|
||||
|
||||
Returns: JSON with SSH key path and connection instructions
|
||||
"""
|
||||
start_time = time.time()
|
||||
log_request_start("GET /api/ssh-config")
|
||||
|
||||
try:
|
||||
ssh_key_path = os.path.expanduser(SSH_KEY_PATH)
|
||||
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
log_request_success("GET /api/ssh-config", duration_ms)
|
||||
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"ssh_key_path": ssh_key_path,
|
||||
"user": "ubuntu"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
log_request_error("GET /api/ssh-config", str(e), duration_ms)
|
||||
raise
|
||||
|
||||
|
||||
@app.route("/api/repos/<path:repo_url>/logs", methods=["GET"])
|
||||
def get_repo_logs(repo_url: str) -> Any:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -107,6 +107,14 @@ function showDashboard() {
|
|||
let allRepos = []
|
||||
let filteredRepos = []
|
||||
|
||||
// Global variables for pagination
|
||||
let currentPage = 1
|
||||
let pageSize = 25
|
||||
let totalCount = 0
|
||||
let filterRunning = false
|
||||
let isRefreshing = false
|
||||
let targetPage = 1 // Track the target page the user wants to navigate to
|
||||
|
||||
/**
|
||||
* Initialize the main application functionality
|
||||
*/
|
||||
|
|
@ -143,6 +151,20 @@ function setupEventListeners() {
|
|||
document.getElementById("add")?.addEventListener("click", addRepo)
|
||||
document.getElementById("update")?.addEventListener("click", updateRepo)
|
||||
|
||||
// Pagination controls
|
||||
document.getElementById("pageInput")?.addEventListener("change", (e) => {
|
||||
const page = parseInt(e.target.value) || 1
|
||||
navigateToPage(page)
|
||||
})
|
||||
document.getElementById("pageSizeSelect")?.addEventListener("change", (e) => {
|
||||
pageSize = parseInt(e.target.value) || 0
|
||||
navigateToPage(1)
|
||||
})
|
||||
document.getElementById("filterRunning")?.addEventListener("change", (e) => {
|
||||
filterRunning = e.target.value === "running"
|
||||
navigateToPage(1)
|
||||
})
|
||||
|
||||
// Tab switching
|
||||
document.getElementById("singleTab")?.addEventListener("click", () => switchTab("single"))
|
||||
document.getElementById("bulkTab")?.addEventListener("click", () => switchTab("bulk"))
|
||||
|
|
@ -191,6 +213,11 @@ function setupEventListeners() {
|
|||
.getElementById("saveAutoTerminationSettings")
|
||||
?.addEventListener("click", saveAutoTerminationSettings)
|
||||
|
||||
// SSH Connect Modal
|
||||
document.getElementById("closeSshConnectModal")?.addEventListener("click", closeSshConnectModal)
|
||||
document.getElementById("closeSshConnectModal2")?.addEventListener("click", closeSshConnectModal)
|
||||
document.getElementById("copySshCommand")?.addEventListener("click", copySshCommand)
|
||||
|
||||
// Modal overlay click handlers
|
||||
document.getElementById("analysisEditorModal")?.addEventListener("click", (e) => {
|
||||
if (e.target.id === "analysisEditorModal") {
|
||||
|
|
@ -215,6 +242,12 @@ function setupEventListeners() {
|
|||
closeInstructionsModal()
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById("sshConnectModal")?.addEventListener("click", (e) => {
|
||||
if (e.target.id === "sshConnectModal") {
|
||||
closeSshConnectModal()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -325,33 +358,11 @@ function handleSearch(event) {
|
|||
const searchTerm = event.target.value.toLowerCase().trim()
|
||||
console.log("[Search] Searching for:", searchTerm)
|
||||
|
||||
if (searchTerm === "") {
|
||||
// Show all repositories
|
||||
filteredRepos = [...allRepos]
|
||||
} else {
|
||||
// Filter repositories based on search term
|
||||
filteredRepos = allRepos.filter((repo) => {
|
||||
const repoUrl = repo.repo_url.toLowerCase()
|
||||
const moduleRoot = (repo.module_root || "").toLowerCase()
|
||||
const testsRoot = (repo.tests_root || "").toLowerCase()
|
||||
const ec2Id = (repo.last_ec2_id || "").toLowerCase()
|
||||
const ipAddress = (repo.last_ec2_ip || "").toLowerCase()
|
||||
// Reset to page 1 when searching
|
||||
navigateToPage(1)
|
||||
|
||||
return (
|
||||
repoUrl.includes(searchTerm) ||
|
||||
moduleRoot.includes(searchTerm) ||
|
||||
testsRoot.includes(searchTerm) ||
|
||||
ec2Id.includes(searchTerm) ||
|
||||
ipAddress.includes(searchTerm)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Sort and render filtered results
|
||||
const sortedRepos = sortRepositories(filteredRepos)
|
||||
renderRepos(sortedRepos)
|
||||
|
||||
console.log(`[Search] Found ${filteredRepos.length} repositories matching "${searchTerm}"`)
|
||||
// Search is performed client-side on already loaded data
|
||||
console.log(`[Search] Searching within ${allRepos.length} loaded repositories`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -373,16 +384,43 @@ function sortRepositories(repos) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Refresh repositories list
|
||||
* Refresh repositories list with pagination and filtering
|
||||
*/
|
||||
async function refreshRepos() {
|
||||
// Prevent multiple simultaneous requests
|
||||
if (isRefreshing) {
|
||||
console.log(`[Repos] Already refreshing, skipping request for page ${targetPage}`)
|
||||
return
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
|
||||
try {
|
||||
console.log("[Repos] Refreshing repositories...")
|
||||
const response = await api("/api/repos/enriched")
|
||||
console.log(`[Repos] Starting refresh for page ${targetPage}`)
|
||||
|
||||
// Build URL with query parameters
|
||||
const params = new URLSearchParams()
|
||||
if (pageSize > 0) {
|
||||
params.append("page", targetPage.toString())
|
||||
params.append("page_size", pageSize.toString())
|
||||
}
|
||||
if (filterRunning) {
|
||||
params.append("filter_running", "true")
|
||||
}
|
||||
|
||||
const url = `/api/repos/enriched${params.toString() ? "?" + params.toString() : ""}`
|
||||
console.log(`[Repos] Fetching URL: ${url}`)
|
||||
const response = await api(url)
|
||||
|
||||
if (response.ok) {
|
||||
// Store all repositories globally
|
||||
allRepos = response.repos
|
||||
totalCount = response.total_count || allRepos.length
|
||||
currentPage = targetPage // Update to show we're now on this page
|
||||
|
||||
console.log(
|
||||
`[Repos] Response received. Loaded page ${currentPage}, totalCount: ${totalCount}`,
|
||||
)
|
||||
|
||||
// Apply current search filter if any
|
||||
const searchInput = document.getElementById("repoSearch")
|
||||
|
|
@ -411,15 +449,83 @@ async function refreshRepos() {
|
|||
// Sort and render repositories
|
||||
const sortedRepos = sortRepositories(filteredRepos)
|
||||
renderRepos(sortedRepos)
|
||||
updatePaginationControls()
|
||||
|
||||
console.log(
|
||||
`[Repos] Loaded ${response.repos.length} repositories (${filteredRepos.length} after filtering)`,
|
||||
`[Repos] Loaded ${totalCount} total repositories, showing page ${currentPage} (${sortedRepos.length} on this page)`,
|
||||
)
|
||||
} else {
|
||||
console.error("[Repos] Failed to load repositories:", response.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Repos] Error refreshing repositories:", error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
console.log(`[Repos] Finished refresh. currentPage: ${currentPage}, targetPage: ${targetPage}`)
|
||||
|
||||
// Check if the target page changed while we were loading
|
||||
if (targetPage !== currentPage) {
|
||||
console.log(`[Repos] Target page changed to ${targetPage}, making another request`)
|
||||
refreshRepos()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific page
|
||||
*/
|
||||
function navigateToPage(page) {
|
||||
console.log(`[Pagination] navigateToPage called with: ${page}`)
|
||||
|
||||
const maxPage = Math.ceil(totalCount / (pageSize || totalCount)) || 1
|
||||
if (page < 1) page = 1
|
||||
if (page > maxPage) page = maxPage
|
||||
|
||||
// Update target page
|
||||
targetPage = page
|
||||
currentPage = page
|
||||
|
||||
// Update UI immediately
|
||||
const pageInput = document.getElementById("pageInput")
|
||||
if (pageInput) {
|
||||
pageInput.value = currentPage
|
||||
}
|
||||
|
||||
// If already refreshing, just update the target and let the current refresh complete
|
||||
// The refreshRepos function will check if we need to load a different page
|
||||
if (!isRefreshing) {
|
||||
console.log(`[Pagination] Starting refresh for page ${page}`)
|
||||
refreshRepos()
|
||||
} else {
|
||||
console.log(`[Pagination] Already refreshing, target page is now ${page}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update pagination controls UI
|
||||
*/
|
||||
function updatePaginationControls() {
|
||||
const maxPage = Math.ceil(totalCount / (pageSize || totalCount)) || 1
|
||||
|
||||
// Update page input
|
||||
const pageInput = document.getElementById("pageInput")
|
||||
if (pageInput) {
|
||||
pageInput.value = currentPage
|
||||
pageInput.max = maxPage
|
||||
}
|
||||
|
||||
// Update total pages display
|
||||
const totalPagesSpan = document.getElementById("totalPages")
|
||||
if (totalPagesSpan) {
|
||||
totalPagesSpan.textContent = `of ${maxPage}`
|
||||
}
|
||||
|
||||
// Update page info
|
||||
const startIdx = pageSize > 0 ? (currentPage - 1) * pageSize + 1 : 1
|
||||
const endIdx = pageSize > 0 ? Math.min(currentPage * pageSize, totalCount) : totalCount
|
||||
const pageInfo = document.getElementById("pageInfo")
|
||||
if (pageInfo) {
|
||||
pageInfo.textContent = `Showing ${startIdx}-${endIdx} of ${totalCount}`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -427,58 +533,68 @@ async function refreshRepos() {
|
|||
* Render repositories in the table
|
||||
*/
|
||||
function renderRepos(repos) {
|
||||
console.log("[UI Debug] Rendering repositories:", repos)
|
||||
const tbody = document.querySelector("#repos tbody")
|
||||
if (!tbody) {
|
||||
console.error("[UI Debug] Table body not found!")
|
||||
return
|
||||
}
|
||||
|
||||
tbody.innerHTML = ""
|
||||
|
||||
repos.forEach((repo, index) => {
|
||||
console.log(`[UI Debug] Processing repo ${index + 1}:`, repo)
|
||||
|
||||
const row = document.createElement("tr")
|
||||
|
||||
// Repository URL
|
||||
const repoCell = document.createElement("td")
|
||||
repoCell.className = "col-repo"
|
||||
const autoTermEnabled = repo.auto_terminate === "true"
|
||||
const autoTermBadge = autoTermEnabled ? "✅ Auto-Term ON" : "❌ Auto-Term OFF"
|
||||
const autoTermClass = autoTermEnabled ? "enabled" : "disabled"
|
||||
repoCell.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-weight: 500;">${repo.repo_url}</span>
|
||||
<button class="btn btn-sm btn-secondary" onclick="viewAnalysis('${repo.repo_url}')" title="View Analysis">
|
||||
📊
|
||||
<div class="repo-cell-content">
|
||||
<div class="repo-url-container">
|
||||
<a href="${repo.repo_url}" target="_blank" rel="noopener noreferrer" class="repo-url-link" title="${repo.repo_url}">
|
||||
${repo.repo_url}
|
||||
</a>
|
||||
<span class="auto-terminate-badge ${autoTermClass}" title="${autoTermBadge}">${autoTermBadge}</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-secondary btn-view-analysis" onclick="viewAnalysis('${repo.repo_url}')" title="View Analysis">
|
||||
<span>📊</span> View Analysis
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
// Module Root
|
||||
const moduleCell = document.createElement("td")
|
||||
moduleCell.className = "col-module"
|
||||
moduleCell.textContent = repo.module_root || "auto"
|
||||
|
||||
// Tests Root
|
||||
const testsCell = document.createElement("td")
|
||||
testsCell.className = "col-tests"
|
||||
testsCell.textContent = repo.tests_root || "auto"
|
||||
|
||||
// Auto-Terminate Status
|
||||
const autoTermCell = document.createElement("td")
|
||||
const autoTermEnabled = repo.auto_terminate === "true"
|
||||
autoTermCell.innerHTML = `
|
||||
<span class="auto-terminate-status ${autoTermEnabled ? "enabled" : "disabled"}">
|
||||
${autoTermEnabled ? "✅ Enabled" : "❌ Disabled"}
|
||||
</span>
|
||||
`
|
||||
|
||||
// Last EC2 ID
|
||||
const ec2Cell = document.createElement("td")
|
||||
ec2Cell.className = "col-ec2"
|
||||
ec2Cell.textContent = repo.last_ec2_id || "None"
|
||||
|
||||
// IP Address
|
||||
const ipCell = document.createElement("td")
|
||||
ipCell.className = "col-ip"
|
||||
ipCell.textContent = repo.last_ec2_ip || "None"
|
||||
|
||||
// Actions
|
||||
const actionsCell = document.createElement("td")
|
||||
actionsCell.className = "col-actions"
|
||||
|
||||
// Only show SSH button if there's a running instance
|
||||
const sshButton =
|
||||
repo.last_ec2_id && repo.last_ec2_id !== "None"
|
||||
? `<button class="btn btn-info btn-sm" onclick="openSshConnect('${repo.repo_url}', '${repo.last_ec2_ip}')" title="SSH Connect">
|
||||
🖥️ SSH
|
||||
</button>`
|
||||
: ""
|
||||
|
||||
actionsCell.innerHTML = `
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-primary btn-sm" onclick="runRepo('${repo.repo_url}')" title="Run Optimization">
|
||||
|
|
@ -496,10 +612,11 @@ function renderRepos(repos) {
|
|||
<button class="btn btn-secondary btn-sm" onclick="configureAutoTermination('${repo.repo_url}')" title="Configure Auto-Termination">
|
||||
⚙️ Auto-Term
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteRepo('${repo.repo_url}')" title="Delete Repository" style="margin-left: 8px;">
|
||||
${sshButton}
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteRepo('${repo.repo_url}')" title="Delete Repository">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
<button class="btn btn-info btn-sm" onclick="openCustomRunModal('${repo.repo_url}')" title="Custom Run" style="margin-left: 8px;">
|
||||
<button class="btn btn-info btn-sm" onclick="openCustomRunModal('${repo.repo_url}')" title="Custom Run">
|
||||
⚙️ Custom Run
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -508,7 +625,6 @@ function renderRepos(repos) {
|
|||
row.appendChild(repoCell)
|
||||
row.appendChild(moduleCell)
|
||||
row.appendChild(testsCell)
|
||||
row.appendChild(autoTermCell)
|
||||
row.appendChild(ec2Cell)
|
||||
row.appendChild(ipCell)
|
||||
row.appendChild(actionsCell)
|
||||
|
|
@ -1528,6 +1644,99 @@ function closeAutoTerminationModal() {
|
|||
window.currentAutoTermRepo = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Open SSH Connect modal
|
||||
*/
|
||||
async function openSshConnect(repoUrl, ipAddress) {
|
||||
const modal = document.getElementById("sshConnectModal")
|
||||
const sshCommandEl = document.getElementById("sshCommand")
|
||||
|
||||
if (!modal || !sshCommandEl) {
|
||||
console.error("[SSH] Modal elements not found!")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch SSH configuration from server
|
||||
const response = await api("/api/ssh-config")
|
||||
|
||||
if (response.ok) {
|
||||
// Hardcoded SSH key path for WSL
|
||||
const sshKeyPath = "/mnt/c/Users/Ashraf/Downloads/Deployed_Optimization_Factory_key_pair.pem"
|
||||
const user = response.user || "ubuntu"
|
||||
|
||||
// Extract org/repo from repository URL
|
||||
const match =
|
||||
repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/) ||
|
||||
repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?$/)
|
||||
const repoName = match ? `${match[1]}/${match[2]}` : "Optimization"
|
||||
|
||||
// Create SSH command with terminal title rename for Ubuntu/Linux/WSL
|
||||
// Set title locally before SSH connection to ensure it works in WSL
|
||||
// Add -o StrictHostKeyChecking=no to skip host verification
|
||||
const sshCommand = `echo -en '\\033]0;${repoName}\\007' && sudo ssh -i "${sshKeyPath}" -o StrictHostKeyChecking=no ${user}@${ipAddress}`
|
||||
|
||||
sshCommandEl.textContent = sshCommand
|
||||
modal.style.display = "flex"
|
||||
|
||||
console.log(`[SSH] Opening SSH connect modal for ${repoUrl} at ${ipAddress}`)
|
||||
} else {
|
||||
console.error("[SSH] Failed to get SSH config:", response.error)
|
||||
alert("Failed to get SSH configuration")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[SSH] Error getting SSH config:", error)
|
||||
// Fallback: show generic SSH command with title
|
||||
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/)
|
||||
const repoName = match ? `${match[1]}/${match[2]}` : "Optimization"
|
||||
const sshKeyPath = "/mnt/c/Users/Ashraf/Downloads/Deployed_Optimization_Factory_key_pair.pem"
|
||||
const sshCommand = `echo -en '\\033]0;${repoName}\\007' && sudo ssh -i "${sshKeyPath}" -o StrictHostKeyChecking=no ubuntu@${ipAddress}`
|
||||
sshCommandEl.textContent = sshCommand
|
||||
modal.style.display = "flex"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close SSH Connect modal
|
||||
*/
|
||||
function closeSshConnectModal() {
|
||||
document.getElementById("sshConnectModal").style.display = "none"
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy SSH command to clipboard
|
||||
*/
|
||||
async function copySshCommand() {
|
||||
const sshCommandEl = document.getElementById("sshCommand")
|
||||
const copyBtn = document.getElementById("copySshCommand")
|
||||
|
||||
if (!sshCommandEl || !copyBtn) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(sshCommandEl.textContent)
|
||||
|
||||
const originalText = copyBtn.innerHTML
|
||||
copyBtn.innerHTML = "✅ Copied!"
|
||||
copyBtn.disabled = true
|
||||
|
||||
setTimeout(() => {
|
||||
copyBtn.innerHTML = originalText
|
||||
copyBtn.disabled = false
|
||||
}, 2000)
|
||||
|
||||
console.log("[SSH] Command copied to clipboard")
|
||||
} catch (error) {
|
||||
console.error("[SSH] Failed to copy:", error)
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement("textarea")
|
||||
textArea.value = sshCommandEl.textContent
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand("copy")
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the instructions modal with comprehensive usage guide
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -119,6 +119,13 @@
|
|||
class="search-input">
|
||||
<span class="search-icon">🔍</span>
|
||||
</div>
|
||||
<div class="filter-container">
|
||||
<label for="filterRunning" class="filter-label">Filter:</label>
|
||||
<select id="filterRunning" class="filter-select">
|
||||
<option value="all">All Repositories</option>
|
||||
<option value="running">Running Optimizations</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="table-actions">
|
||||
<button id="refresh" class="btn btn-secondary">
|
||||
<span>🔄</span> Refresh
|
||||
|
|
@ -130,23 +137,46 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-wrapper">
|
||||
<table id="repos">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Repository</th>
|
||||
<th>Module Root</th>
|
||||
<th>Tests Root</th>
|
||||
<th>Auto-Terminate</th>
|
||||
<th>Last EC2 ID</th>
|
||||
<th>IP Address</th>
|
||||
<th>Actions</th>
|
||||
<th class="col-repo">Repository</th>
|
||||
<th class="col-module">Module Root</th>
|
||||
<th class="col-tests">Tests Root</th>
|
||||
<th class="col-ec2">Last EC2 ID</th>
|
||||
<th class="col-ip">IP Address</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-footer">
|
||||
<div class="pagination-info">
|
||||
<span id="pageInfo">Showing 0-0 of 0</span>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<div class="page-input-container">
|
||||
<label for="pageInput">Page:</label>
|
||||
<input type="number" id="pageInput" min="1" value="1" class="page-input">
|
||||
<span id="totalPages">of 1</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination-size">
|
||||
<label for="pageSizeSelect">Items per page:</label>
|
||||
<select id="pageSizeSelect" class="page-size-select">
|
||||
<option value="10">10</option>
|
||||
<option value="25" selected>25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="0">All</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions Modal -->
|
||||
<div id="instructionsModal" class="modal-overlay" style="display:none;">
|
||||
|
|
@ -243,6 +273,43 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH Connect Modal -->
|
||||
<div id="sshConnectModal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>🖥️ SSH Connect to Instance</h3>
|
||||
<button id="closeSshConnectModal" class="btn btn-secondary btn-sm">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="sshConnectContent">
|
||||
<p style="margin: 0 0 16px 0; color: #64748b; font-size: 14px;">
|
||||
Copy the SSH command below and run it in your terminal to connect to the instance:
|
||||
</p>
|
||||
<div style="background: #1e293b; padding: 16px; border-radius: 8px; position: relative;">
|
||||
<code id="sshCommand"
|
||||
style="color: #e2e8f0; font-family: 'Monaco', 'Menlo', monospace; font-size: 13px; word-break: break-all;"></code>
|
||||
<button id="copySshCommand" class="btn btn-primary btn-sm"
|
||||
style="position: absolute; top: 8px; right: 8px;">
|
||||
📋 Copy
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style="margin-top: 16px; padding: 12px; background: #f8fafc; border-radius: 8px; border-left: 4px solid #3b82f6;">
|
||||
<p style="margin: 0; font-size: 13px; color: #475569;">
|
||||
<strong>Tip:</strong> The command will automatically rename your terminal tab to the
|
||||
repository name.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="closeSshConnectModal2" class="btn btn-secondary">
|
||||
<span>✕</span> Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ body {
|
|||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
max-width: 95%;
|
||||
width: 100%;
|
||||
margin: 20px auto;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
|
|
@ -22,6 +23,24 @@ body {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
color: white;
|
||||
|
|
@ -45,6 +64,12 @@ body {
|
|||
padding: 32px;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.content {
|
||||
padding: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
|
@ -274,22 +299,92 @@ body {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Filter container styles */
|
||||
.filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Table Container - Improved scrolling */
|
||||
.table-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
max-height: 70vh;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.table-wrapper {
|
||||
max-height: 75vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Horizontal scrollbar styling */
|
||||
.table-wrapper::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Vertical scrollbar styling */
|
||||
.table-wrapper::-webkit-scrollbar:vertical {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
|
|
@ -299,7 +394,23 @@ th {
|
|||
padding: 16px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
border-bottom: 2px solid #cbd5e1;
|
||||
white-space: nowrap;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Header alignment classes */
|
||||
th.col-terminate,
|
||||
th.col-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th.col-ec2,
|
||||
th.col-ip {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
|
|
@ -309,6 +420,186 @@ td {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Column-specific alignment */
|
||||
.col-repo,
|
||||
.col-module,
|
||||
.col-tests {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.col-terminate {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.col-terminate .auto-terminate-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 100px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.col-ec2,
|
||||
.col-ip {
|
||||
text-align: left;
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.col-ec2 {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.col-ip {
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
text-align: center;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.col-actions .action-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Repository URL cell */
|
||||
.col-repo {
|
||||
min-width: 350px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.repo-cell-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.repo-url-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.repo-url-link {
|
||||
font-weight: 500;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Make sure the link wraps properly on smaller screens */
|
||||
.repo-url-container {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.repo-url-link:hover {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.repo-url {
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.auto-terminate-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.auto-terminate-badge.enabled {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.auto-terminate-badge.disabled {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.btn-view-analysis {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-view-analysis:hover {
|
||||
background: #4b5563;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
|
||||
.btn-view-analysis:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-view-analysis span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.col-module,
|
||||
.col-tests {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Enhanced cell padding for different column types */
|
||||
.col-terminate td,
|
||||
td.col-terminate {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.col-actions td,
|
||||
td.col-actions {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.col-ec2 td,
|
||||
td.col-ec2,
|
||||
.col-ip td,
|
||||
td.col-ip {
|
||||
padding: 14px 16px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Allow repo URL to wrap properly */
|
||||
.repo-url,
|
||||
.repo-url-link {
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
|
@ -672,6 +963,7 @@ tbody tr:hover {
|
|||
.container {
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
@ -1562,6 +1854,91 @@ tbody tr:hover {
|
|||
}
|
||||
}
|
||||
|
||||
/* Table Footer - Pagination */
|
||||
.table-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-input-container label {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.page-input {
|
||||
width: 60px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-input:focus {
|
||||
outline: none;
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.page-input-container span {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pagination-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-size label {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-size-select:focus {
|
||||
outline: none;
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
|
|
@ -1598,4 +1975,37 @@ tbody tr:hover {
|
|||
.table-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Table footer responsive */
|
||||
.table-footer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination-size {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Better spacing for very large screens */
|
||||
@media (min-width: 1920px) {
|
||||
.container {
|
||||
max-width: 1800px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue