hostbasket/flowbroker/templates/index.html
2025-05-27 11:42:52 +03:00

318 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FlowBroker Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<!-- Optional: Link to your custom style.css if needed, but ensure it doesn't conflict heavily with Bootstrap -->
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">FlowBroker</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Dashboard</a>
</li>
<!-- Add other nav items here later if needed -->
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% if error_message %}
<div class="alert alert-danger" role="alert">
{{ error_message }}
</div>
{% endif %}
{% if success_message %}
<div class="alert alert-success" role="alert">
{{ success_message }}
</div>
{% endif %}
<h2>Active Flows</h2>
<div id="flows-list" class="mb-4">
{% if flows %}
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">UUID</th>
<th scope="col">Status</th>
<th scope="col">Created At</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for flow in flows %}
<tr>
<td>{{ flow.name }}</td>
<td><small>{{ flow.flow_uuid }}</small></td>
<td><span class="badge bg-secondary">{{ flow.status | default(value="Unknown") }}</span></td>
<td>{{ flow.base_data.created_at | date(format="%Y-%m-%d %H:%M:%S") }}</td>
<td>
<a href="/flows/{{ flow.flow_uuid }}" class="btn btn-sm btn-primary">View</a>
<button class="btn btn-sm btn-success run-flow-btn" data-flow-uuid="{{ flow.flow_uuid }}">Run</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No active flows found. You can create one below.</p>
{% endif %}
</div>
<hr class="my-4">
<h2>Runnable Example Scripts</h2>
<div id="example-scripts-list" class="mb-4">
{% if example_scripts %}
<ul class="list-group">
{% for example in example_scripts %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ example.name }}
<button class="btn btn-sm btn-info load-script-btn" data-script-content="{{ example.content | escape }}">Load into Form</button>
</li>
{% endfor %}
</ul>
{% else %}
<p>No example scripts found.</p>
{% endif %}
</div>
<hr class="my-4">
<div class="row">
<div class="col-md-6 mb-4">
<h2>Create New Flow from Rhai Script</h2>
<div id="create-from-rhai-section">
<form action="/flows/create_script" method="POST">
<div class="mb-3">
<label for="rhai_script_content" class="form-label">Rhai Script:</label>
<textarea class="form-control" id="rhai_script_content" name="rhai_script" rows="10" placeholder="Enter your Rhai script here or select an example using the 'Load into Form' buttons above..."></textarea>
</div>
<button type="submit" class="btn btn-primary">Create Flow from Script</button>
</form>
</div>
</div>
<div class="col-md-6 mb-4">
<h2>Create New Flow (Step-by-Step UI)</h2>
<div id="create-step-by-step-section">
<form id="createFlowForm_dynamic" action="/flows/create" method="post">
<div class="mb-3">
<label for="flow_name_dynamic" class="form-label">Flow Name:</label>
<input type="text" id="flow_name_dynamic" name="flow_name" class="form-control" required>
</div>
<hr>
<div id="stepsContainer_dynamic" class="mb-3">
<!-- Steps will be added here by JavaScript -->
<p class="text-muted"><em>Steps will appear here as you add them.</em></p>
</div>
<button type="button" id="addStepBtn_dynamic" class="btn btn-secondary mb-3">Add Step</button>
<hr>
<button type="submit" class="btn btn-primary">Create Flow (Step-by-Step)</button>
</form>
</div>
</div>
</div>
</div>
<!-- Templates for Dynamic Step-by-Step Form -->
<template id="stepTemplate_dynamic">
<div class="step card mb-3" data-step-index="">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Step <span class="step-number"></span></h5>
<button type="button" class="btn btn-danger btn-sm removeStepBtn_dynamic">Remove This Step</button>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Step Description (Optional):</label>
<input type="text" name="steps[X].description" class="form-control step-description_dynamic">
</div>
<h6>Signature Requirements for Step <span class="step-number"></span>:</h6>
<div class="requirementsContainer_dynamic ps-3" data-step-index="">
<!-- Requirements will be added here by JS -->
<p class="text-muted small"><em>Requirements for this step will appear here.</em></p>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm addRequirementBtn_dynamic mt-2" data-step-index="">Add Signature Requirement</button>
</div>
</div>
</template>
<template id="requirementTemplate_dynamic">
<div class="requirement card mb-2" data-req-index="">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong>Requirement <span class="req-number"></span></strong>
<button type="button" class="btn btn-danger btn-sm removeRequirementBtn_dynamic">Remove Requirement</button>
</div>
<div class="mb-2">
<label class="form-label">Message to Sign:</label>
<textarea name="steps[X].requirements[Y].message" rows="2" class="form-control req-message_dynamic" required></textarea>
</div>
<div>
<label class="form-label">Required Public Key:</label>
<input type="text" name="steps[X].requirements[Y].public_key" class="form-control req-pubkey_dynamic" required>
</div>
</div>
</div>
</template>
<!-- End of Templates -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<script>
// Basic script to handle 'Load into Form' for example scripts
document.addEventListener('DOMContentLoaded', function () {
const loadScriptButtons = document.querySelectorAll('.load-script-btn');
loadScriptButtons.forEach(button => {
button.addEventListener('click', function () {
const scriptContent = this.dataset.scriptContent;
const rhaiTextarea = document.querySelector('#rhai_script_content'); // Assuming this ID for the textarea
if (rhaiTextarea) {
rhaiTextarea.value = scriptContent;
// Optionally, scroll to the form or give some visual feedback
rhaiTextarea.focus();
alert('Script loaded into the textarea below!');
} else {
alert('Rhai script textarea not found on the page. It will be added soon.');
}
});
});
// --- Start of Dynamic Step-by-Step Form Logic ---
const stepsContainer_dynamic = document.getElementById('stepsContainer_dynamic');
const addStepBtn_dynamic = document.getElementById('addStepBtn_dynamic');
const stepTemplate_dynamic = document.getElementById('stepTemplate_dynamic');
const requirementTemplate_dynamic = document.getElementById('requirementTemplate_dynamic');
const form_dynamic = document.getElementById('createFlowForm_dynamic');
if (stepsContainer_dynamic && addStepBtn_dynamic && stepTemplate_dynamic && requirementTemplate_dynamic && form_dynamic) { // Check if all elements exist
const updateIndices_dynamic = () => {
const steps = stepsContainer_dynamic.querySelectorAll('.step'); // .step is the class on the root of the cloned template
steps.forEach((step, stepIdx) => {
step.dataset.stepIndex = stepIdx;
step.querySelector('.step-number').textContent = stepIdx + 1;
const descriptionInput = step.querySelector('.step-description_dynamic');
if(descriptionInput) descriptionInput.name = `steps[${stepIdx}].description`;
const addReqBtn = step.querySelector('.addRequirementBtn_dynamic');
if (addReqBtn) addReqBtn.dataset.stepIndex = stepIdx;
const requirements = step.querySelectorAll('.requirementsContainer_dynamic .requirement'); // .requirement is class on root of its template
requirements.forEach((req, reqIdx) => {
req.dataset.reqIndex = reqIdx;
req.querySelector('.req-number').textContent = reqIdx + 1;
const messageTextarea = req.querySelector('.req-message_dynamic');
if(messageTextarea) messageTextarea.name = `steps[${stepIdx}].requirements[${reqIdx}].message`;
const pubkeyInput = req.querySelector('.req-pubkey_dynamic');
if(pubkeyInput) pubkeyInput.name = `steps[${stepIdx}].requirements[${reqIdx}].public_key`;
});
});
// Remove the initial placeholder message if steps are present
const placeholder = stepsContainer_dynamic.querySelector('p.text-muted');
if (steps.length > 0 && placeholder) {
placeholder.style.display = 'none';
} else if (steps.length === 0 && placeholder) {
placeholder.style.display = 'block';
}
};
const addRequirement_dynamic = (currentStepElement, stepIndex) => {
const requirementsContainer = currentStepElement.querySelector('.requirementsContainer_dynamic');
if (!requirementsContainer) return;
const reqFragment = requirementTemplate_dynamic.content.cloneNode(true);
const newRequirement = reqFragment.querySelector('.requirement'); // .requirement is class on root
// Remove placeholder from requirements container if it exists
const reqPlaceholder = requirementsContainer.querySelector('p.text-muted.small');
if (reqPlaceholder) reqPlaceholder.style.display = 'none';
requirementsContainer.appendChild(newRequirement);
updateIndices_dynamic();
};
const addStep_dynamic = () => {
const stepFragment = stepTemplate_dynamic.content.cloneNode(true);
const newStep = stepFragment.querySelector('.step'); // .step is class on root
stepsContainer_dynamic.appendChild(newStep);
const currentStepIndex = stepsContainer_dynamic.querySelectorAll('.step').length - 1;
addRequirement_dynamic(newStep, currentStepIndex); // Add one requirement by default
updateIndices_dynamic();
};
stepsContainer_dynamic.addEventListener('click', (event) => {
if (event.target.classList.contains('removeStepBtn_dynamic')) {
event.target.closest('.step').remove();
if (stepsContainer_dynamic.querySelectorAll('.step').length === 0) {
// addStep_dynamic(); // Optionally re-add a step if all are removed
}
updateIndices_dynamic();
} else if (event.target.classList.contains('addRequirementBtn_dynamic')) {
const stepElement = event.target.closest('.step');
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
addRequirement_dynamic(stepElement, stepIndex);
} else if (event.target.classList.contains('removeRequirementBtn_dynamic')) {
const requirementElement = event.target.closest('.requirement');
const stepElement = event.target.closest('.step');
const requirementsContainer = stepElement.querySelector('.requirementsContainer_dynamic');
requirementElement.remove();
if (requirementsContainer.querySelectorAll('.requirement').length === 0) {
// const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
// addRequirement_dynamic(stepElement, stepIndex); // Optionally re-add a requirement
const reqPlaceholder = requirementsContainer.querySelector('p.text-muted.small');
if (reqPlaceholder) reqPlaceholder.style.display = 'block'; // Show placeholder if no reqs
}
updateIndices_dynamic();
}
});
addStepBtn_dynamic.addEventListener('click', addStep_dynamic);
// Add one step by default when the page loads, if no steps already (e.g. from server-side render)
if (stepsContainer_dynamic.children.length === 1 && stepsContainer_dynamic.firstElementChild.tagName === 'P') { // Only placeholder present
addStep_dynamic();
}
form_dynamic.addEventListener('submit', (event) => {
if (stepsContainer_dynamic.querySelectorAll('.step').length === 0) {
alert('Please add at least one step to the flow.');
event.preventDefault();
return;
}
const steps = stepsContainer_dynamic.querySelectorAll('.step');
for (let i = 0; i < steps.length; i++) {
if (steps[i].querySelectorAll('.requirementsContainer_dynamic .requirement').length === 0) {
alert(`Step ${i + 1} must have at least one signature requirement.`);
event.preventDefault();
return;
}
}
});
} else {
console.warn('One or more elements for the dynamic step-by-step form were not found. JS not initialized.');
}
// --- End of Dynamic Step-by-Step Form Logic ---
});
</script>
</body>
</html>