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);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc