Skip to content

Commit

Permalink
Light branding, and bugixes to user uploads (#626)
Browse files Browse the repository at this point in the history
* branding and user upload fixes

* 5.0b1 release
  • Loading branch information
chrisclark authored Jun 7, 2024
1 parent bf2fdd3 commit bf3278a
Show file tree
Hide file tree
Showing 24 changed files with 431 additions and 82 deletions.
3 changes: 2 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Change Log
This document records all notable changes to `django-sql-explorer <https://github.com/chrisclark/django-sql-explorer>`_.
This project adheres to `Semantic Versioning <https://semver.org/>`_.

`4.4.0b1`_ (2024-06-02)
`5.0.0b1`_ (2024-06-07)
===========================

* Manage DB connections via the UI (and/or Django Admin)
Expand All @@ -18,6 +18,7 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
* Query List home page is sortable.
* Select all / deselect all with AI assistant
* Assistant tests run reliably in CI/CD
* Introduced some branding and styling improvements


`4.3.0`_ (2024-05-27)
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ recursive-include explorer *
recursive-exclude * *.pyc __pycache__ .DS_Store
recursive-include requirements *
include package.json
include vite.config.js
include vite.config.mjs
include README.rst
6 changes: 3 additions & 3 deletions explorer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
__version_info__ = {
"major": 4,
"minor": 4,
"major": 5,
"minor": 0,
"patch": 0,
"releaselevel": "beta",
"serial": 3
"serial": 1
}


Expand Down
3 changes: 2 additions & 1 deletion explorer/ee/db_connections/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from explorer.ee.db_connections.utils import user_dbs_local_dir

from django_cryptography.fields import encrypt

Expand Down Expand Up @@ -41,7 +42,7 @@ def is_upload(self):
@property
def local_name(self):
if self.is_upload:
return os.path.normpath(os.path.join(os.getcwd(), f"user_dbs/{self.name}"))
return os.path.join(user_dbs_local_dir(), self.name)

@classmethod
def from_django_connection(cls, connection_alias):
Expand Down
22 changes: 19 additions & 3 deletions explorer/ee/db_connections/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,26 @@
import io


# TODO deal with uploading the same file / conflicting file again
# Uploading the same filename twice (from the same user) will overwrite the 'old' DB on S3
def upload_sqlite(db_bytes, path):
from explorer.utils import get_s3_bucket
bucket = get_s3_bucket()
bucket.put_object(Key=path, Body=db_bytes, ServerSideEncryption="AES256")


def create_connection_for_uploaded_sqlite(filename, s3_path):
# Aliases have the user_id appended to them so that if two users upload files with the same name
# they don't step on one another. Without this, the *files* would get uploaded separately (because
# the DBs go into user-specific folders on s3), but the *aliases* would be the same. So one user
# could (innocently) upload a file with the same name, and any existing queries would be suddenly pointing
# to this new database connection. Oops!
# TODO: In the future, queries should probably be FK'ed to the ID of the connection, rather than simply
# storing the alias of the connection as a string.
def create_connection_for_uploaded_sqlite(filename, user_id, s3_path):
from explorer.models import DatabaseConnection
base, ext = os.path.splitext(filename)
filename = f"{base}_{user_id}{ext}"
return DatabaseConnection.objects.create(
alias=filename,
alias=f"{filename}",
engine=DatabaseConnection.SQLITE,
name=filename,
host=s3_path
Expand All @@ -41,6 +50,13 @@ def get_sqlite_for_connection(explorer_connection):
return explorer_connection


def user_dbs_local_dir():
d = os.path.normpath(os.path.join(os.getcwd(), "user_dbs"))
if not os.path.exists(d):
os.makedirs(d)
return d


def create_django_style_connection(explorer_connection):

if explorer_connection.is_upload:
Expand Down
2 changes: 1 addition & 1 deletion explorer/ee/db_connections/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def post(self, request):
except Exception as e: # noqa
return JsonResponse({"error": "Error while uploading file."}, status=400)

create_connection_for_uploaded_sqlite(f_name, s3_path)
create_connection_for_uploaded_sqlite(f_name, request.user.id, s3_path)
return JsonResponse({"success": True})
else:
return JsonResponse({"error": "No file provided"}, status=400)
Expand Down
7 changes: 5 additions & 2 deletions explorer/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
EXPLORER_SCHEMA_INCLUDE_TABLE_PREFIXES, EXPLORER_SCHEMA_INCLUDE_VIEWS,
)
from explorer.tasks import build_schema_cache_async
from explorer.utils import get_valid_connection
from explorer.utils import get_valid_connection, InvalidExplorerConnectionException


# These wrappers make it easy to mock and test
Expand Down Expand Up @@ -53,7 +53,10 @@ def schema_json_info(connection_alias):
ret = cache.get(key)
if ret:
return ret
si = schema_info(connection_alias) or []
try:
si = schema_info(connection_alias) or []
except InvalidExplorerConnectionException:
return []
json_schema = transform_to_json_schema(si)
cache.set(key, json_schema)
return json_schema
Expand Down
14 changes: 14 additions & 0 deletions explorer/src/images/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 35 additions & 18 deletions explorer/src/js/uploads.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@ import { getCsrfToken } from "./csrf";
export function setupUploads() {
var dropArea = document.getElementById('drop-area');
var fileElem = document.getElementById('fileElem');
var progressBar = document.getElementById('progress-bar');
var uploadStatus = document.getElementById('upload-status');

if (dropArea) {
dropArea.onclick = function() {
fileElem.click();
};

dropArea.addEventListener('dragover', function(e) {
e.preventDefault(); // Prevent default behavior (Prevent file from being opened)
dropArea.classList.add('bg-info'); // Optional: add a style when dragging over
e.preventDefault();
dropArea.classList.add('bg-info');
});

dropArea.addEventListener('dragleave', function(e) {
dropArea.classList.remove('bg-info'); // Optional: remove style when not dragging over
dropArea.classList.remove('bg-info');
});

dropArea.addEventListener('drop', function(e) {
e.preventDefault();
dropArea.classList.remove('bg-info'); // Optional: remove style after dropping
dropArea.classList.remove('bg-info');

let files = e.dataTransfer.files;
if (files.length) {
Expand All @@ -35,8 +37,6 @@ export function setupUploads() {
};
}



function handleFiles(file) {
uploadFile(file);
}
Expand All @@ -45,17 +45,35 @@ export function setupUploads() {
let formData = new FormData();
formData.append('file', file);

fetch("../upload/", {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
},
body: formData
}).then(response => response.json())
.then(() => {
window.location.reload();
})
.catch(error => console.error('Error:', error));
let xhr = new XMLHttpRequest();
xhr.open('POST', '../upload/', true);
xhr.setRequestHeader('X-CSRFToken', getCsrfToken());

xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
let percentComplete = (event.loaded / event.total) * 100;
progressBar.style.width = percentComplete + '%';
progressBar.setAttribute('aria-valuenow', percentComplete);
progressBar.innerHTML = percentComplete.toFixed(0) + '%';
if (percentComplete > 99) {
uploadStatus.innerHTML = "Upload complete. Parsing and saving to S3...";
}
}
};

xhr.onload = function() {
if (xhr.status === 200) {
window.location.href = "../";
} else {
console.error('Error:', xhr.statusText);
}
};

xhr.onerror = function() {
console.error('Error:', xhr.statusText);
};

xhr.send(formData);
}

document.getElementById("test-connection-btn").addEventListener("click", function() {
Expand All @@ -79,5 +97,4 @@ export function setupUploads() {
})
.catch(error => console.error("Error:", error));
});

}
8 changes: 8 additions & 0 deletions explorer/src/scss/explorer.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
a {
text-decoration: none;
}

