558 lines
24 KiB
HTML
558 lines
24 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ data_source.name }} - Data Source Details{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="content">
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="page-header">
|
|
<div class="page-title">
|
|
<h4>{{ data_source.name }}</h4>
|
|
<h6>Data source details and configuration</h6>
|
|
</div>
|
|
<div class="page-btn">
|
|
<a href="{% url 'analytics:data_source_list' %}" class="btn btn-secondary me-2">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to Sources
|
|
</a>
|
|
<a href="{% url 'analytics:data_source_update' data_source.pk %}" class="btn btn-primary">
|
|
<i class="fas fa-edit me-1"></i>Edit Source
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Main Content -->
|
|
<div class="col-lg-8">
|
|
<!-- Connection Status -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-plug me-2"></i>Connection Status
|
|
</h5>
|
|
<div class="card-tools">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="testConnection()">
|
|
<i class="fas fa-plug me-1"></i>Test Connection
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="status-card text-center">
|
|
{% if data_source.status == 'active' %}
|
|
<i class="fas fa-check-circle fa-3x text-success mb-2"></i>
|
|
<h6 class="text-success">Connected</h6>
|
|
{% elif data_source.status == 'error' %}
|
|
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-2"></i>
|
|
<h6 class="text-danger">Connection Error</h6>
|
|
{% elif data_source.status == 'syncing' %}
|
|
<i class="fas fa-sync-alt fa-3x text-info fa-spin mb-2"></i>
|
|
<h6 class="text-info">Syncing</h6>
|
|
{% else %}
|
|
<i class="fas fa-times-circle fa-3x text-secondary mb-2"></i>
|
|
<h6 class="text-secondary">Disconnected</h6>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<div class="connection-details">
|
|
<div class="row">
|
|
<div class="col-sm-6">
|
|
<div class="detail-item">
|
|
<label class="form-label">Last Connection Test:</label>
|
|
<span class="text-muted">{{ data_source.last_test|default:"Never tested" }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<div class="detail-item">
|
|
<label class="form-label">Response Time:</label>
|
|
<span class="text-muted">{{ data_source.response_time|default:"N/A" }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<div class="detail-item">
|
|
<label class="form-label">Last Sync:</label>
|
|
<span class="text-muted">
|
|
{% if data_source.last_sync %}
|
|
{{ data_source.last_sync|timesince }} ago
|
|
{% else %}
|
|
Never synced
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<div class="detail-item">
|
|
<label class="form-label">Records Count:</label>
|
|
<span class="text-muted">{{ data_source.record_count|default:"0"|floatformat:0 }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if data_source.last_error %}
|
|
<div class="alert alert-danger mt-3">
|
|
<h6 class="alert-heading">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>Last Error
|
|
</h6>
|
|
<p class="mb-0">{{ data_source.last_error }}</p>
|
|
<small class="text-muted">{{ data_source.error_timestamp|timesince }} ago</small>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data Preview -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-table me-2"></i>Data Preview
|
|
</h5>
|
|
<div class="card-tools">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshPreview()">
|
|
<i class="fas fa-sync-alt me-1"></i>Refresh
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="exportData()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if data_source.sample_data %}
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-sm">
|
|
<thead>
|
|
<tr>
|
|
{% for column in data_source.columns %}
|
|
<th>{{ column.name }}</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in data_source.sample_data %}
|
|
<tr>
|
|
{% for value in row %}
|
|
<td>{{ value|truncatechars:50 }}</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-table fa-3x text-muted mb-3"></i>
|
|
<h6 class="text-muted">No Data Available</h6>
|
|
<p class="text-muted">Connect and sync the data source to see preview.</p>
|
|
<button type="button" class="btn btn-primary" onclick="syncData()">
|
|
<i class="fas fa-sync-alt me-1"></i>Sync Data
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sync History -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-history me-2"></i>Sync History
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if data_source.sync_history %}
|
|
<div class="timeline">
|
|
{% for sync in data_source.sync_history %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-marker">
|
|
{% if sync.status == 'success' %}
|
|
<i class="fas fa-check-circle text-success"></i>
|
|
{% elif sync.status == 'error' %}
|
|
<i class="fas fa-exclamation-triangle text-danger"></i>
|
|
{% else %}
|
|
<i class="fas fa-clock text-warning"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div class="timeline-content">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="mb-1">{{ sync.get_status_display }}</h6>
|
|
<p class="text-muted mb-1">{{ sync.message }}</p>
|
|
<small class="text-muted">{{ sync.timestamp|timesince }} ago</small>
|
|
</div>
|
|
<div class="text-end">
|
|
<span class="badge bg-secondary">{{ sync.records_processed }} records</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-history fa-3x text-muted mb-3"></i>
|
|
<h6 class="text-muted">No Sync History</h6>
|
|
<p class="text-muted">Sync history will appear here after the first synchronization.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Basic Information -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="source-info">
|
|
<div class="info-item">
|
|
<label class="form-label">Name:</label>
|
|
<span class="info-value">{{ data_source.name }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label class="form-label">Type:</label>
|
|
<span class="badge bg-primary">{{ data_source.get_source_type_display }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label class="form-label">Description:</label>
|
|
<span class="info-value">{{ data_source.description|default:"No description provided" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label class="form-label">Created By:</label>
|
|
<span class="info-value">{{ data_source.created_by|default:"System" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label class="form-label">Created Date:</label>
|
|
<span class="info-value">{{ data_source.created_at|date:"M d, Y H:i" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label class="form-label">Last Updated:</label>
|
|
<span class="info-value">{{ data_source.updated_at|date:"M d, Y H:i" }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Configuration -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-cog me-2"></i>Configuration
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="config-info">
|
|
{% if data_source.source_type == 'database' %}
|
|
<div class="info-item">
|
|
<label class="form-label">Database Type:</label>
|
|
<span class="info-value">{{ data_source.db_type|default:"Not specified" }}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<label class="form-label">Host:</label>
|
|
<span class="info-value">{{ data_source.host|default:"Not specified" }}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<label class="form-label">Database:</label>
|
|
<span class="info-value">{{ data_source.database|default:"Not specified" }}</span>
|
|
</div>
|
|
{% elif data_source.source_type == 'api' %}
|
|
<div class="info-item">
|
|
<label class="form-label">API URL:</label>
|
|
<span class="info-value">{{ data_source.api_url|default:"Not specified" }}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<label class="form-label">Authentication:</label>
|
|
<span class="info-value">{{ data_source.auth_type|default:"None" }}</span>
|
|
</div>
|
|
{% elif data_source.source_type == 'file' %}
|
|
<div class="info-item">
|
|
<label class="form-label">File Path:</label>
|
|
<span class="info-value">{{ data_source.file_path|default:"Not specified" }}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<label class="form-label">File Format:</label>
|
|
<span class="info-value">{{ data_source.file_format|default:"Not specified" }}</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="info-item">
|
|
<label class="form-label">Sync Frequency:</label>
|
|
<span class="info-value">{{ data_source.get_sync_frequency_display|default:"Manual" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<label class="form-label">Auto Sync:</label>
|
|
<span class="badge {% if data_source.auto_sync %}bg-success{% else %}bg-secondary{% endif %}">
|
|
{% if data_source.auto_sync %}Enabled{% else %}Disabled{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-chart-bar me-2"></i>Statistics
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ data_source.total_syncs|default:"0" }}</div>
|
|
<div class="stat-label">Total Syncs</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ data_source.successful_syncs|default:"0" }}</div>
|
|
<div class="stat-label">Successful</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ data_source.failed_syncs|default:"0" }}</div>
|
|
<div class="stat-label">Failed</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ data_source.avg_sync_time|default:"0" }}s</div>
|
|
<div class="stat-label">Avg Time</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-tools me-2"></i>Actions
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<button type="button" class="btn btn-primary" onclick="syncData()">
|
|
<i class="fas fa-sync-alt me-1"></i>Sync Now
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="testConnection()">
|
|
<i class="fas fa-plug me-1"></i>Test Connection
|
|
</button>
|
|
<a href="{% url 'analytics:data_source_update' data_source.pk %}" class="btn btn-outline-primary">
|
|
<i class="fas fa-edit me-1"></i>Edit Configuration
|
|
</a>
|
|
<button type="button" class="btn btn-outline-info" onclick="exportConfig()">
|
|
<i class="fas fa-download me-1"></i>Export Config
|
|
</button>
|
|
<hr>
|
|
<a href="{% url 'analytics:data_source_delete' data_source.pk %}" class="btn btn-outline-danger">
|
|
<i class="fas fa-trash me-1"></i>Delete Source
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function testConnection() {
|
|
const btn = event.target.closest('button');
|
|
const originalText = btn.innerHTML;
|
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Testing...';
|
|
btn.disabled = true;
|
|
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
alert('Connection test completed successfully!');
|
|
}, 2000);
|
|
}
|
|
|
|
function syncData() {
|
|
if (confirm('Start data synchronization? This may take some time.')) {
|
|
const btn = event.target.closest('button');
|
|
const originalText = btn.innerHTML;
|
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Syncing...';
|
|
btn.disabled = true;
|
|
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
alert('Data synchronization completed successfully!');
|
|
window.location.reload();
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
function refreshPreview() {
|
|
const btn = event.target.closest('button');
|
|
const originalText = btn.innerHTML;
|
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Loading...';
|
|
btn.disabled = true;
|
|
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
window.location.reload();
|
|
}, 1500);
|
|
}
|
|
|
|
function exportData() {
|
|
alert('Exporting data preview...');
|
|
}
|
|
|
|
function exportConfig() {
|
|
alert('Exporting data source configuration...');
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.status-card {
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
height: 100%;
|
|
}
|
|
|
|
.detail-item {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.detail-item label {
|
|
font-weight: 600;
|
|
margin-bottom: 5px;
|
|
display: block;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.info-item {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.info-item label {
|
|
font-weight: 600;
|
|
margin-bottom: 5px;
|
|
display: block;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.info-value {
|
|
color: #6c757d;
|
|
}
|
|
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 30px;
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 15px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #e9ecef;
|
|
}
|
|
|
|
.timeline-item {
|
|
position: relative;
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.timeline-marker {
|
|
position: absolute;
|
|
left: -22px;
|
|
top: 0;
|
|
width: 30px;
|
|
height: 30px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 2px solid #e9ecef;
|
|
}
|
|
|
|
.timeline-content {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
border-left: 3px solid #007bff;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 15px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
padding: 15px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #2c3e50;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.page-btn {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.card-tools {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.timeline {
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.timeline-marker {
|
|
left: -15px;
|
|
width: 25px;
|
|
height: 25px;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.status-card {
|
|
margin-bottom: 20px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|