mirror of
https://github.com/simonw/datasette.git
synced 2026-06-11 19:46:58 +02:00
* Add web UI to edit and delete stored queries Stored query pages now offer Edit and Delete actions in the query actions menu, gated by the update-query and delete-query permissions. - New QueryEditView (GET/POST at /<db>/<query>/-/edit) renders a pre-filled form for editing a query's title, description, SQL and privacy, reusing the create-query analysis UI. Changing the SQL still requires execute-sql; metadata-only edits do not. - QueryDeleteView gains a GET confirmation page and HTML form POST that redirects to the query list, while keeping the existing JSON API. - New default query_actions hook adds the Edit/Delete links for stored (non-config, non-trusted) queries the actor is allowed to manage. Permission semantics (already enforced by default_query_permissions_sql) are surfaced in the UI: owners can always edit/delete their queries; non-private queries can be edited/deleted by any actor with the relevant permission; private queries remain owner-only. Shared the create-query form styles into _query_form_styles.html so the edit form can reuse them. Animated demo: https://github.com/simonw/datasette/pull/2764#issuecomment-4655694668 Closes #2760 https://claude.ai/code/session_019GU9g3pZAERukLKYNa4uAL
133 lines
5.9 KiB
HTML
133 lines
5.9 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Edit query: {{ name }}{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
{{- super() -}}
|
|
{% include "_codemirror.html" %}
|
|
{% include "_execute_write_analysis_styles.html" %}
|
|
{% include "_query_form_styles.html" %}
|
|
{% endblock %}
|
|
|
|
{% block body_class %}query-edit db-{{ database|to_css_class }}{% endblock %}
|
|
|
|
{% block crumbs %}
|
|
{{ crumbs.nav(request=request, database=database) }}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
<div class="query-create-page">
|
|
|
|
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">Edit query: {{ title or name }}</h1>
|
|
|
|
<form class="sql core query-create-form" action="{{ query_url }}/-/edit" method="post" data-analyze-url="{{ urls.database(database) }}/-/queries/analyze">
|
|
<div class="query-create-fields">
|
|
<p class="query-create-field"><label for="query-title">Title</label> <input id="query-title" name="title" type="text" value="{{ title or "" }}"></p>
|
|
<p class="query-create-field"><label>URL</label> <span class="query-create-url-static">{{ query_url }}</span></p>
|
|
<p class="query-create-field"><label for="query-description">Description</label> <textarea id="query-description" name="description" rows="3">{{ description or "" }}</textarea></p>
|
|
</div>
|
|
|
|
<p class="query-create-sql sql-editor"><textarea id="sql-editor" name="sql"{% if sql %} style="height: {{ sql.split("\n")|length + 2 }}em"{% endif %}>{{ sql }}</textarea></p>
|
|
|
|
<p class="query-create-options">
|
|
<span class="query-create-analysis-note" data-query-create-analysis-note aria-live="polite">{% if analysis_error %}This query cannot be saved until the SQL is valid.{% elif not has_sql %}Enter SQL to analyze this query.{% elif analysis_is_write %}This query updates data in the database.{% else %}This is a read-only query.{% endif %}</span>
|
|
<input type="hidden" name="is_private" value="0">
|
|
<label><input type="checkbox" name="is_private" value="1"{% if is_private %} checked{% endif %}> Private</label>
|
|
<span class="query-create-option-note">Queries marked private can only be seen and edited by you, their owner.</span>
|
|
</p>
|
|
<p class="query-create-submit"><input type="submit" value="Save changes" data-query-create-submit{% if save_disabled %} disabled{% endif %}> <a href="{{ query_url }}">Cancel</a></p>
|
|
|
|
<div class="query-create-analysis" id="query-create-analysis-section"{% if not has_sql %} hidden{% endif %}>
|
|
{% if has_sql %}
|
|
<h2>Query operations</h2>
|
|
{% if analysis_error %}
|
|
<p class="message-error">{{ analysis_error }}</p>
|
|
{% elif analysis_rows %}
|
|
<div class="table-wrapper"><table class="execute-write-analysis">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Operation</th>
|
|
<th scope="col">Database</th>
|
|
<th scope="col">Table</th>
|
|
<th scope="col">Required permission</th>
|
|
<th scope="col">Allowed</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in analysis_rows %}
|
|
<tr>
|
|
<td><code>{{ row.operation }}</code></td>
|
|
<td><code>{{ row.database }}</code></td>
|
|
<td><code>{{ row.table }}</code></td>
|
|
<td>{% if row.required_permission %}<code>{{ row.required_permission }}</code>{% else %}<span class="execute-write-analysis-na">n/a</span>{% endif %}</td>
|
|
<td>{% if row.allowed is none %}<span class="execute-write-analysis-na">n/a</span>{% elif row.allowed %}<span class="execute-write-analysis-allowed">yes</span>{% else %}<span class="execute-write-analysis-denied">no</span>{% endif %}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table></div>
|
|
{% else %}
|
|
<p>Analysis will show each affected table and required permission.</p>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</form>
|
|
|
|
</div>
|
|
|
|
{% include "_codemirror_foot.html" %}
|
|
{% include "_sql_parameter_scripts.html" %}
|
|
{% include "_execute_write_analysis_scripts.html" %}
|
|
|
|
<script>
|
|
window.addEventListener("DOMContentLoaded", () => {
|
|
const form = document.querySelector("form.sql.core");
|
|
const analysisSection = document.querySelector("#query-create-analysis-section");
|
|
const submitButton = form
|
|
? form.querySelector("[data-query-create-submit]")
|
|
: null;
|
|
const analysisNote = form
|
|
? form.querySelector("[data-query-create-analysis-note]")
|
|
: null;
|
|
|
|
function updateAnalysisNote(data) {
|
|
if (!analysisNote) {
|
|
return;
|
|
}
|
|
if (data.analysis_error) {
|
|
analysisNote.textContent = "This query cannot be saved until the SQL is valid.";
|
|
} else if (data.has_sql === false) {
|
|
analysisNote.textContent = "Enter SQL to analyze this query.";
|
|
} else if (data.analysis_is_write) {
|
|
analysisNote.textContent = "This query updates data in the database.";
|
|
} else {
|
|
analysisNote.textContent = "This is a read-only query.";
|
|
}
|
|
}
|
|
|
|
window.datasetteSqlParameters.setupSqlParameterRefresh({
|
|
form,
|
|
url: form.dataset.analyzeUrl,
|
|
renderParameters: false,
|
|
onData(data) {
|
|
window.datasetteSqlAnalysis.renderAnalysis(analysisSection, data);
|
|
if (submitButton) {
|
|
submitButton.disabled = data.save_disabled;
|
|
}
|
|
updateAnalysisNote(data);
|
|
},
|
|
onError(error) {
|
|
window.datasetteSqlAnalysis.renderAnalysis(analysisSection, {
|
|
analysis_error: error.message,
|
|
analysis_rows: [],
|
|
});
|
|
if (submitButton) {
|
|
submitButton.disabled = true;
|
|
}
|
|
updateAnalysisNote({ analysis_error: error.message });
|
|
},
|
|
});
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|