.cm-editor {
outline: none !important;
}
Expand Down Expand Up @@ -143,3 +147,7 @@ nav.navbar {
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}

.logo-image {
height: 2rem;
}
4 changes: 3 additions & 1 deletion explorer/src/scss/styles.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
@import "variables";

@import "~bootstrap/scss/bootstrap";

$bootstrap-icons-font-dir: "../../../node_modules/bootstrap-icons/font/fonts";
@import "~bootstrap-icons/font/bootstrap-icons.css";

@import "variables";

@import "explorer";
@import "assistant";

Expand Down
20 changes: 18 additions & 2 deletions explorer/src/scss/variables.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
:root {
--bs-dark-rgb: 44, 62, 80;
@import "../../../node_modules/bootstrap/scss/functions";

$orange: rgb(255, 80, 1);
$blue: rgb(3, 68, 220);
$green: rgb(127, 176, 105);
$primary: $blue;
$dark: rgb(1, 32, 63);
$secondary: $orange;
$warning: $orange;
$danger: $orange;
$success: $green;
$info: rgb(106, 141, 146);
$code-color: $info;
$font-size-base: .8rem;

.btn-secondary, .btn-info {
--bs-btn-color: white !important;
}

8 changes: 6 additions & 2 deletions explorer/templates/connections/database_connection_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ <h2>{% if object %}Edit{% else %}Create New{% endif %} Connection</h2>
<h4>...or upload a SQLite DB or CSV File</h4>
<p>CSV files will get parsed and typed automatically. SQLite databases must <i>not</i> be password protected.</p>
<div id="drop-area" class="p-5 mb-4 bg-light border rounded" style="cursor: pointer">
<p class="lead mb-0">Drag and drop .csv or SQLite .db or <strong>click to upload</strong>.</p>
<input type="file" id="fileElem" style="display:none" accept=".db,.csv">
<p class="lead mb-0">Drag and drop .csv or SQLite .db/.sqlite or <strong>click to upload</strong>.</p>
<input type="file" id="fileElem" style="display:none" accept=".db,.csv,.sqlite">
<div class="progress mt-3" style="height: 20px;">
<div id="progress-bar" class="progress-bar" role="progressbar" style="width: 0;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>
<p id="upload-status" class="mt-2"></p>
</div>
</div>
{% endif %}
Expand Down
10 changes: 7 additions & 3 deletions explorer/templates/explorer/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "SQL Explorer" %}{% if query %} - {{ query.title }}{% elif title %} - {{ title }}{% endif %}</title>
<link rel="icon" type="image/svg+xml" href="{% vite_asset 'images/logo.svg' %}">

