Skip to content

Commit

Permalink
4.0b3 (#581)
Browse files Browse the repository at this point in the history
* js fixes, removing some jquery

* 4.0b3 in setup

* refactor pivot into its own dynamic load

* removing floatthead

* more js cleanup

* exclude stat rows from pivot
  • Loading branch information
chrisclark authored Feb 5, 2024
1 parent 4916604 commit c4278c2
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 192 deletions.
3 changes: 2 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.

`4.0.0.beta2`_ (2024-02-01)
===========================
* Add support for Django 5.0. Drop support for Python < 3.10.
* `#565`_: Front-end modernization. Code completion via CodeMirror 6. Bootstrap5. Vite-based build
* `#566`_: Django 5 support & tests
* `#537`_: S3 signature version support
* `#562`_: Visually show whether the last run was successful
* `#562`_: Record and show whether the last run of each query was successful
* `#571`_: Replace isort and flake8 with Ruff (linting)

`4.0.0.beta1`_ (2024-02-01)
Expand Down
2 changes: 1 addition & 1 deletion explorer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"minor": 0,
"patch": 0,
"releaselevel": "beta",
"serial": 2
"serial": 3
}


Expand Down
8 changes: 0 additions & 8 deletions explorer/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@
from django.conf import settings


# The 'correct' configuration for Explorer looks like:

# EXPLORER_CONNECTIONS = {
# 'Original Database': 'my_important_database_readonly_connection',
# 'Client Database 2': 'other_database_connection'
# }
# EXPLORER_DEFAULT_CONNECTION = 'my_important_database_readonly_connection'

