163 lines
5.3 KiB
HTML
163 lines
5.3 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block content %}
|
|
<style>
|
|
.node-group { cursor: pointer; }
|
|
.node-circle { stroke: #fff; stroke-width: 2px; }
|
|
.node-text { font-size: 10px; pointer-events: none; }
|
|
#loader { display: none; }
|
|
</style>
|
|
|
|
<div class="container mt-5">
|
|
<h1 class="mb-4">System Dashboard</h1>
|
|
|
|
<div class="row">
|
|
<div class="col-md-12 mb-4">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Network Discovery</h5>
|
|
<form id="searchForm" class="input-group">
|
|
<input type="text" id="searchInput" class="form-control" placeholder="Search for a name to map their network...">
|
|
<button class="btn btn-primary" id="searchBtn" type="submit">
|
|
<span id="btnText">Discover</span>
|
|
<div id="loader" class="spinner-border spinner-border-sm" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Network Visualization</h5>
|
|
<div id="graphContainer" style="width: 100%; height: 600px; background: #f8f9fa; border: 1px solid #ddd;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<script>
|
|
document.getElementById('searchForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const query = document.getElementById('searchInput').value;
|
|
const graphContainer = d3.select("#graphContainer");
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
const btnText = document.getElementById('btnText');
|
|
const loader = document.getElementById('loader');
|
|
|
|
// UI Loading state
|
|
searchBtn.disabled = true;
|
|
btnText.textContent = "Searching...";
|
|
loader.style.display = "inline-block";
|
|
graphContainer.html('<p class="p-3 text-muted">Discovering network, please wait...</p>');
|
|
|
|
fetch(`{% url 'core:search_api' %}?q=${encodeURIComponent(query)}`, { method: 'GET' })
|
|
.then(response => {
|
|
if (!response.ok) throw new Error("Search failed");
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
graphContainer.html(''); // clear
|
|
renderGraph(data);
|
|
})
|
|
.catch(err => {
|
|
graphContainer.html(`<p class="p-3 text-danger">Error: ${err.message}</p>`);
|
|
})
|
|
.finally(() => {
|
|
searchBtn.disabled = false;
|
|
btnText.textContent = "Discover";
|
|
loader.style.display = "none";
|
|
});
|
|
});
|
|
|
|
function renderGraph(data) {
|
|
const width = 800;
|
|
const height = 600;
|
|
|
|
const svg = d3.select("#graphContainer")
|
|
.append("svg")
|
|
.attr("viewBox", [0, 0, width, height]);
|
|
|
|
const simulation = d3.forceSimulation(data.nodes)
|
|
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
|
|
.force("charge", d3.forceManyBody().strength(-300))
|
|
.force("center", d3.forceCenter(width / 2, height / 2));
|
|
|
|
const link = svg.append("g")
|
|
.selectAll("line")
|
|
.data(data.links)
|
|
.join("line")
|
|
.attr("stroke", "#999")
|
|
.attr("stroke-width", 1);
|
|
|
|
const node = svg.append("g")
|
|
.selectAll("g")
|
|
.data(data.nodes)
|
|
.join("g")
|
|
.attr("class", "node-group")
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended));
|
|
|
|
node.append("circle")
|
|
.attr("r", 20)
|
|
.attr("class", "node-circle")
|
|
.attr("fill", d => d.type === 'PERSON' ? '#e74c3c' : '#3498db');
|
|
|
|
// Add Image if available
|
|
node.filter(d => d.photo)
|
|
.append("image")
|
|
.attr("xlink:href", d => d.photo)
|
|
.attr("x", -15)
|
|
.attr("y", -15)
|
|
.attr("width", 30)
|
|
.attr("height", 30)
|
|
.attr("clip-path", "circle(15px)");
|
|
|
|
node.append("text")
|
|
.attr("dy", 35)
|
|
.attr("text-anchor", "middle")
|
|
.attr("class", "node-text")
|
|
.text(d => d.name);
|
|
|
|
node.append("text")
|
|
.attr("dy", 48)
|
|
.attr("text-anchor", "middle")
|
|
.attr("class", "node-text")
|
|
.attr("fill", "#666")
|
|
.text(d => d.code || '');
|
|
|
|
simulation.on("tick", () => {
|
|
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y)
|
|
.attr("x2", d => d.target.x).attr("y2", d => d.target.y);
|
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
});
|
|
|
|
function dragstarted(event) {
|
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
event.subject.fx = event.subject.x;
|
|
event.subject.fy = event.subject.y;
|
|
}
|
|
|
|
function dragged(event) {
|
|
event.subject.fx = event.x;
|
|
event.subject.fy = event.y;
|
|
}
|
|
|
|
function dragended(event) {
|
|
if (!event.active) simulation.alphaTarget(0);
|
|
event.subject.fx = null;
|
|
event.subject.fy = null;
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |