add biome linting and formatting for a consistent style and easier contributions
this should prevent useless conflicts
This commit is contained in:
Vendored
+62
-81
@@ -1,83 +1,64 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'sqlpage'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=sqlpage"
|
||||
],
|
||||
"filter": {
|
||||
"name": "sqlpage",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'sqlpage'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=sqlpage",
|
||||
"--package=sqlpage"
|
||||
],
|
||||
"filter": {
|
||||
"name": "sqlpage",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'sqlpage'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=sqlpage",
|
||||
"--package=sqlpage"
|
||||
],
|
||||
"filter": {
|
||||
"name": "sqlpage",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug integration test 'index'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--test=index",
|
||||
"--package=sqlpage"
|
||||
],
|
||||
"filter": {
|
||||
"name": "index",
|
||||
"kind": "test"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'sqlpage'",
|
||||
"cargo": {
|
||||
"args": ["test", "--no-run", "--lib", "--package=sqlpage"],
|
||||
"filter": {
|
||||
"name": "sqlpage",
|
||||
"kind": "lib"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'sqlpage'",
|
||||
"cargo": {
|
||||
"args": ["build", "--bin=sqlpage", "--package=sqlpage"],
|
||||
"filter": {
|
||||
"name": "sqlpage",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'sqlpage'",
|
||||
"cargo": {
|
||||
"args": ["test", "--no-run", "--bin=sqlpage", "--package=sqlpage"],
|
||||
"filter": {
|
||||
"name": "sqlpage",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug integration test 'index'",
|
||||
"cargo": {
|
||||
"args": ["test", "--no-run", "--test=index", "--package=sqlpage"],
|
||||
"filter": {
|
||||
"name": "index",
|
||||
"kind": "test"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+2
-4
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.linkedProjects": [
|
||||
"./Cargo.toml"
|
||||
]
|
||||
}
|
||||
"rust-analyzer.linkedProjects": ["./Cargo.toml"]
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"files": {
|
||||
"ignore": ["examples/official-site/pgconf/**"],
|
||||
"ignoreUnknown": true
|
||||
},
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"useIgnoreFile": true,
|
||||
"clientKind": "git"
|
||||
}
|
||||
}
|
||||
@@ -2,43 +2,43 @@
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: var(--tblr-gray-300);
|
||||
color: var(--tblr-gray-300);
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: var(--tblr-gray-500);
|
||||
color: var(--tblr-gray-500);
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag {
|
||||
color: #f92672;
|
||||
color: #f92672;
|
||||
|
||||
/* We need to reset the 'tag' styles set by tabler */
|
||||
border: 0;
|
||||
display: inherit;
|
||||
height: inherit;
|
||||
border-radius: inherit;
|
||||
padding: 0;
|
||||
background: inherit;
|
||||
box-shadow: inherit;
|
||||
/* We need to reset the 'tag' styles set by tabler */
|
||||
border: 0;
|
||||
display: inherit;
|
||||
height: inherit;
|
||||
border-radius: inherit;
|
||||
padding: 0;
|
||||
background: inherit;
|
||||
box-shadow: inherit;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #ea9999;
|
||||
color: #ea9999;
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: #ae81ff;
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string {
|
||||
color: #97e1a3;
|
||||
color: #97e1a3;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
@@ -46,47 +46,47 @@
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #f8f8f2;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value
|
||||
{
|
||||
color: #e6db74;
|
||||
.token.attr-value {
|
||||
color: #e6db74;
|
||||
}
|
||||
|
||||
|
||||
.token.keyword{
|
||||
color: #95d1ff;
|
||||
.token.keyword {
|
||||
color: #95d1ff;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: var(--tblr-yellow);
|
||||
color: var(--tblr-yellow);
|
||||
}
|
||||
|
||||
.token.important {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token {
|
||||
transition: .3s;
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
code::selection, code ::selection {
|
||||
background: var(--tblr-yellow);
|
||||
color: var(--tblr-gray-900);
|
||||
border-radius: .1em;
|
||||
code::selection,
|
||||
code ::selection {
|
||||
background: var(--tblr-yellow);
|
||||
color: var(--tblr-gray-900);
|
||||
border-radius: .1em;
|
||||
}
|
||||
|
||||
code .token.keyword::selection, code .token.punctuation::selection {
|
||||
color: var(--tblr-gray-800);
|
||||
code .token.keyword::selection,
|
||||
code .token.punctuation::selection {
|
||||
color: var(--tblr-gray-800);
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
}
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
}
|
||||
|
||||
div.dropdown-menu- {
|
||||
min-width: inherit !important;
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
a.dropdown-item- {
|
||||
min-width: inherit !important;
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.slim_item {
|
||||
min-width: inherit !important;
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"database_url": "postgres://my_username:my_password@localhost:5432/my_geographic_database"
|
||||
}
|
||||
"database_url": "postgres://my_username:my_password@localhost:5432/my_geographic_database"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"content_security_policy": "script-src blob: 'self' https://cdn.jsdelivr.net;"
|
||||
}
|
||||
"content_security_policy": "script-src blob: 'self' https://cdn.jsdelivr.net;"
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
const cdn = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/';
|
||||
const cdn = "https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/";
|
||||
|
||||
function on_monaco_load() {
|
||||
// Create an editor div, display it after the '#code-editor' textarea, hide the textarea, and create a Monaco editor in the div with the contents of the textarea
|
||||
// When the form is submitted, set the value of the textarea to the value of the Monaco editor
|
||||
const textarea = document.getElementById('code-editor');
|
||||
const editorDiv = document.createElement('div');
|
||||
editorDiv.style.width = '100%';
|
||||
editorDiv.style.height = '700px';
|
||||
textarea.parentNode.insertBefore(editorDiv, textarea.nextSibling);
|
||||
const monacoConfig = {
|
||||
value: textarea.value,
|
||||
language: 'sql',
|
||||
};
|
||||
// Create an editor div, display it after the '#code-editor' textarea, hide the textarea, and create a Monaco editor in the div with the contents of the textarea
|
||||
// When the form is submitted, set the value of the textarea to the value of the Monaco editor
|
||||
const textarea = document.getElementById("code-editor");
|
||||
const editorDiv = document.createElement("div");
|
||||
editorDiv.style.width = "100%";
|
||||
editorDiv.style.height = "700px";
|
||||
textarea.parentNode.insertBefore(editorDiv, textarea.nextSibling);
|
||||
const monacoConfig = {
|
||||
value: textarea.value,
|
||||
language: "sql",
|
||||
};
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
baseUrl: cdn + 'min/',
|
||||
};
|
||||
const editor = monaco.editor.create(editorDiv, monacoConfig);
|
||||
textarea.style.display = 'none';
|
||||
const form = textarea.form;
|
||||
form.addEventListener('submit', () => {
|
||||
textarea.value = editor.getValue();
|
||||
});
|
||||
self.MonacoEnvironment = {
|
||||
baseUrl: `${cdn}min/`,
|
||||
};
|
||||
const editor = monaco.editor.create(editorDiv, monacoConfig);
|
||||
textarea.style.display = "none";
|
||||
const form = textarea.form;
|
||||
form.addEventListener("submit", () => {
|
||||
textarea.value = editor.getValue();
|
||||
});
|
||||
}
|
||||
|
||||
function set_require_config() {
|
||||
require.config({ paths: { vs: cdn + 'min/vs' } });
|
||||
require(['vs/editor/editor.main'], on_monaco_load);
|
||||
require.config({ paths: { vs: `${cdn}min/vs` } });
|
||||
require(["vs/editor/editor.main"], on_monaco_load);
|
||||
}
|
||||
const loader_script = document.createElement('script');
|
||||
loader_script.src = cdn + 'min/vs/loader.js';
|
||||
const loader_script = document.createElement("script");
|
||||
loader_script.src = `${cdn}min/vs/loader.js`;
|
||||
loader_script.onload = set_require_config;
|
||||
document.head.appendChild(loader_script);
|
||||
document.head.appendChild(loader_script);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"max_uploaded_file_size": 5000000
|
||||
}
|
||||
"max_uploaded_file_size": 5000000
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"sqlite_extensions": [
|
||||
"mod_spatialite"
|
||||
]
|
||||
}
|
||||
"sqlite_extensions": ["mod_spatialite"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"component": "shell",
|
||||
"title": "SQLPage Questions",
|
||||
"icon": "help-hexagon",
|
||||
"link": "/index.sql",
|
||||
"menu_item": [
|
||||
"index",
|
||||
"results",
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
"component": "shell",
|
||||
"title": "SQLPage Questions",
|
||||
"icon": "help-hexagon",
|
||||
"link": "/index.sql",
|
||||
"menu_item": ["index", "results", "admin"]
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
"max_uploaded_file_size": 10485760,
|
||||
"compress_responses": false,
|
||||
"environment": "production"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
hljs.highlightAll()
|
||||
hljs.highlightAll();
|
||||
|
||||
@@ -3,46 +3,46 @@
|
||||
.hljs-prolog,
|
||||
.hljs-meta,
|
||||
.hljs-cdata {
|
||||
color: #a598a3;
|
||||
color: #a598a3;
|
||||
}
|
||||
|
||||
/* Punctuation */
|
||||
.hljs-template-variable,
|
||||
.hljs-punctuation {
|
||||
color: #e9eac7;
|
||||
color: #e9eac7;
|
||||
}
|
||||
|
||||
/* Namespace */
|
||||
.hljs-namespace {
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Property and Tag */
|
||||
.hljs-property {
|
||||
color: #de5f8f;
|
||||
color: #de5f8f;
|
||||
}
|
||||
|
||||
/* Number */
|
||||
.hljs-number {
|
||||
color: #ea9999;
|
||||
color: #ea9999;
|
||||
}
|
||||
|
||||
/* Boolean */
|
||||
.hljs-literal {
|
||||
color: #ae81ff;
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
/* Selector, Attr-name, and String */
|
||||
.hljs-attr {
|
||||
color: #fcfce5;
|
||||
color: #fcfce5;
|
||||
}
|
||||
|
||||
.hljs-name {
|
||||
color: #e4faf6;
|
||||
color: #e4faf6;
|
||||
}
|
||||
.hljs-selector-tag,
|
||||
.hljs-string {
|
||||
color: #97e1a3;
|
||||
color: #97e1a3;
|
||||
}
|
||||
|
||||
/* Operator, Entity, URL, CSS String, and Style String */
|
||||
@@ -51,55 +51,57 @@
|
||||
.hljs-link,
|
||||
.language-css .hljs-string,
|
||||
.style .hljs-string {
|
||||
color: #f8f8f2;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
/* At-rule and Attr-value */
|
||||
.hljs-tag,
|
||||
.hljs-keyword,
|
||||
.hljs-attribute-value {
|
||||
color: #e6db74;
|
||||
color: #e6db74;
|
||||
}
|
||||
|
||||
/* Keyword */
|
||||
.hljs-template-tag,
|
||||
.hljs-keyword {
|
||||
color: #95d1ff;
|
||||
color: #95d1ff;
|
||||
}
|
||||
|
||||
/* Regex and Important */
|
||||
.hljs-regexp,
|
||||
.hljs-important {
|
||||
color: var(--tblr-yellow);
|
||||
color: var(--tblr-yellow);
|
||||
}
|
||||
|
||||
/* Important */
|
||||
.hljs-important {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Entity */
|
||||
.hljs-symbol {
|
||||
cursor: help;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Token transition */
|
||||
.hljs {
|
||||
transition: .3s;
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
/* Code selection */
|
||||
code::selection, code ::selection {
|
||||
background: var(--tblr-yellow);
|
||||
color: var(--tblr-gray-900);
|
||||
border-radius: .1em;
|
||||
code::selection,
|
||||
code ::selection {
|
||||
background: var(--tblr-yellow);
|
||||
color: var(--tblr-gray-900);
|
||||
border-radius: .1em;
|
||||
}
|
||||
|
||||
code .hljs-keyword::selection, code .hljs-punctuation::selection {
|
||||
color: var(--tblr-gray-800);
|
||||
code .hljs-keyword::selection,
|
||||
code .hljs-punctuation::selection {
|
||||
color: var(--tblr-gray-800);
|
||||
}
|
||||
|
||||
/* Pre code padding */
|
||||
pre code {
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
/* Ensure all unordered and ordered lists are left-aligned */
|
||||
ul, ol {
|
||||
text-align: left; /* Aligns bullet points to the left */
|
||||
margin-left: 20px; /* Adds indentation for better readability */
|
||||
padding-left: 20px; /* Adds space between bullet and text */
|
||||
}
|
||||
|
||||
/* Optional: Style for the individual list items */
|
||||
li {
|
||||
margin-bottom: 10px; /* Adds space between list items */
|
||||
}
|
||||
|
||||
/* Optional: Ensure the body text is also left-aligned */
|
||||
body {
|
||||
text-align: left; /* Makes sure the overall page is left-aligned */
|
||||
}
|
||||
/* Ensure all unordered and ordered lists are left-aligned */
|
||||
ul,
|
||||
ol {
|
||||
text-align: left; /* Aligns bullet points to the left */
|
||||
margin-left: 20px; /* Adds indentation for better readability */
|
||||
padding-left: 20px; /* Adds space between bullet and text */
|
||||
}
|
||||
|
||||
/* Optional: Style for the individual list items */
|
||||
li {
|
||||
margin-bottom: 10px; /* Adds space between list items */
|
||||
}
|
||||
|
||||
/* Optional: Ensure the body text is also left-aligned */
|
||||
body {
|
||||
text-align: left; /* Makes sure the overall page is left-aligned */
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"database_url": "sqlite://:memory:"
|
||||
}
|
||||
"database_url": "sqlite://:memory:"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"database_url": "sqlite://:memory:"
|
||||
}
|
||||
"database_url": "sqlite://:memory:"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,25 +2,28 @@
|
||||
// version of a React component written in JSX.
|
||||
|
||||
function MyComponent({ greeting_name }) {
|
||||
const [count, setCount] = React.useState(0);
|
||||
return React.createElement(
|
||||
'button',
|
||||
{
|
||||
onClick: async () => {
|
||||
const r = await fetch('/api.sql');
|
||||
const { total_clicks } = await r.json();
|
||||
setCount(total_clicks)
|
||||
},
|
||||
className: 'btn btn-primary'
|
||||
},
|
||||
count == 0
|
||||
? `Hello, ${greeting_name}. Click me !`
|
||||
: `You clicked me ${count} times!`
|
||||
);
|
||||
const [count, setCount] = React.useState(0);
|
||||
return React.createElement(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
onClick: async () => {
|
||||
const r = await fetch("/api.sql");
|
||||
const { total_clicks } = await r.json();
|
||||
setCount(total_clicks);
|
||||
},
|
||||
className: "btn btn-primary",
|
||||
},
|
||||
count === 0
|
||||
? `Hello, ${greeting_name}. Click me !`
|
||||
: `You clicked me ${count} times!`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const container of document.getElementsByClassName('react_component')) {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
const props = JSON.parse(container.dataset.props);
|
||||
root.render(React.createElement(window[props.react_component_name], props, null));
|
||||
}
|
||||
for (const container of document.getElementsByClassName("react_component")) {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
const props = JSON.parse(container.dataset.props);
|
||||
root.render(
|
||||
React.createElement(window[props.react_component_name], props, null),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
}
|
||||
|
||||
@keyframes neon-glow {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
text-shadow: 0 0 10px #fa2dd1, 0 0 20px #b30890, 2px 3px 30px #ff00cc;
|
||||
}
|
||||
50% {
|
||||
@@ -29,5 +30,5 @@
|
||||
}
|
||||
|
||||
#funky_text * {
|
||||
color: inherit;
|
||||
}
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
+263
-212
@@ -1,231 +1,282 @@
|
||||
/* !include https://cdn.jsdelivr.net/npm/apexcharts@4.0.0/dist/apexcharts.min.js */
|
||||
|
||||
sqlpage_chart = (() => {
|
||||
function sqlpage_chart() {
|
||||
for (const c of document.querySelectorAll("[data-pre-init=chart]")) {
|
||||
try {
|
||||
build_sqlpage_chart(c);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sqlpage_chart() {
|
||||
for (const c of document.querySelectorAll("[data-pre-init=chart]")) {
|
||||
try { build_sqlpage_chart(c) }
|
||||
catch (e) { console.log(e) }
|
||||
const tblrColors = Object.fromEntries(
|
||||
[
|
||||
"azure",
|
||||
"red",
|
||||
"lime",
|
||||
"purple",
|
||||
"yellow",
|
||||
"gray-600",
|
||||
"orange",
|
||||
"pink",
|
||||
"teal",
|
||||
"indigo",
|
||||
"cyan",
|
||||
"green",
|
||||
"blue-lt",
|
||||
"yellow-lt",
|
||||
"pink-lt",
|
||||
"green-lt",
|
||||
"orange-lt",
|
||||
"blue",
|
||||
"gray-500",
|
||||
"gray-400",
|
||||
"gray-300",
|
||||
"gray-200",
|
||||
"gray-100",
|
||||
"gray-50",
|
||||
"black",
|
||||
].map((c) => [
|
||||
c,
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
`--tblr-${c}`,
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
/** @typedef { { [name:string]: {data:{x:number,y:number}[], name:string} } } Series */
|
||||
|
||||
/**
|
||||
* @param {Series} series
|
||||
* @returns {Series} */
|
||||
function align_categories(series) {
|
||||
const new_series = series.map((s) => ({ name: s.name, data: [] }));
|
||||
do {
|
||||
let category = null;
|
||||
series.forEach((s, s_i) => {
|
||||
const point = s.data[0];
|
||||
let new_point = { x: category, y: 0 };
|
||||
if (point) {
|
||||
if (category == null) category = point.x;
|
||||
if (category === point.x) {
|
||||
new_point = s.data.shift();
|
||||
}
|
||||
}
|
||||
new_series[s_i].data.push(new_point);
|
||||
});
|
||||
for (const s of new_series) {
|
||||
s.data[s.data.length - 1].x = category;
|
||||
}
|
||||
} while (category != null);
|
||||
for (const s of new_series) {
|
||||
s.data.pop();
|
||||
}
|
||||
return new_series;
|
||||
}
|
||||
|
||||
|
||||
const tblrColors = Object.fromEntries(['azure', 'red', 'lime', 'purple', 'yellow', 'gray-600', 'orange', 'pink', 'teal', 'indigo', 'cyan', 'green', 'blue-lt', 'yellow-lt', 'pink-lt', 'green-lt', 'orange-lt', 'blue', 'gray-500', 'gray-400', 'gray-300', 'gray-200', 'gray-100', 'gray-50', 'black']
|
||||
.map(c => [c, getComputedStyle(document.documentElement).getPropertyValue('--tblr-' + c)]));
|
||||
|
||||
/** @typedef { { [name:string]: {data:{x:number,y:number}[], name:string} } } Series */
|
||||
|
||||
/**
|
||||
* @param {Series} series
|
||||
* @returns {Series} */
|
||||
function align_categories(series) {
|
||||
const new_series = series.map(s => ({ name: s.name, data: [] }));
|
||||
do {
|
||||
var category = null;
|
||||
series.forEach((s, s_i) => {
|
||||
const point = s.data[0];
|
||||
let new_point = { x: category, y: 0 };
|
||||
if (point) {
|
||||
if (category == null) category = point.x;
|
||||
if (category === point.x) {
|
||||
new_point = s.data.shift();
|
||||
}
|
||||
}
|
||||
new_series[s_i].data.push(new_point);
|
||||
})
|
||||
new_series.forEach(s => s.data[s.data.length - 1].x = category);
|
||||
} while (category != null);
|
||||
new_series.forEach(s => s.data.pop());
|
||||
return new_series;
|
||||
/** @param {HTMLElement} c */
|
||||
function build_sqlpage_chart(c) {
|
||||
const [data_element] = c.getElementsByTagName("data");
|
||||
const data = JSON.parse(data_element.textContent);
|
||||
const chartContainer = c.querySelector(".chart");
|
||||
chartContainer.innerHTML = "";
|
||||
const is_timeseries = !!data.time;
|
||||
/** @type { Series } */
|
||||
const series_map = {};
|
||||
for (const [name, old_x, old_y, z] of data.points) {
|
||||
series_map[name] = series_map[name] || { name, data: [] };
|
||||
let x = old_x;
|
||||
let y = old_y;
|
||||
if (is_timeseries) {
|
||||
if (typeof x === "number") x = new Date(x * 1000);
|
||||
else if (data.type === "rangeBar" && Array.isArray(y))
|
||||
y = y.map((y) => new Date(y).getTime());
|
||||
else x = new Date(x);
|
||||
}
|
||||
series_map[name].data.push({ x, y, z });
|
||||
}
|
||||
if (data.xmin == null) data.xmin = undefined;
|
||||
if (data.xmax == null) data.xmax = undefined;
|
||||
if (data.ymin == null) data.ymin = undefined;
|
||||
if (data.ymax == null) data.ymax = undefined;
|
||||
|
||||
const colors = [
|
||||
...data.colors.filter((c) => c).map((c) => tblrColors[c]),
|
||||
...Object.values(tblrColors),
|
||||
];
|
||||
|
||||
/** @param {HTMLElement} c */
|
||||
function build_sqlpage_chart(c) {
|
||||
const [data_element] = c.getElementsByTagName("data");
|
||||
const data = JSON.parse(data_element.textContent);
|
||||
const chartContainer = c.querySelector('.chart');
|
||||
chartContainer.innerHTML = "";
|
||||
const is_timeseries = !!data.time;
|
||||
/** @type { Series } */
|
||||
const series_map = {};
|
||||
data.points.forEach(([name, x, y, z]) => {
|
||||
series_map[name] = series_map[name] || { name, data: [] }
|
||||
if (is_timeseries) {
|
||||
if (typeof x === 'number') x = new Date(x * 1000) // databases use seconds; JS uses ms
|
||||
else if (data.type === 'rangeBar' && Array.isArray(y)) y = y.map(y => new Date(y).getTime()); // timerange charts
|
||||
else x = new Date(x);
|
||||
let series = Object.values(series_map);
|
||||
|
||||
// tickamount is the number of intervals, not the number of ticks
|
||||
const tickAmount =
|
||||
data.xticks ||
|
||||
Math.min(30, Math.max(...series.map((s) => s.data.length - 1)));
|
||||
|
||||
let labels;
|
||||
const categories =
|
||||
series.length > 0 && typeof series[0].data[0].x === "string";
|
||||
if (data.type === "pie") {
|
||||
labels = data.points.map(([name, x, y]) => x || name);
|
||||
series = data.points.map(([name, x, y]) => y);
|
||||
} else if (categories && data.type === "bar")
|
||||
series = align_categories(series);
|
||||
|
||||
const options = {
|
||||
chart: {
|
||||
type: data.type || "line",
|
||||
fontFamily: "inherit",
|
||||
parentHeightOffset: 0,
|
||||
height: chartContainer.style.height,
|
||||
stacked: !!data.stacked,
|
||||
toolbar: {
|
||||
show: !!data.toolbar,
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
},
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
palette: "palette4",
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: !!data.labels,
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
color: "#f6f8fb",
|
||||
},
|
||||
formatter:
|
||||
data.type === "rangeBar"
|
||||
? (_val, { seriesIndex, w }) => w.config.series[seriesIndex].name
|
||||
: data.type === "pie"
|
||||
? (value, { seriesIndex, w }) =>
|
||||
`${w.config.labels[seriesIndex]}: ${value.toFixed()}%`
|
||||
: (value) => value.toLocaleString(),
|
||||
},
|
||||
fill: {
|
||||
type: data.type === "area" ? "gradient" : "solid",
|
||||
},
|
||||
stroke: {
|
||||
width: data.type === "area" ? 3 : 1,
|
||||
lineCap: "round",
|
||||
curve: "smooth",
|
||||
},
|
||||
xaxis: {
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
min: data.xmin,
|
||||
max: data.xmax,
|
||||
tickAmount,
|
||||
title: {
|
||||
text: data.xtitle || undefined,
|
||||
},
|
||||
type: is_timeseries ? "datetime" : categories ? "category" : undefined,
|
||||
labels: {
|
||||
datetimeUTC: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
logarithmic: !!data.logarithmic,
|
||||
min: data.ymin,
|
||||
max: data.ymax,
|
||||
stepSize: data.ystep,
|
||||
tickAmount: data.yticks,
|
||||
title: {
|
||||
text: data.ytitle || undefined,
|
||||
},
|
||||
},
|
||||
zaxis: {
|
||||
title: {
|
||||
text: data.ztitle || undefined,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: data.marker || 0,
|
||||
strokeWidth: 0,
|
||||
hover: {
|
||||
sizeOffset: 5,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
fillSeriesColor: false,
|
||||
custom:
|
||||
data.type === "bubble" || data.type === "scatter"
|
||||
? bubbleTooltip
|
||||
: undefined,
|
||||
y: {
|
||||
formatter: (value) => {
|
||||
if (is_timeseries && data.type === "rangeBar") {
|
||||
const d = new Date(value);
|
||||
if (d.getHours() === 0 && d.getMinutes() === 0)
|
||||
return d.toLocaleDateString();
|
||||
return d.toLocaleString();
|
||||
}
|
||||
series_map[name].data.push({ x, y, z });
|
||||
})
|
||||
if (data.xmin == null) data.xmin = undefined;
|
||||
if (data.xmax == null) data.xmax = undefined;
|
||||
if (data.ymin == null) data.ymin = undefined;
|
||||
if (data.ymax == null) data.ymax = undefined;
|
||||
const str_val = value.toLocaleString();
|
||||
if (str_val.length > 10 && Number.isNaN(value))
|
||||
return value.toFixed(2);
|
||||
return str_val;
|
||||
},
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: !!data.horizontal || data.type === "rangeBar",
|
||||
borderRadius: 5,
|
||||
},
|
||||
bubble: { minBubbleRadius: 5 },
|
||||
},
|
||||
colors,
|
||||
series,
|
||||
};
|
||||
if (labels) options.labels = labels;
|
||||
const chart = new ApexCharts(chartContainer, options);
|
||||
chart.render();
|
||||
if (window.charts) window.charts.push(chart);
|
||||
else window.charts = [chart];
|
||||
c.removeAttribute("data-pre-init");
|
||||
}
|
||||
|
||||
const colors = [
|
||||
...data.colors.filter(c => c).map(c => tblrColors[c]),
|
||||
...Object.values(tblrColors)
|
||||
];
|
||||
function bubbleTooltip({ series, seriesIndex, dataPointIndex, w }) {
|
||||
const { name, data } = w.config.series[seriesIndex];
|
||||
const point = data[dataPointIndex];
|
||||
|
||||
let series = Object.values(series_map);
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.className = "apexcharts-tooltip-text";
|
||||
tooltip.style.fontFamily = "inherit";
|
||||
|
||||
// tickamount is the number of intervals, not the number of ticks
|
||||
const tickAmount = data.xticks ||
|
||||
Math.min(30, Math.max(...series.map(s => s.data.length - 1)));
|
||||
const seriesName = document.createElement("div");
|
||||
seriesName.className = "apexcharts-tooltip-y-group";
|
||||
seriesName.style.fontWeight = "bold";
|
||||
seriesName.innerText = name;
|
||||
tooltip.appendChild(seriesName);
|
||||
|
||||
let labels;
|
||||
const categories = series.length > 0 && typeof series[0].data[0].x === "string";
|
||||
if (data.type === "pie") {
|
||||
labels = data.points.map(([name, x, y]) => x || name);
|
||||
series = data.points.map(([name, x, y]) => y);
|
||||
} else if (categories && data.type === 'bar') series = align_categories(series);
|
||||
|
||||
const options = {
|
||||
chart: {
|
||||
type: data.type || 'line',
|
||||
fontFamily: 'inherit',
|
||||
parentHeightOffset: 0,
|
||||
height: chartContainer.style.height,
|
||||
stacked: !!data.stacked,
|
||||
toolbar: {
|
||||
show: !!data.toolbar,
|
||||
},
|
||||
animations: {
|
||||
enabled: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
palette: 'palette4',
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: !!data.labels,
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
color: '#f6f8fb',
|
||||
},
|
||||
formatter: (data.type === 'rangeBar') ? (_val, { seriesIndex, w }) => w.config.series[seriesIndex].name :
|
||||
(data.type === 'pie') ? (value, { seriesIndex, w }) => `${w.config.labels[seriesIndex]}: ${value.toFixed()}%`
|
||||
: value => value.toLocaleString(),
|
||||
},
|
||||
fill: {
|
||||
type: data.type === 'area' ? 'gradient' : 'solid',
|
||||
},
|
||||
stroke: {
|
||||
width: data.type === 'area' ? 3 : 1,
|
||||
lineCap: "round",
|
||||
curve: "smooth",
|
||||
},
|
||||
xaxis: {
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
min: data.xmin,
|
||||
max: data.xmax,
|
||||
tickAmount,
|
||||
title: {
|
||||
text: data.xtitle || undefined,
|
||||
},
|
||||
type: is_timeseries ? 'datetime' : categories ? 'category' : undefined,
|
||||
labels: {
|
||||
datetimeUTC: false,
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
logarithmic: !!data.logarithmic,
|
||||
min: data.ymin,
|
||||
max: data.ymax,
|
||||
stepSize: data.ystep,
|
||||
tickAmount: data.yticks,
|
||||
title: {
|
||||
text: data.ytitle || undefined,
|
||||
}
|
||||
},
|
||||
zaxis: {
|
||||
title: {
|
||||
text: data.ztitle || undefined,
|
||||
}
|
||||
},
|
||||
markers: {
|
||||
size: data.marker || 0,
|
||||
strokeWidth: 0,
|
||||
hover: {
|
||||
sizeOffset: 5,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
fillSeriesColor: false,
|
||||
custom: (data.type === 'bubble' || data.type === 'scatter') ? bubbleTooltip : undefined,
|
||||
y: {
|
||||
formatter: (value) => {
|
||||
if (is_timeseries && data.type === 'rangeBar') {
|
||||
const d = new Date(value);
|
||||
if (d.getHours() === 0 && d.getMinutes() === 0) return d.toLocaleDateString();
|
||||
return d.toLocaleString();
|
||||
}
|
||||
const str_val = value.toLocaleString();
|
||||
if (str_val.length > 10 && value != value | 0) return value.toFixed(2);
|
||||
return str_val;
|
||||
}
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: !!data.horizontal || data.type === 'rangeBar',
|
||||
borderRadius: 5,
|
||||
},
|
||||
bubble: { minBubbleRadius: 5, },
|
||||
},
|
||||
colors,
|
||||
series,
|
||||
};
|
||||
if (labels) options.labels = labels;
|
||||
const chart = new ApexCharts(chartContainer, options);
|
||||
chart.render();
|
||||
if (window.charts) window.charts.push(chart);
|
||||
else window.charts = [chart];
|
||||
c.removeAttribute("data-pre-init");
|
||||
for (const axis of ["x", "y", "z"]) {
|
||||
const value = point[axis];
|
||||
if (value == null) continue;
|
||||
const axisValue = document.createElement("div");
|
||||
axisValue.className = "apexcharts-tooltip-y-group";
|
||||
let axis_conf = w.config[`${axis}axis`];
|
||||
if (axis_conf.length) axis_conf = axis_conf[0];
|
||||
const title = axis_conf.title.text || axis;
|
||||
const labelSpan = document.createElement("span");
|
||||
labelSpan.className = "apexcharts-tooltip-text-y-label";
|
||||
labelSpan.innerText = `${title}: `;
|
||||
axisValue.appendChild(labelSpan);
|
||||
const valueSpan = document.createElement("span");
|
||||
valueSpan.className = "apexcharts-tooltip-text-y-value";
|
||||
valueSpan.innerText = value;
|
||||
axisValue.appendChild(valueSpan);
|
||||
tooltip.appendChild(axisValue);
|
||||
}
|
||||
return tooltip.outerHTML;
|
||||
}
|
||||
|
||||
function bubbleTooltip({ series, seriesIndex, dataPointIndex, w }) {
|
||||
const { name, data } = w.config.series[seriesIndex];
|
||||
const point = data[dataPointIndex];
|
||||
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'apexcharts-tooltip-text';
|
||||
tooltip.style.fontFamily = 'inherit';
|
||||
|
||||
const seriesName = document.createElement('div');
|
||||
seriesName.className = 'apexcharts-tooltip-y-group';
|
||||
seriesName.style.fontWeight = 'bold';
|
||||
seriesName.innerText = name;
|
||||
tooltip.appendChild(seriesName);
|
||||
|
||||
for (const axis of ['x', 'y', 'z']) {
|
||||
const value = point[axis];
|
||||
if (value == null) continue;
|
||||
const axisValue = document.createElement('div');
|
||||
axisValue.className = 'apexcharts-tooltip-y-group';
|
||||
let axis_conf = w.config[axis + 'axis'];
|
||||
if (axis_conf.length) axis_conf = axis_conf[0];
|
||||
const title = axis_conf.title.text || axis;
|
||||
const labelSpan = document.createElement('span');
|
||||
labelSpan.className = 'apexcharts-tooltip-text-y-label';
|
||||
labelSpan.innerText = title + ': ';
|
||||
axisValue.appendChild(labelSpan);
|
||||
const valueSpan = document.createElement('span');
|
||||
valueSpan.className = 'apexcharts-tooltip-text-y-value';
|
||||
valueSpan.innerText = value;
|
||||
axisValue.appendChild(valueSpan);
|
||||
tooltip.appendChild(axisValue);
|
||||
}
|
||||
return tooltip.outerHTML;
|
||||
}
|
||||
|
||||
return sqlpage_chart;
|
||||
return sqlpage_chart;
|
||||
})();
|
||||
|
||||
add_init_fn(sqlpage_chart);
|
||||
add_init_fn(sqlpage_chart);
|
||||
|
||||
+3
-2
@@ -58,7 +58,8 @@ code {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.apexcharts-text, .apexcharts-datalabel {
|
||||
.apexcharts-text,
|
||||
.apexcharts-datalabel {
|
||||
fill: var(--tblr-body-color) !important;
|
||||
font-weight: var(--tblr-body-font-weight);
|
||||
}
|
||||
}
|
||||
|
||||
+84
-43
@@ -5,8 +5,8 @@ function sqlpage_card() {
|
||||
for (const c of document.querySelectorAll("[data-pre-init=card]")) {
|
||||
const source = c.dataset.embed;
|
||||
fetch(c.dataset.embed)
|
||||
.then(res => res.text())
|
||||
.then(html => {
|
||||
.then((res) => res.text())
|
||||
.then((html) => {
|
||||
const body = c.querySelector(".card-content");
|
||||
body.innerHTML = html;
|
||||
c.removeAttribute("data-pre-init");
|
||||
@@ -15,10 +15,10 @@ function sqlpage_card() {
|
||||
spinner.parentNode.removeChild(spinner);
|
||||
}
|
||||
const fragLoadedEvt = new CustomEvent("fragment-loaded", {
|
||||
bubbles: true
|
||||
bubbles: true,
|
||||
});
|
||||
c.dispatchEvent(fragLoadedEvt);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,19 +28,25 @@ function table_search_sort(el) {
|
||||
const search_input = el.querySelector("input.search");
|
||||
const sort_buttons = [...el.querySelectorAll("button.sort[data-sort]")];
|
||||
const item_parent = el.querySelector("tbody");
|
||||
const items = [...item_parent.querySelectorAll("tr")].map(el => ({
|
||||
const items = [...item_parent.querySelectorAll("tr")].map((el) => ({
|
||||
el,
|
||||
sort_keys: sort_buttons.map(b => {
|
||||
const sort_key = el.getElementsByClassName(b.dataset.sort)[0]?.textContent;
|
||||
sort_keys: sort_buttons.map((b) => {
|
||||
const sort_key = el.getElementsByClassName(b.dataset.sort)[0]
|
||||
?.textContent;
|
||||
return { num: Number.parseFloat(sort_key), str: sort_key };
|
||||
})
|
||||
}),
|
||||
}));
|
||||
function onSearch() {
|
||||
const lower_search = search_input.value.toLowerCase().split(/\s+/).filter(s => s);
|
||||
items.forEach(item => {
|
||||
const show = lower_search.every(s => item.el.textContent.toLowerCase().includes(s));
|
||||
const lower_search = search_input.value
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((s) => s);
|
||||
for (const item of items) {
|
||||
const show = lower_search.every((s) =>
|
||||
item.el.textContent.toLowerCase().includes(s),
|
||||
);
|
||||
item.el.style.display = show ? "" : "none";
|
||||
});
|
||||
}
|
||||
}
|
||||
if (search_input) {
|
||||
search_input.addEventListener("input", onSearch);
|
||||
@@ -49,15 +55,22 @@ function table_search_sort(el) {
|
||||
sort_buttons.forEach((button, button_index) => {
|
||||
button.addEventListener("click", function sort_items() {
|
||||
const sort_desc = button.classList.contains("asc");
|
||||
sort_buttons.forEach(b => b.classList.remove("asc", "desc"));
|
||||
for (const b of sort_buttons) {
|
||||
b.classList.remove("asc", "desc");
|
||||
}
|
||||
button.classList.add(sort_desc ? "desc" : "asc");
|
||||
const multiplier = sort_desc ? -1 : 1;
|
||||
items.sort((a, b) => {
|
||||
const a_key = a.sort_keys[button_index];
|
||||
const b_key = b.sort_keys[button_index];
|
||||
return multiplier * ((isNaN(a_key.num) || isNaN(b_key.num)) ? a_key.str.localeCompare(b_key.str) : a_key.num - b_key.num);
|
||||
return (
|
||||
multiplier *
|
||||
(Number.isNaN(a_key.num) || Number.isNaN(b_key.num)
|
||||
? a_key.str.localeCompare(b_key.str)
|
||||
: a_key.num - b_key.num)
|
||||
);
|
||||
});
|
||||
item_parent.append(...items.map(item => item.el));
|
||||
item_parent.append(...items.map((item) => item.el));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -79,13 +92,17 @@ function sqlpage_map() {
|
||||
// Add the leaflet js and css to the page
|
||||
const leaflet_css = document.createElement("link");
|
||||
leaflet_css.rel = "stylesheet";
|
||||
leaflet_css.href = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css";
|
||||
leaflet_css.integrity = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
|
||||
leaflet_css.href =
|
||||
"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css";
|
||||
leaflet_css.integrity =
|
||||
"sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
|
||||
leaflet_css.crossOrigin = "anonymous";
|
||||
document.head.appendChild(leaflet_css);
|
||||
const leaflet_js = document.createElement("script");
|
||||
leaflet_js.src = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js";
|
||||
leaflet_js.integrity = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
|
||||
leaflet_js.src =
|
||||
"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js";
|
||||
leaflet_js.integrity =
|
||||
"sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
|
||||
leaflet_js.crossOrigin = "anonymous";
|
||||
leaflet_js.nonce = nonce;
|
||||
leaflet_js.onload = onLeafletLoad;
|
||||
@@ -96,7 +113,7 @@ function sqlpage_map() {
|
||||
onLeafletLoad();
|
||||
}
|
||||
function parseCoords(coords) {
|
||||
return coords && coords.split(",").map(c => Number.parseFloat(c));
|
||||
return coords?.split(",").map((c) => Number.parseFloat(c));
|
||||
}
|
||||
function onLeafletLoad() {
|
||||
is_leaflet_loaded = true;
|
||||
@@ -108,16 +125,19 @@ function sqlpage_map() {
|
||||
const map = L.map(m, { attributionControl: !!attribution });
|
||||
const zoom = m.dataset.zoom;
|
||||
const center = parseCoords(m.dataset.center);
|
||||
if (tile_source) L.tileLayer(tile_source, { attribution, maxZoom }).addTo(map);
|
||||
if (tile_source)
|
||||
L.tileLayer(tile_source, { attribution, maxZoom }).addTo(map);
|
||||
map._sqlpage_markers = [];
|
||||
for (const marker_elem of m.getElementsByClassName("marker")) {
|
||||
setTimeout(addMarker, 0, marker_elem, map);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (center == null && map._sqlpage_markers.length) {
|
||||
map.fitBounds(map._sqlpage_markers.map(m =>
|
||||
m.getLatLng ? m.getLatLng() : m.getBounds()
|
||||
));
|
||||
map.fitBounds(
|
||||
map._sqlpage_markers.map((m) =>
|
||||
m.getLatLng ? m.getLatLng() : m.getBounds(),
|
||||
),
|
||||
);
|
||||
if (zoom != null) map.setZoom(+zoom);
|
||||
} else map.setView(center, +zoom);
|
||||
}, 100);
|
||||
@@ -132,22 +152,28 @@ function sqlpage_map() {
|
||||
color: marker_elem.dataset.color,
|
||||
title: marker_elem.getElementsByTagName("h3")[0].textContent.trim(),
|
||||
};
|
||||
const marker =
|
||||
dataset.coords ? createMarker(marker_elem, options)
|
||||
: createGeoJSONMarker(marker_elem, options);
|
||||
const marker = dataset.coords
|
||||
? createMarker(marker_elem, options)
|
||||
: createGeoJSONMarker(marker_elem, options);
|
||||
marker.addTo(map);
|
||||
map._sqlpage_markers.push(marker);
|
||||
if (options.title) marker.bindPopup(marker_elem);
|
||||
else if (marker_elem.dataset.link) marker.on('click', () => window.location = marker_elem.dataset.link);
|
||||
else if (marker_elem.dataset.link) {
|
||||
marker.on("click", () => {
|
||||
window.location.href = marker_elem.dataset.link;
|
||||
});
|
||||
}
|
||||
}
|
||||
function createMarker(marker_elem, options) {
|
||||
const coords = parseCoords(marker_elem.dataset.coords);
|
||||
const icon_obj = marker_elem.getElementsByClassName("mapicon")[0];
|
||||
if (icon_obj) {
|
||||
const size = 1.5 * +(options.size || icon_obj.firstChild?.getAttribute('width') || 24);
|
||||
const size =
|
||||
1.5 *
|
||||
+(options.size || icon_obj.firstChild?.getAttribute("width") || 24);
|
||||
options.icon = L.divIcon({
|
||||
html: icon_obj,
|
||||
className: `border-0 bg-${options.color || 'primary'} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`,
|
||||
className: `border-0 bg-${options.color || "primary"} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`,
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2],
|
||||
});
|
||||
@@ -164,7 +190,7 @@ function sqlpage_map() {
|
||||
return { ...options, ...properties };
|
||||
}
|
||||
function pointToLayer(feature, latlng) {
|
||||
marker_elem.dataset.coords = latlng.lat + "," + latlng.lng;
|
||||
marker_elem.dataset.coords = `${latlng.lat},${latlng.lng}`;
|
||||
return createMarker(marker_elem, { ...options, ...feature.properties });
|
||||
}
|
||||
return L.geoJSON(geojson, { style, pointToLayer });
|
||||
@@ -172,7 +198,9 @@ function sqlpage_map() {
|
||||
}
|
||||
|
||||
function sqlpage_form() {
|
||||
const file_inputs = document.querySelectorAll("input[type=file][data-max-size]");
|
||||
const file_inputs = document.querySelectorAll(
|
||||
"input[type=file][data-max-size]",
|
||||
);
|
||||
for (const input of file_inputs) {
|
||||
const max_size = +input.dataset.maxSize;
|
||||
input.addEventListener("change", function () {
|
||||
@@ -181,7 +209,9 @@ function sqlpage_form() {
|
||||
for (const { size } of this.files) {
|
||||
if (size > max_size) {
|
||||
input.classList.add("is-invalid");
|
||||
return input.setCustomValidity(`File size must be less than ${max_size / 1000} kB.`);
|
||||
return input.setCustomValidity(
|
||||
`File size must be less than ${max_size / 1000} kB.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -189,12 +219,16 @@ function sqlpage_form() {
|
||||
}
|
||||
|
||||
function get_tabler_color(name) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue('--tblr-' + name);
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(
|
||||
`--tblr-${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
function load_scripts() {
|
||||
const addjs = document.querySelectorAll("[data-sqlpage-js]");
|
||||
const existing_scripts = new Set([...document.querySelectorAll("script")].map(s => s.src));
|
||||
const existing_scripts = new Set(
|
||||
[...document.querySelectorAll("script")].map((s) => s.src),
|
||||
);
|
||||
for (const el of addjs) {
|
||||
const js = new URL(el.dataset.sqlpageJs, window.location.href).href;
|
||||
if (existing_scripts.has(js)) continue;
|
||||
@@ -206,12 +240,11 @@ function load_scripts() {
|
||||
}
|
||||
|
||||
function add_init_fn(f) {
|
||||
document.addEventListener('DOMContentLoaded', f);
|
||||
document.addEventListener('fragment-loaded', f);
|
||||
document.addEventListener("DOMContentLoaded", f);
|
||||
document.addEventListener("fragment-loaded", f);
|
||||
if (document.readyState !== "loading") setTimeout(f, 0);
|
||||
}
|
||||
|
||||
|
||||
add_init_fn(sqlpage_table);
|
||||
add_init_fn(sqlpage_map);
|
||||
add_init_fn(sqlpage_card);
|
||||
@@ -221,11 +254,19 @@ add_init_fn(load_scripts);
|
||||
function init_bootstrap_components(event) {
|
||||
if (window.bootstrap) {
|
||||
const fragment = event.target;
|
||||
fragment.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el));
|
||||
fragment.querySelectorAll('[data-bs-toggle="popover"]').forEach(el => new bootstrap.Popover(el));
|
||||
fragment.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(el => new bootstrap.Dropdown(el));
|
||||
fragment.querySelectorAll('[data-bs-ride="carousel"]').forEach(el => new bootstrap.Carousel(el));
|
||||
for (const el of fragment.querySelectorAll('[data-bs-toggle="tooltip"]')) {
|
||||
new bootstrap.Tooltip(el);
|
||||
}
|
||||
for (const el of fragment.querySelectorAll('[data-bs-toggle="popover"]')) {
|
||||
new bootstrap.Popover(el);
|
||||
}
|
||||
for (const el of fragment.querySelectorAll('[data-bs-toggle="dropdown"]')) {
|
||||
new bootstrap.Dropdown(el);
|
||||
}
|
||||
for (const el of fragment.querySelectorAll('[data-bs-ride="carousel"]')) {
|
||||
new bootstrap.Carousel(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('fragment-loaded', init_bootstrap_components);
|
||||
document.addEventListener("fragment-loaded", init_bootstrap_components);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* !include https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.popular.min.js */
|
||||
|
||||
function sqlpage_select_dropdown() {
|
||||
for (const s of document.querySelectorAll("[data-pre-init=select-dropdown]")) {
|
||||
for (const s of document.querySelectorAll(
|
||||
"[data-pre-init=select-dropdown]",
|
||||
)) {
|
||||
// See: https://github.com/orchidjs/tom-select/issues/716
|
||||
// By default, TomSelect will not retain the focus if s is already focused
|
||||
// This is a workaround to fix that
|
||||
@@ -15,4 +17,4 @@ function sqlpage_select_dropdown() {
|
||||
}
|
||||
}
|
||||
|
||||
add_init_fn(sqlpage_select_dropdown);
|
||||
add_init_fn(sqlpage_select_dropdown);
|
||||
|
||||
@@ -1,134 +1,139 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const BASE = 'http://localhost:8080/';
|
||||
const BASE = "http://localhost:8080/";
|
||||
|
||||
test('Open documentation', async ({ page }) => {
|
||||
test("Open documentation", async ({ page }) => {
|
||||
await page.goto(BASE);
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/SQLPage.*/);
|
||||
|
||||
// open the submenu
|
||||
await page.getByText('Documentation', { exact: true }).first().click();
|
||||
await page.getByText('All Components').click();
|
||||
const components = ['form', 'map', 'chart', 'button'];
|
||||
await page.getByText("Documentation", { exact: true }).first().click();
|
||||
await page.getByText("All Components").click();
|
||||
const components = ["form", "map", "chart", "button"];
|
||||
for (const component of components) {
|
||||
await expect(page.getByRole('link', { name: component }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: component }).first(),
|
||||
).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('chart', async ({ page }) => {
|
||||
await page.goto(BASE + '/documentation.sql?component=chart#component');
|
||||
await expect(page.getByText('Loading...')).not.toBeVisible();
|
||||
await expect(page.locator('.apexcharts-canvas').first()).toBeVisible();
|
||||
test("chart", async ({ page }) => {
|
||||
await page.goto(`${BASE}/documentation.sql?component=chart#component`);
|
||||
await expect(page.getByText("Loading...")).not.toBeVisible();
|
||||
await expect(page.locator(".apexcharts-canvas").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('map', async ({ page }) => {
|
||||
await page.goto(BASE + '/documentation.sql?component=map#component');
|
||||
await expect(page.getByText('Loading...')).not.toBeVisible();
|
||||
await expect(page.locator('.leaflet-marker-icon').first()).toBeVisible();
|
||||
test("map", async ({ page }) => {
|
||||
await page.goto(`${BASE}/documentation.sql?component=map#component`);
|
||||
await expect(page.getByText("Loading...")).not.toBeVisible();
|
||||
await expect(page.locator(".leaflet-marker-icon").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('form example', async ({ page }) => {
|
||||
await page.goto(BASE + '/examples/multistep-form');
|
||||
test("form example", async ({ page }) => {
|
||||
await page.goto(`${BASE}/examples/multistep-form`);
|
||||
// Single selection matching the value or label
|
||||
await page.getByLabel('From').selectOption('Paris');
|
||||
await page.getByText('Next').click();
|
||||
await page.getByLabel(/\bTo\b/).selectOption('Mexico');
|
||||
await page.getByText('Next').click();
|
||||
await page.getByLabel('Number of Adults').fill('1');
|
||||
await page.getByText('Next').click();
|
||||
await page.getByLabel('Passenger 1 (adult)').fill('John Doe');
|
||||
await page.getByText('Book the flight').click();
|
||||
await expect(page.getByText('John Doe').first()).toBeVisible();
|
||||
await page.getByLabel("From").selectOption("Paris");
|
||||
await page.getByText("Next").click();
|
||||
await page.getByLabel(/\bTo\b/).selectOption("Mexico");
|
||||
await page.getByText("Next").click();
|
||||
await page.getByLabel("Number of Adults").fill("1");
|
||||
await page.getByText("Next").click();
|
||||
await page.getByLabel("Passenger 1 (adult)").fill("John Doe");
|
||||
await page.getByText("Book the flight").click();
|
||||
await expect(page.getByText("John Doe").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('File upload', async ({ page }) => {
|
||||
test("File upload", async ({ page }) => {
|
||||
await page.goto(BASE);
|
||||
await page.getByRole('button', { name: 'Examples', exact: true }).click();
|
||||
await page.getByText('File uploads').click();
|
||||
await page.getByRole("button", { name: "Examples", exact: true }).click();
|
||||
await page.getByText("File uploads").click();
|
||||
const my_svg = '<svg><text y="20">Hello World</text></svg>';
|
||||
// @ts-ignore
|
||||
const buffer = Buffer.from(my_svg);
|
||||
await page.getByLabel('Picture').setInputFiles({
|
||||
name: 'small.svg',
|
||||
mimeType: 'image/svg+xml',
|
||||
await page.getByLabel("Picture").setInputFiles({
|
||||
name: "small.svg",
|
||||
mimeType: "image/svg+xml",
|
||||
buffer,
|
||||
});
|
||||
await page.getByRole('button', { name: 'Upload picture' }).click();
|
||||
await expect(page.locator('img[src^=data]').first().getAttribute('src')).resolves.toBe('data:image/svg+xml;base64,' + buffer.toString('base64'));
|
||||
await page.getByRole("button", { name: "Upload picture" }).click();
|
||||
await expect(
|
||||
page.locator("img[src^=data]").first().getAttribute("src"),
|
||||
).resolves.toBe(`data:image/svg+xml;base64,${buffer.toString("base64")}`);
|
||||
});
|
||||
|
||||
test('Authentication example', async ({ page }) => {
|
||||
await page.goto(BASE + '/examples/authentication/login.sql');
|
||||
await expect(page.locator('h1', { hasText: 'Authentication' })).toBeVisible();
|
||||
test("Authentication example", async ({ page }) => {
|
||||
await page.goto(`${BASE}/examples/authentication/login.sql`);
|
||||
await expect(page.locator("h1", { hasText: "Authentication" })).toBeVisible();
|
||||
|
||||
const usernameInput = page.getByLabel('Username');
|
||||
const passwordInput = page.getByLabel('Password');
|
||||
const loginButton = page.getByRole('button', { name: 'Log in' });
|
||||
const usernameInput = page.getByLabel("Username");
|
||||
const passwordInput = page.getByLabel("Password");
|
||||
const loginButton = page.getByRole("button", { name: "Log in" });
|
||||
|
||||
await expect(usernameInput).toBeVisible();
|
||||
await expect(passwordInput).toBeVisible();
|
||||
await expect(loginButton).toBeVisible();
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin');
|
||||
await usernameInput.fill("admin");
|
||||
await passwordInput.fill("admin");
|
||||
await loginButton.click();
|
||||
|
||||
await expect(page.getByText('You are logged in as admin')).toBeVisible();
|
||||
await expect(page.getByText("You are logged in as admin")).toBeVisible();
|
||||
});
|
||||
|
||||
test('table filtering', async ({ page }) => {
|
||||
await page.goto(BASE + '/documentation.sql?component=table');
|
||||
const tableSection = page.locator('.table-responsive', {
|
||||
has: page.getByRole('cell', { name: 'Chart' })
|
||||
test("table filtering", async ({ page }) => {
|
||||
await page.goto(`${BASE}/documentation.sql?component=table`);
|
||||
const tableSection = page.locator(".table-responsive", {
|
||||
has: page.getByRole("cell", { name: "Chart" }),
|
||||
});
|
||||
|
||||
const searchInput = tableSection.getByPlaceholder('Search…');
|
||||
await searchInput.fill('chart');
|
||||
await expect(tableSection.getByRole('cell', { name: 'Chart' })).toBeVisible();
|
||||
await expect(tableSection.getByRole('cell', { name: 'Table' })).not.toBeVisible();
|
||||
const searchInput = tableSection.getByPlaceholder("Search…");
|
||||
await searchInput.fill("chart");
|
||||
await expect(tableSection.getByRole("cell", { name: "Chart" })).toBeVisible();
|
||||
await expect(
|
||||
tableSection.getByRole("cell", { name: "Table" }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('table sorting', async ({ page }) => {
|
||||
await page.goto(BASE + '/documentation.sql?component=table');
|
||||
const tableSection = page.locator('.table-responsive', {
|
||||
has: page.getByRole('cell', { name: '31456' })
|
||||
test("table sorting", async ({ page }) => {
|
||||
await page.goto(`${BASE}/documentation.sql?component=table`);
|
||||
const tableSection = page.locator(".table-responsive", {
|
||||
has: page.getByRole("cell", { name: "31456" }),
|
||||
});
|
||||
|
||||
// Test numeric sorting on id column
|
||||
await tableSection.getByRole('button', { name: 'id' }).click();
|
||||
let ids = await tableSection.locator('td.id').allInnerTexts();
|
||||
let numericIds = ids.map(id => Number.parseInt(id));
|
||||
await tableSection.getByRole("button", { name: "id" }).click();
|
||||
let ids = await tableSection.locator("td.id").allInnerTexts();
|
||||
let numericIds = ids.map((id) => Number.parseInt(id));
|
||||
const sortedIds = [...numericIds].sort((a, b) => a - b);
|
||||
expect(numericIds).toEqual(sortedIds);
|
||||
|
||||
// Test reverse sorting
|
||||
await tableSection.getByRole('button', { name: 'id' }).click();
|
||||
ids = await tableSection.locator('td.id').allInnerTexts();
|
||||
numericIds = ids.map(id => Number.parseInt(id));
|
||||
await tableSection.getByRole("button", { name: "id" }).click();
|
||||
ids = await tableSection.locator("td.id").allInnerTexts();
|
||||
numericIds = ids.map((id) => Number.parseInt(id));
|
||||
const reverseSortedIds = [...numericIds].sort((a, b) => b - a);
|
||||
expect(numericIds).toEqual(reverseSortedIds);
|
||||
|
||||
// Test amount in stock column sorting
|
||||
await tableSection.getByRole('button', { name: 'Amount in stock' }).click();
|
||||
const amounts = await tableSection.locator('td.Amount').allInnerTexts();
|
||||
const numericAmounts = amounts.map(amount => Number.parseInt(amount));
|
||||
await tableSection.getByRole("button", { name: "Amount in stock" }).click();
|
||||
const amounts = await tableSection.locator("td.Amount").allInnerTexts();
|
||||
const numericAmounts = amounts.map((amount) => Number.parseInt(amount));
|
||||
const sortedAmounts = [...numericAmounts].sort((a, b) => a - b);
|
||||
expect(numericAmounts).toEqual(sortedAmounts);
|
||||
});
|
||||
|
||||
|
||||
test('no console errors on table page', async ({ page }) => {
|
||||
test("no console errors on table page", async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(BASE + '/documentation.sql?component=table');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.goto(`${BASE}/documentation.sql?component=table`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
@@ -11,7 +11,7 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './.',
|
||||
testDir: "./.",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
@@ -21,21 +21,21 @@ export default defineConfig({
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
// {
|
||||
|
||||
Reference in New Issue
Block a user