EXPLORER_CONNECTIONS = getattr(settings, "EXPLORER_CONNECTIONS", {})
EXPLORER_DEFAULT_CONNECTION = getattr(
settings, "EXPLORER_DEFAULT_CONNECTION", None
Expand Down
226 changes: 99 additions & 127 deletions explorer/src/js/explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ import { explorerSetup } from "./codemirror-config";
import cookie from 'cookiejs';
import List from 'list.js'

import 'floatthead'
import { getCsrfToken } from "./csrf";
import { toggleFavorite } from "./favorites";

import {pivotJq} from "./pivot";
import {csvFromTable} from "./table-to-csv";

import {schemaCompletionSource, StandardSQL} from "@codemirror/lang-sql";
import {StateEffect} from "@codemirror/state";

Expand All @@ -38,7 +34,6 @@ export class ExplorerEditor {
this.$rows = $("#rows");
this.$form = $("form");
this.$snapshotField = $("#id_snapshot");
this.$paramFields = this.$form.find(".param");
this.docChanged = false;

this.$submit = $("#refresh_play_button, #save_button");
Expand All @@ -56,8 +51,6 @@ export class ExplorerEditor {
this.docChanged = true;
});

pivotJq($);

this.bind();

if (cookie.get("schema_sidebar_open") === 'true') {
Expand All @@ -66,11 +59,12 @@ export class ExplorerEditor {
}

getParams() {
var o = false;
if(this.$paramFields.length) {
let o = false;
const params = document.querySelectorAll("form .param");
if (params.length) {
o = {};
this.$paramFields.each(function() {
o[$(this).data("param")] = $(this).val();
params.forEach((param) => {
o[param.dataset.param] = param.value;
});
}
return o;
Expand All @@ -84,14 +78,6 @@ export class ExplorerEditor {
return encodeURIComponent(args.join("|"));
}

savePivotState(state) {
const picked = (({ aggregatorName, rows, cols, rendererName, vals }) => ({ aggregatorName, rows, cols, rendererName, vals }))(state);
const jsonString = JSON.stringify(picked);
let bmark = btoa(jsonString);
let $el = $("#pivot-bookmark");
$el.attr("href", $el.data("baseurl") + "#" + bmark);
}

updateQueryString(key, value, url) {
// http://stackoverflow.com/a/11654596/221390
if (!url) url = window.location.href;
Expand Down Expand Up @@ -125,32 +111,36 @@ export class ExplorerEditor {
let sqlText = this.editor.state.doc.toString();
let editor = this.editor;

$.ajax({
url: "../format/",
type: "POST",
var formData = new FormData();
formData.append('sql', sqlText); // Append the SQL text to the form data

// Make the fetch call
fetch("../format/", {
method: "POST",
headers: {
// 'Content-Type': 'application/x-www-form-urlencoded', // Not needed when using FormData, as the browser sets it along with the boundary
'X-CSRFToken': getCsrfToken()
},
data: {
sql: sqlText
},
success: function(data) {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: data.formatted
}
})
}.bind(this)
});
body: formData // Use the FormData object as the body
})
.then(response => response.json()) // Parse the JSON response
.then(data => {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: data.formatted
}
});
})
.catch(error => console.error('Error:', error));
}

showRows() {
var rows = this.$rows.val(),
$form = $("#editor");
$form.attr("action", this.updateQueryString("rows", rows, window.location.href));
$form.submit();
let rows = document.getElementById("rows").value;
let form = document.getElementById("editor");
form.setAttribute("action", this.updateQueryString("rows", rows, window.location.href));
form.submit();
}

showSchema(noAutofocus) {
Expand All @@ -173,43 +163,41 @@ export class ExplorerEditor {
var schema$ = $("#schema");
schema$.removeClass("col-3");
schema$.hide();
$(this).hide();
$("#hide_schema_button").hide();
$("#show_schema_button").show();
cookie.set("schema_sidebar_open", 'false');
return false;
}

handleBeforeUnload = (event) => {
if (clientRoute === 'query_detail' && this.docChanged) {
const confirmationMessage = "You have unsaved changes to your query.";
event.returnValue = confirmationMessage;
return confirmationMessage;
}
};

bind() {

$(window).on("beforeunload", function () {
// Only do this if changed-input is on the page and we"re not on the playground page.
if (clientRoute === 'query_detail' && this.docChanged) {
return "You have unsaved changes to your query.";
window.addEventListener("beforeunload", this.handleBeforeUnload)

document.addEventListener("submit", (event) => {
// Disable unsaved changes warning when submitting the editor form
if (event.target.id === "editor") {
window.removeEventListener("beforeunload", this.handleBeforeUnload);
}
}.bind(this));
})

// Disable unsaved changes warning when submitting the editor form
$(document).on("submit", "#editor", function(event){
// disable warning
$(window).off("beforeunload");
});
// Define the beforeUnloadHandler function for easier add/remove

let button = document.querySelector("#button-excel");
if (button) {
button.addEventListener("click", e => {
let table = document.querySelector(".pvtTable");
if (typeof (table) != 'undefined' && table != null) {
csvFromTable(table);
}
});
}

document.querySelectorAll('.query_favorite_toggle').forEach(function(element) {
element.addEventListener('click', toggleFavorite);
});

$("#show_schema_button").click(this.showSchema);
$("#hide_schema_button").click(this.hideSchema);
document.getElementById('show_schema_button').addEventListener('click', this.showSchema.bind(this));
document.getElementById('hide_schema_button').addEventListener('click', this.hideSchema.bind(this));


$("#format_button").click(function(e) {
e.preventDefault();
Expand Down Expand Up @@ -284,79 +272,63 @@ export class ExplorerEditor {
this.$form.attr("action", url);
}.bind(this));

$(".stats-expand").click(function(e) {
e.preventDefault();
$(".stats-expand").hide();
$(".stats-wrapper").show();
this.$table.floatThead("reflow");
}.bind(this));
document.querySelectorAll('.stats-expand').forEach(element => {
element.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.stats-expand').forEach(el => el.style.display = 'none');
document.querySelectorAll('.stats-wrapper').forEach(el => el.style.display = '');
});
});

$("#counter-toggle").click(function(e) {
e.preventDefault();
$(".counter").toggle();
this.$table.floatThead("reflow");
}.bind(this));
let counterToggle = document.getElementById('counter-toggle');
if (counterToggle) {
counterToggle.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.counter').forEach(el => {
el.style.display = el.style.display === 'none' ? '' : 'none';
});
});
}

$(".sort").click(function(e) {
var t = $(e.target).data("sort");
var dir = $(e.target).data("dir");
$(".sort").addClass("bi-chevron-expand");
$(".sort").removeClass("bi-chevron-down");
$(".sort").removeClass("bi-chevron-up");
if (dir === "asc"){
$(e.target).data("dir", "desc");
$(e.target).addClass("bi-chevron-up");
$(e.target).removeClass("bi-chevron-down");
$(e.target).removeClass("bi-chevron-expand");
} else {
$(e.target).data("dir", "asc");
$(e.target).addClass("bi-chevron-down");
$(e.target).removeClass("bi-chevron-up");
$(e.target).removeClass("bi-chevron-expand");
}
var vals = [];
var ct = 0;
while (ct < this.$table.find("th").length) {
vals.push(ct++);
}
var options = {
valueNames: vals
};
var tableList = new List("preview", options);
tableList.sort(t, { order: dir });
}.bind(this));
// List.js setup for the preview pane to support sorting
let thElements = document.querySelector('#preview').querySelectorAll('th');
new List('preview', {
valueNames: Array.from(thElements, (_, index) => index)
});

