ai_upgrade_assistant-0.2.0-alpha2/js/upgrade-path-interactive.js
js/upgrade-path-interactive.js
(function ($, Drupal, drupalSettings) {
'use strict';
Drupal.behaviors.upgradePathInteractive = {
attach: function (context, settings) {
if (!drupalSettings.aiUpgradeAssistant || !drupalSettings.aiUpgradeAssistant.upgradePath) {
return;
}
const path = drupalSettings.aiUpgradeAssistant.upgradePath;
const container = document.getElementById('upgrade-path-viz');
if (!container) {
return;
}
// Initialize D3 visualization
const width = container.clientWidth;
const height = container.clientHeight;
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
// Set up zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
const g = svg.append('g');
svg.call(zoom);
// Initialize different views
const views = {
tree: this.initTreeView,
timeline: this.initTimelineView,
dependency: this.initDependencyView
};
// Set up view toggle handlers
$('.view-toggle', context).once('upgrade-path').on('click', function () {
const viewType = $(this).data('view');
if (views[viewType]) {
$('.view-toggle').removeClass('active');
$(this).addClass('active');
views[viewType].call(Drupal.behaviors.upgradePathInteractive, g, path);
}
});
// Initialize filters
this.initFilters(context, g, path);
// Initialize zoom controls
this.initZoomControls(context, svg, zoom);
// Show tree view by default
this.initTreeView(g, path);
// Initialize step details panel
this.initStepDetails(context);
// Initialize progress tracking
this.initProgressTracking(context, path);
},
initTreeView: function (g, path) {
// Clear existing visualization
g.selectAll('*').remove();
const treeLayout = d3.tree()
.size([800, 600])
.separation((a, b) => a.parent === b.parent ? 1 : 2);
// Convert path data to hierarchy
const root = d3.hierarchy(this.convertPathToTree(path));
const treeData = treeLayout(root);
// Draw links
g.selectAll('.link')
.data(treeData.links())
.enter()
.append('path')
.attr('class', 'link')
.attr('d', d3.linkVertical()
.x(d => d.x)
.y(d => d.y));
// Draw nodes
const nodes = g.selectAll('.node')
.data(treeData.descendants())
.enter()
.append('g')
.attr('class', d => `node ${d.data.type || ''}`)
.attr('transform', d => `translate(${d.x},${d.y})`);
nodes.append('circle')
.attr('r', 10)
.attr('class', d => d.data.automated ? 'automated' : '');
nodes.append('text')
.attr('dy', '.35em')
.attr('x', d => d.children ? -13 : 13)
.style('text-anchor', d => d.children ? 'end' : 'start')
.text(d => d.data.name);
// Add click handlers
nodes.on('click', (event, d) => {
this.showStepDetails(d.data);
});
},
initTimelineView: function (g, path) {
// Clear existing visualization
g.selectAll('*').remove();
const timeScale = d3.scaleLinear()
.domain([0, path.steps.length - 1])
.range([50, 750]);
// Draw timeline
g.append('line')
.attr('class', 'timeline')
.attr('x1', 50)
.attr('y1', 300)
.attr('x2', 750)
.attr('y2', 300);
// Draw steps
const steps = g.selectAll('.timeline-step')
.data(path.steps)
.enter()
.append('g')
.attr('class', d => `timeline-step ${d.type}`)
.attr('transform', (d, i) => `translate(${timeScale(i)},300)`);
steps.append('circle')
.attr('r', 8)
.attr('class', d => d.automated ? 'automated' : '');
steps.append('text')
.attr('dy', -15)
.attr('text-anchor', 'middle')
.text(d => d.type.replace(/_/g, ' '));
// Add click handlers
steps.on('click', (event, d) => {
this.showStepDetails(d);
});
},
initDependencyView: function (g, path) {
// Clear existing visualization
g.selectAll('*').remove();
const simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(400, 300));
const { nodes, links } = this.convertPathToDependencyGraph(path);
// Draw links
const link = g.append('g')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('class', 'dependency-link');
// Draw nodes
const node = g.append('g')
.selectAll('g')
.data(nodes)
.enter()
.append('g')
.attr('class', d => `dependency-node ${d.type || ''}`)
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
node.append('circle')
.attr('r', 10)
.attr('class', d => d.automated ? 'automated' : '');
node.append('text')
.attr('dy', '.35em')
.attr('x', 15)
.text(d => d.name);
// Add click handlers
node.on('click', (event, d) => {
this.showStepDetails(d);
});
simulation
.nodes(nodes)
.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})`);
});
simulation.force('link')
.links(links);
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;
}
},
initFilters: function (context, g, path) {
$('.filter-type, .filter-automated', context).once('upgrade-path').on('change', function () {
const activeTypes = $('.filter-type:checked').map(function () {
return $(this).val();
}).get();
const showAutomated = $('.filter-automated').is(':checked');
g.selectAll('.node, .timeline-step, .dependency-node')
.style('opacity', function (d) {
const typeMatch = activeTypes.includes(d.type);
const automatedMatch = !d.automated || showAutomated;
return typeMatch && automatedMatch ? 1 : 0.2;
});
});
},
initZoomControls: function (context, svg, zoom) {
$('.zoom-in', context).once('upgrade-path').on('click', () => {
svg.transition().call(zoom.scaleBy, 1.2);
});
$('.zoom-out', context).once('upgrade-path').on('click', () => {
svg.transition().call(zoom.scaleBy, 0.8);
});
$('.zoom-fit', context).once('upgrade-path').on('click', () => {
const bounds = svg.node().getBBox();
const dx = bounds.width;
const dy = bounds.height;
const x = bounds.x + dx / 2;
const y = bounds.y + dy / 2;
const scale = 0.9 / Math.max(dx / width, dy / height);
const translate = [width / 2 - scale * x, height / 2 - scale * y];
svg.transition()
.call(zoom.transform, d3.zoomIdentity
.translate(translate[0], translate[1])
.scale(scale));
});
},
initStepDetails: function (context) {
const template = $('#step-details-template', context).html();
const panel = $('#selected-step-details', context);
this.showStepDetails = function (step) {
panel.html(template);
panel.find('.step-title').text(step.description);
panel.find('.step-type').text(step.type.replace(/_/g, ' '));
if (step.confidence) {
panel.find('.step-confidence').text(`${Math.round(step.confidence * 100)}% confidence`);
}
panel.find('.step-automation').text(step.automated ? 'Automated' : 'Manual');
panel.find('.step-description').html(step.details || '');
if (step.risks) {
const risks = panel.find('.step-risks');
risks.empty();
step.risks.forEach(risk => {
risks.append(`<div class="risk ${risk.level}">${risk.description}</div>`);
});
}
panel.find('.view-details').attr('href', step.detailsUrl);
};
},
initProgressTracking: function (context, path) {
let completedSteps = 0;
const totalSteps = path.steps.length;
const progressBar = $('.progress-bar', context);
const completedCounter = $('.completed-steps', context);
function updateProgress() {
const percentage = (completedSteps / totalSteps) * 100;
progressBar.css('width', `${percentage}%`);
completedCounter.text(completedSteps);
}
$('.apply-step, .skip-step', context).once('upgrade-path').on('click', function () {
completedSteps++;
updateProgress();
$(this).closest('.step-details-content').addClass('completed');
});
$('.start-upgrade', context).once('upgrade-path').on('click', function () {
completedSteps = 0;
updateProgress();
$('.step-details-content.completed').removeClass('completed');
});
$('.export-progress', context).once('upgrade-path').on('click', function () {
const progress = {
module: path.module,
completedSteps: completedSteps,
totalSteps: totalSteps,
timestamp: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(progress, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `upgrade-progress-${path.module}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
},
convertPathToTree: function (path) {
return {
name: path.module,
children: path.steps.map(step => ({
name: step.description,
type: step.type,
automated: step.automated,
...step
}))
};
},
convertPathToDependencyGraph: function (path) {
const nodes = [
{ id: path.module, name: path.module, type: 'module' },
...path.steps.map((step, i) => ({
id: `step-${i}`,
name: step.type,
type: step.type,
automated: step.automated,
...step
}))
];
const links = path.steps.map((step, i) => ({
source: path.module,
target: `step-${i}`
}));
// Add dependency links between steps
path.steps.forEach((step, i) => {
if (step.dependencies) {
step.dependencies.forEach(dep => {
const targetIndex = path.steps.findIndex(s => s.id === dep);
if (targetIndex !== -1) {
links.push({
source: `step-${i}`,
target: `step-${targetIndex}`
});
}
});
}
});
return { nodes, links };
}
};
})(jQuery, Drupal, drupalSettings);
