mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 03:11:10 +01:00
Re-organize gradle modules.
This commit is contained in:
committed by
jeffrey-signal
parent
f4863efb2e
commit
e162eb27c7
81
lib/spinner/src/main/assets/browse.hbs
Normal file
81
lib/spinner/src/main/assets/browse.hbs
Normal 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>
|
||||
165
lib/spinner/src/main/assets/css/main.css
Normal file
165
lib/spinner/src/main/assets/css/main.css
Normal 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;
|
||||
}
|
||||
73
lib/spinner/src/main/assets/css/tooltips.css
Normal file
73
lib/spinner/src/main/assets/css/tooltips.css
Normal 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;
|
||||
}
|
||||
8
lib/spinner/src/main/assets/error.hbs
Normal file
8
lib/spinner/src/main/assets/error.hbs
Normal file
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
{{> partials/head title="Error :(" }}
|
||||
<body>
|
||||
Hit an exception while trying to serve the page :(
|
||||
<hr/>
|
||||
{{{this}}}
|
||||
</body>
|
||||
</html>
|
||||
33
lib/spinner/src/main/assets/js/main.js
Normal file
33
lib/spinner/src/main/assets/js/main.js
Normal 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();
|
||||
324
lib/spinner/src/main/assets/js/tooltips.js
Normal file
324
lib/spinner/src/main/assets/js/tooltips.js
Normal 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();
|
||||
}
|
||||
290
lib/spinner/src/main/assets/logs.hbs
Normal file
290
lib/spinner/src/main/assets/logs.hbs
Normal 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>
|
||||
107
lib/spinner/src/main/assets/overview.hbs
Normal file
107
lib/spinner/src/main/assets/overview.hbs
Normal 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>
|
||||
14
lib/spinner/src/main/assets/partials/head.hbs
Normal file
14
lib/spinner/src/main/assets/partials/head.hbs
Normal 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>
|
||||
38
lib/spinner/src/main/assets/partials/prefix.hbs
Normal file
38
lib/spinner/src/main/assets/partials/prefix.hbs
Normal 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>
|
||||
1
lib/spinner/src/main/assets/partials/suffix.hbs
Normal file
1
lib/spinner/src/main/assets/partials/suffix.hbs
Normal file
@@ -0,0 +1 @@
|
||||
<script src="/js/main.js" type="text/javascript"></script>
|
||||
2
lib/spinner/src/main/assets/partials/tooltips.hbs
Normal file
2
lib/spinner/src/main/assets/partials/tooltips.hbs
Normal file
@@ -0,0 +1,2 @@
|
||||
<link rel="stylesheet" href="/css/tooltips.css">
|
||||
<script src="/js/tooltips.js"></script>
|
||||
34
lib/spinner/src/main/assets/plugin.hbs
Normal file
34
lib/spinner/src/main/assets/plugin.hbs
Normal 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>
|
||||
176
lib/spinner/src/main/assets/query.hbs
Normal file
176
lib/spinner/src/main/assets/query.hbs
Normal 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)"> </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>
|
||||
50
lib/spinner/src/main/assets/recent.hbs
Normal file
50
lib/spinner/src/main/assets/recent.hbs
Normal 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>
|
||||
Reference in New Issue
Block a user