$("#preview-tab-label").click(function() {
this.$table.floatThead("reflow");
}.bind(this));
document.querySelectorAll('.sort').forEach(sortButton => {
sortButton.addEventListener('click', function(e) {
const target = e.target;

// Reset icons on all sort buttons
document.querySelectorAll('.sort').forEach(btn => {
btn.classList.add('bi-chevron-expand');
btn.classList.remove('bi-chevron-down', 'bi-chevron-up');
});

if ( target.classList.contains('asc') ) {
target.classList.replace('bi-chevron-expand', 'bi-chevron-up');
target.classList.remove('bi-chevron-down');
} else {
target.classList.replace('bi-chevron-expand', 'bi-chevron-down');
target.classList.remove('bi-chevron-up');
}

var pivotState = window.location.hash;
var navToPivot = false;
if (!pivotState) {
pivotState = {onRefresh: this.savePivotState};
} else {
try {
pivotState = JSON.parse(atob(pivotState.substr(1)));
pivotState["onRefresh"] = this.savePivotState;
navToPivot = true;
} catch(e) {
pivotState = {onRefresh: this.savePivotState};
}
}
}.bind(this));
});

$(".pivot-table").pivotUI(this.$table, pivotState);
if (navToPivot) {
let pivotEl = document.querySelector('#nav-pivot-tab')
pivotEl.click()
const tabEl = document.querySelector('button[data-bs-target="#nav-pivot"]')
if (tabEl) {
tabEl.addEventListener('shown.bs.tab', event => {
import('./pivot-setup').then(({pivotSetup}) => pivotSetup($));
});
}

setTimeout(function() {
this.$table.floatThead({
scrollContainer: function() {
return this.$table.closest(".overflow-wrapper");
}.bind(this)
});
}.bind(this), 1);
// Pretty hacky, but at the moment URL hashes are only used for storing pivot state, so this is a safe
// way of checking if we are following a link to a pivot table.
if (window.location.hash) {
document.querySelector('#nav-pivot-tab').click();
}

this.$rows.change(function() { this.showRows(); }.bind(this));
this.$rows.keyup(function(event) {
Expand Down
5 changes: 2 additions & 3 deletions explorer/src/js/favorites.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {getCsrfToken} from "./csrf";

export async function toggleFavorite() {
// Assuming this function is bound to an element, 'this' refers to that element
let queryId = this.dataset.id; // or this.getAttribute('data-id');
let favoriteUrl = this.dataset.url; // or this.getAttribute('data-url');
let queryId = this.dataset.id;
let favoriteUrl = this.dataset.url;

try {
let response = await fetch(favoriteUrl, {
Expand Down
22 changes: 17 additions & 5 deletions explorer/src/js/main.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
/*
This is the entrypoint for the client code, and for the Vite build. The basic
idea is to map a function to each page/url of the app that sets up the JS
needed for that page. The clientRoute and queryId are defined in base.html
template. clientRoute's value comes from the name of the Django URL pattern for
the page. The dynamic import() allows Vite to chunk the JS and only load what's
necessary for each page. Concretely, this matters because the pages with SQL
Editors require fairly heavy JS (CodeMirror).
*/

import * as bootstrap from 'bootstrap'; // eslint-disable-line no-unused-vars

const route_initializers = {
explorer_index: import('./query-list').then(({setupQueryList}) => setupQueryList),
query_detail: () => import('./explorer').then(({ExplorerEditor}) => new ExplorerEditor(queryId)),
query_create: () => import('./explorer').then(({ExplorerEditor}) => new ExplorerEditor(queryId)),
explorer_playground: () => import('./explorer').then(({ExplorerEditor}) => new ExplorerEditor(queryId)),
explorer_schema: import('./schema').then(({setupSchema}) => setupSchema),
explorer_index: () => import('./query-list').then(({setupQueryList}) => setupQueryList()),
query_detail: () => import('./explorer').then(({ExplorerEditor}) => new ExplorerEditor(queryId)),
query_create: () => import('./explorer').then(({ExplorerEditor}) => new ExplorerEditor('new')),
explorer_playground: () => import('./explorer').then(({ExplorerEditor}) => new ExplorerEditor('new')),
explorer_schema: () => import('./schema').then(({setupSchema}) => setupSchema()),
};

document.addEventListener('DOMContentLoaded', function() {
Expand Down
Loading

0 comments on commit c4278c2

Please sign in to comment.