Files
thrillwiki_django_no_react/static/js/components/version-comparison.js

314 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Version Comparison Component
class VersionComparison {
constructor(options = {}) {
this.container = null;
this.versions = new Map();
this.selectedVersions = new Set();
this.maxSelections = options.maxSelections || 3;
this.onCompare = options.onCompare || (() => {});
this.onRollback = options.onRollback || (() => {});
this.timeline = null;
}
initialize(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
throw new Error(`Container element with id "${containerId}" not found`);
}
this._initializeTimeline();
}
setVersions(versions) {
this.versions = new Map(versions.map(v => [v.name, v]));
this._updateTimeline();
this.render();
}
_initializeTimeline() {
this.timeline = document.createElement('div');
this.timeline.className = 'version-timeline';
this.container.appendChild(this.timeline);
}
_updateTimeline() {
const sortedVersions = Array.from(this.versions.values())
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
this.timeline.innerHTML = `
<div class="timeline-track">
${this._renderTimelineDots(sortedVersions)}
</div>
<div class="timeline-labels">
${this._renderTimelineLabels(sortedVersions)}
</div>
`;
}
_renderTimelineDots(versions) {
return versions.map(version => `
<div class="timeline-point ${this.selectedVersions.has(version.name) ? 'selected' : ''}"
data-version="${version.name}"
style="--impact-score: ${version.comparison_metadata.impact_score || 0}"
onclick="this._toggleVersionSelection('${version.name}')">
<div class="point-indicator"></div>
${this._renderImpactIndicator(version)}
</div>
`).join('');
}
_renderImpactIndicator(version) {
const impact = version.comparison_metadata.impact_score || 0;
const size = Math.max(8, Math.min(24, impact * 24)); // 8-24px based on impact
return `
<div class="impact-indicator"
style="width: ${size}px; height: ${size}px"
title="Impact Score: ${Math.round(impact * 100)}%">
</div>
`;
}
_renderTimelineLabels(versions) {
return versions.map(version => `
<div class="timeline-label" data-version="${version.name}">
<div class="version-name">${version.name}</div>
<div class="version-date">
${new Date(version.created_at).toLocaleDateString()}
</div>
</div>
`).join('');
}
render() {
if (!this.container) return;
const selectedVersionsArray = Array.from(this.selectedVersions);
this.container.innerHTML = `
<div class="version-comparison-tool">
<div class="comparison-header">
<h3>Version Comparison</h3>
<div class="selected-versions">
${this._renderSelectedVersions(selectedVersionsArray)}
</div>
<div class="comparison-actions">
${this._renderActionButtons(selectedVersionsArray)}
</div>
</div>
${this.timeline.outerHTML}
<div class="comparison-content">
${this._renderComparisonContent(selectedVersionsArray)}
</div>
</div>
`;
this.attachEventListeners();
}
_renderSelectedVersions(selectedVersions) {
return selectedVersions.map((version, index) => `
<div class="selected-version">
<span class="version-label">Version ${index + 1}:</span>
<span class="version-value">${version}</span>
<button class="remove-version" onclick="this._removeVersion('${version}')">
×
</button>
</div>
`).join('');
}
_renderActionButtons(selectedVersions) {
const canCompare = selectedVersions.length >= 2;
const canRollback = selectedVersions.length === 1;
return `
${canCompare ? `
<button class="compare-button" onclick="this._handleCompare()">
Compare Versions
</button>
` : ''}
${canRollback ? `
<button class="rollback-button" onclick="this._handleRollback()">
Rollback to Version
</button>
` : ''}
`;
}
_renderComparisonContent(selectedVersions) {
if (selectedVersions.length < 2) {
return `
<div class="comparison-placeholder">
Select at least two versions to compare
</div>
`;
}
return `
<div class="comparison-results">
<div class="results-loading">
Computing differences...
</div>
</div>
`;
}
_toggleVersionSelection(versionName) {
if (this.selectedVersions.has(versionName)) {
this.selectedVersions.delete(versionName);
} else if (this.selectedVersions.size < this.maxSelections) {
this.selectedVersions.add(versionName);
} else {
// Show max selections warning
this._showWarning(`Maximum ${this.maxSelections} versions can be compared`);
return;
}
this.render();
}
_removeVersion(versionName) {
this.selectedVersions.delete(versionName);
this.render();
}
async _handleCompare() {
const selectedVersions = Array.from(this.selectedVersions);
if (selectedVersions.length < 2) return;
try {
const results = await this.onCompare(selectedVersions);
this._renderComparisonResults(results);
} catch (error) {
console.error('Comparison failed:', error);
this._showError('Failed to compare versions');
}
}
async _handleRollback() {
const selectedVersion = Array.from(this.selectedVersions)[0];
if (!selectedVersion) return;
try {
await this.onRollback(selectedVersion);
// Handle successful rollback
} catch (error) {
console.error('Rollback failed:', error);
this._showError('Failed to rollback version');
}
}
_renderComparisonResults(results) {
const resultsContainer = this.container.querySelector('.comparison-results');
if (!resultsContainer) return;
resultsContainer.innerHTML = `
<div class="results-content">
${results.map(diff => this._renderDiffSection(diff)).join('')}
</div>
`;
}
_renderDiffSection(diff) {
return `
<div class="diff-section">
<div class="diff-header">
<h4>Changes: ${diff.version1}${diff.version2}</h4>
<div class="diff-stats">
<span class="computation-time">
Computed in ${diff.computation_time.toFixed(2)}s
</span>
<span class="impact-score">
Impact Score: ${Math.round(diff.impact_score * 100)}%
</span>
</div>
</div>
<div class="changes-list">
${this._renderChanges(diff.changes)}
</div>
</div>
`;
}
_renderChanges(changes) {
return changes.map(change => `
<div class="change-item">
<div class="change-header">
<span class="change-type">${change.type}</span>
<span class="change-file">${change.file}</span>
</div>
<div class="change-content">
<div class="old-value">
<div class="value-header">Previous</div>
<pre>${this._escapeHtml(change.old_value)}</pre>
</div>
<div class="new-value">
<div class="value-header">New</div>
<pre>${this._escapeHtml(change.new_value)}</pre>
</div>
</div>
</div>
`).join('');
}
_showWarning(message) {
const warning = document.createElement('div');
warning.className = 'comparison-warning';
warning.textContent = message;
this.container.appendChild(warning);
setTimeout(() => warning.remove(), 3000);
}
_showError(message) {
const error = document.createElement('div');
error.className = 'comparison-error';
error.textContent = message;
this.container.appendChild(error);
setTimeout(() => error.remove(), 3000);
}
_escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
attachEventListeners() {
// Timeline scroll handling
const timeline = this.container.querySelector('.version-timeline');
if (timeline) {
let isDown = false;
let startX;
let scrollLeft;
timeline.addEventListener('mousedown', (e) => {
isDown = true;
timeline.classList.add('active');
startX = e.pageX - timeline.offsetLeft;
scrollLeft = timeline.scrollLeft;
});
timeline.addEventListener('mouseleave', () => {
isDown = false;
timeline.classList.remove('active');
});
timeline.addEventListener('mouseup', () => {
isDown = false;
timeline.classList.remove('active');
});
timeline.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - timeline.offsetLeft;
const walk = (x - startX) * 2;
timeline.scrollLeft = scrollLeft - walk;
});
}
}
}
export default VersionComparison;