SIDEL_ScriptsManager/app/templates/dashboard.html

749 lines
30 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ t.dashboard }} - {{ t.app_title }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-house"></i> {{ t.dashboard }}</h1>
<button class="btn btn-outline-primary" id="refresh-scripts">
<i class="bi bi-arrow-clockwise"></i> Refresh Scripts
</button>
</div>
</div>
</div>
<!-- System Status Row -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card dashboard-card-blue">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5 class="card-title">{{ t.script_groups }}</h5>
<h3 class="mb-0">{{ script_groups|length }}</h3>
</div>
<div class="align-self-center">
<i class="bi bi-folder2 display-6"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card dashboard-card-green">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5 class="card-title">{{ t.projects }}</h5>
<h3 class="mb-0">{{ user_projects|length }}</h3>
</div>
<div class="align-self-center">
<i class="bi bi-kanban display-6"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card dashboard-card-purple">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5 class="card-title">User Level</h5>
<h6 class="mb-0">{{ t.user_level[current_user.user_level] }}</h6>
</div>
<div class="align-self-center">
<i class="bi bi-person-badge display-6"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card dashboard-card-orange">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5 class="card-title">{{ t.status }}</h5>
<h6 class="mb-0" id="system-status">Loading...</h6>
</div>
<div class="align-self-center">
<i class="bi bi-activity display-6"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Active Project Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-folder-plus"></i> Active Project
</h5>
<div>
<button class="btn btn-sm btn-outline-primary" id="manage-projects-btn">
<i class="bi bi-gear"></i> Manage Projects
</button>
<button class="btn btn-sm btn-success" id="new-project-btn">
<i class="bi bi-plus"></i> New Project
</button>
</div>
</div>
<div class="card-body">
<div id="active-project-info">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-2" id="project-loading">
</div>
<span>Loading active project...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Script Groups -->
<div class="row">
<div class="col-12">
<h2><i class="bi bi-collection"></i> {{ t.script_groups }}</h2>
{% if script_groups %}
<div class="row">
{% for group in script_groups %}
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100 script-group-card" data-group-id="{{ group.id }}">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-folder2-open text-primary"></i>
{{ group.name }}
</h5>
<span class="badge bg-{{ 'success' if group.is_active else 'secondary' }}">
{{ 'Active' if group.is_active else 'Inactive' }}
</span>
</div>
<div class="card-body">
<p class="card-text">
{% if group.description %}
{% set desc_dict = group.description | fromjson %}
{% if desc_dict is mapping %}
{{ desc_dict.get(current_lang, desc_dict.get('en', 'No description')) }}
{% else %}
{{ group.description }}
{% endif %}
{% else %}
No description available
{% endif %}
</p>
<div class="mb-2">
<small class="text-muted">
<i class="bi bi-shield-check"></i>
Required Level: {{ t.user_level.get(group.required_level, group.required_level) }}
</small>
</div>
{% if group.conda_environment %}
<div class="mb-2">
<small class="text-muted">
<i class="bi bi-cpu"></i>
Environment: {{ group.conda_environment }}
</small>
</div>
{% endif %}
<div class="mb-2">
<small class="text-muted">
<i class="bi bi-file-code"></i>
Scripts: <span class="scripts-count" data-group-id="{{ group.id }}">Loading...</span>
</small>
</div>
</div>
<div class="card-footer">
<a href="{{ url_for('script_group_view', group_id=group.id) }}" class="btn btn-primary btn-sm">
<i class="bi bi-play-circle"></i> {{ t.execute }} {{ t.scripts }}
</a>
{% if current_user.user_level in ['admin', 'superuser'] %}
<button class="btn btn-outline-secondary btn-sm" onclick="editGroup({{ group.id }})">
<i class="bi bi-pencil"></i> Edit
</button>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-folder-x display-1 text-muted"></i>
<h3 class="text-muted mt-3">No Script Groups Found</h3>
<p class="text-muted">Add script directories to <code>app/backend/script_groups/</code> to get started.</p>
<button class="btn btn-primary" id="refresh-scripts">
<i class="bi bi-arrow-clockwise"></i> Refresh Scripts
</button>
</div>
{% endif %}
</div>
</div>
<!-- Active Processes (if any) -->
<div class="row mt-4" id="active-processes-section" style="display: none;">
<div class="col-12">
<h3><i class="bi bi-cpu"></i> Active Processes</h3>
<div class="card">
<div class="card-body">
<div id="active-processes-list">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load script counts for each group
loadScriptCounts();
// Load system status
loadSystemStatus();
// Load active processes
loadActiveProcesses();
// Refresh scripts button
document.getElementById('refresh-scripts').addEventListener('click', function() {
refreshScripts();
});
});
function loadScriptCounts() {
document.querySelectorAll('.scripts-count').forEach(element => {
const groupId = element.dataset.groupId;
fetch(`/api/script-groups/${groupId}/scripts`)
.then(response => response.json())
.then(scripts => {
element.textContent = scripts.length;
})
.catch(error => {
console.error('Error loading script count:', error);
element.textContent = 'Error';
});
});
}
function loadSystemStatus() {
fetch('/health')
.then(response => response.json())
.then(data => {
const statusElement = document.getElementById('system-status');
if (data.status === 'healthy') {
statusElement.textContent = 'Healthy';
statusElement.className = 'mb-0 text-success';
} else {
statusElement.textContent = 'Issues';
statusElement.className = 'mb-0 text-danger';
}
})
.catch(error => {
console.error('Error loading system status:', error);
document.getElementById('system-status').textContent = 'Unknown';
});
}
function loadActiveProcesses() {
// This would load active processes for the current user
// Implementation depends on the API endpoint
}
function refreshScripts() {
const button = document.getElementById('refresh-scripts');
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i> Refreshing...';
button.disabled = true;
// Trigger script discovery
fetch('/api/script-groups/refresh', { method: 'POST' })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
location.reload(); // Reload page to show updated scripts
} else {
alert('Error refreshing scripts: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error refreshing scripts:', error);
alert('Error refreshing scripts: ' + error.message);
})
.finally(() => {
button.innerHTML = originalHTML;
button.disabled = false;
});
}
function editGroup(groupId) {
// Load group details and show editing modal
fetch(`/api/script-groups/${groupId}`)
.then(response => response.json())
.then(group => {
showGroupEditModal(group);
})
.catch(error => {
console.error('Error loading group details:', error);
alert('Error loading group details: ' + error.message);
});
}
function showGroupEditModal(group) {
// Create modal HTML
const modalHtml = `
<div class="modal fade" id="groupEditModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Group: ${group.name}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="groupEditForm">
<div class="mb-3">
<label for="groupName" class="form-label">Group Name</label>
<input type="text" class="form-control" id="groupName"
value="${group.name}" required>
</div>
<div class="mb-3">
<label for="condaEnvironment" class="form-label">Conda Environment</label>
<select class="form-control" id="condaEnvironment" required>
<option value="base" ${group.conda_environment === 'base' ? 'selected' : ''}>base</option>
</select>
</div>
<div class="mb-3">
<label for="requiredLevel" class="form-label">Required Permission Level</label>
<select class="form-control" id="requiredLevel" required>
<option value="viewer" ${group.required_level === 'viewer' ? 'selected' : ''}>Viewer</option>
<option value="operator" ${group.required_level === 'operator' ? 'selected' : ''}>Operator</option>
<option value="admin" ${group.required_level === 'admin' ? 'selected' : ''}>Admin</option>
<option value="superuser" ${group.required_level === 'superuser' ? 'selected' : ''}>Superuser</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveGroupChanges(${group.id})">Save Changes</button>
</div>
</div>
</div>
</div>
`;
// Remove any existing modal
const existingModal = document.getElementById('groupEditModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body and show it
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Load conda environments
loadCondaEnvironments(group.conda_environment);
// Show modal using Bootstrap 5 API
const modal = new bootstrap.Modal(document.getElementById('groupEditModal'));
modal.show();
}
function loadCondaEnvironments(currentEnv) {
fetch('/api/conda/environments')
.then(response => response.json())
.then(environments => {
const select = document.getElementById('condaEnvironment');
select.innerHTML = '';
// Add base environment
const baseOption = new Option('base', 'base', false, currentEnv === 'base');
select.add(baseOption);
// Add other environments
environments.forEach(env => {
if (env.name !== 'base') {
const option = new Option(env.name, env.name, false, currentEnv === env.name);
select.add(option);
}
});
})
.catch(error => {
console.error('Error loading conda environments:', error);
// Keep the default base option if error occurs
});
}
function saveGroupChanges(groupId) {
const updateData = {
name: document.getElementById('groupName').value,
conda_environment: document.getElementById('condaEnvironment').value,
required_level: document.getElementById('requiredLevel').value
};
fetch(`/api/script-groups/${groupId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updateData)
})
.then(response => response.json())
.then(result => {
if (result.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('groupEditModal'));
modal.hide();
alert('Group updated successfully!');
// Refresh the page to show updated group info
location.reload();
} else {
alert('Error updating group: ' + (result.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error updating group:', error);
alert('Error updating group: ' + error.message);
});
}
// Project Management Functions
function loadActiveProject() {
fetch('/api/projects/active')
.then(response => response.json())
.then(data => {
const projectInfo = document.getElementById('active-project-info');
const loading = document.getElementById('project-loading');
loading.style.display = 'none';
if (data.active_project) {
const project = data.active_project;
projectInfo.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">${project.project_name}</h6>
<small class="text-muted">
${project.description || 'No description'}
</small>
</div>
<div>
<span class="badge bg-success">Active</span>
</div>
</div>
`;
} else {
projectInfo.innerHTML = `
<div class="text-muted">
<i class="bi bi-info-circle me-2"></i>
No active project. Create or select a project to get started.
</div>
`;
}
})
.catch(error => {
console.error('Error loading active project:', error);
document.getElementById('project-loading').style.display = 'none';
document.getElementById('active-project-info').innerHTML = `
<div class="text-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Error loading active project
</div>
`;
});
}
function showProjectManagementModal() {
const modalHtml = `
<div class="modal fade" id="projectManagementModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Project Management</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="projects-list">
<div class="text-center">
<div class="spinner-border" role="status"></div>
<p class="mt-2">Loading projects...</p>
</div>
</div>
</div>
</div>
</div>
</div>
`;
// Remove existing modal
const existingModal = document.getElementById('projectManagementModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('projectManagementModal'));
modal.show();
loadProjectsList();
}
function loadProjectsList() {
fetch('/api/projects')
.then(response => response.json())
.then(projects => {
const projectsList = document.getElementById('projects-list');
if (projects.length === 0) {
projectsList.innerHTML = `
<div class="text-center text-muted">
<i class="bi bi-folder-x display-4"></i>
<p class="mt-2">No projects found</p>
</div>
`;
return;
}
const projectsHtml = projects.map(project => `
<div class="card mb-2">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">${project.project_name}</h6>
<small class="text-muted">${project.description || 'No description'}</small>
<br>
<small class="text-muted">
Created: ${new Date(project.created_at).toLocaleDateString()}
${project.last_accessed ? ', Last used: ' + new Date(project.last_accessed).toLocaleDateString() : ''}
</small>
</div>
<div>
${project.is_default ? '<span class="badge bg-secondary me-2">Default</span>' : ''}
<button class="btn btn-sm btn-outline-primary me-1"
onclick="setActiveProject(${project.id})">
<i class="bi bi-check-circle"></i> Set Active
</button>
${!project.is_default ? `
<button class="btn btn-sm btn-outline-danger"
onclick="deleteProject(${project.id})">
<i class="bi bi-trash"></i>
</button>
` : ''}
</div>
</div>
</div>
</div>
`).join('');
projectsList.innerHTML = projectsHtml;
})
.catch(error => {
console.error('Error loading projects:', error);
document.getElementById('projects-list').innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Error loading projects
</div>
`;
});
}
function setActiveProject(projectId) {
fetch(`/api/projects/${projectId}/set-active`, {
method: 'POST'
})
.then(response => response.json())
.then(result => {
if (result.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('projectManagementModal'));
modal.hide();
loadActiveProject(); // Refresh active project display
alert('Project set as active successfully!');
} else {
alert('Error setting active project: ' + (result.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error setting active project:', error);
alert('Error setting active project: ' + error.message);
});
}
function deleteProject(projectId) {
if (!confirm('Are you sure you want to delete this project? This action cannot be undone.')) {
return;
}
fetch(`/api/projects/${projectId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
if (result.success) {
loadProjectsList(); // Refresh projects list
loadActiveProject(); // Refresh active project display
alert('Project deleted successfully!');
} else {
alert('Error deleting project: ' + (result.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error deleting project:', error);
alert('Error deleting project: ' + error.message);
});
}
function showNewProjectModal() {
const modalHtml = `
<div class="modal fade" id="newProjectModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create New Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="newProjectForm">
<div class="mb-3">
<label for="projectName" class="form-label">Project Name</label>
<input type="text" class="form-control" id="projectName" required>
</div>
<div class="mb-3">
<label for="projectGroup" class="form-label">Script Group</label>
<select class="form-control" id="projectGroup" required>
<option value="">Select a script group...</option>
</select>
</div>
<div class="mb-3">
<label for="projectDescription" class="form-label">Description (Optional)</label>
<textarea class="form-control" id="projectDescription" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createProject()">Create Project</button>
</div>
</div>
</div>
</div>
`;
const existingModal = document.getElementById('newProjectModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Load script groups for selection
loadScriptGroupsForProject();
const modal = new bootstrap.Modal(document.getElementById('newProjectModal'));
modal.show();
}
function loadScriptGroupsForProject() {
fetch('/api/script-groups')
.then(response => response.json())
.then(groups => {
const select = document.getElementById('projectGroup');
select.innerHTML = '<option value="">Select a script group...</option>';
groups.forEach(group => {
const option = new Option(group.name, group.id);
select.add(option);
});
})
.catch(error => {
console.error('Error loading script groups:', error);
});
}
function createProject() {
const form = document.getElementById('newProjectForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const projectData = {
project_name: document.getElementById('projectName').value,
group_id: parseInt(document.getElementById('projectGroup').value),
description: document.getElementById('projectDescription').value
};
fetch('/api/projects', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(projectData)
})
.then(response => response.json())
.then(result => {
if (result.id) {
const modal = bootstrap.Modal.getInstance(document.getElementById('newProjectModal'));
modal.hide();
loadActiveProject(); // Refresh active project display
alert('Project created successfully!');
} else {
alert('Error creating project: ' + (result.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error creating project:', error);
alert('Error creating project: ' + error.message);
});
}
// Initialize project management when page loads
document.addEventListener('DOMContentLoaded', function() {
loadActiveProject();
// Event listeners for project management
document.getElementById('manage-projects-btn').addEventListener('click', showProjectManagementModal);
document.getElementById('new-project-btn').addEventListener('click', showNewProjectModal);
});
</script>
<style>
.script-group-card {
transition: transform 0.2s;
}
.script-group-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
{% endblock %}