Merge branch 'main' into login
This commit is contained in:
@@ -54,7 +54,7 @@ jobs:
|
||||
db_url: "mssql://root:Password123!@127.0.0.1/sqlpage"
|
||||
- database: odbc
|
||||
container: postgres
|
||||
db_url: "Driver=/usr/lib/x86_64-linux-gnu/odbc/psqlodbcw.so;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!"
|
||||
db_url: "Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!"
|
||||
setup_odbc: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -74,6 +74,8 @@ jobs:
|
||||
env:
|
||||
DATABASE_URL: ${{ matrix.db_url }}
|
||||
RUST_BACKTRACE: 1
|
||||
MALLOC_CHECK_: 3
|
||||
MALLOC_PERTURB_: 10
|
||||
|
||||
windows_test:
|
||||
runs-on: windows-latest
|
||||
|
||||
+9
-1
@@ -1,7 +1,15 @@
|
||||
# CHANGELOG.md
|
||||
|
||||
## v0.38.1
|
||||
## v0.39.0
|
||||
- Ability to execute sql for URL paths with another extension. If you create sitemap.xml.sql, it will be executed for example.com/sitemap.xml
|
||||
- Display source line info in errors even when the database does not return a precise error position. In this case, the entire problematic SQL statement is referenced.
|
||||
- The shell with a vertical sidebar can now have "active" elements, just like the horizontal header bar.
|
||||
- New `edit_url`, `delete_url`, and `custom_actions` properties in the [table](https://sql-page.com/component.sql?component=table) component to easily add nice icon buttons to a table.
|
||||
- SQLPage now sets the [`Server-Timing` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing) in development. So when you have a page that loads slowly, you can open your browser's network inspector, click on the slow request, then open the timing tab to understand where it's spending its time.
|
||||
- <img width="1250" height="1263" alt="image" src="https://github.com/user-attachments/assets/6781a31f-e342-4e8c-8506-bc47049ce313" />
|
||||
- Fixed a memory corruption issue in the builtin odbc driver manager
|
||||
- ODBC: fix using globally installed system drivers by their name in debian-based linux distributions.
|
||||
|
||||
|
||||
## v0.38.0
|
||||
- Added support for the Open Database Connectivity (ODBC) standard.
|
||||
|
||||
Generated
+108
-113
@@ -8,7 +8,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -31,14 +31,14 @@ dependencies = [
|
||||
"actix-tls",
|
||||
"actix-utils",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"brotli 8.0.2",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more 2.0.1",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http 0.2.12",
|
||||
@@ -214,7 +214,7 @@ dependencies = [
|
||||
"cookie",
|
||||
"derive_more 2.0.1",
|
||||
"encoding_rs",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"impl-more",
|
||||
@@ -325,7 +325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
|
||||
dependencies = [
|
||||
"android-properties",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"cc",
|
||||
"cesu8",
|
||||
"jni",
|
||||
@@ -699,9 +699,9 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||
|
||||
[[package]]
|
||||
name = "bigdecimal"
|
||||
version = "0.4.8"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013"
|
||||
checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
@@ -718,7 +718,7 @@ version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
@@ -740,11 +740,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.4"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -862,7 +862,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"log",
|
||||
"polling",
|
||||
"rustix 0.38.44",
|
||||
@@ -872,9 +872,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.41"
|
||||
version = "1.2.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
|
||||
checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -936,9 +936,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.49"
|
||||
version = "4.5.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f"
|
||||
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -946,9 +946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.49"
|
||||
version = "4.5.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730"
|
||||
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1382,9 +1382,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
|
||||
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
@@ -1722,9 +1722,9 @@ checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.4"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9"
|
||||
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -1753,6 +1753,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1984,7 +1990,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2018,10 +2024,6 @@ name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -2029,7 +2031,7 @@ version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2037,6 +2039,11 @@ name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@@ -2326,9 +2333,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.4"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
@@ -2338,9 +2345,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
@@ -2424,9 +2431,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
version = "0.3.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -2515,9 +2522,9 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "libflate"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e"
|
||||
checksum = "249fa21ba2b59e8cbd69e722f5b31e1b466db96c937ae3de23e8b99ead0d1383"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"core2",
|
||||
@@ -2528,12 +2535,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libflate_lz77"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d"
|
||||
checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c"
|
||||
dependencies = [
|
||||
"core2",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.16.0",
|
||||
"rle-decode-fast",
|
||||
]
|
||||
|
||||
@@ -2559,7 +2566,7 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"redox_syscall 0.5.18",
|
||||
]
|
||||
@@ -2701,7 +2708,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
@@ -2815,9 +2822,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.4"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a"
|
||||
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
@@ -2825,9 +2832,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.4"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
|
||||
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
@@ -2876,7 +2883,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -2892,7 +2899,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-core-location",
|
||||
@@ -2916,7 +2923,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
@@ -2958,7 +2965,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"dispatch",
|
||||
"libc",
|
||||
@@ -2983,7 +2990,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
@@ -2995,7 +3002,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
@@ -3018,7 +3025,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
@@ -3050,7 +3057,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-core-location",
|
||||
@@ -3073,8 +3080,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "odbc-sys"
|
||||
version = "0.27.3"
|
||||
source = "git+https://github.com/sqlpage/odbc-sys?branch=no-autotools#ae3e15446bb2c5c191f05e7c6affc37dfd6fcabe"
|
||||
version = "0.27.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1896e52e97c2f0cf997cc627380f1af1ecb3f6c29ce6175047cd38adaadb46f5"
|
||||
dependencies = [
|
||||
"unix-odbc",
|
||||
]
|
||||
@@ -3096,9 +3104,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openidconnect"
|
||||
@@ -3474,9 +3482,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -3589,7 +3597,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3695,7 +3703,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3760,7 +3768,7 @@ version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
@@ -3773,7 +3781,7 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
@@ -3782,9 +3790,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.33"
|
||||
version = "0.23.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c"
|
||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -3844,9 +3852,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
@@ -3943,7 +3951,7 @@ version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -4024,7 +4032,7 @@ version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
@@ -4075,15 +4083,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.15.0"
|
||||
version = "3.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5"
|
||||
checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.0.4",
|
||||
"serde_core",
|
||||
@@ -4094,9 +4102,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.15.0"
|
||||
version = "3.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27"
|
||||
checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955"
|
||||
dependencies = [
|
||||
"darling 0.21.3",
|
||||
"proc-macro2",
|
||||
@@ -4219,7 +4227,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlpage"
|
||||
version = "0.38.1"
|
||||
version = "0.39.0"
|
||||
dependencies = [
|
||||
"actix-multipart",
|
||||
"actix-rt",
|
||||
@@ -4298,7 +4306,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bigdecimal",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -4320,7 +4328,7 @@ dependencies = [
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"itoa",
|
||||
"libc",
|
||||
"libsqlite3-sys",
|
||||
@@ -4425,9 +4433,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4654,7 +4662,7 @@ version = "0.23.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
|
||||
dependencies = [
|
||||
"indexmap 2.11.4",
|
||||
"indexmap 2.12.0",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
@@ -4772,9 +4780,9 @@ checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -4805,8 +4813,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unix-odbc"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/sqlpage/odbc-sys?branch=no-autotools#ae3e15446bb2c5c191f05e7c6affc37dfd6fcabe"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8579f2e2aaba57c09f10990cf9ab50eef8c0155820ed8a72d962c1c05af4a8a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -4905,9 +4914,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.104"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -4916,25 +4925,11 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.54"
|
||||
version = "0.4.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
|
||||
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -4945,9 +4940,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.104"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -4955,31 +4950,31 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.104"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.104"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.81"
|
||||
version = "0.3.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
|
||||
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -5337,7 +5332,7 @@ checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732"
|
||||
dependencies = [
|
||||
"android-activity",
|
||||
"atomic-waker",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"calloop",
|
||||
"cfg_aliases",
|
||||
@@ -5413,7 +5408,7 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"dlib",
|
||||
"log",
|
||||
"once_cell",
|
||||
|
||||
+2
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sqlpage"
|
||||
version = "0.38.1"
|
||||
version = "0.39.0"
|
||||
edition = "2021"
|
||||
description = "Build data user interfaces entirely in SQL. A web server that takes .sql files and formats the query result using pre-made configurable professional-looking components."
|
||||
keywords = ["web", "sql", "framework"]
|
||||
@@ -79,7 +79,7 @@ clap = { version = "4.5.17", features = ["derive"] }
|
||||
tokio-util = "0.7.12"
|
||||
openidconnect = { version = "4.0.0", default-features = false }
|
||||
encoding_rs = "0.8.35"
|
||||
odbc-sys = { version = "0.27.1", optional = true }
|
||||
odbc-sys = { version = "0.27.4", optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
@@ -87,10 +87,6 @@ default = []
|
||||
odbc-static = ["odbc-sys", "odbc-sys/vendored-unix-odbc"]
|
||||
lambda-web = ["dep:lambda-web", "odbc-static"]
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
odbc-sys = { git = "https://github.com/sqlpage/odbc-sys", branch = "no-autotools" }
|
||||
|
||||
[build-dependencies]
|
||||
awc = { version = "3", features = ["rustls-0_23-webpki-roots"] }
|
||||
rustls = "0.23"
|
||||
|
||||
@@ -164,12 +164,12 @@ To run on a server, you can use [the docker image](https://hub.docker.com/r/lova
|
||||
custom components, and migrations
|
||||
(see [configuration.md](./configuration.md)) to `/etc/sqlpage` in the container.
|
||||
- For instance, you can use:
|
||||
- `docker run -it --name sqlpage -p 8080:8080 --volume "$(pwd)/source:/var/www" --volume "$(pwd)/configuration:/etc/sqlpage:ro" --rm sqlpage/SQLPage`
|
||||
- `docker run -it --name sqlpage -p 80:8080 --volume "$(pwd)/source:/var/www" --volume "$(pwd)/configuration:/etc/sqlpage:ro" --rm lovasoa/sqlpage`
|
||||
- And place your website in a folder named `source` and your `sqlpage.json` in a folder named `configuration`.
|
||||
- If you want to build your own docker image, taking the raw sqlpage image as a base is not recommended, since it is extremely stripped down and probably won't contain the dependencies you need. Instead, you can take debian as a base and simply copy the sqlpage binary from the official image to your own image:
|
||||
- ```Dockerfile
|
||||
FROM debian:stable-slim
|
||||
COPY --from=sqlpage/SQLPage:main /usr/local/bin/sqlpage /usr/local/bin/sqlpage
|
||||
COPY --from=lovasoa/sqlpage:main /usr/local/bin/sqlpage /usr/local/bin/sqlpage
|
||||
```
|
||||
|
||||
We provide compiled binaries only for the x86_64 architecture, but provide docker images for other architectures, including arm64 and armv7. If you want to run SQLPage on a Raspberry Pi or
|
||||
@@ -359,4 +359,4 @@ Check out our [Contributing Guide](./CONTRIBUTING.md) for detailed instructions
|
||||
Our windows binaries are digitally signed, so they should be recognized as safe by Windows.
|
||||
Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/). [Contributors](https://github.com/sqlpage/SQLPage/graphs/contributors), [Owners](https://github.com/orgs/sqlpage/people?query=role%3Aowner).
|
||||
|
||||
This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it
|
||||
This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it
|
||||
|
||||
@@ -29,6 +29,7 @@ async fn main() {
|
||||
] {
|
||||
h.await.unwrap();
|
||||
}
|
||||
set_odbc_rpath();
|
||||
}
|
||||
|
||||
fn make_client() -> awc::Client {
|
||||
@@ -171,3 +172,14 @@ fn make_url_path(url: &str) -> PathBuf {
|
||||
);
|
||||
sqlpage_artefacts.join(filename)
|
||||
}
|
||||
|
||||
/// On debian-based linux distributions, odbc drivers are installed in /usr/lib/<target>-linux-gnu/odbc
|
||||
/// which is not in the default library search path.
|
||||
fn set_odbc_rpath() {
|
||||
if cfg!(all(target_os = "linux", feature = "odbc-static")) {
|
||||
println!(
|
||||
"cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/{}-linux-gnu/odbc",
|
||||
std::env::var("TARGET").unwrap().split('-').next().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,11 +810,15 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S
|
||||
('money', 'Name of a numeric column whose values should be displayed as currency amounts, in the currency defined by the `currency` property. This argument can be repeated multiple times.', 'TEXT', TRUE, TRUE),
|
||||
('currency', 'The ISO 4217 currency code (e.g., USD, EUR, GBP, etc.) to use when formatting monetary values.', 'TEXT', TRUE, TRUE),
|
||||
('number_format_digits', 'Maximum number of decimal digits to display for numeric values.', 'INTEGER', TRUE, TRUE),
|
||||
('edit_url', 'If set, an edit button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the edit button will take the user to that URL. Added in v0.39.0', 'TEXT', TRUE, TRUE),
|
||||
('delete_url', 'If set, a delete button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the delete button will take the user to that URL. Added in v0.39.0', 'TEXT', TRUE, TRUE),
|
||||
('custom_actions', 'If set, a column of custom action buttons will be added to each row. The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button). Added in v0.39.0', 'JSON', TRUE, TRUE),
|
||||
-- row level
|
||||
('_sqlpage_css_class', 'For advanced users. Sets a css class on the table row. Added in v0.8.0.', 'TEXT', FALSE, TRUE),
|
||||
('_sqlpage_color', 'Sets the background color of the row. Added in v0.8.0.', 'COLOR', FALSE, TRUE),
|
||||
('_sqlpage_footer', 'Sets this row as the table footer. It is recommended that this parameter is applied to the last row. Added in v0.34.0.', 'BOOLEAN', FALSE, TRUE),
|
||||
('_sqlpage_id', 'Sets the id of the html tabler row element. Allows you to make links targeting a specific row in a table.', 'TEXT', FALSE, TRUE)
|
||||
('_sqlpage_id', 'Sets the id of the html tabler row element. Allows you to make links targeting a specific row in a table.', 'TEXT', FALSE, TRUE),
|
||||
('_sqlpage_actions', 'Sets custom action buttons for this specific row in addition to any defined at the table level, The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button). Added in v0.39.0', 'JSON', FALSE, TRUE)
|
||||
) x;
|
||||
|
||||
INSERT INTO example(component, description, properties) VALUES
|
||||
@@ -994,7 +998,124 @@ GROUP BY
|
||||
This will generate a table with the stores in the first column, and the items in the following columns, with the quantity sold in each store for each item.
|
||||
|
||||
', NULL
|
||||
);
|
||||
),
|
||||
(
|
||||
'table',
|
||||
'## Using Action Buttons in a table.
|
||||
|
||||
### Preset Actions: `edit_url` & `delete_url`
|
||||
Since edit and delete are common actions, the `table` component has dedicated `edit_url` and `delete_url` properties to add buttons for these actions.
|
||||
The value of these properties should be a URL, containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row.
|
||||
|
||||
### Column with fixed action buttons
|
||||
|
||||
You may want to add custom action buttons to your table rows, for instance to view details, download a file, or perform a custom operation.
|
||||
For this, the `table` component has a `custom_actions` top-level property that lets you define a column of buttons, each button defined by a name, an icon, a link, and an optional tooltip.
|
||||
|
||||
### Column with variable action buttons
|
||||
|
||||
The `table` component also supports the row level `_sqlpage_actions` column in your data table.
|
||||
This is helpful if you want a more complex logic, for instance to disable a button on some rows, or to change the link or icon based on the row data.
|
||||
|
||||
> WARNING!
|
||||
> If the number of array items in `_sqlpage_actions` is not consistent across all rows, the table may not render correctly.
|
||||
> You can leave blank spaces by including an object with only the `name` property.
|
||||
|
||||
The table has a column of buttons, each button defined by the `_sqlpage_actions` column at the table level, and by the `_sqlpage_actions` property at the row level.
|
||||
### `custom_actions` & `_sqlpage_actions` JSON properties.
|
||||
Each button is defined by the following properties:
|
||||
* `name`: sets the column header and the tooltip if no tooltip is provided,
|
||||
* `tooltip`: text to display when hovering over the button,
|
||||
* `link`: the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row,
|
||||
* `icon`: the tabler icon name or image link to display on the button
|
||||
|
||||
### Example using all of the above
|
||||
'
|
||||
,
|
||||
json('[
|
||||
{
|
||||
"component": "table",
|
||||
"edit_url": "/examples/show_variables.sql?action=edit&update_id={id}",
|
||||
"delete_url": "/examples/show_variables.sql?action=delete&delete_id={id}",
|
||||
"custom_actions": [
|
||||
{
|
||||
"name": "history",
|
||||
"tooltip": "View Standard History",
|
||||
"link": "/examples/show_variables.sql?action=history&standard_id={id}",
|
||||
"icon": "history"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CalStd",
|
||||
"vendor": "PharmaCo",
|
||||
"Product": "P1234",
|
||||
"lot number": "T23523",
|
||||
"status": "Available",
|
||||
"expires on": "2026-10-13",
|
||||
"_sqlpage_id": 32,
|
||||
"_sqlpage_actions": [
|
||||
{
|
||||
"name": "View PDF",
|
||||
"tooltip": "View Presentation",
|
||||
"link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf",
|
||||
"icon": "file-type-pdf"
|
||||
},
|
||||
{
|
||||
"name": "Action",
|
||||
"tooltip": "Set In Use",
|
||||
"link": "/examples/show_variables.sql?action=set_in_use&standard_id=32",
|
||||
"icon": "caret-right"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CalStd",
|
||||
"vendor": "PharmaCo",
|
||||
"Product": "P1234",
|
||||
"lot number": "T2352",
|
||||
"status": "In Use",
|
||||
"expires on": "2026-10-14",
|
||||
"_sqlpage_id": 33,
|
||||
"_sqlpage_actions": [
|
||||
{
|
||||
"name": "View PDF",
|
||||
"tooltip": "View Presentation",
|
||||
"link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf",
|
||||
"icon": "file-type-pdf"
|
||||
},
|
||||
{
|
||||
"name": "Action",
|
||||
"tooltip": "Retire Standard",
|
||||
"link": "/examples/show_variables.sql?action=retire&standard_id=33",
|
||||
"icon": "test-pipe-off"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CalStd",
|
||||
"vendor": "PharmaCo",
|
||||
"Product": "P1234",
|
||||
"lot number": "A123",
|
||||
"status": "Discarded",
|
||||
"expires on": "2026-09-30",
|
||||
"_sqlpage_id": 31,
|
||||
"_sqlpage_actions": [
|
||||
{
|
||||
"name": "View PDF",
|
||||
"tooltip": "View Presentation",
|
||||
"link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf",
|
||||
"icon": "file-type-pdf"
|
||||
},
|
||||
{
|
||||
"name": "Action"
|
||||
}
|
||||
]
|
||||
}
|
||||
]'
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
|
||||
INSERT INTO component(name, icon, description) VALUES
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
{{~#with (parse_json this)}}
|
||||
{{#if (or (or this.title this.icon) this.image)}}
|
||||
<li class="nav-item{{#if this.submenu}} dropdown{{/if}}{{#if this.active}} active{{/if}}">
|
||||
<a class="nav-link {{#if this.submenu}}dropdown-toggle{{/if}}" href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
|
||||
<a class="nav-link {{#if this.active}}active {{/if}}{{#if this.submenu}}dropdown-toggle{{/if}}" href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
|
||||
{{~#if this.submenu}}
|
||||
data-bs-toggle="dropdown"
|
||||
{{!-- commented out until this is fixed: https://github.com/tabler/tabler/issues/2485
|
||||
@@ -125,7 +125,7 @@
|
||||
<div class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
|
||||
{{~#each this.submenu~}}
|
||||
{{#if (or (or this.title this.icon) this.image)}}
|
||||
<a class="dropdown-item" href="{{this.link}}" {{#if this.target}}target="{{this.target}}"{{/if}}>
|
||||
<a class="dropdown-item{{#if this.active}} active{{/if}}" href="{{this.link}}" {{#if this.target}}target="{{this.target}}"{{/if}}>
|
||||
{{~#if this.image~}}
|
||||
<span {{~#if this.title}} class="me-1"{{/if}}>
|
||||
{{~#if (eq ../this.size 'sm')}}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{{#if (or search initial_search_value)}}
|
||||
<div class="p-3">
|
||||
<input
|
||||
id="{{component_index}}-search"
|
||||
type="search"
|
||||
class="form-control form-control-rounded fs-6 search"
|
||||
placeholder="{{default search_placeholder 'Search…'}}"
|
||||
@@ -52,6 +53,18 @@
|
||||
</th>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{#if ../edit_url}}<th class="_col_edit text-center">Edit</th>{{/if}}
|
||||
{{#if ../delete_url}}<th class="_col_delete text-center">Delete</th>{{/if}}
|
||||
{{#if ../custom_actions}}
|
||||
{{#each ../custom_actions}}
|
||||
<th class="_col_custom text-center">{{this.name}}</th>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if _sqlpage_actions}}
|
||||
{{#each _sqlpage_actions}}
|
||||
<th class="_col_action text-center">{{this.name}}</th>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody list">{{#delay}}</tbody>{{/delay}}
|
||||
@@ -78,6 +91,38 @@
|
||||
</td>
|
||||
{{/if~}}
|
||||
{{~/each~}}
|
||||
{{#if ../edit_url}}
|
||||
<td class="align-middle _col_edit">
|
||||
<a href="{{replace ../edit_url '{id}' _sqlpage_id}}" class="align-middle link-secondary _col_edit" data-action="edit" title="Edit">
|
||||
{{~icon_img 'edit'~}}
|
||||
</a>
|
||||
</td>
|
||||
{{/if}}
|
||||
{{#if ../delete_url}}
|
||||
<td class="align-middle _col_delete text-center">
|
||||
<a href="{{replace ../delete_url '{id}' _sqlpage_id}}" class="align-middle link-secondary _col_delete" data-action="delete" title="Delete">
|
||||
{{~icon_img 'trash'~}}
|
||||
</a>
|
||||
</td>
|
||||
{{/if}}
|
||||
{{#if ../custom_actions}}
|
||||
{{#each ../custom_actions}}
|
||||
<td class="align-middle _col_{{this.name}} text-center">
|
||||
<a href="{{replace this.link '{id}' ../_sqlpage_id}}" class="align-middle link-secondary _col_{{this.name}}" data-action="{{this.name}}" title="{{default this.tooltip this.name}}">{{!Title property sets the tooltip text}}
|
||||
{{~icon_img this.icon~}}
|
||||
</a>
|
||||
</td>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if _sqlpage_actions}}
|
||||
{{#each _sqlpage_actions}}
|
||||
<td class="align-middle _col_{{this.name}} text-center">
|
||||
<a href="{{replace this.link '{id}' ../_sqlpage_id}}" class="align-middle link-secondary _col_{{this.name}}" data-action="{{this.name}}" title="{{default this.tooltip this.name}}">
|
||||
{{~icon_img this.icon~}}
|
||||
</a>
|
||||
</td>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
{{!~
|
||||
After this <tr> has been rendered, if this was a footer, we need to reopen a new <tbody>
|
||||
|
||||
+10
-2
@@ -80,7 +80,7 @@ pub enum PageContext {
|
||||
/// Handles the first SQL statements, before the headers have been sent to
|
||||
pub struct HeaderContext {
|
||||
app_state: Arc<AppState>,
|
||||
request_context: RequestContext,
|
||||
pub request_context: RequestContext,
|
||||
pub writer: ResponseWriter,
|
||||
response: HttpResponseBuilder,
|
||||
has_status: bool,
|
||||
@@ -368,7 +368,14 @@ impl HeaderContext {
|
||||
Ok(PageContext::Header(self))
|
||||
}
|
||||
|
||||
async fn start_body(self, data: JsonValue) -> anyhow::Result<PageContext> {
|
||||
fn add_server_timing_header(&mut self) {
|
||||
if let Some(header_value) = self.request_context.server_timing.header_value() {
|
||||
self.response.insert_header(("Server-Timing", header_value));
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_body(mut self, data: JsonValue) -> anyhow::Result<PageContext> {
|
||||
self.add_server_timing_header();
|
||||
let html_renderer =
|
||||
HtmlRenderContext::new(self.app_state, self.request_context, self.writer, data)
|
||||
.await
|
||||
@@ -382,6 +389,7 @@ impl HeaderContext {
|
||||
}
|
||||
|
||||
pub fn close(mut self) -> HttpResponse {
|
||||
self.add_server_timing_header();
|
||||
self.response.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,41 +27,47 @@ impl std::fmt::Display for NiceDatabaseError {
|
||||
)?;
|
||||
if let sqlx::error::Error::Database(db_err) = &self.db_err {
|
||||
let Some(mut offset) = db_err.offset() else {
|
||||
return write!(f, "{}", self.query);
|
||||
write!(f, "{}", self.query)?;
|
||||
self.show_position_info(f)?;
|
||||
return Ok(());
|
||||
};
|
||||
for line in self.query.lines() {
|
||||
if offset > line.len() {
|
||||
offset -= line.len() + 1;
|
||||
} else {
|
||||
highlight_line_offset(f, line, offset);
|
||||
if let Some(query_position) = self.query_position {
|
||||
let start_line = query_position.start.line;
|
||||
let end_line = query_position.end.line;
|
||||
if start_line == end_line {
|
||||
write!(f, "{}: line {}", self.source_file.display(), start_line)?;
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}: lines {} to {}",
|
||||
self.source_file.display(),
|
||||
start_line,
|
||||
end_line
|
||||
)?;
|
||||
}
|
||||
}
|
||||
self.show_position_info(f)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
write!(f, "{}", self.query)
|
||||
write!(f, "{}", self.query)?;
|
||||
self.show_position_info(f)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NiceDatabaseError {
|
||||
fn show_position_info(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(f, "\n{}", self.source_file.display())?;
|
||||
let _: () = if let Some(query_position) = self.query_position {
|
||||
let start_line = query_position.start.line;
|
||||
let end_line = query_position.end.line;
|
||||
if start_line == end_line {
|
||||
write!(f, ": line {start_line}")?;
|
||||
} else {
|
||||
write!(f, ": lines {start_line} to {end_line}")?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NiceDatabaseError {}
|
||||
|
||||
/// Display a database error with a highlighted line and character offset.
|
||||
/// Display a database error without any position information
|
||||
#[must_use]
|
||||
pub fn display_db_error(
|
||||
source_file: &Path,
|
||||
|
||||
@@ -52,13 +52,13 @@ pub fn stream_query_results_with_conn<'a>(
|
||||
for res in &sql_file.statements {
|
||||
match res {
|
||||
ParsedStatement::CsvImport(csv_import) => {
|
||||
let connection = take_connection(&request.app_state.db, db_connection).await?;
|
||||
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
|
||||
log::debug!("Executing CSV import: {csv_import:?}");
|
||||
run_csv_import(connection, csv_import, request).await.with_context(|| format!("Failed to import the CSV file {:?} into the table {:?}", csv_import.uploaded_file, csv_import.table_name))?;
|
||||
},
|
||||
ParsedStatement::StmtWithParams(stmt) => {
|
||||
let query = bind_parameters(stmt, request, db_connection).await?;
|
||||
let connection = take_connection(&request.app_state.db, db_connection).await?;
|
||||
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
|
||||
log::trace!("Executing query {:?}", query.sql);
|
||||
let mut stream = connection.fetch_many(query);
|
||||
let mut error = None;
|
||||
@@ -192,7 +192,7 @@ async fn execute_set_variable_query<'a>(
|
||||
source_file: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
let query = bind_parameters(statement, request, db_connection).await?;
|
||||
let connection = take_connection(&request.app_state.db, db_connection).await?;
|
||||
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
|
||||
log::debug!(
|
||||
"Executing query to set the {variable:?} variable: {:?}",
|
||||
query.sql
|
||||
@@ -276,6 +276,7 @@ fn vars_and_name<'a, 'b>(
|
||||
async fn take_connection<'a>(
|
||||
db: &'a Database,
|
||||
conn: &'a mut DbConn,
|
||||
request: &RequestInfo,
|
||||
) -> anyhow::Result<&'a mut PoolConnection<sqlx::Any>> {
|
||||
if let Some(c) = conn {
|
||||
return Ok(c);
|
||||
@@ -283,6 +284,7 @@ async fn take_connection<'a>(
|
||||
match db.connection.acquire().await {
|
||||
Ok(c) => {
|
||||
log::debug!("Acquired a database connection");
|
||||
request.server_timing.record("db_conn");
|
||||
*conn = Some(c);
|
||||
Ok(conn.as_mut().unwrap())
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ use sqlparser::dialect::{
|
||||
};
|
||||
use sqlparser::parser::{Parser, ParserError};
|
||||
use sqlparser::tokenizer::Token::{self, SemiColon, EOF};
|
||||
use sqlparser::tokenizer::{TokenWithSpan, Tokenizer};
|
||||
use sqlparser::tokenizer::{Location, Span, TokenWithSpan, Tokenizer};
|
||||
use sqlx::any::AnyKind;
|
||||
use std::fmt::Write;
|
||||
use std::ops::ControlFlow;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
@@ -241,11 +242,29 @@ fn extract_query_start(stmt: &impl Spanned) -> SourceSpan {
|
||||
}
|
||||
|
||||
fn syntax_error(err: ParserError, parser: &Parser, sql: &str) -> ParsedStatement {
|
||||
let location = parser.peek_token_no_skip().span;
|
||||
ParsedStatement::Error(anyhow::Error::from(err).context(format!(
|
||||
"Parsing failed: SQLPage couldn't understand the SQL file. Please check for syntax errors:\n\n{}",
|
||||
quote_source_with_highlight(sql, location.start.line, location.start.column)
|
||||
)))
|
||||
let Span {
|
||||
start: Location {
|
||||
line: start_line,
|
||||
column: start_column,
|
||||
},
|
||||
end: Location { line: end_line, .. },
|
||||
} = parser.peek_token_no_skip().span;
|
||||
|
||||
let mut msg = String::from(
|
||||
"Parsing failed: SQLPage couldn't understand the SQL file. Please check for syntax errors on ",
|
||||
);
|
||||
if start_line == end_line {
|
||||
write!(&mut msg, "line {start_line}:").unwrap();
|
||||
} else {
|
||||
write!(&mut msg, "lines {start_line} to {end_line}:").unwrap();
|
||||
}
|
||||
write!(
|
||||
&mut msg,
|
||||
"\n{}",
|
||||
quote_source_with_highlight(sql, start_line, start_column)
|
||||
)
|
||||
.unwrap();
|
||||
ParsedStatement::Error(anyhow::Error::from(err).context(msg))
|
||||
}
|
||||
|
||||
fn dialect_for_db(dbms: SupportedDatabase) -> Box<dyn Dialect> {
|
||||
|
||||
+17
-4
@@ -7,6 +7,7 @@ use crate::webserver::content_security_policy::ContentSecurityPolicy;
|
||||
use crate::webserver::database::execute_queries::stop_at_first_error;
|
||||
use crate::webserver::database::{execute_queries::stream_query_results_with_conn, DbItem};
|
||||
use crate::webserver::http_request_info::extract_request_info;
|
||||
use crate::webserver::server_timing::ServerTiming;
|
||||
use crate::webserver::ErrorWithStatus;
|
||||
use crate::{AppConfig, AppState, ParsedSqlFile, DEFAULT_404_FILE};
|
||||
use actix_web::dev::{fn_service, ServiceFactory, ServiceRequest};
|
||||
@@ -46,6 +47,7 @@ pub struct RequestContext {
|
||||
pub is_embedded: bool,
|
||||
pub source_path: PathBuf,
|
||||
pub content_security_policy: ContentSecurityPolicy,
|
||||
pub server_timing: Arc<ServerTiming>,
|
||||
}
|
||||
|
||||
async fn stream_response(stream: impl Stream<Item = DbItem>, mut renderer: AnyRenderBodyContext) {
|
||||
@@ -106,7 +108,10 @@ async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
|
||||
let mut stream = Box::pin(database_entries);
|
||||
while let Some(item) = stream.next().await {
|
||||
let page_context = match item {
|
||||
DbItem::Row(data) => head_context.handle_row(data).await?,
|
||||
DbItem::Row(data) => {
|
||||
head_context.request_context.server_timing.record("row");
|
||||
head_context.handle_row(data).await?
|
||||
}
|
||||
DbItem::FinishedQuery => {
|
||||
log::debug!("finished query");
|
||||
continue;
|
||||
@@ -163,18 +168,21 @@ enum ResponseWithWriter<S> {
|
||||
async fn render_sql(
|
||||
srv_req: &mut ServiceRequest,
|
||||
sql_file: Arc<ParsedSqlFile>,
|
||||
server_timing: ServerTiming,
|
||||
) -> actix_web::Result<HttpResponse> {
|
||||
let app_state = srv_req
|
||||
.app_data::<web::Data<AppState>>()
|
||||
.ok_or_else(|| ErrorInternalServerError("no state"))?
|
||||
.clone() // Cheap reference count increase
|
||||
.clone()
|
||||
.into_inner();
|
||||
|
||||
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state))
|
||||
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state), server_timing)
|
||||
.await
|
||||
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
|
||||
log::debug!("Received a request with the following parameters: {req_param:?}");
|
||||
|
||||
req_param.server_timing.record("parse_req");
|
||||
|
||||
let (resp_send, resp_recv) = tokio::sync::oneshot::channel::<HttpResponse>();
|
||||
let source_path: PathBuf = sql_file.source_path.clone();
|
||||
actix_web::rt::spawn(async move {
|
||||
@@ -182,6 +190,7 @@ async fn render_sql(
|
||||
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
|
||||
source_path,
|
||||
content_security_policy: ContentSecurityPolicy::with_random_nonce(),
|
||||
server_timing: Arc::clone(&req_param.server_timing),
|
||||
};
|
||||
let mut conn = None;
|
||||
let database_entries_stream =
|
||||
@@ -275,13 +284,17 @@ async fn process_sql_request(
|
||||
sql_path: PathBuf,
|
||||
) -> actix_web::Result<HttpResponse> {
|
||||
let app_state: &web::Data<AppState> = req.app_data().expect("app_state");
|
||||
let server_timing = ServerTiming::for_env(app_state.config.environment);
|
||||
|
||||
let sql_file = app_state
|
||||
.sql_file_cache
|
||||
.get_with_privilege(app_state, &sql_path, false)
|
||||
.await
|
||||
.with_context(|| format!("Unable to read SQL file \"{}\"", sql_path.display()))
|
||||
.map_err(|e| anyhow_err_to_actix(e, app_state))?;
|
||||
render_sql(req, sql_file).await
|
||||
server_timing.record("sql_file");
|
||||
|
||||
render_sql(req, sql_file, server_timing).await
|
||||
}
|
||||
|
||||
async fn serve_file(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::webserver::server_timing::ServerTiming;
|
||||
use crate::AppState;
|
||||
use actix_multipart::form::bytes::Bytes;
|
||||
use actix_multipart::form::tempfile::TempFile;
|
||||
@@ -42,6 +43,7 @@ pub struct RequestInfo {
|
||||
pub clone_depth: u8,
|
||||
pub raw_body: Option<Vec<u8>>,
|
||||
pub oidc_claims: Option<OidcClaims>,
|
||||
pub server_timing: Arc<super::server_timing::ServerTiming>,
|
||||
}
|
||||
|
||||
impl RequestInfo {
|
||||
@@ -62,6 +64,7 @@ impl RequestInfo {
|
||||
clone_depth: self.clone_depth + 1,
|
||||
raw_body: self.raw_body.clone(),
|
||||
oidc_claims: self.oidc_claims.clone(),
|
||||
server_timing: Arc::clone(&self.server_timing),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +81,7 @@ impl Clone for RequestInfo {
|
||||
pub(crate) async fn extract_request_info(
|
||||
req: &mut ServiceRequest,
|
||||
app_state: Arc<AppState>,
|
||||
server_timing: ServerTiming,
|
||||
) -> anyhow::Result<RequestInfo> {
|
||||
let (http_req, payload) = req.parts_mut();
|
||||
let method = http_req.method().clone();
|
||||
@@ -123,6 +127,7 @@ pub(crate) async fn extract_request_info(
|
||||
clone_depth: 0,
|
||||
raw_body,
|
||||
oidc_claims,
|
||||
server_timing: Arc::new(server_timing),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -275,7 +280,7 @@ async fn is_file_field_empty(
|
||||
mod test {
|
||||
use super::super::http::SingleOrVec;
|
||||
use super::*;
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::{app_config::AppConfig, webserver::server_timing::ServerTiming};
|
||||
use actix_web::{http::header::ContentType, test::TestRequest};
|
||||
|
||||
#[actix_web::test]
|
||||
@@ -284,7 +289,8 @@ mod test {
|
||||
serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
|
||||
let mut service_request = TestRequest::default().to_srv_request();
|
||||
let app_data = Arc::new(AppState::init(&config).await.unwrap());
|
||||
let request_info = extract_request_info(&mut service_request, app_data)
|
||||
let server_timing = ServerTiming::default();
|
||||
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(request_info.post_variables.len(), 0);
|
||||
@@ -302,7 +308,8 @@ mod test {
|
||||
.set_payload("my_array[]=3&my_array[]=Hello%20World&repeated=1&repeated=2")
|
||||
.to_srv_request();
|
||||
let app_data = Arc::new(AppState::init(&config).await.unwrap());
|
||||
let request_info = extract_request_info(&mut service_request, app_data)
|
||||
let server_timing = ServerTiming::default();
|
||||
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -351,7 +358,8 @@ mod test {
|
||||
)
|
||||
.to_srv_request();
|
||||
let app_data = Arc::new(AppState::init(&config).await.unwrap());
|
||||
let request_info = extract_request_info(&mut service_request, app_data)
|
||||
let server_timing = ServerTiming::enabled(false);
|
||||
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -38,6 +38,7 @@ pub mod http_client;
|
||||
pub mod http_request_info;
|
||||
mod https;
|
||||
pub mod request_variables;
|
||||
pub mod server_timing;
|
||||
|
||||
pub use database::Database;
|
||||
pub use error_with_status::ErrorWithStatus;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::fmt::Write;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::app_config::DevOrProd;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerTiming {
|
||||
enabled: bool,
|
||||
created_at: Instant,
|
||||
events: Mutex<Vec<PerfEvent>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PerfEvent {
|
||||
time: Instant,
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
impl Default for ServerTiming {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
created_at: Instant::now(),
|
||||
events: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerTiming {
|
||||
#[must_use]
|
||||
pub fn enabled(enabled: bool) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn for_env(env: DevOrProd) -> Self {
|
||||
Self::enabled(!env.is_prod())
|
||||
}
|
||||
|
||||
pub fn record(&self, name: &'static str) {
|
||||
if self.enabled {
|
||||
self.events.lock().unwrap().push(PerfEvent {
|
||||
time: Instant::now(),
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header_value(&self) -> Option<String> {
|
||||
if !self.enabled {
|
||||
return None;
|
||||
}
|
||||
let evts = self.events.lock().unwrap();
|
||||
let mut s = String::with_capacity(evts.len() * 16);
|
||||
let mut last = self.created_at;
|
||||
for (i, PerfEvent { name, time }) in evts.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push_str(", ");
|
||||
}
|
||||
let millis = time.saturating_duration_since(last).as_millis();
|
||||
write!(&mut s, "{name};dur={millis}").ok()?;
|
||||
last = *time;
|
||||
}
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
@@ -213,3 +213,87 @@ test("modal", async ({ page }) => {
|
||||
await modal.getByRole("button", { label: "Close" }).first().click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("table action buttons - edit_url and delete_url", async ({ page }) => {
|
||||
await page.goto(`${BASE}/documentation.sql?component=table`);
|
||||
const tableSection = page.locator(".table-responsive", {
|
||||
has: page.getByRole("cell", { name: "PharmaCo" }),
|
||||
});
|
||||
|
||||
const editButton = tableSection.getByTitle("Edit").first();
|
||||
await expect(editButton).toBeVisible();
|
||||
await expect(editButton).toHaveAttribute("href", /action=edit&update_id=\d+/);
|
||||
|
||||
const deleteButton = tableSection.getByTitle("Delete").first();
|
||||
await expect(deleteButton).toBeVisible();
|
||||
await expect(deleteButton).toHaveAttribute(
|
||||
"href",
|
||||
/action=delete&delete_id=\d+/,
|
||||
);
|
||||
});
|
||||
|
||||
test("table action buttons - custom_actions", async ({ page }) => {
|
||||
await page.goto(`${BASE}/documentation.sql?component=table`);
|
||||
const tableSection = page.locator(".table-responsive", {
|
||||
has: page.getByRole("cell", { name: "PharmaCo" }),
|
||||
});
|
||||
|
||||
const historyButton = tableSection
|
||||
.getByTitle("View Standard History")
|
||||
.first();
|
||||
await expect(historyButton).toBeVisible();
|
||||
await expect(historyButton).toHaveAttribute(
|
||||
"href",
|
||||
/action=history&standard_id=\d+/,
|
||||
);
|
||||
});
|
||||
|
||||
test("table action buttons - _sqlpage_actions", async ({ page }) => {
|
||||
await page.goto(`${BASE}/documentation.sql?component=table`);
|
||||
const tableSection = page.locator(".table-responsive", {
|
||||
has: page.getByRole("cell", { name: "PharmaCo" }),
|
||||
});
|
||||
|
||||
const pdfButtons = tableSection.getByTitle("View Presentation");
|
||||
await expect(pdfButtons.first()).toBeVisible();
|
||||
await expect(pdfButtons).toHaveCount(3);
|
||||
|
||||
const firstPdfButton = pdfButtons.first();
|
||||
await expect(firstPdfButton).toHaveAttribute(
|
||||
"href",
|
||||
"https://sql-page.com/pgconf/2024-sqlpage-badass.pdf",
|
||||
);
|
||||
|
||||
const setInUseButton = tableSection.getByTitle("Set In Use");
|
||||
await expect(setInUseButton).toBeVisible();
|
||||
await expect(setInUseButton).toHaveAttribute(
|
||||
"href",
|
||||
/action=set_in_use&standard_id=32/,
|
||||
);
|
||||
|
||||
const retireButton = tableSection.getByTitle("Retire Standard");
|
||||
await expect(retireButton).toBeVisible();
|
||||
await expect(retireButton).toHaveAttribute(
|
||||
"href",
|
||||
/action=retire&standard_id=33/,
|
||||
);
|
||||
});
|
||||
|
||||
test("table action buttons - disabled action", async ({ page }) => {
|
||||
await page.goto(`${BASE}/documentation.sql?component=table`);
|
||||
const tableSection = page.locator(".table-responsive", {
|
||||
has: page.getByRole("cell", { name: "PharmaCo" }),
|
||||
});
|
||||
|
||||
const viewPresentationButtons = tableSection.getByTitle("View Presentation");
|
||||
await expect(viewPresentationButtons).toHaveCount(3);
|
||||
|
||||
const actionColumnButtons = tableSection.locator(
|
||||
"td._col_Action a[data-action='Action']",
|
||||
);
|
||||
await expect(actionColumnButtons).toHaveCount(3);
|
||||
|
||||
const emptyActionButton = actionColumnButtons.last();
|
||||
await expect(emptyActionButton).toHaveAttribute("href", "null");
|
||||
await expect(emptyActionButton).toHaveAttribute("title", "Action");
|
||||
});
|
||||
|
||||
Generated
+19
-19
@@ -10,17 +10,17 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.45.3",
|
||||
"@types/node": "^22.1.0"
|
||||
"@types/node": "^24.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
|
||||
"integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
||||
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.53.1"
|
||||
"playwright": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -30,13 +30,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.33.tgz",
|
||||
"integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==",
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
@@ -55,13 +55,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
|
||||
"integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
||||
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.53.1"
|
||||
"playwright-core": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -74,9 +74,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
|
||||
"integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -87,9 +87,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.45.3",
|
||||
"@types/node": "^22.1.0"
|
||||
"@types/node": "^24.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ mod core;
|
||||
mod data_formats;
|
||||
mod errors;
|
||||
mod requests;
|
||||
mod server_timing;
|
||||
pub mod sql_test_files;
|
||||
mod transactions;
|
||||
mod uploads;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use sqlpage::webserver::http::main_handler;
|
||||
|
||||
use crate::common::{get_request_to, make_app_data_from_config, test_config};
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_server_timing_disabled_in_production() -> actix_web::Result<()> {
|
||||
let mut config = test_config();
|
||||
config.environment = sqlpage::app_config::DevOrProd::Production;
|
||||
let app_data = make_app_data_from_config(config).await;
|
||||
|
||||
let req = crate::common::get_request_to_with_data(
|
||||
"/tests/sql_test_files/it_works_simple.sql",
|
||||
app_data,
|
||||
)
|
||||
.await?
|
||||
.to_srv_request();
|
||||
let resp = main_handler(req).await?;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert!(
|
||||
resp.headers().get("Server-Timing").is_none(),
|
||||
"Server-Timing header should not be present in production mode"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_server_timing_enabled_in_development() -> actix_web::Result<()> {
|
||||
let mut config = test_config();
|
||||
config.environment = sqlpage::app_config::DevOrProd::Development;
|
||||
let app_data = make_app_data_from_config(config).await;
|
||||
|
||||
let req = crate::common::get_request_to_with_data(
|
||||
"/tests/sql_test_files/it_works_sqrt.sql",
|
||||
app_data,
|
||||
)
|
||||
.await?
|
||||
.to_srv_request();
|
||||
let resp = main_handler(req).await?;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let server_timing_header = resp
|
||||
.headers()
|
||||
.get("Server-Timing")
|
||||
.expect("Server-Timing header should be present in development mode");
|
||||
let header_value = server_timing_header.to_str().unwrap();
|
||||
|
||||
assert!(
|
||||
header_value.contains("sql_file;dur="),
|
||||
"Should contain sql_file timing: {header_value}"
|
||||
);
|
||||
assert!(
|
||||
header_value.contains("parse_req;dur="),
|
||||
"Should contain parse_req timing: {header_value}"
|
||||
);
|
||||
assert!(
|
||||
header_value.contains("db_conn;dur="),
|
||||
"Should contain db_conn timing: {header_value}"
|
||||
);
|
||||
assert!(
|
||||
header_value.contains("row;dur="),
|
||||
"Should contain row timing: {header_value}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_server_timing_format() -> actix_web::Result<()> {
|
||||
let req = get_request_to("/tests/sql_test_files/it_works_sqrt.sql")
|
||||
.await?
|
||||
.to_srv_request();
|
||||
let resp = main_handler(req).await?;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let server_timing_header = resp.headers().get("Server-Timing").unwrap();
|
||||
let header_value = server_timing_header.to_str().unwrap();
|
||||
|
||||
let parts: Vec<&str> = header_value.split(", ").collect();
|
||||
assert!(parts.len() >= 4, "Should have at least 4 timing events");
|
||||
|
||||
for part in parts {
|
||||
assert!(
|
||||
part.contains(";dur="),
|
||||
"Each part should have name;dur= format: {part}"
|
||||
);
|
||||
let dur_parts: Vec<&str> = part.split(";dur=").collect();
|
||||
assert_eq!(dur_parts.len(), 2, "Should have name and duration: {part}");
|
||||
let duration: f64 = dur_parts[1]
|
||||
.parse()
|
||||
.expect("Duration should be a valid number");
|
||||
assert!(
|
||||
duration >= 0.0,
|
||||
"Duration should be non-negative: {duration}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user