progress
This commit is contained in:
@@ -64,6 +64,18 @@ body {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-indicator.scanning {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.completed {
|
||||
background-color: #00ff41;
|
||||
}
|
||||
|
||||
.status-indicator.error {
|
||||
background-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -266,6 +278,7 @@ input[type="text"]:focus, select:focus {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
@@ -274,6 +287,23 @@ input[type="text"]:focus, select:focus {
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
box-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
/* Visualization Panel */
|
||||
@@ -292,6 +322,37 @@ input[type="text"]:focus, select:focus {
|
||||
position: relative;
|
||||
background-color: #1a1a1a;
|
||||
border-top: 1px solid #444;
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
.graph-container.expanded {
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.graph-controls {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.graph-control-btn {
|
||||
background: rgba(42, 42, 42, 0.9);
|
||||
border: 1px solid #555;
|
||||
color: #c7c7c7;
|
||||
padding: 0.5rem;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.graph-control-btn:hover {
|
||||
border-color: #00ff41;
|
||||
color: #00ff41;
|
||||
background: rgba(42, 42, 42, 1);
|
||||
}
|
||||
|
||||
.graph-placeholder {
|
||||
@@ -333,6 +394,20 @@ input[type="text"]:focus, select:focus {
|
||||
border-top: 1px solid #444;
|
||||
}
|
||||
|
||||
.legend-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.legend-title {
|
||||
font-size: 0.7rem;
|
||||
color: #00ff41;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -344,6 +419,7 @@ input[type="text"]:focus, select:focus {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.legend-edge {
|
||||
@@ -353,10 +429,16 @@ input[type="text"]:focus, select:focus {
|
||||
|
||||
.legend-edge.high-confidence {
|
||||
background-color: #00ff41;
|
||||
box-shadow: 0 0 3px rgba(0, 255, 65, 0.5);
|
||||
}
|
||||
|
||||
.legend-edge.medium-confidence {
|
||||
background-color: #ff9900;
|
||||
box-shadow: 0 0 3px rgba(255, 153, 0, 0.5);
|
||||
}
|
||||
|
||||
.legend-edge.low-confidence {
|
||||
background-color: #666666;
|
||||
}
|
||||
|
||||
/* Provider Panel */
|
||||
@@ -375,9 +457,11 @@ input[type="text"]:focus, select:focus {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.provider-item:hover {
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
@@ -389,6 +473,7 @@ input[type="text"]:focus, select:focus {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.provider-status.enabled {
|
||||
@@ -401,12 +486,78 @@ input[type="text"]:focus, select:focus {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.provider-status.api-key-required {
|
||||
background-color: #5c4c2c;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.provider-stats {
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.provider-stat-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.provider-stat-value {
|
||||
color: #00ff41;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.provider-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.node-info-popup {
|
||||
position: fixed;
|
||||
background: rgba(42, 42, 42, 0.95);
|
||||
border: 1px solid #555;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
color: #c7c7c7;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
max-width: 300px;
|
||||
z-index: 1001;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.node-info-title {
|
||||
color: #00ff41;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #444;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.node-info-detail {
|
||||
margin-bottom: 0.25rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.node-info-label {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.node-info-value {
|
||||
color: #c7c7c7;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background-color: #0a0a0a;
|
||||
@@ -437,6 +588,7 @@ input[type="text"]:focus, select:focus {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@@ -447,6 +599,18 @@ input[type="text"]:focus, select:focus {
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@@ -480,6 +644,12 @@ input[type="text"]:focus, select:focus {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
color: #999;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -495,6 +665,7 @@ input[type="text"]:focus, select:focus {
|
||||
|
||||
.detail-value {
|
||||
color: #c7c7c7;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@@ -552,6 +723,40 @@ input[type="text"]:focus, select:focus {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(26, 26, 26, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #444;
|
||||
border-top: 3px solid #00ff41;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 1rem;
|
||||
color: #999;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff6b6b !important;
|
||||
border-color: #ff6b6b !important;
|
||||
@@ -598,4 +803,101 @@ input[type="text"]:focus, select:focus {
|
||||
|
||||
.amber {
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
|
||||
.apikey-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.apikey-section label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #c7c7c7;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.apikey-section input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #555;
|
||||
color: #c7c7c7;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.apikey-section input[type="password"]:focus {
|
||||
outline: none;
|
||||
border-color: #00ff41;
|
||||
box-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
|
||||
}
|
||||
|
||||
.apikey-help {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin-top: 0.25rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
.message-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1002;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.message-toast {
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
.message-toast.success {
|
||||
background: #2c5c34;
|
||||
border-left: 4px solid #00ff41;
|
||||
}
|
||||
|
||||
.message-toast.error {
|
||||
background: #5c2c2c;
|
||||
border-left: 4px solid #ff6b6b;
|
||||
}
|
||||
|
||||
.message-toast.warning {
|
||||
background: #5c4c2c;
|
||||
border-left: 4px solid #ff9900;
|
||||
}
|
||||
|
||||
.message-toast.info {
|
||||
background: #2c3e5c;
|
||||
border-left: 4px solid #00aaff;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Graph visualization module for DNSRecon
|
||||
* Handles network graph rendering using vis.js
|
||||
* Handles network graph rendering using vis.js with enhanced Phase 2 features
|
||||
*/
|
||||
|
||||
class GraphManager {
|
||||
@@ -10,40 +10,57 @@ class GraphManager {
|
||||
this.nodes = new vis.DataSet();
|
||||
this.edges = new vis.DataSet();
|
||||
this.isInitialized = false;
|
||||
|
||||
// Graph options for cybersecurity theme
|
||||
this.currentLayout = 'physics';
|
||||
this.nodeInfoPopup = null;
|
||||
|
||||
// Enhanced graph options for Phase 2
|
||||
this.options = {
|
||||
nodes: {
|
||||
shape: 'dot',
|
||||
size: 12,
|
||||
size: 15,
|
||||
font: {
|
||||
size: 11,
|
||||
size: 12,
|
||||
color: '#c7c7c7',
|
||||
face: 'Roboto Mono, monospace',
|
||||
background: 'rgba(26, 26, 26, 0.8)',
|
||||
strokeWidth: 1,
|
||||
background: 'rgba(26, 26, 26, 0.9)',
|
||||
strokeWidth: 2,
|
||||
strokeColor: '#000000'
|
||||
},
|
||||
borderWidth: 2,
|
||||
borderColor: '#444',
|
||||
shadow: {
|
||||
enabled: true,
|
||||
color: 'rgba(0, 0, 0, 0.3)',
|
||||
size: 3,
|
||||
x: 1,
|
||||
y: 1
|
||||
color: 'rgba(0, 0, 0, 0.5)',
|
||||
size: 5,
|
||||
x: 2,
|
||||
y: 2
|
||||
},
|
||||
scaling: {
|
||||
min: 8,
|
||||
max: 20
|
||||
min: 10,
|
||||
max: 30,
|
||||
label: {
|
||||
enabled: true,
|
||||
min: 8,
|
||||
max: 16
|
||||
}
|
||||
},
|
||||
chosen: {
|
||||
node: (values, id, selected, hovering) => {
|
||||
values.borderColor = '#00ff41';
|
||||
values.borderWidth = 3;
|
||||
values.shadow = true;
|
||||
values.shadowColor = 'rgba(0, 255, 65, 0.6)';
|
||||
values.shadowSize = 10;
|
||||
}
|
||||
}
|
||||
},
|
||||
edges: {
|
||||
width: 2,
|
||||
color: {
|
||||
color: '#444',
|
||||
color: '#555',
|
||||
highlight: '#00ff41',
|
||||
hover: '#ff9900'
|
||||
hover: '#ff9900',
|
||||
inherit: false
|
||||
},
|
||||
font: {
|
||||
size: 10,
|
||||
@@ -56,131 +73,218 @@ class GraphManager {
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
scaleFactor: 0.8,
|
||||
scaleFactor: 1,
|
||||
type: 'arrow'
|
||||
}
|
||||
},
|
||||
smooth: {
|
||||
enabled: true,
|
||||
type: 'dynamic',
|
||||
roundness: 0.5
|
||||
roundness: 0.6
|
||||
},
|
||||
shadow: {
|
||||
enabled: true,
|
||||
color: 'rgba(0, 0, 0, 0.2)',
|
||||
size: 2,
|
||||
color: 'rgba(0, 0, 0, 0.3)',
|
||||
size: 3,
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
chosen: {
|
||||
edge: (values, id, selected, hovering) => {
|
||||
values.color = '#00ff41';
|
||||
values.width = 4;
|
||||
values.shadow = true;
|
||||
values.shadowColor = 'rgba(0, 255, 65, 0.4)';
|
||||
}
|
||||
}
|
||||
},
|
||||
physics: {
|
||||
enabled: true,
|
||||
stabilization: {
|
||||
enabled: true,
|
||||
iterations: 100,
|
||||
updateInterval: 25
|
||||
iterations: 150,
|
||||
updateInterval: 50
|
||||
},
|
||||
barnesHut: {
|
||||
gravitationalConstant: -2000,
|
||||
centralGravity: 0.3,
|
||||
springLength: 95,
|
||||
springConstant: 0.04,
|
||||
damping: 0.09,
|
||||
avoidOverlap: 0.1
|
||||
gravitationalConstant: -3000,
|
||||
centralGravity: 0.4,
|
||||
springLength: 120,
|
||||
springConstant: 0.05,
|
||||
damping: 0.1,
|
||||
avoidOverlap: 0.2
|
||||
},
|
||||
maxVelocity: 50,
|
||||
maxVelocity: 30,
|
||||
minVelocity: 0.1,
|
||||
solver: 'barnesHut',
|
||||
timestep: 0.35,
|
||||
timestep: 0.4,
|
||||
adaptiveTimestep: true
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
hoverConnectedEdges: true,
|
||||
selectConnectedEdges: true,
|
||||
tooltipDelay: 200,
|
||||
tooltipDelay: 300,
|
||||
hideEdgesOnDrag: false,
|
||||
hideNodesOnDrag: false
|
||||
hideNodesOnDrag: false,
|
||||
zoomView: true,
|
||||
dragView: true,
|
||||
multiselect: true
|
||||
},
|
||||
layout: {
|
||||
improvedLayout: true
|
||||
improvedLayout: true,
|
||||
randomSeed: 2
|
||||
}
|
||||
};
|
||||
|
||||
this.setupEventHandlers();
|
||||
|
||||
this.createNodeInfoPopup();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the network graph
|
||||
* Create floating node info popup
|
||||
*/
|
||||
createNodeInfoPopup() {
|
||||
this.nodeInfoPopup = document.createElement('div');
|
||||
this.nodeInfoPopup.className = 'node-info-popup';
|
||||
this.nodeInfoPopup.style.display = 'none';
|
||||
document.body.appendChild(this.nodeInfoPopup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the network graph with enhanced features
|
||||
*/
|
||||
initialize() {
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const data = {
|
||||
nodes: this.nodes,
|
||||
edges: this.edges
|
||||
};
|
||||
|
||||
|
||||
this.network = new vis.Network(this.container, data, this.options);
|
||||
this.setupNetworkEvents();
|
||||
this.isInitialized = true;
|
||||
|
||||
|
||||
// Hide placeholder
|
||||
const placeholder = this.container.querySelector('.graph-placeholder');
|
||||
if (placeholder) {
|
||||
placeholder.style.display = 'none';
|
||||
}
|
||||
|
||||
console.log('Graph initialized successfully');
|
||||
|
||||
// Add graph controls
|
||||
this.addGraphControls();
|
||||
|
||||
console.log('Enhanced graph initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize graph:', error);
|
||||
this.showError('Failed to initialize visualization');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup network event handlers
|
||||
* Add interactive graph controls
|
||||
*/
|
||||
addGraphControls() {
|
||||
const controlsContainer = document.createElement('div');
|
||||
controlsContainer.className = 'graph-controls';
|
||||
controlsContainer.innerHTML = `
|
||||
<button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
|
||||
<button class="graph-control-btn" id="graph-reset" title="Reset View">[RESET]</button>
|
||||
<button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
|
||||
<button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
|
||||
`;
|
||||
|
||||
this.container.appendChild(controlsContainer);
|
||||
|
||||
// Add control event listeners
|
||||
document.getElementById('graph-fit').addEventListener('click', () => this.fitView());
|
||||
document.getElementById('graph-reset').addEventListener('click', () => this.resetView());
|
||||
document.getElementById('graph-physics').addEventListener('click', () => this.togglePhysics());
|
||||
document.getElementById('graph-cluster').addEventListener('click', () => this.toggleClustering());
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup enhanced network event handlers
|
||||
*/
|
||||
setupNetworkEvents() {
|
||||
if (!this.network) return;
|
||||
|
||||
// Node click event
|
||||
|
||||
// Node click event with enhanced details
|
||||
this.network.on('click', (params) => {
|
||||
if (params.nodes.length > 0) {
|
||||
const nodeId = params.nodes[0];
|
||||
this.showNodeDetails(nodeId);
|
||||
this.highlightNodeConnections(nodeId);
|
||||
} else {
|
||||
this.clearHighlights();
|
||||
}
|
||||
});
|
||||
|
||||
// Hover events for tooltips
|
||||
|
||||
// Enhanced hover events
|
||||
this.network.on('hoverNode', (params) => {
|
||||
const nodeId = params.node;
|
||||
const node = this.nodes.get(nodeId);
|
||||
if (node) {
|
||||
this.showTooltip(params.pointer.DOM, node);
|
||||
this.showNodeInfoPopup(params.pointer.DOM, node);
|
||||
this.highlightConnectedNodes(nodeId, true);
|
||||
}
|
||||
});
|
||||
|
||||
this.network.on('blurNode', () => {
|
||||
this.hideTooltip();
|
||||
|
||||
this.network.on('blurNode', (params) => {
|
||||
this.hideNodeInfoPopup();
|
||||
this.clearHoverHighlights();
|
||||
});
|
||||
|
||||
// Stabilization events
|
||||
|
||||
// Edge hover events
|
||||
this.network.on('hoverEdge', (params) => {
|
||||
const edgeId = params.edge;
|
||||
const edge = this.edges.get(edgeId);
|
||||
if (edge) {
|
||||
this.showEdgeInfo(params.pointer.DOM, edge);
|
||||
}
|
||||
});
|
||||
|
||||
this.network.on('blurEdge', () => {
|
||||
this.hideNodeInfoPopup();
|
||||
});
|
||||
|
||||
// Double-click to focus on node
|
||||
this.network.on('doubleClick', (params) => {
|
||||
if (params.nodes.length > 0) {
|
||||
const nodeId = params.nodes[0];
|
||||
this.focusOnNode(nodeId);
|
||||
}
|
||||
});
|
||||
|
||||
// Context menu (right-click)
|
||||
this.network.on('oncontext', (params) => {
|
||||
params.event.preventDefault();
|
||||
if (params.nodes.length > 0) {
|
||||
this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Stabilization events with progress
|
||||
this.network.on('stabilizationProgress', (params) => {
|
||||
const progress = params.iterations / params.total;
|
||||
this.updateStabilizationProgress(progress);
|
||||
});
|
||||
|
||||
|
||||
this.network.on('stabilizationIterationsDone', () => {
|
||||
this.onStabilizationComplete();
|
||||
});
|
||||
|
||||
// Selection events
|
||||
this.network.on('select', (params) => {
|
||||
console.log('Selected nodes:', params.nodes);
|
||||
console.log('Selected edges:', params.edges);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update graph with new data
|
||||
* Update graph with new data and enhanced processing
|
||||
* @param {Object} graphData - Graph data from backend
|
||||
*/
|
||||
updateGraph(graphData) {
|
||||
@@ -188,37 +292,48 @@ class GraphManager {
|
||||
console.warn('Invalid graph data received');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Initialize if not already done
|
||||
if (!this.isInitialized) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
// Process nodes
|
||||
|
||||
// Process nodes with enhanced attributes
|
||||
const processedNodes = graphData.nodes.map(node => this.processNode(node));
|
||||
const processedEdges = graphData.edges.map(edge => this.processEdge(edge));
|
||||
|
||||
// Update datasets
|
||||
this.nodes.clear();
|
||||
this.edges.clear();
|
||||
this.nodes.add(processedNodes);
|
||||
this.edges.add(processedEdges);
|
||||
|
||||
// Fit the view if this is the first update or graph is small
|
||||
if (processedNodes.length <= 10) {
|
||||
setTimeout(() => this.fitView(), 500);
|
||||
|
||||
// Update datasets with animation
|
||||
const existingNodeIds = this.nodes.getIds();
|
||||
const existingEdgeIds = this.edges.getIds();
|
||||
|
||||
// Add new nodes with fade-in animation
|
||||
const newNodes = processedNodes.filter(node => !existingNodeIds.includes(node.id));
|
||||
const newEdges = processedEdges.filter(edge => !existingEdgeIds.includes(edge.id));
|
||||
|
||||
// Update existing data
|
||||
this.nodes.update(processedNodes);
|
||||
this.edges.update(processedEdges);
|
||||
|
||||
// Highlight new additions briefly
|
||||
if (newNodes.length > 0 || newEdges.length > 0) {
|
||||
setTimeout(() => this.highlightNewElements(newNodes, newEdges), 100);
|
||||
}
|
||||
|
||||
console.log(`Graph updated: ${processedNodes.length} nodes, ${processedEdges.length} edges`);
|
||||
|
||||
// Auto-fit view for small graphs or first update
|
||||
if (processedNodes.length <= 10 || existingNodeIds.length === 0) {
|
||||
setTimeout(() => this.fitView(), 800);
|
||||
}
|
||||
|
||||
console.log(`Enhanced graph updated: ${processedNodes.length} nodes, ${processedEdges.length} edges (${newNodes.length} new nodes, ${newEdges.length} new edges)`);
|
||||
} catch (error) {
|
||||
console.error('Failed to update graph:', error);
|
||||
console.error('Failed to update enhanced graph:', error);
|
||||
this.showError('Failed to update visualization');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process node data for visualization
|
||||
* Process node data with enhanced styling and metadata
|
||||
* @param {Object} node - Raw node data
|
||||
* @returns {Object} Processed node data
|
||||
*/
|
||||
@@ -230,25 +345,32 @@ class GraphManager {
|
||||
color: this.getNodeColor(node.type),
|
||||
size: this.getNodeSize(node.type),
|
||||
borderColor: this.getNodeBorderColor(node.type),
|
||||
metadata: node.metadata || {}
|
||||
shape: this.getNodeShape(node.type),
|
||||
metadata: node.metadata || {},
|
||||
type: node.type
|
||||
};
|
||||
|
||||
// Add type-specific styling
|
||||
if (node.type === 'domain') {
|
||||
processedNode.shape = 'dot';
|
||||
} else if (node.type === 'ip') {
|
||||
processedNode.shape = 'square';
|
||||
} else if (node.type === 'certificate') {
|
||||
processedNode.shape = 'diamond';
|
||||
} else if (node.type === 'asn') {
|
||||
processedNode.shape = 'triangle';
|
||||
|
||||
// Add confidence-based styling
|
||||
if (node.confidence) {
|
||||
processedNode.borderWidth = Math.max(2, Math.floor(node.confidence * 5));
|
||||
}
|
||||
|
||||
|
||||
// Add special styling for important nodes
|
||||
if (this.isImportantNode(node)) {
|
||||
processedNode.shadow = {
|
||||
enabled: true,
|
||||
color: 'rgba(0, 255, 65, 0.6)',
|
||||
size: 10,
|
||||
x: 2,
|
||||
y: 2
|
||||
};
|
||||
}
|
||||
|
||||
return processedNode;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process edge data for visualization
|
||||
* Process edge data with enhanced styling and metadata
|
||||
* @param {Object} edge - Raw edge data
|
||||
* @returns {Object} Processed edge data
|
||||
*/
|
||||
@@ -262,12 +384,29 @@ class GraphManager {
|
||||
title: this.createEdgeTooltip(edge),
|
||||
width: this.getEdgeWidth(confidence),
|
||||
color: this.getEdgeColor(confidence),
|
||||
dashes: confidence < 0.6 ? [5, 5] : false
|
||||
dashes: confidence < 0.6 ? [5, 5] : false,
|
||||
metadata: {
|
||||
relationship_type: edge.label,
|
||||
confidence_score: confidence,
|
||||
source_provider: edge.source_provider,
|
||||
discovery_timestamp: edge.discovery_timestamp
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Add animation for high-confidence edges
|
||||
if (confidence >= 0.8) {
|
||||
processedEdge.shadow = {
|
||||
enabled: true,
|
||||
color: 'rgba(0, 255, 65, 0.3)',
|
||||
size: 5,
|
||||
x: 1,
|
||||
y: 1
|
||||
};
|
||||
}
|
||||
|
||||
return processedEdge;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format node label for display
|
||||
* @param {string} nodeId - Node identifier
|
||||
@@ -281,7 +420,7 @@ class GraphManager {
|
||||
}
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format edge label for display
|
||||
* @param {string} relationshipType - Type of relationship
|
||||
@@ -290,7 +429,7 @@ class GraphManager {
|
||||
*/
|
||||
formatEdgeLabel(relationshipType, confidence) {
|
||||
if (!relationshipType) return '';
|
||||
|
||||
|
||||
const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
|
||||
return `${relationshipType} ${confidenceText}`;
|
||||
}
|
||||
@@ -309,7 +448,7 @@ class GraphManager {
|
||||
};
|
||||
return colors[nodeType] || '#ffffff';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get node border color based on type
|
||||
* @param {string} nodeType - Node type
|
||||
@@ -324,7 +463,7 @@ class GraphManager {
|
||||
};
|
||||
return borderColors[nodeType] || '#666666';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get node size based on type
|
||||
* @param {string} nodeType - Node type
|
||||
@@ -340,6 +479,21 @@ class GraphManager {
|
||||
return sizes[nodeType] || 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enhanced node shape based on type
|
||||
* @param {string} nodeType - Node type
|
||||
* @returns {string} Shape name
|
||||
*/
|
||||
getNodeShape(nodeType) {
|
||||
const shapes = {
|
||||
'domain': 'dot',
|
||||
'ip': 'square',
|
||||
'certificate': 'diamond',
|
||||
'asn': 'triangle'
|
||||
};
|
||||
return shapes[nodeType] || 'dot';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edge color based on confidence
|
||||
* @param {number} confidence - Confidence score
|
||||
@@ -354,7 +508,7 @@ class GraphManager {
|
||||
return '#666666'; // Low confidence - gray
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get edge width based on confidence
|
||||
* @param {number} confidence - Confidence score
|
||||
@@ -411,7 +565,20 @@ class GraphManager {
|
||||
tooltip += `</div>`;
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if node is important based on connections or metadata
|
||||
* @param {Object} node - Node data
|
||||
* @returns {boolean} True if node is important
|
||||
*/
|
||||
isImportantNode(node) {
|
||||
// Mark nodes as important based on criteria
|
||||
if (node.type === 'domain' && node.id.includes('www.')) return false;
|
||||
if (node.metadata && node.metadata.connection_count > 3) return true;
|
||||
if (node.type === 'asn') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show node details in modal
|
||||
* @param {string} nodeId - Node identifier
|
||||
@@ -419,7 +586,7 @@ class GraphManager {
|
||||
showNodeDetails(nodeId) {
|
||||
const node = this.nodes.get(nodeId);
|
||||
if (!node) return;
|
||||
|
||||
|
||||
// Trigger custom event for main application to handle
|
||||
const event = new CustomEvent('nodeSelected', {
|
||||
detail: { nodeId, node }
|
||||
@@ -428,22 +595,249 @@ class GraphManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show tooltip
|
||||
* Show enhanced node info popup
|
||||
* @param {Object} position - Mouse position
|
||||
* @param {Object} node - Node data
|
||||
*/
|
||||
showTooltip(position, node) {
|
||||
// Tooltip is handled by vis.js automatically
|
||||
// This method is for custom tooltip implementation if needed
|
||||
showNodeInfoPopup(position, node) {
|
||||
if (!this.nodeInfoPopup) return;
|
||||
|
||||
const html = `
|
||||
<div class="node-info-title">${node.id}</div>
|
||||
<div class="node-info-detail">
|
||||
<span class="node-info-label">Type:</span>
|
||||
<span class="node-info-value">${node.type || 'Unknown'}</span>
|
||||
</div>
|
||||
${node.metadata && Object.keys(node.metadata).length > 0 ?
|
||||
'<div class="node-info-detail"><span class="node-info-label">Details:</span><span class="node-info-value">Click for more</span></div>' :
|
||||
''}
|
||||
`;
|
||||
|
||||
this.nodeInfoPopup.innerHTML = html;
|
||||
this.nodeInfoPopup.style.display = 'block';
|
||||
this.nodeInfoPopup.style.left = position.x + 15 + 'px';
|
||||
this.nodeInfoPopup.style.top = position.y - 10 + 'px';
|
||||
|
||||
// Ensure popup stays in viewport
|
||||
const rect = this.nodeInfoPopup.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth) {
|
||||
this.nodeInfoPopup.style.left = position.x - rect.width - 15 + 'px';
|
||||
}
|
||||
if (rect.bottom > window.innerHeight) {
|
||||
this.nodeInfoPopup.style.top = position.y - rect.height + 10 + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show edge information tooltip
|
||||
* @param {Object} position - Mouse position
|
||||
* @param {Object} edge - Edge data
|
||||
*/
|
||||
showEdgeInfo(position, edge) {
|
||||
if (!this.nodeInfoPopup) return;
|
||||
|
||||
const confidence = edge.metadata ? edge.metadata.confidence_score : 0;
|
||||
const provider = edge.metadata ? edge.metadata.source_provider : 'Unknown';
|
||||
|
||||
const html = `
|
||||
<div class="node-info-title">${edge.metadata ? edge.metadata.relationship_type : 'Relationship'}</div>
|
||||
<div class="node-info-detail">
|
||||
<span class="node-info-label">Confidence:</span>
|
||||
<span class="node-info-value">${(confidence * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="node-info-detail">
|
||||
<span class="node-info-label">Provider:</span>
|
||||
<span class="node-info-value">${provider}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.nodeInfoPopup.innerHTML = html;
|
||||
this.nodeInfoPopup.style.display = 'block';
|
||||
this.nodeInfoPopup.style.left = position.x + 15 + 'px';
|
||||
this.nodeInfoPopup.style.top = position.y - 10 + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide tooltip
|
||||
* Hide node info popup
|
||||
*/
|
||||
hideTooltip() {
|
||||
// Tooltip hiding is handled by vis.js automatically
|
||||
hideNodeInfoPopup() {
|
||||
if (this.nodeInfoPopup) {
|
||||
this.nodeInfoPopup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight node connections
|
||||
* @param {string} nodeId - Node to highlight
|
||||
*/
|
||||
highlightNodeConnections(nodeId) {
|
||||
const connectedNodes = this.network.getConnectedNodes(nodeId);
|
||||
const connectedEdges = this.network.getConnectedEdges(nodeId);
|
||||
|
||||
// Update node colors
|
||||
const nodeUpdates = connectedNodes.map(id => ({
|
||||
id: id,
|
||||
borderColor: '#ff9900',
|
||||
borderWidth: 3
|
||||
}));
|
||||
|
||||
nodeUpdates.push({
|
||||
id: nodeId,
|
||||
borderColor: '#00ff41',
|
||||
borderWidth: 4
|
||||
});
|
||||
|
||||
// Update edge colors
|
||||
const edgeUpdates = connectedEdges.map(id => ({
|
||||
id: id,
|
||||
color: { color: '#ff9900' },
|
||||
width: 3
|
||||
}));
|
||||
|
||||
this.nodes.update(nodeUpdates);
|
||||
this.edges.update(edgeUpdates);
|
||||
|
||||
// Store for cleanup
|
||||
this.highlightedElements = {
|
||||
nodes: connectedNodes.concat([nodeId]),
|
||||
edges: connectedEdges
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight connected nodes on hover
|
||||
* @param {string} nodeId - Node ID
|
||||
* @param {boolean} highlight - Whether to highlight or unhighlight
|
||||
*/
|
||||
highlightConnectedNodes(nodeId, highlight) {
|
||||
const connectedNodes = this.network.getConnectedNodes(nodeId);
|
||||
const connectedEdges = this.network.getConnectedEdges(nodeId);
|
||||
|
||||
if (highlight) {
|
||||
// Dim all other elements
|
||||
this.dimUnconnectedElements([nodeId, ...connectedNodes], connectedEdges);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dim elements not connected to the specified nodes
|
||||
* @param {Array} nodeIds - Node IDs to keep highlighted
|
||||
* @param {Array} edgeIds - Edge IDs to keep highlighted
|
||||
*/
|
||||
dimUnconnectedElements(nodeIds, edgeIds) {
|
||||
const allNodes = this.nodes.get();
|
||||
const allEdges = this.edges.get();
|
||||
|
||||
const nodeUpdates = allNodes.map(node => ({
|
||||
id: node.id,
|
||||
opacity: nodeIds.includes(node.id) ? 1 : 0.3
|
||||
}));
|
||||
|
||||
const edgeUpdates = allEdges.map(edge => ({
|
||||
id: edge.id,
|
||||
opacity: edgeIds.includes(edge.id) ? 1 : 0.1
|
||||
}));
|
||||
|
||||
this.nodes.update(nodeUpdates);
|
||||
this.edges.update(edgeUpdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all highlights
|
||||
*/
|
||||
clearHighlights() {
|
||||
if (this.highlightedElements) {
|
||||
// Reset highlighted nodes
|
||||
const nodeUpdates = this.highlightedElements.nodes.map(id => {
|
||||
const originalNode = this.nodes.get(id);
|
||||
return {
|
||||
id: id,
|
||||
borderColor: this.getNodeBorderColor(originalNode.type),
|
||||
borderWidth: 2
|
||||
};
|
||||
});
|
||||
|
||||
// Reset highlighted edges
|
||||
const edgeUpdates = this.highlightedElements.edges.map(id => {
|
||||
const originalEdge = this.edges.get(id);
|
||||
return {
|
||||
id: id,
|
||||
color: this.getEdgeColor(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5),
|
||||
width: this.getEdgeWidth(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5)
|
||||
};
|
||||
});
|
||||
|
||||
this.nodes.update(nodeUpdates);
|
||||
this.edges.update(edgeUpdates);
|
||||
|
||||
this.highlightedElements = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear hover highlights
|
||||
*/
|
||||
clearHoverHighlights() {
|
||||
const allNodes = this.nodes.get();
|
||||
const allEdges = this.edges.get();
|
||||
|
||||
const nodeUpdates = allNodes.map(node => ({ id: node.id, opacity: 1 }));
|
||||
const edgeUpdates = allEdges.map(edge => ({ id: edge.id, opacity: 1 }));
|
||||
|
||||
this.nodes.update(nodeUpdates);
|
||||
this.edges.update(edgeUpdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight newly added elements
|
||||
* @param {Array} newNodes - New nodes
|
||||
* @param {Array} newEdges - New edges
|
||||
*/
|
||||
highlightNewElements(newNodes, newEdges) {
|
||||
// Briefly highlight new nodes
|
||||
const nodeHighlights = newNodes.map(node => ({
|
||||
id: node.id,
|
||||
borderColor: '#00ff41',
|
||||
borderWidth: 4,
|
||||
shadow: {
|
||||
enabled: true,
|
||||
color: 'rgba(0, 255, 65, 0.8)',
|
||||
size: 15,
|
||||
x: 2,
|
||||
y: 2
|
||||
}
|
||||
}));
|
||||
|
||||
// Briefly highlight new edges
|
||||
const edgeHighlights = newEdges.map(edge => ({
|
||||
id: edge.id,
|
||||
color: '#00ff41',
|
||||
width: 4
|
||||
}));
|
||||
|
||||
this.nodes.update(nodeHighlights);
|
||||
this.edges.update(edgeHighlights);
|
||||
|
||||
// Reset after animation
|
||||
setTimeout(() => {
|
||||
const nodeResets = newNodes.map(node => ({
|
||||
id: node.id,
|
||||
borderColor: this.getNodeBorderColor(node.type),
|
||||
borderWidth: 2,
|
||||
shadow: node.shadow || { enabled: false }
|
||||
}));
|
||||
|
||||
const edgeResets = newEdges.map(edge => ({
|
||||
id: edge.id,
|
||||
color: this.getEdgeColor(edge.metadata ? edge.metadata.confidence_score : 0.5),
|
||||
width: this.getEdgeWidth(edge.metadata ? edge.metadata.confidence_score : 0.5)
|
||||
}));
|
||||
|
||||
this.nodes.update(nodeResets);
|
||||
this.edges.update(edgeResets);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stabilization progress
|
||||
* @param {number} progress - Progress value (0-1)
|
||||
@@ -452,14 +846,71 @@ class GraphManager {
|
||||
// Could show a progress indicator if needed
|
||||
console.log(`Graph stabilization: ${(progress * 100).toFixed(1)}%`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle stabilization completion
|
||||
*/
|
||||
onStabilizationComplete() {
|
||||
console.log('Graph stabilization complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus view on specific node
|
||||
* @param {string} nodeId - Node to focus on
|
||||
*/
|
||||
focusOnNode(nodeId) {
|
||||
const nodePosition = this.network.getPositions([nodeId]);
|
||||
if (nodePosition[nodeId]) {
|
||||
this.network.moveTo({
|
||||
position: nodePosition[nodeId],
|
||||
scale: 1.5,
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easingFunction: 'easeInOutQuart'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle physics simulation
|
||||
*/
|
||||
togglePhysics() {
|
||||
const currentPhysics = this.network.physics.physicsEnabled;
|
||||
this.network.setOptions({ physics: !currentPhysics });
|
||||
|
||||
const button = document.getElementById('graph-physics');
|
||||
if (button) {
|
||||
button.textContent = currentPhysics ? '[PHYSICS OFF]' : '[PHYSICS ON]';
|
||||
button.style.color = currentPhysics ? '#ff9900' : '#00ff41';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle node clustering
|
||||
*/
|
||||
toggleClustering() {
|
||||
// Simple clustering by node type
|
||||
const clusterOptionsByType = {
|
||||
joinCondition: (childOptions) => {
|
||||
return childOptions.type === 'domain';
|
||||
},
|
||||
clusterNodeProperties: {
|
||||
id: 'domain-cluster',
|
||||
borderWidth: 3,
|
||||
shape: 'database',
|
||||
label: 'Domains',
|
||||
color: '#00ff41'
|
||||
}
|
||||
};
|
||||
|
||||
if (this.network.clustering.isCluster('domain-cluster')) {
|
||||
this.network.clustering.openCluster('domain-cluster');
|
||||
} else {
|
||||
this.network.clustering.cluster(clusterOptionsByType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fit the view to show all nodes
|
||||
*/
|
||||
@@ -473,7 +924,7 @@ class GraphManager {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the view to initial state
|
||||
*/
|
||||
@@ -489,21 +940,21 @@ class GraphManager {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear the graph
|
||||
*/
|
||||
clear() {
|
||||
this.nodes.clear();
|
||||
this.edges.clear();
|
||||
|
||||
|
||||
// Show placeholder
|
||||
const placeholder = this.container.querySelector('.graph-placeholder');
|
||||
if (placeholder) {
|
||||
placeholder.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
* @param {string} message - Error message
|
||||
@@ -515,25 +966,7 @@ class GraphManager {
|
||||
placeholder.style.color = '#ff6b6b';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup control event handlers
|
||||
*/
|
||||
setupEventHandlers() {
|
||||
// Reset view button
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const resetBtn = document.getElementById('reset-view');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => this.resetView());
|
||||
}
|
||||
|
||||
const fitBtn = document.getElementById('fit-view');
|
||||
if (fitBtn) {
|
||||
fitBtn.addEventListener('click', () => this.fitView());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get network statistics
|
||||
* @returns {Object} Statistics object
|
||||
@@ -545,7 +978,7 @@ class GraphManager {
|
||||
//isStabilized: this.network ? this.network.isStabilized() : false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export graph as image (if needed for future implementation)
|
||||
* @param {string} format - Image format ('png', 'jpeg')
|
||||
@@ -553,7 +986,7 @@ class GraphManager {
|
||||
*/
|
||||
exportAsImage(format = 'png') {
|
||||
if (!this.network) return null;
|
||||
|
||||
|
||||
// This would require additional vis.js functionality
|
||||
// Placeholder for future implementation
|
||||
console.log('Image export not yet implemented');
|
||||
|
||||
@@ -447,11 +447,19 @@ class DNSReconApp {
|
||||
try {
|
||||
console.log('Updating status display...');
|
||||
|
||||
// Update status text
|
||||
// Update status text with animation
|
||||
if (this.elements.scanStatus) {
|
||||
this.elements.scanStatus.textContent = this.formatStatus(status.status);
|
||||
console.log('Updated status display:', status.status);
|
||||
const formattedStatus = this.formatStatus(status.status);
|
||||
if (this.elements.scanStatus.textContent !== formattedStatus) {
|
||||
this.elements.scanStatus.textContent = formattedStatus;
|
||||
this.elements.scanStatus.classList.add('fade-in');
|
||||
setTimeout(() => this.elements.scanStatus.classList.remove('fade-in'), 300);
|
||||
}
|
||||
|
||||
// Add status-specific classes for styling
|
||||
this.elements.scanStatus.className = `status-value status-${status.status}`;
|
||||
}
|
||||
|
||||
if (this.elements.targetDisplay) {
|
||||
this.elements.targetDisplay.textContent = status.target_domain || 'None';
|
||||
}
|
||||
@@ -465,9 +473,16 @@ class DNSReconApp {
|
||||
this.elements.indicatorsDisplay.textContent = status.indicators_processed || 0;
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
// Update progress bar with smooth animation
|
||||
if (this.elements.progressFill) {
|
||||
this.elements.progressFill.style.width = `${status.progress_percentage}%`;
|
||||
|
||||
// Add pulsing animation for active scans
|
||||
if (status.status === 'running') {
|
||||
this.elements.progressFill.parentElement.classList.add('scanning');
|
||||
} else {
|
||||
this.elements.progressFill.parentElement.classList.remove('scanning');
|
||||
}
|
||||
}
|
||||
|
||||
// Update session ID
|
||||
@@ -492,12 +507,16 @@ class DNSReconApp {
|
||||
case 'running':
|
||||
this.setUIState('scanning');
|
||||
this.showSuccess('Scan is running');
|
||||
// Reset polling frequency for active scans
|
||||
this.pollFrequency = 2000;
|
||||
this.updateConnectionStatus('active');
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
this.setUIState('completed');
|
||||
this.stopPolling();
|
||||
this.showSuccess('Scan completed successfully');
|
||||
this.updateConnectionStatus('completed');
|
||||
// Force a final graph update
|
||||
console.log('Scan completed - forcing final graph update');
|
||||
setTimeout(() => this.updateGraph(), 100);
|
||||
@@ -507,20 +526,54 @@ class DNSReconApp {
|
||||
this.setUIState('failed');
|
||||
this.stopPolling();
|
||||
this.showError('Scan failed');
|
||||
this.updateConnectionStatus('error');
|
||||
break;
|
||||
|
||||
case 'stopped':
|
||||
this.setUIState('stopped');
|
||||
this.stopPolling();
|
||||
this.showSuccess('Scan stopped');
|
||||
this.updateConnectionStatus('stopped');
|
||||
break;
|
||||
|
||||
case 'idle':
|
||||
this.setUIState('idle');
|
||||
this.stopPolling();
|
||||
this.updateConnectionStatus('idle');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update connection status indicator
|
||||
* @param {string} status - Connection status
|
||||
*/
|
||||
updateConnectionStatus(status) {
|
||||
if (!this.elements.connectionStatus) return;
|
||||
|
||||
const statusColors = {
|
||||
'idle': '#c7c7c7',
|
||||
'active': '#00ff41',
|
||||
'completed': '#00aa2e',
|
||||
'stopped': '#ff9900',
|
||||
'error': '#ff6b6b'
|
||||
};
|
||||
|
||||
this.elements.connectionStatus.style.backgroundColor = statusColors[status] || '#c7c7c7';
|
||||
|
||||
const statusText = this.elements.connectionStatus.parentElement?.querySelector('.status-text');
|
||||
if (statusText) {
|
||||
const statusTexts = {
|
||||
'idle': 'System Ready',
|
||||
'active': 'Scanning Active',
|
||||
'completed': 'Scan Complete',
|
||||
'stopped': 'Scan Stopped',
|
||||
'error': 'Connection Error'
|
||||
};
|
||||
|
||||
statusText.textContent = statusTexts[status] || 'System Online';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set UI state based on scan status
|
||||
@@ -532,10 +585,17 @@ class DNSReconApp {
|
||||
switch (state) {
|
||||
case 'scanning':
|
||||
this.isScanning = true;
|
||||
if (this.elements.startScan) this.elements.startScan.disabled = true;
|
||||
if (this.elements.stopScan) this.elements.stopScan.disabled = false;
|
||||
if (this.elements.startScan) {
|
||||
this.elements.startScan.disabled = true;
|
||||
this.elements.startScan.classList.add('loading');
|
||||
}
|
||||
if (this.elements.stopScan) {
|
||||
this.elements.stopScan.disabled = false;
|
||||
this.elements.stopScan.classList.remove('loading');
|
||||
}
|
||||
if (this.elements.targetDomain) this.elements.targetDomain.disabled = true;
|
||||
if (this.elements.maxDepth) this.elements.maxDepth.disabled = true;
|
||||
if (this.elements.configureApiKeys) this.elements.configureApiKeys.disabled = true;
|
||||
break;
|
||||
|
||||
case 'idle':
|
||||
@@ -543,10 +603,17 @@ class DNSReconApp {
|
||||
case 'failed':
|
||||
case 'stopped':
|
||||
this.isScanning = false;
|
||||
if (this.elements.startScan) this.elements.startScan.disabled = false;
|
||||
if (this.elements.stopScan) this.elements.stopScan.disabled = true;
|
||||
if (this.elements.startScan) {
|
||||
this.elements.startScan.disabled = false;
|
||||
this.elements.startScan.classList.remove('loading');
|
||||
}
|
||||
if (this.elements.stopScan) {
|
||||
this.elements.stopScan.disabled = true;
|
||||
this.elements.stopScan.classList.add('loading');
|
||||
}
|
||||
if (this.elements.targetDomain) this.elements.targetDomain.disabled = false;
|
||||
if (this.elements.maxDepth) this.elements.maxDepth.disabled = false;
|
||||
if (this.elements.configureApiKeys) this.elements.configureApiKeys.disabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -580,20 +647,42 @@ class DNSReconApp {
|
||||
|
||||
for (const [name, info] of Object.entries(providers)) {
|
||||
const providerItem = document.createElement('div');
|
||||
providerItem.className = 'provider-item';
|
||||
providerItem.className = 'provider-item fade-in';
|
||||
|
||||
const status = info.enabled ? 'enabled' : 'disabled';
|
||||
const statusClass = info.enabled ? 'enabled' : 'disabled';
|
||||
let statusClass = 'disabled';
|
||||
let statusText = 'Disabled';
|
||||
|
||||
if (info.enabled) {
|
||||
statusClass = 'enabled';
|
||||
statusText = 'Enabled';
|
||||
} else if (info.requires_api_key) {
|
||||
statusClass = 'api-key-required';
|
||||
statusText = 'API Key Required';
|
||||
}
|
||||
|
||||
providerItem.innerHTML = `
|
||||
<div>
|
||||
<div class="provider-header">
|
||||
<div class="provider-name">${name.toUpperCase()}</div>
|
||||
<div class="provider-stats">
|
||||
Requests: ${info.statistics.total_requests || 0} |
|
||||
Success Rate: ${(info.statistics.success_rate || 0).toFixed(1)}%
|
||||
<div class="provider-status ${statusClass}">${statusText}</div>
|
||||
</div>
|
||||
<div class="provider-stats">
|
||||
<div class="provider-stat">
|
||||
<span class="provider-stat-label">Requests:</span>
|
||||
<span class="provider-stat-value">${info.statistics.total_requests || 0}</span>
|
||||
</div>
|
||||
<div class="provider-stat">
|
||||
<span class="provider-stat-label">Success Rate:</span>
|
||||
<span class="provider-stat-value">${(info.statistics.success_rate || 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="provider-stat">
|
||||
<span class="provider-stat-label">Relationships:</span>
|
||||
<span class="provider-stat-value">${info.statistics.relationships_found || 0}</span>
|
||||
</div>
|
||||
<div class="provider-stat">
|
||||
<span class="provider-stat-label">Rate Limit:</span>
|
||||
<span class="provider-stat-value">${info.rate_limit}/min</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-status ${statusClass}">${status}</div>
|
||||
`;
|
||||
|
||||
this.elements.providerList.appendChild(providerItem);
|
||||
@@ -614,7 +703,7 @@ class DNSReconApp {
|
||||
|
||||
let detailsHtml = '';
|
||||
detailsHtml += `<div class="detail-row"><span class="detail-label">Identifier:</span><span class="detail-value">${nodeId}</span></div>`;
|
||||
detailsHtml += `<div class="detail-row"><span class="detail-label">Type:</span><span class="detail-value">${node.metadata.type || 'Unknown'}</span></div>`;
|
||||
detailsHtml += `<div class="detail-row"><span class="detail-label">Type:</span><span class="detail-value">${node.metadata.type || node.type || 'Unknown'}</span></div>`;
|
||||
|
||||
if (node.metadata) {
|
||||
for (const [key, value] of Object.entries(node.metadata)) {
|
||||
@@ -624,6 +713,12 @@ class DNSReconApp {
|
||||
}
|
||||
}
|
||||
|
||||
// Add timestamps if available
|
||||
if (node.added_timestamp) {
|
||||
const addedDate = new Date(node.added_timestamp);
|
||||
detailsHtml += `<div class="detail-row"><span class="detail-label">Added:</span><span class="detail-value">${addedDate.toLocaleString()}</span></div>`;
|
||||
}
|
||||
|
||||
if (this.elements.modalDetails) {
|
||||
this.elements.modalDetails.innerHTML = detailsHtml;
|
||||
}
|
||||
@@ -645,12 +740,24 @@ class DNSReconApp {
|
||||
* @returns {boolean} True if data has changed
|
||||
*/
|
||||
hasGraphChanged(graphData) {
|
||||
// Simple check based on node and edge counts
|
||||
// Simple check based on node and edge counts and timestamps
|
||||
const currentStats = this.graphManager.getStatistics();
|
||||
const newNodeCount = graphData.nodes ? graphData.nodes.length : 0;
|
||||
const newEdgeCount = graphData.edges ? graphData.edges.length : 0;
|
||||
|
||||
const changed = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount;
|
||||
// Check if counts changed
|
||||
const countsChanged = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount;
|
||||
|
||||
// Also check if we have new timestamp data
|
||||
const hasNewTimestamp = graphData.statistics &&
|
||||
graphData.statistics.last_modified &&
|
||||
graphData.statistics.last_modified !== this.lastGraphTimestamp;
|
||||
|
||||
if (hasNewTimestamp) {
|
||||
this.lastGraphTimestamp = graphData.statistics.last_modified;
|
||||
}
|
||||
|
||||
const changed = countsChanged || hasNewTimestamp;
|
||||
|
||||
console.log(`Graph change check: Current(${currentStats.nodeCount}n, ${currentStats.edgeCount}e) vs New(${newNodeCount}n, ${newEdgeCount}e) = ${changed}`);
|
||||
|
||||
@@ -816,6 +923,10 @@ class DNSReconApp {
|
||||
this.showMessage(message, 'info');
|
||||
}
|
||||
|
||||
showWarning(message) {
|
||||
this.showMessage(message, 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
* @param {string} message - Error message
|
||||
@@ -828,13 +939,7 @@ class DNSReconApp {
|
||||
* Show connection error
|
||||
*/
|
||||
showConnectionError() {
|
||||
if (this.elements.connectionStatus) {
|
||||
this.elements.connectionStatus.style.backgroundColor = '#ff6b6b';
|
||||
}
|
||||
const statusText = this.elements.connectionStatus?.parentElement?.querySelector('.status-text');
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Connection Error';
|
||||
}
|
||||
this.updateConnectionStatus('error');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -848,24 +953,12 @@ class DNSReconApp {
|
||||
// Create message element
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `message-toast message-${type}`;
|
||||
messageElement.style.cssText = `
|
||||
background: ${this.getMessageColor(type)};
|
||||
color: #fff;
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||||
border-left: 4px solid ${this.getMessageBorderColor(type)};
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
`;
|
||||
|
||||
messageElement.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>${message}</span>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 12px 20px;">
|
||||
<span style="flex: 1;">${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
style="background: none; border: none; color: #fff; cursor: pointer; font-size: 16px; margin-left: 10px;">×</button>
|
||||
style="background: none; border: none; color: #fff; cursor: pointer; font-size: 16px; margin-left: 10px; opacity: 0.7;">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -888,13 +981,8 @@ class DNSReconApp {
|
||||
}
|
||||
|
||||
// Update connection status to show activity
|
||||
if (type === 'success' && this.elements.connectionStatus) {
|
||||
this.elements.connectionStatus.style.backgroundColor = '#00ff41';
|
||||
setTimeout(() => {
|
||||
if (this.elements.connectionStatus) {
|
||||
this.elements.connectionStatus.style.backgroundColor = '#00ff41';
|
||||
}
|
||||
}, 2000);
|
||||
if (type === 'success' && this.consecutiveErrors === 0) {
|
||||
this.updateConnectionStatus(this.isScanning ? 'active' : 'idle');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user