{% extends "base_content.html" %}
{% block title %}Explore {{ entity }}/{{ database }}{% endblock %}
{% block page_content %}
<div class="max-w-screen-xl mx-auto px-6">
<div class="max-w-screen-xl mx-auto">
<div class="breadcrumbs mb-4">
<a href="/{{ entity }}" class="hover:underline">{{ entity }}</a> /
<span class="font-semibold">{{ database }}</span> ({{ database_type }})
</div>
<ul data-uk-tab class="mb-6" id="database-tabs">
<li><a class="px-4 pb-3 pt-2" href="#query">Query</a></li>
{% if can_manage_database %}
<li><a class="px-4 pb-3 pt-2" href="#sharing">Sharing</a></li>
<li><a class="px-4 pb-3 pt-2" href="#snapshots">Snapshots</a></li>
{% endif %}
</ul>
<ul class="uk-switcher mt-4">
<li>
{% if highest_query_access_level %}
<div class="query-interface">
<h3 class="text-lg font-medium mb-2">Database querying</h3>
<p class="text-muted-foreground mb-4">Select, add, and update data.</p>
<form
id="query-form"
class="mb-4"
action="/{{ entity }}/{{ database }}/query"
method="post"
hx-post="/{{ entity }}/{{ database }}/query"
hx-target="#query-results"
hx-target-400="#query-results"
hx-swap="innerHTML">
<div class="mb-2">
<textarea id="query" name="query" rows="5"
class="p-4 w-full border rounded focus:border-blue-500"
placeholder="Enter a SQL query, like 'SELECT * FROM your_table LIMIT 10'"></textarea>
</div>
<div>
<button type="submit" class="uk-btn uk-btn-primary" disabled id="run-query-btn">
Run query
</button>
<script>
let lastExecutedQuery = '';
function showStaleOverlay() {
const overlays = document.querySelectorAll('#query-results .stale-overlay');
overlays.forEach(overlay => {
overlay.classList.remove('hidden');
});
}
function hideStaleOverlay() {
const overlays = document.querySelectorAll('#query-results .stale-overlay');
overlays.forEach(overlay => {
overlay.classList.add('hidden');
});
}
function hasQueryResults() {
return document.querySelector('#query-results .query-results-wrapper, #query-results .query-error-wrapper') !== null;
}
document.addEventListener('DOMContentLoaded', function() {
const queryTextarea = document.getElementById('query');
const runButton = document.getElementById('run-query-btn');
const queryForm = document.getElementById('query-form');
runButton.disabled = queryTextarea.value.trim() === '';
queryTextarea.addEventListener('input', function() {
const currentQuery = this.value;
runButton.disabled = currentQuery.trim() === '';
if (hasQueryResults() && currentQuery !== lastExecutedQuery && lastExecutedQuery !== '') {
showStaleOverlay();
}
});
document.body.addEventListener('htmx:beforeRequest', function(event) {
if (event.target === queryForm) {
hideStaleOverlay();
}
});
document.body.addEventListener('htmx:afterRequest', function(event) {
if (event.target === queryForm) {
lastExecutedQuery = queryTextarea.value;
}
});
});
</script>
</div>
</form>
<div id="query-results">
</div>
</div>
{% else %}
<div class="uk-alert" data-uk-alert="">
<div class="uk-alert-title">You don't have query access to this database.</div>
{% if logged_in_entity %}
<p>You can request access from the database owner or fork a copy.</p>
{% else %}
<p>
<a href="/log_in?next=/{{ entity }}/{{ database }}" class="underline">Login</a>
is required for query access.
</p>
{% endif %}
</div>
{% endif %}
</li>
<li>
<div class="sharing-interface">
<h3 class="text-lg font-medium mb-2">Database sharing</h3>
<p class="text-muted-foreground mb-4">Manage who can access this database and what permissions they have.</p>
<div class="uk-card uk-card-default mb-4">
<div class="uk-card-header">
<h4 class="uk-card-title">Public sharing level</h4>
</div>
<div class="uk-card-body">
<form id="public-sharing-form" class="space-y-4">
<div class="uk-btn-group" data-uk-button-radio>
<button
type="button"
class="uk-btn uk-btn-default{% if public_sharing_level == 'no-access' %} uk-active{% endif %}"
data-value="no-access"
onclick="setPublicSharingLevel(this, 'no-access')">
Private
</button>
<button
type="button"
class="uk-btn uk-btn-default{% if public_sharing_level == 'fork' %} uk-active{% endif %}"
data-value="fork"
onclick="setPublicSharingLevel(this, 'fork')">
Forkable
</button>
<button
type="button"
class="uk-btn uk-btn-default{% if public_sharing_level == 'read-only' %} uk-active{% endif %}"
data-value="read-only"
onclick="setPublicSharingLevel(this, 'read-only')">
Read-only
</button>
</div>
<input type="hidden" id="public-sharing-level-value" name="public_sharing_level" value="">
<div>
<button type="button" id="update-public-sharing-btn" class="uk-btn uk-btn-primary" disabled
hx-post="/{{ entity }}/{{ database }}/update_public_sharing"
hx-target="#public-sharing-results"
hx-target-400="#public-sharing-results"
hx-swap="innerHTML"
hx-include="#public-sharing-level-value">
Update sharing level
</button>
</div>
</form>
<div id="public-sharing-results" class="mt-2"></div>
</div>
</div>
<div class="uk-card uk-card-default">
<div class="uk-card-header">
<h4 class="uk-card-title">Share with specific user</h4>
</div>
<div class="uk-card-body">
<form id="entity-sharing-form" class="space-y-4"
hx-post="/{{ entity }}/{{ database }}/share"
hx-target="#sharing-results"
hx-target-400="#sharing-results"
hx-swap="innerHTML">
<div class="flex flex-col md:flex-row md:gap-4">
<div class="flex-none md:w-1/3">
<label for="share-entity" class="block text-sm font-medium mb-1">Username</label>
<input
type="text"
id="share-entity"
name="entity"
class="p-2 border rounded focus:border-blue-500 w-full"
placeholder="Enter username"
onInput="checkEntitySharingForm()"
required>
</div>
<div class="flex-grow mt-2 md:mt-0">
<label class="block text-sm font-medium mb-1">Access level</label>
<div class="uk-btn-group" data-uk-button-radio>
<button
type="button"
class="uk-btn uk-btn-default"
data-value="read-only"
onclick="setEntitySharingLevel(this, 'read-only')">
Read-only
</button>
<button
type="button"
class="uk-btn uk-btn-default"
data-value="read-write"
onclick="setEntitySharingLevel(this, 'read-write')">
Read-write
</button>
<button
type="button"
class="uk-btn uk-btn-default"
data-value="manager"
onclick="setEntitySharingLevel(this, 'manager')">
Manager
</button>
</div>
<input type="hidden" id="entity-sharing-level-value" name="sharing_level" value="">
</div>
</div>
<div>
<button type="submit" id="share-entity-btn" class="uk-btn uk-btn-primary" disabled>
Update access
</button>
</div>
</form>
<div id="sharing-results" class="mt-2"></div>
<div id="share-list-container">
<div class="mt-4 text-center">
<div uk-spinner></div>
<p class="text-sm text-muted-foreground mt-2">Loading permissions...</p>
</div>
</div>
</div>
</div>
<div id="remove-share-modal" class="uk-flex-top" data-uk-modal>
<div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
<h2 class="uk-modal-title">Remove access</h2>
<p class="mt-1">Are you sure you want to remove access for <strong id="remove-username"></strong>?</p>
<p class="uk-text-right mt-4">
<button class="uk-btn uk-btn-default uk-modal-close" type="button">Cancel</button>
<button
class="uk-btn uk-btn-destructive"
id="confirm-remove-btn"
type="button"
onclick="removePermission()">
Remove access
</button>
</p>
<form id="remove-share-form" style="display: none;"
hx-post="/{{ entity }}/{{ database }}/share"
hx-target="#sharing-results"
hx-target-400="#sharing-results"
hx-swap="innerHTML">
<input type="hidden" name="entity" id="remove-entity-input">
<input type="hidden" name="sharing_level" value="no-access">
</form>
</div>
</div>
<script>
let originalPublicSharingLevel = '';
let selectedPublicSharingLevel = '';
let selectedEntitySharingLevel = '';
let removeEntityName = '';
let restoreSnapshotId = '';
function setPublicSharingLevel(button, value) {
selectedPublicSharingLevel = value;
document.getElementById('public-sharing-level-value').value = value;
const buttons = button.parentElement.querySelectorAll('button');
buttons.forEach(btn => {
btn.classList.remove('uk-active');
});
button.classList.add('uk-active');
const updateBtn = document.getElementById('update-public-sharing-btn');
updateBtn.disabled = (value === originalPublicSharingLevel);
}
function setEntitySharingLevel(button, value) {
selectedEntitySharingLevel = value;
document.getElementById('entity-sharing-level-value').value = value;
const buttons = button.parentElement.querySelectorAll('button');
buttons.forEach(btn => {
btn.classList.remove('uk-active');
});
button.classList.add('uk-active');
checkEntitySharingForm();
}
function checkEntitySharingForm() {
const entityInput = document.getElementById('share-entity');
const submitBtn = document.getElementById('share-entity-btn');
const hasEntity = entityInput.value.trim() !== '';
const hasLevel = selectedEntitySharingLevel !== '';
submitBtn.disabled = !(hasEntity && hasLevel);
}
function editPermission(entityName, currentLevel) {
document.getElementById('share-entity').value = entityName;
const entitySharingForm = document.getElementById('entity-sharing-form');
const levelButton = entitySharingForm.querySelector(`[data-value="${currentLevel}"]`);
if (levelButton) {
setEntitySharingLevel(levelButton, currentLevel);
}
document.getElementById('entity-sharing-form').scrollIntoView({ behavior: 'smooth' });
}
function confirmRemovePermission(entityName) {
removeEntityName = entityName;
document.getElementById('remove-username').textContent = entityName;
document.getElementById('remove-entity-input').value = entityName;
document.getElementById('sharing-results').innerHTML = '';
UIkit.modal('#remove-share-modal').show();
}
function removePermission() {
htmx.trigger('#remove-share-form', 'submit');
UIkit.modal('#remove-share-modal').hide();
}
function loadPermissions() {
fetch('/{{ entity }}/{{ database }}/permissions')
.then(response => response.text())
.then(html => {
document.getElementById('share-list-container').innerHTML = html;
})
.catch(error => {
console.error('Error loading share list:', error);
document.getElementById('share-list-container').innerHTML =
'<div class="mt-4"><p class="text-sm text-red-600">Error loading permissions.</p></div>';
});
}
function loadSnapshots() {
fetch('/{{ entity }}/{{ database }}/snapshots')
.then(response => response.text())
.then(html => {
document.getElementById('snapshots-list-container').innerHTML = html;
})
.catch(error => {
console.error('Error loading snapshots:', error);
document.getElementById('snapshots-list-container').innerHTML =
'<div class="mt-4"><p class="text-sm text-red-600">Error loading snapshots.</p></div>';
});
}
function confirmRestoreSnapshot(snapshotId, snapshotDate) {
restoreSnapshotId = snapshotId;
const truncatedId = snapshotId.substring(0, 10) + '...';
document.getElementById('restore-snapshot-id').textContent = truncatedId;
document.getElementById('restore-snapshot-date').textContent = snapshotDate;
document.getElementById('restore-snapshot-input').value = snapshotId;
document.getElementById('restore-snapshot-results').innerHTML = '';
UIkit.modal('#restore-snapshot-modal').show();
}
function restoreSnapshot() {
htmx.trigger('#restore-snapshot-form', 'submit');
UIkit.modal('#restore-snapshot-modal').hide();
}
function setActiveTabFromHash() {
const hash = window.location.hash;
const tabs = document.querySelectorAll('#database-tabs li');
const tabContents = document.querySelectorAll('.uk-switcher li');
let activeIndex = 0; if (hash === '#sharing' && tabs.length > 1) {
activeIndex = 1;
loadPermissions();
} else if (hash === '#snapshots' && tabs.length > 2) {
activeIndex = 2;
loadSnapshots();
}
tabs.forEach(tab => tab.classList.remove('uk-active'));
tabContents.forEach(content => content.style.display = 'none');
if (tabs[activeIndex]) {
tabs[activeIndex].classList.add('uk-active');
}
if (tabContents[activeIndex]) {
tabContents[activeIndex].style.display = 'block';
}
}
window.addEventListener('hashchange', setActiveTabFromHash);
function addTabClickHandlers() {
const tabLinks = document.querySelectorAll('#database-tabs a');
tabLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const hash = this.getAttribute('href');
window.location.hash = hash;
});
});
}
document.addEventListener('DOMContentLoaded', function() {
addTabClickHandlers();
setActiveTabFromHash();
originalPublicSharingLevel = '{{ public_sharing_level }}';
const currentButton = document.querySelector('[data-value="{{ public_sharing_level }}"]');
if (currentButton) {
setPublicSharingLevel(currentButton, '{{ public_sharing_level }}');
}
document.body.addEventListener('htmx:beforeRequest', function(event) {
if (event.target.id === 'update-public-sharing-btn') {
document.getElementById('public-sharing-results').innerHTML = '';
} else if (event.target.closest('#entity-sharing-form')) {
document.getElementById('sharing-results').innerHTML = '';
}
});
document.body.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.xhr.status === 200 && event.target.id === 'update-public-sharing-btn') {
originalPublicSharingLevel = selectedPublicSharingLevel;
event.target.disabled = true;
} else if (event.detail.xhr.status === 200 &&
(event.target.closest('#entity-sharing-form') || event.target.closest('#remove-share-form'))) {
loadPermissions();
if (event.target.closest('#entity-sharing-form')) {
document.getElementById('share-entity').value = '';
selectedEntitySharingLevel = '';
document.getElementById('entity-sharing-level-value').value = '';
document.querySelectorAll('#entity-sharing-form .uk-btn-group button').forEach(btn => {
btn.classList.remove('uk-active');
});
checkEntitySharingForm();
}
if (event.target.closest('#remove-share-form')) {
UIkit.modal('#remove-share-modal').hide();
}
} else if (event.detail.xhr.status === 200 && event.target.closest('#restore-snapshot-form')) {
loadSnapshots();
UIkit.modal('#restore-snapshot-modal').hide();
}
});
});
</script>
</div>
</li>
<li>
<div class="snapshots-interface">
<h3 class="text-lg font-medium mb-2">Database snapshots</h3>
<p class="text-muted-foreground mb-4">View and restore database snapshots.</p>
<div id="restore-snapshot-results" class="mb-4"></div>
<div id="snapshots-list-container">
<div class="mt-4 text-center">
<div uk-spinner></div>
<p class="text-sm text-muted-foreground mt-2">Loading snapshots...</p>
</div>
</div>
<div id="restore-snapshot-modal" class="uk-flex-top" data-uk-modal>
<div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
<h2 class="uk-modal-title">Restore snapshot</h2>
<p class="mt-1">Are you sure you want to restore the database from snapshot <code id="restore-snapshot-id"></code> (Created: <span id="restore-snapshot-date"></span>)?</p>
<p class="uk-text-muted text-sm mt-2">This will replace the current database content with the snapshot data.</p>
<p class="uk-text-right mt-4">
<button class="uk-btn uk-btn-default uk-modal-close" type="button">Cancel</button>
<button
class="uk-btn uk-btn-primary"
id="confirm-restore-btn"
type="button"
onclick="restoreSnapshot()">
Restore database
</button>
</p>
<form id="restore-snapshot-form" style="display: none;"
hx-post="/{{ entity }}/{{ database }}/restore_snapshot"
hx-target="#restore-snapshot-results"
hx-target-400="#restore-snapshot-results"
hx-swap="innerHTML">
<input type="hidden" name="snapshot_id" id="restore-snapshot-input">
</form>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
{% endblock %}