Files
devstar/templates/repo/actions/debug.tmpl
2025-11-27 10:26:09 +08:00

450 lines
14 KiB
Handlebars

{{template "base/head" .}}
<div class="page-content repository actions debug-page">
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<div class="ui stackable grid">
<!-- Left Panel: Workflow Editor -->
<div class="eight wide column">
<div class="ui segment">
<h3 class="ui header">
{{ctx.Locale.Tr "actions.workflow.debug"}}
<span class="ui grey text">({{.WorkflowID}})</span>
</h3>
<div class="ui form">
<!-- Workflow Editor -->
<div class="field">
<label>{{ctx.Locale.Tr "actions.workflow.content"}}</label>
<textarea id="workflowEditor" class="ui textarea" style="height: 400px; font-family: monospace;" placeholder="Workflow YAML content"></textarea>
</div>
<!-- Debug Parameters -->
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "actions.workflow.ref"}}</label>
<select id="refSelect" class="ui dropdown">
<option value="">{{ctx.Locale.Tr "repo.default_branch"}}</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "actions.workflow.event"}}</label>
<select id="eventSelect" class="ui dropdown">
<option value="push">push</option>
<option value="pull_request">pull_request</option>
<option value="workflow_dispatch">workflow_dispatch</option>
<option value="issues">issues</option>
<option value="issue_comment">issue_comment</option>
<option value="pull_request_review">pull_request_review</option>
</select>
</div>
</div>
<!-- Workflow Inputs -->
<div class="field">
<label>{{ctx.Locale.Tr "actions.workflow.inputs"}}</label>
<div id="inputsContainer" class="ui segment"></div>
<button type="button" class="ui mini button" id="addInputBtn">
{{svg "octicon-plus"}} {{ctx.Locale.Tr "add"}}
</button>
</div>
<!-- Environment Variables -->
<div class="field">
<label>{{ctx.Locale.Tr "actions.workflow.env"}}</label>
<div id="envContainer" class="ui segment"></div>
<button type="button" class="ui mini button" id="addEnvBtn">
{{svg "octicon-plus"}} {{ctx.Locale.Tr "add"}}
</button>
</div>
<!-- Action Buttons -->
<div class="ui form-actions">
<button type="button" class="ui primary button" id="runBtn" onclick="runDebugWorkflow()">
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.workflow.run"}}
</button>
<a href="{{$.ActionsURL}}" class="ui button">{{ctx.Locale.Tr "cancel"}}</a>
</div>
</div>
</div>
</div>
<!-- Right Panel: Logs and Status -->
<div class="eight wide column">
<div class="ui segment">
<h3 class="ui header">
{{ctx.Locale.Tr "actions.workflow.logs"}}
</h3>
<!-- Status Info -->
<div id="statusInfo" style="display: none;" class="ui info message">
<p>{{ctx.Locale.Tr "actions.workflow.running"}}: <a id="runLink" href="#" target="_blank"></a></p>
</div>
<!-- Logs Container -->
<div id="logsContainer" style="border: 1px solid #ddd; padding: 10px; height: 500px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace; font-size: 12px;">
<p style="color: #999;">{{ctx.Locale.Tr "actions.workflow.logs_placeholder"}}</p>
</div>
<!-- Refresh and Copy Buttons -->
<div style="margin-top: 10px;">
<button type="button" class="ui mini button" id="refreshBtn" onclick="refreshLogs()" style="display: none;">
{{svg "octicon-sync"}} {{ctx.Locale.Tr "refresh"}}
</button>
<button type="button" class="ui mini button" id="copyBtn" onclick="copyLogs()" style="display: none;">
{{svg "octicon-copy"}} {{ctx.Locale.Tr "copy"}}
</button>
</div>
</div>
<!-- Error Messages -->
<div id="errorMessage" style="display: none;" class="ui error message">
<p id="errorText"></p>
</div>
</div>
</div>
</div>
</div>
<script>
const debugSessionID = {{.DebugSessionID}};
const workflowID = "{{.WorkflowID}}";
const defaultBranch = "{{.DefaultBranch}}";
const actionsURL = "{{.ActionsURL}}";
const debugAPIURL = "{{.DebugAPIURL}}";
const csrfToken = "{{.CsrfToken}}";
let currentRunIndex = null;
let logsAutoRefreshInterval = null;
// Initialize editor with workflow content
document.addEventListener('DOMContentLoaded', function() {
const editor = document.getElementById('workflowEditor');
editor.value = `{{.WorkflowContent}}`;
// Initialize dropdowns
$('.ui.dropdown').dropdown();
// Populate ref select with branches and tags
populateRefs();
// Setup input/env add buttons
document.getElementById('addInputBtn').addEventListener('click', addInputField);
document.getElementById('addEnvBtn').addEventListener('click', addEnvField);
});
function populateRefs() {
const refSelect = document.getElementById('refSelect');
// Add default branch
const option = document.createElement('option');
option.value = defaultBranch;
option.text = defaultBranch + ' (default)';
refSelect.appendChild(option);
// TODO: fetch branches and tags from API
}
function addInputField() {
const container = document.getElementById('inputsContainer');
const id = 'input_' + Date.now();
const field = document.createElement('div');
field.className = 'ui form';
field.id = id;
field.innerHTML = `
<div class="two fields">
<div class="field">
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.input_name"}}" class="input-key">
</div>
<div class="field">
<div class="ui input">
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.input_value"}}" class="input-value">
<button type="button" class="ui icon button" onclick="document.getElementById('${id}').remove()">
{{svg "octicon-trash"}}
</button>
</div>
</div>
</div>
`;
container.appendChild(field);
}
function addEnvField() {
const container = document.getElementById('envContainer');
const id = 'env_' + Date.now();
const field = document.createElement('div');
field.className = 'ui form';
field.id = id;
field.innerHTML = `
<div class="two fields">
<div class="field">
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.env_name"}}" class="env-key">
</div>
<div class="field">
<div class="ui input">
<input type="text" placeholder="{{ctx.Locale.Tr "actions.workflow.env_value"}}" class="env-value">
<button type="button" class="ui icon button" onclick="document.getElementById('${id}').remove()">
{{svg "octicon-trash"}}
</button>
</div>
</div>
</div>
`;
container.appendChild(field);
}
function getInputsObject() {
const inputs = {};
document.querySelectorAll('#inputsContainer .input-key').forEach((key, index) => {
const keyElem = document.querySelectorAll('#inputsContainer .input-key')[index];
const valueElem = document.querySelectorAll('#inputsContainer .input-value')[index];
if (keyElem.value && valueElem.value) {
inputs[keyElem.value] = valueElem.value;
}
});
return inputs;
}
function getEnvObject() {
const env = {};
document.querySelectorAll('#envContainer .env-key').forEach((key, index) => {
const keyElem = document.querySelectorAll('#envContainer .env-key')[index];
const valueElem = document.querySelectorAll('#envContainer .env-value')[index];
if (keyElem.value && valueElem.value) {
env[keyElem.value] = valueElem.value;
}
});
return env;
}
function runDebugWorkflow() {
const workflowContent = document.getElementById('workflowEditor').value;
const ref = document.getElementById('refSelect').value || defaultBranch;
const event = document.getElementById('eventSelect').value;
const inputs = getInputsObject();
const env = getEnvObject();
if (!workflowContent) {
showError('{{ctx.Locale.Tr "actions.workflow.content_empty"}}');
return;
}
const runBtn = document.getElementById('runBtn');
runBtn.classList.add('loading');
runBtn.disabled = true;
const payload = {
workflow_content: workflowContent,
ref: ref,
event: event,
inputs: inputs,
env: env
};
console.log('Submitting to:', `${debugAPIURL}/${debugSessionID}/run`);
console.log('Payload:', payload);
console.log('CSRF Token:', csrfToken);
const headers = {
'Content-Type': 'application/json',
'X-Csrf-Token': csrfToken,
};
fetch(`${debugAPIURL}/${debugSessionID}/run`, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
})
.then(response => {
console.log('Response status:', response.status, 'OK:', response.ok);
if (!response.ok) {
return response.text().then(text => {
console.log('Error response:', text);
throw new Error(`HTTP ${response.status}: ${text}`);
});
}
return response.json();
})
.then(data => {
console.log('Response data:', data);
if (data.success || data.run_index) {
currentRunIndex = data.run_index;
document.getElementById('statusInfo').style.display = 'block';
document.getElementById('runLink').href = `${actionsURL}/runs/${currentRunIndex}`;
document.getElementById('runLink').textContent = `Run #${currentRunIndex}`;
document.getElementById('errorMessage').style.display = 'none';
document.getElementById('refreshBtn').style.display = 'inline-block';
document.getElementById('copyBtn').style.display = 'inline-block';
// Start auto-refreshing logs
refreshLogs();
logsAutoRefreshInterval = setInterval(refreshLogs, 2000);
} else {
showError(data.error || data.message || '{{ctx.Locale.Tr "error"}}');
}
})
.catch(error => {
console.error('Fetch error:', error);
showError('Failed to run workflow: ' + error.message);
})
.finally(() => {
runBtn.classList.remove('loading');
runBtn.disabled = false;
});
}
function refreshLogs() {
if (!currentRunIndex) return;
console.log('Refreshing logs for run index:', currentRunIndex);
// Get logs from the action run API via POST
const payload = {
LogCursors: []
};
fetch(`${actionsURL}/runs/${currentRunIndex}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Csrf-Token': csrfToken,
},
body: JSON.stringify(payload),
})
.then(response => {
console.log('Log fetch response status:', response.status, response.statusText);
console.log('Content-Type:', response.headers.get('Content-Type'));
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Check if response is JSON
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
return response.json();
} else {
// If not JSON, get text and try to parse
return response.text().then(text => {
console.log('Response is not JSON, attempting to parse');
try {
return JSON.parse(text);
} catch (e) {
console.error('Failed to parse response as JSON:', text.substring(0, 200));
throw new Error('Response is not valid JSON. Content: ' + text.substring(0, 500));
}
});
}
})
.then(data => {
console.log('Log data received:', data);
const logsContainer = document.getElementById('logsContainer');
if (!data) {
logsContainer.textContent = 'No response data';
return;
}
if (data.logs && data.logs.stepsLog && data.logs.stepsLog.length > 0) {
let logContent = '';
data.logs.stepsLog.forEach((stepLog, index) => {
console.log(`Step ${index}:`, stepLog);
if (stepLog.rawOutput) {
logContent += stepLog.rawOutput + '\n';
} else if (stepLog.lines && Array.isArray(stepLog.lines)) {
stepLog.lines.forEach(line => {
if (line && line.content) {
logContent += line.content + '\n';
}
});
}
});
if (logContent.trim()) {
logsContainer.textContent = logContent;
} else {
logsContainer.textContent = 'Waiting for logs...';
}
} else if (data.state && data.state.run) {
// Show run status if logs not ready
logsContainer.textContent = 'Status: ' + (data.state.run.status || 'waiting') + '\n\nWaiting for logs...';
} else {
logsContainer.textContent = 'Waiting for logs...';
}
logsContainer.scrollTop = logsContainer.scrollHeight;
})
.catch(error => {
console.error('Error fetching logs:', error);
document.getElementById('logsContainer').textContent = 'Error: ' + error.message;
});
}
function copyLogs() {
const logsContainer = document.getElementById('logsContainer');
const text = logsContainer.textContent;
navigator.clipboard.writeText(text).then(() => {
// Show success message
const copyBtn = document.getElementById('copyBtn');
const originalText = copyBtn.innerHTML;
copyBtn.innerHTML = '{{svg "octicon-check"}} Copied!';
setTimeout(() => {
copyBtn.innerHTML = originalText;
}, 2000);
});
}
function showError(message) {
const errorMsg = document.getElementById('errorMessage');
document.getElementById('errorText').textContent = message;
errorMsg.style.display = 'block';
// Clear after 5 seconds
setTimeout(() => {
errorMsg.style.display = 'none';
}, 5000);
}
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (logsAutoRefreshInterval) {
clearInterval(logsAutoRefreshInterval);
}
});
</script>
<style>
.debug-page .ui.segment {
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.debug-page #workflowEditor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.5;
}
.debug-page #logsContainer {
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.5;
}
.debug-page .ui.form-actions {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid rgba(34, 36, 38, 0.15);
}
.debug-page .ui.form-actions .button {
margin-right: 10px;
}
.debug-page .input-key,
.debug-page .input-value,
.debug-page .env-key,
.debug-page .env-value {
margin: 0;
}
</style>
{{template "base/footer" .}}