{% block style %}
{% vite_asset 'scss/styles.scss' %}
Expand Down Expand Up @@ -42,8 +43,11 @@ <h2>This is easy to fix, I promise!</h2>
{% block sql_explorer_content_takeover %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3">
<div class="container bd-gutter flex-wrap flex-lg-nowrap">
<a class="navbar-brand{% if view_name == 'explorer_index' %} btn btn-outline-light text-white{% endif %}"
href="{% url 'explorer_index' %}">{% trans "Explorer Home" %}</a>
<a class="navbar-brand{% if view_name == 'explorer_index' %} btn text-white{% endif %} d-flex align-items-center"
href="{% url 'explorer_index' %}">
<img src="{% vite_asset 'images/logo.svg' %}" alt="Logo" class="me-2 logo-image" >
{% trans "SQL Explorer" %}
</a>
<ul class="nav nav-pills">
{% if can_change %}
<li class="nav-item">
Expand All @@ -56,7 +60,7 @@ <h2>This is easy to fix, I promise!</h2>
</li>
{% if db_connections_enabled and can_manage_connections %}
<li class="nav-item">
<a class="nav-link{% if view_name == 'db_connections' %} active{% endif %}"
<a class="nav-link{% if view_name == 'explorer_connections' %} active{% endif %}"
href="{% url 'explorer_connections' %}"><i class="small me-1 bi-globe"></i>{% trans "Connections" %}</a>
</li>
{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion explorer/templates/explorer/query_favorites.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

{% block sql_explorer_content %}
<div class="container">
<h2>{% trans "Favorite Queries" %}</h2>
<h3>{% trans "Favorite Queries" %}</h3>
<table class="table table-striped query-list">
<thead>
<tr>
Expand Down
19 changes: 16 additions & 3 deletions explorer/templatetags/vite.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,26 @@ def get_script(file: str) -> str:
return mark_safe(f'<script type="module" src="{base_url}{file}"></script>')


def get_asset(file: str) -> str:
if app_settings.VITE_DEV_MODE is False:
return mark_safe(f"{VITE_OUTPUT_DIR}{file}")
else:
return mark_safe(f"http://{VITE_SERVER_HOST}:{VITE_SERVER_PORT}/{VITE_DEV_DIR}{file}")


@register.simple_tag
def vite_asset(filename: str):
if app_settings.VITE_DEV_MODE is False:
filename = os.path.basename(filename)
if str(filename).endswith("scss"):
if app_settings.VITE_DEV_MODE is False:
filename = os.path.basename(filename)
return get_css_link(filename)
return get_script(filename)
if str(filename).endswith("js"):
if app_settings.VITE_DEV_MODE is False:
filename = os.path.basename(filename)
return get_script(filename)

# Non js/scss assets respect directory structure so don't need to do the filename rewrite
return get_asset(filename)


@register.simple_tag
Expand Down
File renamed without changes.
20 changes: 20 additions & 0 deletions explorer/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,23 @@ def test_create_db_connection_from_django_connection(self):
c = DatabaseConnection.from_django_connection(app_settings.EXPLORER_DEFAULT_CONNECTION)
self.assertEqual(c.name, "tst1")
self.assertEqual(c.alias, "default")

@patch("os.makedirs")
@patch("os.path.exists", return_value=False)
@patch("os.getcwd", return_value="/mocked/path")
def test_local_name_calls_user_dbs_local_dir(self, mock_getcwd, mock_exists, mock_makedirs):
connection = DatabaseConnection(
alias="test",
engine=DatabaseConnection.SQLITE,
name="test_db.sqlite3",
host="some-s3-bucket",
)

local_name = connection.local_name
expected_path = "/mocked/path/user_dbs/test_db.sqlite3"

# Check if the local_name property returns the correct path
self.assertEqual(local_name, expected_path)

# Ensure os.makedirs was called once since the directory does not exist
mock_makedirs.assert_called_once_with("/mocked/path/user_dbs")
Loading

0 comments on commit bf3278a

Please sign in to comment.