OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / deploy-ui.js

224 lines · javascript ref:
/**
 * Deploy UI — deploy dashboard
 */

import { renderPage } from './page-layout.js'

const PAGE_CSS = `
    .section { margin-bottom: 40px; }
    .section h2 { margin-bottom: 15px; font-size: 20px; }
    .repo-card {
      background: #161b22; border: 1px solid #30363d;
      border-radius: 6px; padding: 20px; margin-bottom: 15px;
    }
    .repo-card-header {
      display: flex; justify-content: space-between;
      align-items: center; margin-bottom: 15px;
    }
    .repo-card-header h3 { font-size: 18px; }
    .repo-meta {
      display: flex; gap: 20px; font-size: 13px;
      color: #8b949e; margin-bottom: 15px;
    }
    .repo-meta span { display: flex; align-items: center; gap: 5px; }
    .badge.seeding { background: #238636; color: white; }
    .badge.idle { background: #6e7681; color: white; }
    .badge.configured { background: #1f6feb; color: white; }
    .checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; }
    .deploy-actions { display: flex; gap: 10px; align-items: center; }
    .setup-form {
      background: #0d1117; border: 1px solid #30363d;
      border-radius: 6px; padding: 20px; margin-top: 15px; display: none;
    }
    .setup-form.visible { display: block; }
`

const PAGE_SCRIPTS = `
    function getBasePath() {
      const parts = window.location.pathname.split('/');
      if (parts.length > 2 && parts[1] !== 'deploy') return '/' + parts[1];
      return '';
    }

    function showStatus(msg, type) {
      const area = document.getElementById('status-area');
      area.innerHTML = '<div class="status-msg ' + type + '">' + msg + '</div>';
      if (type !== 'info') setTimeout(() => { area.innerHTML = ''; }, 5000);
    }

    async function setupDeploy(repoName) {
      const form = document.getElementById('setup-' + repoName);
      if (form) form.classList.toggle('visible');
    }

    async function saveConfig(repoName) {
      const channel = document.getElementById('channel-' + repoName).value.trim();
      if (!channel) { alert('Channel name is required'); return; }
      const autoRelease = document.getElementById('autoRelease-' + repoName).checked;
      const autoSeed = document.getElementById('autoSeed-' + repoName).checked;
      const autoDeploy = document.getElementById('autoDeploy-' + repoName).checked;
      try {
        const resp = await window.fetch(getBasePath() + '/api/deploy/config/' + repoName, {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ channel, autoRelease, autoSeed, autoDeploy, enabled: true })
        });
        const data = await resp.json();
        if (data.success) { showStatus('Deploy configured for ' + repoName, 'success'); setTimeout(() => location.reload(), 1000); }
        else showStatus('Error: ' + data.error, 'error');
      } catch (err) { showStatus('Error: ' + err.message, 'error'); }
    }

    async function triggerDeploy(repoName) {
      const btn = event.target;
      btn.disabled = true; btn.innerHTML = '<span class="spinner"></span>Deploying...';
      showStatus('Deploying ' + repoName + '...', 'info');
      try {
        const resp = await window.fetch(getBasePath() + '/api/deploy/trigger/' + repoName, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
        });
        const data = await resp.json();
        if (data.success) { showStatus('Deployed ' + repoName + ' v' + data.version + ' in ' + data.duration + 'ms', 'success'); loadLog(repoName); loadStatus(repoName); }
        else showStatus('Deploy failed: ' + data.error, 'error');
      } catch (err) { showStatus('Deploy error: ' + err.message, 'error'); }
      finally { btn.disabled = false; btn.textContent = 'Deploy Now'; }
    }

    async function disableDeploy(repoName) {
      if (!confirm('Disable deploy for ' + repoName + '?')) return;
      try {
        const resp = await window.fetch(getBasePath() + '/api/deploy/config/' + repoName, {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ channel: '_disabled', enabled: false })
        });
        const data = await resp.json();
        if (data.success) { showStatus('Deploy disabled for ' + repoName, 'success'); setTimeout(() => location.reload(), 1000); }
      } catch (err) { showStatus('Error: ' + err.message, 'error'); }
    }

    async function loadLog(repoName) {
      try {
        const resp = await window.fetch(getBasePath() + '/api/deploy/log/' + repoName);
        const entries = await resp.json();
        const logEl = document.getElementById('deploy-log');
        if (entries.length === 0) { logEl.innerHTML = '<div class="empty-state">No deploys yet for ' + repoName + '</div>'; return; }
        let html = '<table class="data-table"><thead><tr><th>Time</th><th>Version</th><th>Channel</th><th>Duration</th></tr></thead><tbody>';
        for (const e of entries) html += '<tr><td>' + new Date(e.timestamp).toLocaleString() + '</td><td>' + (e.version||'-') + '</td><td>' + (e.channel||'-') + '</td><td>' + (e.duration||0) + 'ms</td></tr>';
        html += '</tbody></table>';
        logEl.innerHTML = html;
      } catch (err) { console.error('Failed to load log:', err); }
    }

    async function loadStatus(repoName) {
      try {
        const resp = await window.fetch(getBasePath() + '/api/deploy/status/' + repoName);
        const data = await resp.json();
        const el = document.getElementById('status-' + repoName);
        if (!el) return;
        let parts = [];
        if (data.lastVersion) parts.push('v' + data.lastVersion);
        if (data.seeding) parts.push('<span class="badge seeding">Seeding</span>');
        else parts.push('<span class="badge idle">Not seeding</span>');
        if (data.lastDeploy) parts.push(new Date(data.lastDeploy).toLocaleString());
        el.innerHTML = parts.join(' &middot; ');
        const keyEl = document.getElementById('key-' + repoName);
        if (keyEl && data.lastKey) keyEl.innerHTML = '<code style="font-size:12px;color:#58a6ff;word-break:break-all;">pear://' + data.lastKey + '</code>';
      } catch (err) { console.error('Status error:', err); }
    }
`

