187 lines
No EOL
11 KiB
HTML
187 lines
No EOL
11 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ action }} Endpoint - Mock API Admin{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="content-header">
|
||
<h1><i class="bi bi-pencil-square"></i> {{ action }} Endpoint</h1>
|
||
<p class="lead">Configure a mock API endpoint.</p>
|
||
</div>
|
||
|
||
{% if error %}
|
||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||
{{ error }}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="row">
|
||
<div class="col-lg-8">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<form method="post" action="{{ form_action }}" id="endpoint-form">
|
||
{% if endpoint and endpoint.id %}
|
||
<input type="hidden" name="id" value="{{ endpoint.id }}">
|
||
{% endif %}
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label for="route" class="form-label">Route <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control {% if errors and errors.route %}is-invalid{% endif %}" id="route" name="route" value="{{ endpoint.route if endpoint else '' }}" placeholder="/api/users/{id}" required>
|
||
<div class="invalid-feedback">
|
||
{{ errors.route if errors and errors.route else 'Route must start with / and contain no consecutive slashes or ..' }}
|
||
</div>
|
||
<div class="form-text">
|
||
The path for the endpoint, e.g., <code>/api/users</code> or <code>/api/users/{id}</code>.
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label for="method" class="form-label">HTTP Method <span class="text-danger">*</span></label>
|
||
<select class="form-select {% if errors and errors.method %}is-invalid{% endif %}" id="method" name="method" required>
|
||
<option value="">Select method</option>
|
||
{% for m in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'] %}
|
||
<option value="{{ m }}" {% if endpoint and endpoint.method == m %}selected{% endif %}>{{ m }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<div class="invalid-feedback">
|
||
{{ errors.method if errors and errors.method else 'Please select a valid HTTP method.' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="response_body" class="form-label">Response Body <span class="text-danger">*</span></label>
|
||
<textarea class="form-control {% if errors and errors.response_body %}is-invalid{% endif %}" id="response_body" name="response_body" rows="8" required>{{ endpoint.response_body if endpoint else '{\n "message": "Hello, world!",\n "timestamp": {{ timestamp }}\n}' }}</textarea>
|
||
<div class="invalid-feedback">
|
||
{{ errors.response_body if errors and errors.response_body else 'Response body is required.' }}
|
||
</div>
|
||
<div class="form-text">
|
||
Jinja2 template. Available variables: <code>path_*</code>, <code>query_*</code>, <code>header_*</code>, <code>body_*</code>, <code>timestamp</code>, <code>datetime</code>, <code>request_id</code>, <code>method</code>, <code>url</code>, <code>client_host</code>, and any custom variables defined below.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-4 mb-3">
|
||
<label for="response_code" class="form-label">Response Code</label>
|
||
<input type="number" class="form-control {% if errors and errors.response_code %}is-invalid{% endif %}" id="response_code" name="response_code" value="{{ endpoint.response_code if endpoint else 200 }}" min="100" max="599">
|
||
<div class="invalid-feedback">
|
||
{{ errors.response_code if errors and errors.response_code else 'Response code must be between 100 and 599.' }}
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<label for="content_type" class="form-label">Content-Type</label>
|
||
<input type="text" class="form-control {% if errors and errors.content_type %}is-invalid{% endif %}" id="content_type" name="content_type" value="{{ endpoint.content_type if endpoint else 'application/json' }}" placeholder="application/json">
|
||
<div class="invalid-feedback">
|
||
{{ errors.content_type if errors and errors.content_type else 'Content-Type header value.' }}
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<label for="delay_ms" class="form-label">Delay (ms)</label>
|
||
<input type="number" class="form-control {% if errors and errors.delay_ms %}is-invalid{% endif %}" id="delay_ms" name="delay_ms" value="{{ endpoint.delay_ms if endpoint else 0 }}" min="0" max="30000">
|
||
<div class="invalid-feedback">
|
||
{{ errors.delay_ms if errors and errors.delay_ms else 'Artificial delay in milliseconds (0‑30000).' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" role="switch" id="is_active" name="is_active" {% if endpoint and endpoint.is_active %}checked{% endif %}>
|
||
<label class="form-check-label" for="is_active">Endpoint is active</label>
|
||
</div>
|
||
<div class="form-text">
|
||
Inactive endpoints will not be registered as routes.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="variables" class="form-label">Default Variables (JSON)</label>
|
||
<textarea class="form-control {% if errors and errors.variables %}is-invalid{% endif %}" id="variables" name="variables" rows="4">{{ endpoint.variables | tojson(indent=2) if endpoint and endpoint.variables else '{\n "app": "mockapi"\n}' }}</textarea>
|
||
<div class="invalid-feedback">
|
||
{{ errors.variables if errors and errors.variables else 'Must be valid JSON.' }}
|
||
</div>
|
||
<div class="form-text">
|
||
Default template variables as a JSON object. Will be merged with request context.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="headers" class="form-label">Custom Response Headers (JSON)</label>
|
||
<textarea class="form-control {% if errors and errors.headers %}is-invalid{% endif %}" id="headers" name="headers" rows="4">{{ endpoint.headers | tojson(indent=2) if endpoint and endpoint.headers else '{}' }}</textarea>
|
||
<div class="invalid-feedback">
|
||
{{ errors.headers if errors and errors.headers else 'Must be valid JSON.' }}
|
||
</div>
|
||
<div class="form-text">
|
||
Additional headers to include in the response, e.g., <code>{"X-Custom-Header": "value"}</code>.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||
<a href="/admin/endpoints" class="btn btn-outline-secondary me-md-2">Cancel</a>
|
||
<button type="submit" class="btn btn-primary">Save Endpoint</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Help</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<h6>Route Parameters</h6>
|
||
<p>Use <code>{param}</code> in the route to capture path parameters. Example: <code>/api/users/{id}</code> will make <code>id</code> available as <code>{{ '{{ id }}' }}</code> or <code>{{ '{{ path_id }}' }}</code>.</p>
|
||
|
||
<h6>Template Variables</h6>
|
||
<ul class="small">
|
||
<li><code>path_*</code> – path parameters</li>
|
||
<li><code>query_*</code> – query parameters</li>
|
||
<li><code>header_*</code> – request headers</li>
|
||
<li><code>body_*</code> – request body fields (if JSON)</li>
|
||
<li><code>timestamp</code> – Unix timestamp</li>
|
||
<li><code>datetime</code> – formatted date/time</li>
|
||
<li><code>request_id</code> – unique request ID</li>
|
||
</ul>
|
||
|
||
<h6>Example Response Body</h6>
|
||
<pre class="bg-light p-2 rounded"><code>{
|
||
"id": {{ '{{ path_id }}' }},
|
||
"name": "User {{ '{{ path_id }}' }}",
|
||
"timestamp": {{ '{{ timestamp }}' }},
|
||
"query": {{ '{{ query_search }}' | default('null') }}
|
||
}</code></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_scripts %}
|
||
<script>
|
||
// Simple JSON validation for textareas
|
||
function validateJSON(textareaId) {
|
||
const textarea = document.getElementById(textareaId);
|
||
try {
|
||
JSON.parse(textarea.value);
|
||
textarea.classList.remove('is-invalid');
|
||
return true;
|
||
} catch (e) {
|
||
textarea.classList.add('is-invalid');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
document.getElementById('variables')?.addEventListener('blur', () => validateJSON('variables'));
|
||
document.getElementById('headers')?.addEventListener('blur', () => validateJSON('headers'));
|
||
|
||
document.getElementById('endpoint-form')?.addEventListener('submit', function(e) {
|
||
let valid = true;
|
||
if (!validateJSON('variables')) valid = false;
|
||
if (!validateJSON('headers')) valid = false;
|
||
if (!valid) e.preventDefault();
|
||
});
|
||
</script>
|
||
{% endblock %} |