Re-organize gradle modules.

This commit is contained in:
Greyson Parrelli
2025-12-31 11:56:13 -05:00
committed by jeffrey-signal
parent f4863efb2e
commit e162eb27c7
1444 changed files with 111 additions and 144 deletions

View File

@@ -0,0 +1,81 @@
<html>
{{> partials/head title="Browse" }}
<body>
{{> partials/prefix isBrowse=true}}
<!-- Table Selector -->
<form action="browse" method="post">
<select name="table">
{{#each tableNames}}
<option value="{{this}}" {{eq table this yes="selected" no=""}}>{{this}}</option>
{{/each}}
</select>
<input type="hidden" name="db" value="{{database}}" />
<input type="submit" value="browse" />
</form>
<!-- Data -->
{{#if table}}
<h1>{{table}}</h1>
{{else}}
<h1>Data</h1>
{{/if}}
{{#if queryResult}}
<p>Viewing rows {{pagingData.startRow}}-{{pagingData.endRow}} of {{pagingData.rowCount}}.</p>
<!-- Paging Controls -->
<form action="browse" method="post">
<input type="hidden" name="table" value="{{table}}" />
<input type="hidden" name="pageSize" value="{{pagingData.pageSize}}" />
<input type="hidden" name="pageIndex" value="{{pagingData.pageIndex}}" />
<input type="submit" name="action" value="first" {{#if pagingData.firstPage}}disabled{{/if}} />
<input type="submit" name="action" value="previous" {{#if pagingData.firstPage}}disabled{{/if}} />
<input type="submit" name="action" value="next" {{#if pagingData.lastPage}}disabled{{/if}} />
<input type="submit" name="action" value="last" {{#if pagingData.lastPage}}disabled{{/if}} />
</form>
<!-- Data Rows -->
<table>
<tr>
{{#each queryResult.columns}}
<th>{{this}}</th>
{{/each}}
</tr>
{{#each queryResult.rows}}
<tr>
{{#each this}}
<td><pre>{{#if (eq this null)}}<em class="null">null</em>{{else}}{{{this}}}{{/if}}</pre></td>
{{/each}}
</tr>
{{/each}}
</table>
{{else}}
<div>Select a table from above and click 'browse'.</div>
{{/if}}
<br />
<!-- Paging Controls -->
<form action="browse" method="post">
<input type="hidden" name="table" value="{{table}}" />
<input type="hidden" name="pageSize" value="{{pagingData.pageSize}}" />
<input type="hidden" name="pageIndex" value="{{pagingData.pageIndex}}" />
<input type="submit" name="action" value="first" {{#if pagingData.firstPage}}disabled{{/if}} />
<input type="submit" name="action" value="previous" {{#if pagingData.firstPage}}disabled{{/if}} />
<input type="submit" name="action" value="next" {{#if pagingData.lastPage}}disabled{{/if}} />
<input type="submit" name="action" value="last" {{#if pagingData.lastPage}}disabled{{/if}} />
</form>
{{> partials/suffix}}
{{> partials/tooltips}}
<script type="text/javascript">
initializeTooltips();
</script>
</body>
</html>

View File

@@ -0,0 +1,165 @@
:root {
--background-color: #fff;
--table-header-background-color: #f0f0f0;
--text-color: #000;
--border-color: #000;
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #333;
--table-header-background-color: #444;
--text-color: #fff;
--border-color: #888;
}
a {
color: #aaf;
}
}
[data-theme="dark"] {
--background-color: #333;
--table-header-background-color: #444;
--text-color: #fff;
--border-color: #888;
a {
color: #aaf;
}
}
html, body {
font-family: 'Roboto Mono', monospace;
font-variant-ligatures: none;
font-size: 12px;
width: 100%;
background: var(--background-color);
color: var(--text-color);
}
body {
margin: 0;
padding: 8px;
}
select, input, button {
font-family: 'Roboto Mono', monospace;
font-variant-ligatures: none;
font-size: 1rem;
}
table, th, td {
border: 1px solid black;
font-size: 1rem;
}
th, td {
padding: 8px;
}
th {
position: sticky;
top: 0;
background: var(--background-color);
}
.handsontable th {
color: var(--text-color);
background: var(--table-header-background-color);
}
.handsontable thead th.ht__highlight {
color: var(--text-color);
background: var(--table-header-background-color);
}
.handsontable td {
color: var(--text-color);
background: var(--background-color);
}
.query-input {
width: calc(100% - 18px);
border: 1px solid var(--border-color);
border-radius: 4px;
height: 200px;
margin-bottom: 2px;
}
li.active {
font-weight: bold;
}
ol.tabs {
margin: 16px 0px 8px 0px;
padding: 0px;
font-size: 0px;
}
.tabs li {
list-style-type: none;
display: inline-block;
padding: 8px;
border-bottom: 1px solid var(--border-color);
font-size: 1rem;
}
.tabs li.active {
border: 1px solid var(--border-color);
border-bottom: 0;
}
.tabs a {
text-decoration: none;
color: var(--text-color);
}
.collapse-header {
cursor: pointer;
}
.collapse-header:before {
content: "⯈ ";
font-size: 1rem;
}
.collapse-header.active:before {
content: "⯆ ";
font-size: 1rem;
}
h2.collapse-header, h2.collapse-header+div {
margin-left: 16px;
}
.hidden {
display: none;
}
table.device-info {
margin-bottom: 16px;
}
table.device-info, table.device-info tr, table.device-info td {
border: 0;
padding: 2px;
font-size: 0.75rem;
font-style: italic;
}
.null {
color: #666
}
#grow-button {
width: calc(100% - 18px);
height: 0.75rem;
margin-bottom: 8px;
}
#theme-toggle {
position: absolute;
top: 8px;
right: 8px;
}

View File

@@ -0,0 +1,73 @@
td {
overflow: visible !important;
}
td pre {
overflow: visible !important;
}
.popup {
position: relative;
cursor: help;
text-decoration: underline dotted;
display: inline-block;
}
.popup .tooltip {
visibility: hidden;
background-color: #555;
color: #fff;
text-align: left;
border-radius: 4px;
padding: 8px 12px;
position: absolute;
z-index: 1000;
bottom: 200%;
left: 50%;
transform: translateX(-50%);
white-space: pre-wrap;
max-width: 600px;
min-width: 200px;
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
line-height: 1.5;
}
.popup .tooltip.align-left {
left: 0;
transform: translateX(0);
}
.popup .tooltip.align-right {
left: auto;
right: 0;
transform: translateX(0);
}
.popup .tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}
.popup .tooltip.align-left::after {
left: 20px;
margin-left: 0;
}
.popup .tooltip.align-right::after {
left: auto;
right: 20px;
margin-left: 0;
}
.popup:hover .tooltip {
visibility: visible;
opacity: 1;
}

View File

@@ -0,0 +1,8 @@
<html>
{{> partials/head title="Error :(" }}
<body>
Hit an exception while trying to serve the page :(
<hr/>
{{{this}}}
</body>
</html>

View File

@@ -0,0 +1,33 @@
function init() {
document.querySelectorAll('.collapse-header').forEach(elem => {
elem.onclick = () => {
console.log('clicked');
elem.classList.toggle('active');
document.getElementById(elem.dataset.for).classList.toggle('hidden');
document.dispatchEvent(new CustomEvent('header-toggle', {
detail: document.getElementById(elem.dataset.for)
}))
}
});
document.querySelector('#database-selector').onchange = (e) => {
window.location.href = window.location.href.split('?')[0] + '?db=' + e.target.value;
}
document.querySelector('#theme-toggle').onclick = function() {
if (document.body.getAttribute('data-theme') === 'dark') {
document.body.removeAttribute('data-theme');
localStorage.removeItem('theme');
} else {
document.body.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
}
}
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.body.setAttribute('data-theme', savedTheme);
}
}
init();

View File

@@ -0,0 +1,324 @@
function adjustTooltipPosition(tooltipSpan) {
// Reset any previous alignment classes
tooltipSpan.classList.remove('align-left', 'align-right');
// Get tooltip and viewport dimensions
const tooltipRect = tooltipSpan.getBoundingClientRect();
const viewportWidth = window.innerWidth;
// Check if tooltip overflows on the left
if (tooltipRect.left < 0) {
tooltipSpan.classList.add('align-left');
}
// Check if tooltip overflows on the right
else if (tooltipRect.right > viewportWidth) {
tooltipSpan.classList.add('align-right');
}
}
function initializeRecipientTooltips() {
const popupElements = document.querySelectorAll('.popup[data-recipient-id]');
const cache = {};
popupElements.forEach(element => {
const recipientId = element.getAttribute('data-recipient-id');
const tooltipSpan = element.querySelector('.tooltip');
if (!recipientId || !tooltipSpan) {
return;
}
let hasLoaded = false;
element.addEventListener('mouseenter', async function() {
if (hasLoaded) {
return;
}
if (cache[recipientId]) {
tooltipSpan.textContent = cache[recipientId];
adjustTooltipPosition(tooltipSpan);
hasLoaded = true;
return;
}
try {
const response = await fetch(`/api?api=recipient&id=${recipientId}`);
if (!response.ok) {
tooltipSpan.textContent = `Error loading recipient`;
hasLoaded = true;
return;
}
const data = await response.json();
const lines = [];
// Name (primary)
let nameLine = data.isGroup ? `Group: ${data.name || recipientId}` : `Name: ${data.name || recipientId}`;
if (data.nickname) {
nameLine += ` (${data.nickname})`;
}
lines.push(nameLine);
// Group-specific information
if (data.isGroup) {
if (data.groupType) {
lines.push(`Type: ${data.groupType}`);
}
if (data.groupId) {
lines.push(`Group ID: ${data.groupId}`);
}
if (data.participantCount !== null && data.participantCount !== undefined) {
const memberText = data.participantCount === 1 ? 'member' : 'members';
lines.push(`Members: ${data.participantCount} ${memberText}`);
}
const groupStatus = data.isActiveGroup ? 'Active' : 'Inactive';
lines.push(`Status: ${groupStatus}`);
} else {
// Individual-specific information
// ACI
if (data.aci) {
lines.push(`ACI: ${data.aci}`);
}
// PNI
if (data.pni) {
lines.push(`PNI: ${data.pni}`);
}
// Username
if (data.username) {
lines.push(`Username: @${data.username}`);
}
// Phone number
if (data.e164) {
lines.push(`Phone: ${data.e164}`);
}
// About/Bio
if (data.about) {
const aboutText = data.aboutEmoji ? `${data.aboutEmoji} ${data.about}` : data.about;
lines.push(`About: ${aboutText}`);
}
// Note
if (data.note) {
lines.push(`Note: 📝 ${data.note}`);
}
// Status indicators
const statusParts = [];
if (data.isBlocked) {
statusParts.push('🚫 Blocked');
}
if (data.isSystemContact) {
statusParts.push('📱 Contact');
}
if (statusParts.length > 0) {
lines.push(`Status: ${statusParts.join(' | ')}`);
}
}
const displayText = lines.join('\n');
tooltipSpan.textContent = displayText;
adjustTooltipPosition(tooltipSpan);
cache[recipientId] = displayText;
hasLoaded = true;
} catch (error) {
console.error('Error fetching recipient data:', error);
tooltipSpan.textContent = `Error: ${error.message}`;
hasLoaded = true;
}
});
});
}
function initializeThreadTooltips() {
const popupElements = document.querySelectorAll('.popup[data-thread-id]');
const cache = {};
popupElements.forEach(element => {
const threadId = element.getAttribute('data-thread-id');
const tooltipSpan = element.querySelector('.tooltip');
if (!threadId || !tooltipSpan) {
return;
}
let hasLoaded = false;
element.addEventListener('mouseenter', async function() {
if (hasLoaded) {
return;
}
if (cache[threadId]) {
tooltipSpan.textContent = cache[threadId];
adjustTooltipPosition(tooltipSpan);
hasLoaded = true;
return;
}
try {
const response = await fetch(`/api?api=thread&id=${threadId}`);
if (!response.ok) {
tooltipSpan.textContent = `Error loading thread`;
hasLoaded = true;
return;
}
const data = await response.json();
const lines = [];
// Thread ID
lines.push(`Thread ID: ${data.threadId}`);
// Recipient
lines.push(`Recipient: ${data.recipientName} (ID: ${data.recipientId})`);
// Unread count
if (data.unreadCount > 0) {
lines.push(`Unread: ${data.unreadCount}`);
}
// Unread self mentions
if (data.unreadSelfMentionsCount > 0) {
lines.push(`Unread Mentions: ${data.unreadSelfMentionsCount}`);
}
// Snippet
if (data.snippet) {
const snippetPreview = data.snippet.length > 50
? data.snippet.substring(0, 50) + '...'
: data.snippet;
lines.push(`Snippet: ${snippetPreview}`);
}
// Date
if (data.date) {
const date = new Date(data.date);
lines.push(`Last Activity: ${date.toLocaleString()}`);
}
// Status flags
const statusParts = [];
if (data.pinned) {
statusParts.push('📌 Pinned');
}
if (data.archived) {
statusParts.push('📦 Archived');
}
if (statusParts.length > 0) {
lines.push(`Status: ${statusParts.join(' | ')}`);
}
const displayText = lines.join('\n');
tooltipSpan.textContent = displayText;
adjustTooltipPosition(tooltipSpan);
cache[threadId] = displayText;
hasLoaded = true;
} catch (error) {
console.error('Error fetching thread data:', error);
tooltipSpan.textContent = `Error: ${error.message}`;
hasLoaded = true;
}
});
});
}
function initializeMessageTooltips() {
const popupElements = document.querySelectorAll('.popup[data-message-id]');
const cache = {};
popupElements.forEach(element => {
const messageId = element.getAttribute('data-message-id');
const tooltipSpan = element.querySelector('.tooltip');
if (!messageId || !tooltipSpan) {
return;
}
let hasLoaded = false;
element.addEventListener('mouseenter', async function() {
if (hasLoaded) {
return;
}
if (cache[messageId]) {
tooltipSpan.textContent = cache[messageId];
adjustTooltipPosition(tooltipSpan);
hasLoaded = true;
return;
}
try {
const response = await fetch(`/api?api=message&id=${messageId}`);
if (!response.ok) {
tooltipSpan.textContent = `Error loading message`;
hasLoaded = true;
return;
}
const data = await response.json();
const lines = [];
// Message ID
lines.push(`Message ID: ${data.messageId}`);
// Type
lines.push(`Type: ${data.type}`);
// From
lines.push(`From: ${data.fromRecipientName} (ID: ${data.fromRecipientId})`);
// To
if (data.toRecipientName) {
lines.push(`To: ${data.toRecipientName} (ID: ${data.toRecipientId})`);
}
// Thread ID
lines.push(`Thread ID: ${data.threadId}`);
// Body
if (data.body) {
const bodyPreview = data.body.length > 50
? data.body.substring(0, 50) + '...'
: data.body;
lines.push(`Body: ${bodyPreview}`);
}
// Dates
if (data.dateSent) {
const dateSent = new Date(data.dateSent);
lines.push(`Sent: ${dateSent.toLocaleString()}`);
}
if (data.dateReceived) {
const dateReceived = new Date(data.dateReceived);
lines.push(`Received: ${dateReceived.toLocaleString()}`);
}
const displayText = lines.join('\n');
tooltipSpan.textContent = displayText;
adjustTooltipPosition(tooltipSpan);
cache[messageId] = displayText;
hasLoaded = true;
} catch (error) {
console.error('Error fetching message data:', error);
tooltipSpan.textContent = `Error: ${error.message}`;
hasLoaded = true;
}
});
});
}
function initializeTooltips() {
initializeRecipientTooltips();
initializeThreadTooltips();
initializeMessageTooltips();
}

View File

@@ -0,0 +1,290 @@
<html>
{{> partials/head title="Home" }}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap" rel="stylesheet">
<style type="text/css">
html, body {
overflow: hidden;
}
h1.collapse-header {
font-size: 1.35rem;
}
h2.collapse-header {
font-size: 1.15rem;
}
#log-container {
width: calc(100% - 32px);
height: calc(100% - 325px);
overflow-y: scroll;
overflow-x: auto;
border: 1px solid black;
resize: vertical;
white-space: pre;
font-family: 'JetBrains Mono', monospace;
background-color: #2b2b2b;
margin-top: 8px;
border-radius: 5px;
border: 1px solid black;
padding: 8px;
}
#logs {
width: 100%;
}
.log-verbose {
color: #8a8a8a;
}
.log-debug {
color: #5ca72b;
}
.log-info{
color: #46bbb9;
}
.log-warn{
color: #d6cb37;
}
.log-error{
color: #ff6b68;
}
#follow-button.enabled {
opacity: 0.5;
}
#toolbar {
width: calc(100% - 14px);
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
}
#toolbar #filter-text {
flex-grow: 1;
margin-left: 8px;
}
#socket-status {
width: 16px;
height: 16px;
border-radius: 16px;
margin: 3px 0 3px 3px;
border: 1px solid black;
}
#socket-status.connected {
background-color: #5ca72b;
}
#socket-status.connecting {
background-color: #d6cb37;
}
#socket-status.disconnected {
background-color: #cc0000;
}
</style>
<body>
{{> partials/prefix isLogs=true}}
<div id="toolbar">
<button onclick="onFollowClicked()" id="follow-button">Follow</button>
<input type="text" id="filter-text" placeholder="Filter..." />
<div id="socket-status"></div>
</div>
<div id="log-container"></div>
{{> partials/suffix }}
<script>
const logs = []
const logTable = document.getElementById('logs')
const logContainer = document.getElementById('log-container')
const followButton = document.getElementById('follow-button')
const filterText = document.getElementById('filter-text')
const statusOrb = document.getElementById('socket-status')
let followLogs = false
let programaticScroll = false
let filter = null
function main() {
initWebSocket()
setFollowState(true)
logContainer.innerHTML = ''
filterText.addEventListener('input', onFilterChanged)
logContainer.addEventListener('scroll', () => {
if (programaticScroll) {
programaticScroll = false
} else {
setFollowState(false)
}
})
}
function onFollowClicked() {
setFollowState(true)
scrollToBottom()
}
function onFilterChanged(event) {
filter = event.target.value
logContainer.innerHTML = ''
logs
.filter(it => logMatches(it, filter))
.map(it => logToDiv(it))
.forEach(it => logContainer.appendChild(it))
setFollowState(true)
scrollToBottom()
}
function initWebSocket() {
const websocket = new WebSocket(`ws://${window.location.host}/logs/websocket`)
let keepAliveTimer = null
statusOrb.className = 'connecting'
websocket.onopen = () => {
console.log("[open] Connection established");
console.log("Sending to server");
statusOrb.className = 'connected'
keepAliveTimer = setInterval(() => websocket.send('keepalive'), 1000)
}
websocket.onclose = () => {
console.log('[close] Closed!')
statusOrb.className = 'disconnected'
if (keepAliveTimer != null) {
clearInterval(keepAliveTimer)
keepAliveTimer = null
}
setTimeout(() => initWebSocket(), 1000)
}
websocket.onmessage = onWebSocketMessage
}
function onWebSocketMessage(event) {
const log = JSON.parse(event.data)
logs.push(log)
if (logs.length > 5_000) {
logs.shift()
}
if (filter == null || logMatches(log, filter)) {
logContainer.appendChild(logToDiv(log))
}
if (followLogs) {
scrollToBottom()
}
}
function logToDiv(log) {
const div = document.createElement('div')
const linePrefix = `[${log.thread}] ${log.time} ${log.tag} ${log.level} `
let stackTraceString = log.stackTrace
if (stackTraceString != null) {
stackTraceString = ' \n' + stackTraceString
}
let textContent = `${linePrefix}${log.message || ''}${stackTraceString || ''}`
textContent = indentOverflowLines(textContent, linePrefix.length)
div.textContent = textContent
div.classList.add(levelToClass(log.level))
return div
}
function levelToClass(level) {
switch (level) {
case 'V': return 'log-verbose'
case 'D': return 'log-debug'
case 'I': return 'log-info'
case 'W': return 'log-warn'
case 'E': return 'log-error'
default: return ''
}
}
function setFollowState(value) {
if (followLogs === value) {
return
}
followLogs = value
if (followLogs) {
followButton.classList.add('enabled')
followButton.disabled = true
} else {
followButton.classList.remove('enabled')
followButton.disabled = false
}
}
function scrollToBottom() {
programaticScroll = true
logContainer.scrollTop = logContainer.scrollHeight
}
function indentOverflowLines(text, indent) {
const lines = text.split('\n')
if (lines.length > 1) {
const spaces = ' '.repeat(indent)
const overflow = lines.slice(1)
const indented = overflow.map(it => spaces + it).join('\n')
return lines[0] + '\n' + indented
} else {
return text
}
}
function logMatches(log, filter) {
if (log.tag != null && log.tag.indexOf(filter) >= 0) {
return true
}
if (log.message != null && log.message.indexOf(filter) >= 0) {
return true
}
if (log.stackTrace != null && log.stackTrace.indexOf(filter) >= 0) {
return true
}
return false
}
main()
</script>
</body>
</html>

View File

@@ -0,0 +1,107 @@
<html>
{{> partials/head title="Home" }}
<style type="text/css">
h1.collapse-header {
font-size: 1.35rem;
}
h2.collapse-header {
font-size: 1.15rem;
}
</style>
<body>
{{> partials/prefix isOverview=true}}
<h1 class="collapse-header" data-for="table-creates">Tables</h1>
<div id="table-creates" class="hidden">
{{#if tables}}
{{#each tables}}
<h2 class="collapse-header" data-for="table-create-{{@index}}">{{name}}</h2>
<div id="table-create-{{@index}}" class="hidden">{{{sql}}}</div>
{{/each}}
{{else}}
None.
{{/if}}
</div>
<h1 class="collapse-header" data-for="index-creates">Indices</h1>
<div id="index-creates" class="hidden">
{{#if indices}}
{{#each indices}}
<h2 class="collapse-header active" data-for="index-create-{{@index}}">{{name}}</h2>
<div id="index-create-{{@index}}">{{{sql}}}</div>
{{/each}}
{{else}}
None.
{{/if}}
</div>
<h1 class="collapse-header" data-for="trigger-creates">Triggers</h1>
<div id="trigger-creates" class="hidden">
{{#if triggers}}
{{#each triggers}}
<h2 class="collapse-header active" data-for="trigger-create-{{@index}}">{{name}}</h2>
<div id="trigger-create-{{@index}}">{{{sql}}}</div>
{{/each}}
{{else}}
None.
{{/if}}
</div>
<h1 class="collapse-header" data-for="foreign-key-creates">Foreign Keys</h1>
<div id="foreign-key-creates" class="hidden">
{{#if foreignKeys}}
<table>
<tr>
<th>Column</th>
<th>Depends On</th>
<th>On Delete</th>
</tr>
{{#each foreignKeys}}
<tr>
<td>{{table}}.{{column}}</td>
<td>{{dependsOnTable}}.{{dependsOnColumn}}</td>
<td>{{onDelete}}</td>
</tr>
{{/each}}
</table>
<h2>Without Labels</h2>
<pre class="mermaid">
flowchart LR
{{#each foreignKeys}}
id_{{table}}[{{table}}] --> id_{{dependsOnTable}}[{{dependsOnTable}}]
{{/each}}
</pre>
<h2>With Labels</h2>
<pre class="mermaid">
flowchart LR
{{#each foreignKeys}}
id_{{table}}[{{table}}] -- "{{column}} 🠖 {{dependsOnColumn}}" --> id_{{dependsOnTable}}[{{dependsOnTable}}]
{{/each}}
</pre>
{{else}}
None.
{{/if}}
</div>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: false });
document.addEventListener('header-toggle', (e) => {
if (e.detail.id === 'foreign-key-creates') {
mermaid.init('.mermaid')
}
})
</script>
{{> partials/suffix }}
</body>
</html>

View File

@@ -0,0 +1,14 @@
<head>
<title>Spinner - {{ title }}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌀</text></svg>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<link href="/css/main.css" rel="stylesheet">
<style type="text/css">
</style>
</head>

View File

@@ -0,0 +1,38 @@
<h1>SPINNER - {{environment}}</h1>
<table class="device-info">
{{#each deviceInfo}}
<tr>
<td>{{@key}}</td>
<td>{{this}}</td>
</tr>
{{/each}}
</table>
<div>
Download Trace: <a href="/trace">Link</a>
</div>
<button id="theme-toggle">Toggle theme</button>
<br />
<div>
Database:
<select id="database-selector">
{{#each databases}}
<option value="{{this}}" {{eq database this yes="selected" no=""}}>{{this}}</option>
{{/each}}
</select>
</div>
<ol class="tabs">
<li {{#if isOverview}}class="active"{{/if}}><a href="/?db={{database}}">Overview</a></li>
<li {{#if isBrowse}}class="active"{{/if}}><a href="/browse?db={{database}}">Browse</a></li>
<li {{#if isQuery}}class="active"{{/if}}><a href="/query?db={{database}}">Query</a></li>
<li {{#if isRecent}}class="active"{{/if}}><a href="/recent?db={{database}}">Recent</a></li>
<li {{#if isLogs}}class="active"{{/if}}><a href="/logs?db={{database}}">Logs</a></li>
{{#each plugins}}
<li {{#if (eq name activePlugin.name)}}class="active"{{/if}}><a href="{{path}}">{{name}}</a></li>
{{/each}}
</ol>

View File

@@ -0,0 +1 @@
<script src="/js/main.js" type="text/javascript"></script>

View File

@@ -0,0 +1,2 @@
<link rel="stylesheet" href="/css/tooltips.css">
<script src="/js/tooltips.js"></script>

View File

@@ -0,0 +1,34 @@
<html>
{{> partials/head title=activePlugin.name }}
<body>
{{> partials/prefix}}
{{#if (eq "table" pluginResult.type)}}
<h1>Data</h1>
{{pluginResult.rowCount}} row(s). <br />
<br />
<table>
<tr>
{{#each pluginResult.columns}}
<th>{{this}}</th>
{{/each}}
</tr>
{{#each pluginResult.rows}}
<tr>
{{#each this}}
<td><pre>{{{this}}}</pre></td>
{{/each}}
</tr>
{{/each}}
</table>
{{/if}}
{{#if (eq "string" pluginResult.type)}}
<p>{{pluginResult.text}}</p>
{{/if}}
{{#if (eq "html" pluginResult.type)}}
{{{pluginResult.html}}}
{{/if}}
{{> partials/suffix }}
</body>
</html>

View File

@@ -0,0 +1,176 @@
<html>
{{> partials/head title="Query" }}
<body>
{{> partials/prefix isQuery=true}}
<!-- Query Input -->
<form action="query" method="post" id="query-form">
<div class="query-input">{{query}}</div>
<button id="grow-button" onclick="onGrowClicked(event)">&nbsp;</button>
<input type="hidden" name="query" id="query" />
<input type="hidden" name="db" value="{{database}}" />
<input type="submit" name="action" value="run" />
or
<input type="submit" name="action" value="analyze" />
or
<button onclick="onFormatClicked(event)">format</button>
</form>
<!-- Container for previous queries -->
<h1 class="collapse-header" data-for="history-container">Query History</h1>
<table id="history-container" class="hidden"></table>
<!-- Query Result -->
<h1>Data</h1>
{{#if queryResult}}
{{queryResult.rowCount}} row(s). <br />
{{queryResult.timeToFirstRow}} ms to read the first row. <br />
{{queryResult.timeToReadRows}} ms to read the rest of the rows. <br />
<br />
<table>
<tr>
{{#each queryResult.columns}}
<th>{{this}}</th>
{{/each}}
</tr>
{{#each queryResult.rows}}
<tr>
{{#each this}}
<td><pre>{{#if (eq this null)}}<em class="null">null</em>{{else}}{{{this}}}{{/if}}</pre></td>
{{/each}}
</tr>
{{/each}}
</table>
{{else}}
No data.
{{/if}}
{{> partials/suffix}}
{{> partials/tooltips}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql-formatter/4.0.2/sql-formatter.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.5/ace.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.5/mode-sql.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.5/theme-github.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.5/theme-github_dark.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
let editor;
function main() {
//document.querySelector('.query-input').addEventListener('keypress', submitOnEnter);
document.getElementById('query-form').addEventListener('submit', onQuerySubmitted, false);
renderQueryHistory();
initializeTooltips();
editor = ace.edit(document.querySelector('.query-input'), {
mode: 'ace/mode/sql',
theme: isDarkTheme() ? 'ace/theme/github_dark' : 'ace/theme/github',
selectionStyle: 'text',
showPrintMargin: false
});
editor.setFontSize(13);
editor.commands.addCommand({
name: "Run",
bindKey: {win: "Ctrl-Return", mac: "Command-Return"},
exec: triggerSubmit
});
editor.commands.addCommand({
name: "Run (Cody)",
bindKey: {win: "Shift-Return", mac: "Shift-Return"},
exec: triggerSubmit
});
editor.commands.addCommand({
name: "Format",
bindKey: {win: "Ctrl-Shift-F", mac: "Command-Shift-F"},
exec: formatSql
});
}
function onFormatClicked(e) {
formatSql();
e.preventDefault();
}
function formatSql() {
let formatted = sqlFormatter.format(editor.getValue()).replaceAll("! =", "!=").replaceAll("| |", "||");
editor.setValue(formatted, formatted.length);
}
function triggerSubmit() {
onQuerySubmitted();
document.getElementById('query-form').submit();
}
function onGrowClicked(e) {
let element = document.querySelector('.query-input');
let currentHeight = parseInt(element.style.height) || 200;
element.style.height = currentHeight + 100;
e.preventDefault();
}
function onQuerySubmitted() {
const query = editor.getValue();
document.getElementById('query').value = query;
let history = getQueryHistory();
if (history.length > 0 && history[0] === query) {
console.log('Query already at the top of the history, not saving.');
return;
}
history.unshift(query);
history = history.slice(0, 25);
localStorage.setItem('query-history', JSON.stringify(history));
}
function renderQueryHistory() {
const container = document.getElementById('history-container');
let history = getQueryHistory();
if (history.length > 0) {
let i = 0;
for (let item of history) {
container.innerHTML += `
<tr>
<td><button onclick="onHistoryItemClicked(${i})">^</button></td>
<td id="history-item-${i}">${item}</td>
</tr>
`;
i++;
}
} else {
container.innerHTML = '<em>None</em>';
}
}
function onHistoryItemClicked(i) {
let item = document.getElementById(`history-item-${i}`).innerText;
editor.setValue(item, item.length);
}
function getQueryHistory() {
const historyRaw = localStorage.getItem('query-history') || "[]";
return JSON.parse(historyRaw);
}
function isDarkTheme() {
return document.body.getAttribute('data-theme') === 'dark'
}
main();
</script>
</body>
</html>

View File

@@ -0,0 +1,50 @@
<html>
{{> partials/head title="Home" }}
<style type="text/css">
h1.collapse-header {
font-size: 1.35rem;
}
h2.collapse-header {
font-size: 1.15rem;
}
table.recent {
width: 100%;
}
</style>
<body>
{{> partials/prefix isRecent=true}}
{{#if recentSql}}
<table class="recent">
{{#each recentSql}}
<tr>
<td>
{{formattedTime}}
</td>
<td>
<form action="query" method="post">
<input type="hidden" name="db" value="{{database}}" />
<input type="hidden" name="query" value="{{query}}" />
<input type="submit" name="action" value="run" />
<input type="submit" name="action" value="analyze" />
</form>
</td>
<td>{{query}}</td>
</tr>
{{/each}}
</table>
{{else}}
No recent queries.
{{/if}}
{{> partials/suffix }}
<script>
function onAnalyzeClicked(id) {
document.getElementById
}
</script>
</body>
</html>