export function renderDeployDashboard (repos = [], pathPrefix = '') {
  const configured = repos.filter(r => r.deploy && r.deploy.enabled)
  const unconfigured = repos.filter(r => !r.deploy || !r.deploy.enabled)

  const content = `
    <div id="status-area"></div>

    <div class="section">
      <h2>Configured (${configured.length})</h2>
      <div id="configured-repos">
        ${configured.length === 0 ? '<div class="empty-state">No repos configured for deploy yet.</div>' : ''}
        ${configured.map(r => renderConfiguredRepo(r)).join('')}
      </div>
    </div>

    <div class="section">
      <h2>Available (${unconfigured.length})</h2>
      <div id="unconfigured-repos">
        ${unconfigured.length === 0 ? '<div class="empty-state">All repos are configured.</div>' : ''}
        ${unconfigured.map(r => renderUnconfiguredRepo(r)).join('')}
      </div>
    </div>

    <div class="section">
      <h2>Recent Deploys</h2>
      <div id="deploy-log" class="card">
        <div class="empty-state">Select a repo to view deploy history.</div>
      </div>
    </div>
  `

  const scripts = PAGE_SCRIPTS + '\n    ' + configured.map(r => `loadStatus('${r.name}');`).join('\n    ')

  return renderPage('Deploy', content, {
    pathPrefix,
    activePage: 'deploy',
    css: PAGE_CSS,
    scripts
  })
}

function renderConfiguredRepo (repo) {
  const d = repo.deploy
  return `
    <div class="repo-card">
      <div class="repo-card-header">
        <h3>${repo.name}</h3>
        <div class="deploy-actions">
          <button onclick="triggerDeploy('${repo.name}')">Deploy Now</button>
          <button class="secondary small" onclick="loadLog('${repo.name}')">View Log</button>
          <button class="danger small" onclick="disableDeploy('${repo.name}')">Disable</button>
        </div>
      </div>
      <div class="repo-meta">
        <span>Channel: <strong>${d.channel}</strong></span>
        <span>Auto-release: ${d.autoRelease ? 'Yes' : 'No'}</span>
        <span>Auto-seed: ${d.autoSeed ? 'Yes' : 'No'}</span>
        <span>Auto-deploy: ${d.autoDeploy !== false ? 'Yes' : 'No'}</span>
      </div>
      <div id="key-${repo.name}" class="repo-meta">${d.lastKey ? '<code style="font-size:12px;color:#58a6ff;word-break:break-all;">pear://' + d.lastKey + '</code>' : ''}</div>
      <div id="status-${repo.name}" class="repo-meta">Loading status...</div>
    </div>
  `
}

function renderUnconfiguredRepo (repo) {
  return `
    <div class="repo-card">
      <div class="repo-card-header">
        <h3>${repo.name}</h3>
        <button class="secondary" onclick="setupDeploy('${repo.name}')">Configure Deploy</button>
      </div>
      <div id="setup-${repo.name}" class="setup-form">
        <div class="form-group">
          <label for="channel-${repo.name}">Pear Channel Name</label>
          <input type="text" id="channel-${repo.name}" value="${repo.name}" placeholder="e.g. my-app">
          <div class="help-text">Used in pear stage/release/seed commands. Usually matches the app name.</div>
        </div>
        <div class="form-group">
          <label class="checkbox-label"><input type="checkbox" id="autoRelease-${repo.name}" checked> Auto-release after staging</label>
        </div>
        <div class="form-group">
          <label class="checkbox-label"><input type="checkbox" id="autoSeed-${repo.name}" checked> Auto-seed after release</label>
        </div>
        <div class="form-group">
          <label class="checkbox-label"><input type="checkbox" id="autoDeploy-${repo.name}" checked> Auto-deploy on git push</label>
        </div>
        <div style="display: flex; gap: 10px; margin-top: 15px;">
          <button onclick="saveConfig('${repo.name}')">Save Configuration</button>
          <button class="secondary" onclick="setupDeploy('${repo.name}')">Cancel</button>
        </div>
      </div>
    </div>
  `
}