diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..46235b79 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,46 @@ +name: End to end Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 10 + defaults: + run: + working-directory: ./tests/end-to-end + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up cargo cache + uses: Swatinem/rust-cache@378c8285a4eaf12899d11bea686a763e906956af + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: 'npm' + cache-dependency-path: ./tests/end-to-end/package-lock.json + - name: Install dependencies + run: | + npm ci + npx playwright install --with-deps chromium + - name: build sqlpage + run: cargo build + working-directory: ./examples/official-site + - name: start official site and wait for it to be ready + timeout-minutes: 1 + run: | + cargo run 2>/tmp/stderrlog & + tail -f /tmp/stderrlog | grep -q "started successfully" + working-directory: ./examples/official-site + - name: Run Playwright tests + run: npx playwright test + - name: show server logs + if: failure() + run: cat /tmp/stderrlog + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: ./tests/end-to-end/playwright-report/ + retention-days: 30 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0fad2c..824d9531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,56 @@ # CHANGELOG.md -## unreleased +## 0.26.0 (2024-08-06) +### Components +#### Card +New `width` attribute in the [card](https://sql.ophir.dev/documentation.sql?component=card#component) component to set the width of the card. This finally allows you to create custom layouts, by combining the `embed` and `width` attributes of the card component! This also updates the default layout of the card component: when `columns` is not set, there is now a default of 4 columns instead of 5. -- re-add a link to the website title in the shell component -- add `text` and `post_html` properties to the [html](https://sql.ophir.dev/documentation.sql?component=html#component) component. This allows to include sanitized user-generated content in the middle of custom HTML. - - allow loading javascript ESM modules in the shell component +![image](https://github.com/user-attachments/assets/98425bd8-c576-4628-9ae2-db3ba4650019) + + +#### Datagrid +fix [datagrid](https://sql.ophir.dev/documentation.sql?component=datagrid#component) color pills display when they contain long text. + +![image](https://github.com/user-attachments/assets/3b7dba27-8812-410c-a383-2b62d6a286ac) + +#### Table +Fixed a bug that could cause issues with other components when a table was empty. +Improved handling of empty tables. Added a new `empty_description` attribute, which defaults to `No data`. This allows you to display a custom message when a table is empty. + +![image](https://github.com/user-attachments/assets/c370f841-20c5-4cbf-8c9e-7318dce9b87c) + +#### Form + - Fixed a bug where a form input with a value of `0` would diplay as empty instead of showing the `0`. + - Reduced the margin at the botton of forms to fix the appearance of forms that are validated by a `button` component declared separately from the form. + +#### Shell +Fixed ugly wrapping of items in the header when the page title is long. We now have a nice text ellipsis (...) when the title is too long. +![image](https://github.com/user-attachments/assets/3ac22d98-dde5-49c2-8f72-45ee7595fe82) + +Fixed the link to the website title in the shell component. + +Allow loading javascript ESM modules in the shell component with the new `javascript_module` property. + +#### html +Added `text` and `post_html` properties to the [html](https://sql.ophir.dev/documentation.sql?component=html#component) component. This allows to include sanitized user-generated content in the middle of custom HTML. + +```sql +select + 'html' as component; +select + 'Username: ' as html, + 'username that will be safely escaped: <"& ' as text, + '' as post_html; +``` + +### Other - allow customizing the [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) in the configuration. + - the new default *content security policy* is both more secure and easier to use. You can now include inline javascript in your custom components with ``. - update to [sqlparser v0.49.0](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md#0490-2024-07-23) - support [`WITH ORDINALITY`](https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-TABLEFUNCTIONS) in postgres `FROM` clauses - update to [handlebars-rs v6](https://github.com/sunng87/handlebars-rust/blob/master/CHANGELOG.md#600---2024-07-20) + - fix the "started successfully" message being displayed before the error message when the server failed to start. + - add support for using the system's native SSL Certificate Authority (CA) store in `sqlpage.fetch`. See the new `system_root_ca_certificates` configuration option. ## 0.25.0 (2024-07-13) diff --git a/Cargo.lock b/Cargo.lock index 7bc7ad05..81abb7d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,16 +135,16 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" +checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "futures-util", - "mio 0.8.11", + "mio", "socket2", "tokio", "tracing", @@ -343,9 +343,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -358,33 +358,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -782,9 +782,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "bytestring" @@ -797,9 +797,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" dependencies = [ "jobserver", "libc", @@ -827,9 +827,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "concurrent-queue" @@ -912,6 +912,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -1186,9 +1196,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6dc8c8ff84895b051f07a0e65f975cf225131742531338752abfb324e4449ff" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", "regex", @@ -1196,9 +1206,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06676b12debf7bba6903559720abca942d3a66b8acb88815fd2c7c6537e9ade1" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", @@ -1263,9 +1273,9 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", "miniz_oxide", @@ -1713,9 +1723,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -1723,9 +1733,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" @@ -1874,9 +1884,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b694a822684ddb75df4d657029161431bcb4a85c1856952f845b76912bc6fec" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -1985,18 +1995,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.0.1" @@ -2005,6 +2003,7 @@ checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ "hermit-abi 0.3.9", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -2084,9 +2083,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" dependencies = [ "memchr", ] @@ -2106,6 +2105,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "option-ext" version = "0.2.0" @@ -2343,9 +2348,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro2" @@ -2438,9 +2446,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -2611,11 +2619,24 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -2623,9 +2644,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -2644,12 +2665,44 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.23" @@ -2678,12 +2731,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "indexmap", "itoa", + "memchr", "ryu", "serde", ] @@ -2699,9 +2753,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -2815,7 +2869,7 @@ dependencies = [ [[package]] name = "sqlpage" -version = "0.25.0" +version = "0.26.0" dependencies = [ "actix-multipart", "actix-rt", @@ -2844,7 +2898,9 @@ dependencies = [ "password-hash", "percent-encoding", "rand", + "rustls", "rustls-acme", + "rustls-native-certs", "serde", "serde_json", "sqlparser", @@ -3037,12 +3093,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", "windows-sys 0.52.0", ] @@ -3124,14 +3181,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.1", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -3188,9 +3245,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -3200,18 +3257,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", @@ -3394,9 +3451,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" @@ -3665,9 +3722,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] @@ -3713,6 +3770,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -3744,18 +3802,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.0" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.12+zstd.1.5.6" +version = "2.0.13+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 131f7175..836013e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlpage" -version = "0.25.0" +version = "0.26.0" edition = "2021" description = "A SQL-only web application framework. Takes .sql files and formats the query result using pre-made configurable professional-looking components." keywords = ["web", "sql", "framework"] @@ -58,6 +58,8 @@ base64 = "0.22" rustls-acme = "0.9.2" dotenvy = "0.15.7" csv-async = { version = "1.2.6", features = ["tokio"] } +rustls = { version = "0.22.0" } # keep in sync with actix-web, awc, rustls-acme, and sqlx +rustls-native-certs = "0.7.0" awc = { version = "3", features = ["rustls-0_22-webpki-roots"] } [build-dependencies] diff --git a/Dockerfile b/Dockerfile index a5ffb7c1..aef94289 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM rust:1.79-slim AS builder +FROM --platform=$BUILDPLATFORM rust:1.80-slim AS builder WORKDIR /usr/src/sqlpage ARG TARGETARCH ARG BUILDARCH diff --git a/README.md b/README.md index ab16ae81..c5843625 100644 --- a/README.md +++ b/README.md @@ -130,21 +130,6 @@ select - [MySQL](https://www.mysql.com/), and other compatible databases such as *MariaDB* and *TiDB*. - [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server), and all compatible databases and providers such as *Azure SQL* and *Amazon RDS*. -## How it works - -![architecture diagram](./docs/architecture-detailed.png) - -SQLPage is a [web server](https://en.wikipedia.org/wiki/Web_server) written in -[rust](https://en.wikipedia.org/wiki/Rust_(programming_language)) -and distributed as a single executable file. -When it receives a request to a URL ending in `.sql`, it finds the corresponding -SQL file, runs it on the database, -passing it information from the web request as SQL statement parameters. -When the database starts returning rows for the query, -SQLPage maps each piece of information in the row to a parameter -in one of its pre-defined components' templates, and streams the result back -to the user's browser. - ## Get started [Read the official *get started* guide on SQLPage's website](https://sql.ophir.dev/get_started.sql). @@ -191,10 +176,24 @@ An alternative for Mac OS users is to use [SQLPage's homebrew package](https://f - In a terminal, run the following commands: - `brew install sqlpage` +## How it works + +![architecture diagram](./docs/architecture-detailed.png) + +SQLPage is a [web server](https://en.wikipedia.org/wiki/Web_server) written in +[rust](https://en.wikipedia.org/wiki/Rust_(programming_language)) +and distributed as a single executable file. +When it receives a request to a URL ending in `.sql`, it finds the corresponding +SQL file, runs it on the database, +passing it information from the web request as SQL statement parameters. +When the database starts returning rows for the query, +SQLPage maps each piece of information in the row to a parameter +in one of its pre-defined components' templates, and streams the result back +to the user's browser. ## Examples - - [TODO list](./examples/todo%20application/): a simple todo list application, illustrating how to create a basic CRUD application with SQLPage. +- [TODO list](./examples/todo%20application/): a simple todo list application, illustrating how to create a basic CRUD application with SQLPage. - [Plots, Tables, forms, and interactivity](./examples/plots%20tables%20and%20forms/): a short well-commented demo showing how to use plots, tables, forms, and interactivity to filter data based on an URL parameter. - [Tiny splitwise clone](./examples/splitwise): a shared expense tracker app - [Corporate Conundrum](./examples/corporate-conundrum/): a board game implemented in SQL @@ -208,7 +207,7 @@ An alternative for Mac OS users is to use [SQLPage's homebrew package](https://f - [Advanced authentication example using PostgreSQL stored procedures](https://github.com/mnesarco/sqlpage_auth_example) - [Complex web application in SQLite with user management, file uploads, plots, maps, tables, menus, ...](https://github.com/DSMejantel/Ecole_inclusive) - [Single sign-on](./examples/single%20sign%20on): An example of how to implement OAuth and OpenID Connect (OIDC) authentication in SQLPage. The demo also includes a CAS (Central Authentication Service) client. - - [Dark theme](./examples/light-dark-toggle/) : demonstrates how to let the user toggle between a light theme and a dark theme, and store the user's preference. +- [Dark theme](./examples/light-dark-toggle/) : demonstrates how to let the user toggle between a light theme and a dark theme, and store the user's preference. You can try all the examples online without installing anything on your computer using [SQLPage's online demo on replit](https://replit.com/@pimaj62145/SQLPage). @@ -315,3 +314,14 @@ We provide a set of components that look decent out of the box, so that you can However, if you really want to write your own HTML and CSS, you can do it by creating your own components. Just create a [`.handlebars`](https://handlebarsjs.com/guide/) file in `sqlpage/templates` and write your HTML and CSS in it. ([example](./sqlpage/templates/alert.handlebars)) + +## Download + +SQLPage is available for download on the from multiple sources: + +[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/lovasoa/sqlpage/total?label=direct%20download)](https://github.com/lovasoa/SQLpage/releases/latest) +[![Docker Pulls](https://img.shields.io/docker/pulls/lovasoa/sqlpage?label=docker%3A%20lovasoa%2Fsqlpage)](https://hub.docker.com/r/lovasoa/sqlpage) +[![homebrew downloads](https://img.shields.io/homebrew/installs/dq/sqlpage?label=homebrew%20downloads&labelColor=%232e2a24&color=%23f9d094)](https://formulae.brew.sh/formula/sqlpage#default) +[![Scoop Version](https://img.shields.io/scoop/v/sqlpage?labelColor=%23696573&color=%23d7d4db)](https://scoop.sh/#/apps?q=sqlpage&id=305b3437817cd197058954a2f76ac1cf0e444116) +[![Crates.io Total Downloads](https://img.shields.io/crates/d/sqlpage?label=crates.io%20download&labelColor=%23264323&color=%23f9f7ec)](https://crates.io/crates/sqlpage) +[![](https://img.shields.io/badge/Nix-pkg-rgb(126,%20185,%20227))](https://search.nixos.org/packages?channel=unstable&show=sqlpage&from=0&size=50&sort=relevance&type=packages&query=sqlpage) \ No newline at end of file diff --git a/configuration.md b/configuration.md index 8c3d4089..359a98c9 100644 --- a/configuration.md +++ b/configuration.md @@ -30,7 +30,8 @@ Here are the available configuration options and their default values: | `https_certificate_cache_dir` | ./sqlpage/https | A writeable directory where to cache the certificates, so that SQLPage can serve https traffic immediately when it restarts. | | `https_acme_directory_url` | https://acme-v02.api.letsencrypt.org/directory | The URL of the ACME directory to use when requesting a certificate. | | `environment` | development | The environment in which SQLPage is running. Can be either `development` or `production`. In `production` mode, SQLPage will hide error messages and stack traces from the user, and will cache sql files in memory to avoid reloading them from disk. | -| `content_security_policy` | | The [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to set in the HTTP headers. If you get CSP errors in the browser console, you can set this to the empty string to disable CSP. | +| `content_security_policy` | `script-src 'self' 'nonce-XXX` | The [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to set in the HTTP headers. If you get CSP errors in the browser console, you can set this to the empty string to disable CSP. | +| `system_root_ca_certificates` | false | Whether to use the system root CA certificates to validate SSL certificates when making http requests with `sqlpage.fetch`. If set to false, SQLPage will use its own set of root CA certificates. If the `SSL_CERT_FILE` or `SSL_CERT_DIR` environment variables are set, they will be used instead of the system root CA certificates. | Multiple configuration file formats are supported: you can use a [`.json5`](https://json5.org/) file, a [`.toml`](https://toml.io/) file, or a [`.yaml`](https://en.wikipedia.org/wiki/YAML#Syntax) file. diff --git a/db-test-setup/mssql/Dockerfile b/db-test-setup/mssql/Dockerfile index bd1d56ff..f0a25691 100644 --- a/db-test-setup/mssql/Dockerfile +++ b/db-test-setup/mssql/Dockerfile @@ -1,4 +1,4 @@ -ARG VERSION=2019-latest +ARG VERSION=2022-latest FROM mcr.microsoft.com/mssql/server:${VERSION} # Create a config directory diff --git a/db-test-setup/mssql/entrypoint.sh b/db-test-setup/mssql/entrypoint.sh index 56f1c018..c3166af0 100644 --- a/db-test-setup/mssql/entrypoint.sh +++ b/db-test-setup/mssql/entrypoint.sh @@ -7,7 +7,7 @@ pid=$! sleep 15 # Run the setup script to create the DB and the schema in the DB -/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql +/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql -No # Wait for sqlservr to exit wait -n $pid diff --git a/docker-compose.yml b/docker-compose.yml index f714c1cb..f807284c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: ports: ["1433:1433"] build: { context: "db-test-setup/mssql" } healthcheck: - test: /opt/mssql-tools/bin/sqlcmd -S localhost -U root -P "Password123!" -Q "SELECT 1" -b -o /dev/null + test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U root -P "Password123!" -Q "SELECT 1" -b -o /dev/null -No interval: 10s timeout: 3s retries: 10 diff --git a/examples/multiple-choice-question/README.md b/examples/multiple-choice-question/README.md new file mode 100644 index 00000000..92fb0c35 --- /dev/null +++ b/examples/multiple-choice-question/README.md @@ -0,0 +1,24 @@ +# SQLPage multiple choice question example + +This is a very simple example of a website that stores a list of +possible answers to a multiple choice question in a database table, +and then displays the question and the possible answers to the user. + +When the user selects an answer, the website will save the user's +choice in the database and display other users' choices as well. + +## Screenshots + +| Question answering form | Results table | Question edition | +| --- | --- | --- | +| ![Question answering form](screenshots/main_form.png) | ![Results table](screenshots/results.png) | ![Question edition](screenshots/admin.png) | + +## How to run + +Just run the sqlpage binary (`./sqlpage.bin`) from this folder. + +## Interesting files + +[admin.sql](admin.sql) uses the [dynamic component](https://sql.ophir.dev/documentation.sql?component=dynamic#component) to create a single page with one form per MCQ option. + +[website_header.json](website_header.json) contains the [shell](https://sql.ophir.dev/documentation.sql?component=shell#component) that is then used in all pages using the `dynamic` component to create a consistent look and feel between pages. \ No newline at end of file diff --git a/examples/multiple-choice-question/admin.sql b/examples/multiple-choice-question/admin.sql new file mode 100644 index 00000000..37b6b246 --- /dev/null +++ b/examples/multiple-choice-question/admin.sql @@ -0,0 +1,36 @@ +select 'dynamic' as component, sqlpage.read_file_as_text('website_header.json') as properties; + +select 'alert' as component, 'Saved' as title, 'success' as color where $saved is not null; +select 'alert' as component, 'Deleted' as title, 'danger' as color where $deleted is not null; +select 'alert' as component, 'This option cannot be deleted' as title, 'danger' as color, 'If an option has already been chosen by at least one respondant, then it cannot be deleted' as description where $cannot_delete is not null; + +select 'dynamic' as component, + json_array( + json_object( + 'component', 'form', + 'title', CONCAT('Option ', id), + 'action', CONCAT('edit_option.sql?id=', id), + 'validate', '', + 'id', CONCAT('option', id) + ), + json_object( + 'type', 'text', + 'name', 'profile_description', + 'label', 'Profile description', + 'value', profile_description + ), + json_object( + 'type', 'number', + 'name', 'score', + 'min', 0, + 'label', 'Score', + 'value', score + ), + json_object('component', 'button', 'size', 'sm'), + json_object('title', 'Delete', 'outline', 'danger', 'icon', 'trash', 'link', CONCAT('delete_option.sql?id=', id)), + json_object('title', 'Save', 'outline', 'success', 'icon', 'device-floppy', 'form', CONCAT('option', id)) + ) as properties +from dog_lover_profiles; + +select 'button' as component, 'center' as justify; +select 'Create new question' as title, 'create_question.sql' as link; \ No newline at end of file diff --git a/examples/multiple-choice-question/create_question.sql b/examples/multiple-choice-question/create_question.sql new file mode 100644 index 00000000..9894f16c --- /dev/null +++ b/examples/multiple-choice-question/create_question.sql @@ -0,0 +1,4 @@ +insert into dog_lover_profiles(profile_description, score) values ('', 50) +returning + 'redirect' as component, + 'admin.sql' as link; \ No newline at end of file diff --git a/examples/multiple-choice-question/delete_option.sql b/examples/multiple-choice-question/delete_option.sql new file mode 100644 index 00000000..e6ecf4be --- /dev/null +++ b/examples/multiple-choice-question/delete_option.sql @@ -0,0 +1,7 @@ +select 'redirect' as component, 'admin.sql?cannot_delete' as link +where exists (select 1 from answers where profile_id = $id); + +delete from dog_lover_profiles where id = $id +returning + 'redirect' as component, + 'admin.sql?deleted' as link; \ No newline at end of file diff --git a/examples/multiple-choice-question/edit_option.sql b/examples/multiple-choice-question/edit_option.sql new file mode 100644 index 00000000..bd70bee5 --- /dev/null +++ b/examples/multiple-choice-question/edit_option.sql @@ -0,0 +1,6 @@ +update dog_lover_profiles +set profile_description = :profile_description, score = :score +where id = $id +returning + 'redirect' as component, + 'admin.sql?saved' as link; \ No newline at end of file diff --git a/examples/multiple-choice-question/index.sql b/examples/multiple-choice-question/index.sql new file mode 100644 index 00000000..6049fd18 --- /dev/null +++ b/examples/multiple-choice-question/index.sql @@ -0,0 +1,9 @@ +select 'dynamic' as component, sqlpage.read_file_as_text('website_header.json') as properties; + +SELECT + 'form' AS component, + 'What dog lover are you ?' AS title, + 'process.sql' AS action; + +select 'radio' as type, 'profile' as name, id as value, profile_description as label +from dog_lover_profiles; \ No newline at end of file diff --git a/examples/multiple-choice-question/process.sql b/examples/multiple-choice-question/process.sql new file mode 100644 index 00000000..0fa81dfd --- /dev/null +++ b/examples/multiple-choice-question/process.sql @@ -0,0 +1,4 @@ +insert into answers(profile_id) +select CAST(:profile as integer) +where :profile is not null +returning 'redirect' as component, 'results.sql' as link; \ No newline at end of file diff --git a/examples/multiple-choice-question/results.sql b/examples/multiple-choice-question/results.sql new file mode 100644 index 00000000..917e77c3 --- /dev/null +++ b/examples/multiple-choice-question/results.sql @@ -0,0 +1,7 @@ +select 'dynamic' as component, sqlpage.read_file_as_text('website_header.json') as properties; + +select timestamp, profile_description, score from answers +inner join dog_lover_profiles on dog_lover_profiles.id = answers.profile_id; + +select 'csv' as component; +select * from answers; \ No newline at end of file diff --git a/examples/multiple-choice-question/screenshots/admin.png b/examples/multiple-choice-question/screenshots/admin.png new file mode 100644 index 00000000..36347a47 Binary files /dev/null and b/examples/multiple-choice-question/screenshots/admin.png differ diff --git a/examples/multiple-choice-question/screenshots/main_form.png b/examples/multiple-choice-question/screenshots/main_form.png new file mode 100644 index 00000000..3ccc7924 Binary files /dev/null and b/examples/multiple-choice-question/screenshots/main_form.png differ diff --git a/examples/multiple-choice-question/screenshots/results.png b/examples/multiple-choice-question/screenshots/results.png new file mode 100644 index 00000000..57bdd0ed Binary files /dev/null and b/examples/multiple-choice-question/screenshots/results.png differ diff --git a/examples/multiple-choice-question/sqlpage/migrations/0001_create_users_table.sql b/examples/multiple-choice-question/sqlpage/migrations/0001_create_users_table.sql new file mode 100644 index 00000000..6dd15e82 --- /dev/null +++ b/examples/multiple-choice-question/sqlpage/migrations/0001_create_users_table.sql @@ -0,0 +1,14 @@ +create table dog_lover_profiles( + id integer primary key, + profile_description text not null, + score integer not null +); + +insert into dog_lover_profiles(profile_description, score) + values ('I love dogs', 100), ('I hate them', 0); + +create table answers( + id integer primary key, + profile_id integer not null references dog_lover_profiles(id), + timestamp timestamp not null default current_timestamp +); \ No newline at end of file diff --git a/examples/multiple-choice-question/website_header.json b/examples/multiple-choice-question/website_header.json new file mode 100644 index 00000000..235afce0 --- /dev/null +++ b/examples/multiple-choice-question/website_header.json @@ -0,0 +1,11 @@ +{ + "component": "shell", + "title": "SQLPage Questions", + "icon": "help-hexagon", + "link": "/index.sql", + "menu_item": [ + "index", + "results", + "admin" + ] +} \ No newline at end of file diff --git a/examples/official-site/custom_components.sql b/examples/official-site/custom_components.sql index 2ddbfe41..530ec2c9 100644 --- a/examples/official-site/custom_components.sql +++ b/examples/official-site/custom_components.sql @@ -144,6 +144,19 @@ SQLPage adds the following attributes to the context of your components: - `@component_index` : the index of the current component in the page. Useful to generate unique ids or classes. - `@row_index` : the index of the current row in the current component. Useful to implement special behavior on the first row, for instance. + - `@csp_nonce` : a random nonce that you must use as the `nonce` attribute of your ` +``` ## Overwriting the default components diff --git a/examples/official-site/documentation.sql b/examples/official-site/documentation.sql index c94d9a0d..bc0bd3e3 100644 --- a/examples/official-site/documentation.sql +++ b/examples/official-site/documentation.sql @@ -6,7 +6,10 @@ where $component is not null and not exists (select 1 from component where name -- This line, at the top of the page, tells web browsers to keep the page locally in cache once they have it. select 'http_header' as component, 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', coalesce($component || ' - ', '') || 'SQLPage Documentation' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; select 'text' as component, format('SQLPage v%s documentation', sqlpage.version()) as title; select ' diff --git a/examples/official-site/examples/handle_picture_upload.sql b/examples/official-site/examples/handle_picture_upload.sql index e6a38907..66700335 100644 --- a/examples/official-site/examples/handle_picture_upload.sql +++ b/examples/official-site/examples/handle_picture_upload.sql @@ -10,7 +10,7 @@ select 'Your picture' as title, 'Uploaded file type: ' || sqlpage.uploaded_file_mime_type('my_file') as description where $data_url is not null; -select 'form' as component; +select 'form' as component, 'Upload picture' as validate; select 'my_file' as name, 'file' as type, 'Picture' as label; select 'text' as component, ' diff --git a/examples/official-site/highlightjs-launch.js b/examples/official-site/highlightjs-launch.js new file mode 100644 index 00000000..221b45bd --- /dev/null +++ b/examples/official-site/highlightjs-launch.js @@ -0,0 +1 @@ +hljs.highlightAll() \ No newline at end of file diff --git a/examples/official-site/highlightjs-tabler-theme.css b/examples/official-site/highlightjs-tabler-theme.css new file mode 100644 index 00000000..87606b97 --- /dev/null +++ b/examples/official-site/highlightjs-tabler-theme.css @@ -0,0 +1,105 @@ +/* Comments, Prolog, Doctype, and Cdata */ +.hljs-comment, +.hljs-prolog, +.hljs-meta, +.hljs-cdata { + color: var(--tblr-gray-300); +} + +/* Punctuation */ +.hljs-template-variable, +.hljs-punctuation { + color: #e9eac7; +} + +/* Namespace */ +.hljs-namespace { + opacity: .7; +} + +/* Property and Tag */ +.hljs-property { + color: #de5f8f; +} + +/* Number */ +.hljs-number { + color: #ea9999; +} + +/* Boolean */ +.hljs-literal { + color: #ae81ff; +} + +/* Selector, Attr-name, and String */ +.hljs-attr { + color: #fcfce5; +} + +.hljs-name { + color: #e4faf6; +} +.hljs-selector-tag, +.hljs-string { + color: #97e1a3; +} + +/* Operator, Entity, URL, CSS String, and Style String */ +.hljs-operator, +.hljs-symbol, +.hljs-link, +.language-css .hljs-string, +.style .hljs-string { + color: #f8f8f2; +} + +/* At-rule and Attr-value */ +.hljs-tag, +.hljs-keyword, +.hljs-attribute-value { + color: #e6db74; +} + +/* Keyword */ +.hljs-template-tag, +.hljs-keyword { + color: #95d1ff; +} + +/* Regex and Important */ +.hljs-regexp, +.hljs-important { + color: var(--tblr-yellow); +} + +/* Important */ +.hljs-important { + font-weight: bold; +} + +/* Entity */ +.hljs-symbol { + cursor: help; +} + +/* Token transition */ +.hljs { + transition: .3s; +} + +/* Code selection */ +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); +} + +/* Pre code padding */ +pre code { + padding: 0; +} diff --git a/examples/official-site/prism-tabler-theme.css b/examples/official-site/prism-tabler-theme.css deleted file mode 100644 index 63b4611f..00000000 --- a/examples/official-site/prism-tabler-theme.css +++ /dev/null @@ -1,92 +0,0 @@ -.token.comment, -.token.prolog, -.token.doctype, -.token.cdata { - color: var(--tblr-gray-300); -} - -.token.punctuation { - color: var(--tblr-gray-500); -} - -.namespace { - opacity: .7; -} - -.token.property, -.token.tag { - 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; -} - -.token.number { - color: #ea9999; -} - -.token.boolean { - color: #ae81ff; -} - -.token.selector, -.token.attr-name, -.token.string { - color: #97e1a3; -} - -.token.operator, -.token.entity, -.token.url, -.language-css .token.string, -.style .token.string { - color: #f8f8f2; -} - -.token.atrule, -.token.attr-value -{ - color: #e6db74; -} - - -.token.keyword{ -color: #95d1ff; -} - -.token.regex, -.token.important { - color: var(--tblr-yellow); -} - -.token.important { - font-weight: bold; -} - -.token.entity { - cursor: help; -} - -.token { - transition: .3s; -} - -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); -} - -pre code { - padding: 0; -} \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index ae51e3bc..df947013 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -670,6 +670,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('overflow', 'Whether to to let "wide" tables overflow across the right border and enable browser-based horizontal scrolling.', 'BOOLEAN', TRUE, TRUE), ('small', 'Whether to use compact table.', 'BOOLEAN', TRUE, TRUE), ('description','Description of the table content and helps users with screen readers to find a table and understand what it’s.','TEXT',TRUE,TRUE), + ('empty_description', 'Text to display if the table does not contain any row. Defaults to "no data".', 'TEXT', 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.', 'TEXT', FALSE, TRUE) @@ -708,7 +709,11 @@ INSERT INTO example(component, description, properties) VALUES {"name": "USS Constellation", "registry": "NCC-1974", "class":"Constellation"}, {"name": "USS Dakota", "registry": "NCC-63892", "class":"Akira"} ]' - ) + )), + ( + 'table', + 'An empty table with a friendly message', + json('[{"component":"table", "empty_description": "Nothing to see here at the moment."}]') ); @@ -733,72 +738,28 @@ INSERT INTO example(component, description, properties) VALUES INSERT INTO component(name, icon, description) VALUES - ('dynamic', 'repeat', 'A special component that can be used to render other components, the number and properties of which are not known in advance.'); + ('dynamic', 'repeat', 'Renders other components, given their properties as JSON. +If you are looking for a way to run FOR loops, to share similar code between pages of your site, +or to render multiple components for every line returned by your SQL query, then this is the component to use'); INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'dynamic', * FROM (VALUES -- top level - ('properties', 'A json object or array that contains the names and properties of other components', 'JSON', TRUE, TRUE) + ('properties', 'A json object or array that contains the names and properties of other components.', 'JSON', TRUE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES - ('dynamic', 'Rendering a text paragraph dynamically.', json('[{"component":"dynamic", "properties": "[{\"component\":\"text\"}, {\"contents\":\"Blah\", \"bold\":true}]"}]')), - ('dynamic', ' -## Dynamic shell - -On databases without a native JSON type (such as the default SQLite database), -you can use the `dynamic` component to generate -json data to pass to components that expect it. - -This example generates a menu similar to the [shell example](?component=shell#component), but without using a native JSON type. - -```sql -SELECT ''dynamic'' AS component, json_object( - ''component'', ''shell'', - ''title'', ''SQLPage documentation'', - ''link'', ''/'', - ''menu_item'', json_array( - json_object( - ''link'', ''index.sql'', - ''title'', ''Home'' - ), - json_object( - ''title'', ''Community'', - ''submenu'', json_array( - json_object( - ''link'', ''blog.sql'', - ''title'', ''Blog'' - ), - json_object( - ''link'', ''//github.com/lovasoa/sqlpage/issues'', - ''title'', ''Issues'' - ), - json_object( - ''link'', ''//github.com/lovasoa/sqlpage/discussions'', - ''title'', ''Discussions'' - ), - json_object( - ''link'', ''//github.com/lovasoa/sqlpage'', - ''title'', ''Github'' - ) - ) - ) - ) -) AS properties -``` - -[View the result of this query, as well as an example of how to generate a dynamic menu -based on the database contents](./examples/dynamic_shell.sql). -', NULL), + ('dynamic', 'The dynamic component has a single top-level property named `properties`, but it can render any number of other components. +Let''s start with something simple to illustrate the logic. We''ll render a `text` component with two row-level properties: `contents` and `italics`. +', json('[{"component":"dynamic", "properties": "[{\"component\":\"text\"}, {\"contents\":\"Hello, I am a dynamic component !\", \"italics\":true}]"}]')), ('dynamic', ' ## Static component data stored in `.json` files You can also store the data for a component in a `.json` file, and load it using the `dynamic` component. -This can be useful to store the data for a component in a separate file, -shared between multiple pages, -and avoid having to escape quotes in SQL strings. +This is particularly useful to create a single [shell](?component=shell#component) defining the site''s overall appearance and menus, +and displaying it on all pages without duplicating its code. -For instance, the following query will load the data for a `shell` component from the file `shell.json`: +The following will load the data for a `shell` component from a file named `shell.json` : ```sql SELECT ''dynamic'' AS component, sqlpage.read_file_as_text(''shell.json'') AS properties; @@ -822,6 +783,37 @@ and `shell.json` would be placed at the website''s root and contain the followin ] } ``` +', NULL), + ('dynamic', ' +## Dynamic shell + +On databases without a native JSON type (such as the default SQLite database), +you can use the `dynamic` component to generate +json data to pass to components that expect it. + +This example generates a menu similar to the [shell example](?component=shell#component), but without using a native JSON type. + +```sql +SELECT ''dynamic'' AS component, '' +{ + "component": "shell", + "title": "SQLPage documentation", + "link": "/", + "menu_item": [ + {"link": "index.sql", "title": "Home"}, + {"title": "Community", "submenu": [ + {"link": "blog.sql", "title": "Blog"}, + {"link": "https//github.com/lovasoa/sqlpage/issues", "title": "Issues"}, + {"link": "https//github.com/lovasoa/sqlpage/discussions", "title": "Discussions"}, + {"link": "https//github.com/lovasoa/sqlpage", "title": "Github"} + ]} + ] +} +'' AS properties +``` + +[View the result of this query, as well as an example of how to generate a dynamic menu +based on the database contents](./examples/dynamic_shell.sql). ', NULL); INSERT INTO component(name, icon, description) VALUES @@ -907,9 +899,14 @@ You see the [page layouts demo](./examples/layouts.sql) for a live example of th "language": "en-US", "description": "Documentation for the SQLPage low-code web application framework.", "font": "Poppins", - "javascript": ["https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js", - "https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js"], - "css": "/prism-tabler-theme.css", + "javascript": [ + "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js", + "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/languages/sql.min.js", + "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/languages/handlebars.min.js", + "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/languages/json.min.js", + "/highlightjs-launch.js" + ], + "css": "/highlightjs-tabler-theme.css", "footer": "Official [SQLPage](https://sql.ophir.dev) documentation" }]')), ('shell', ' diff --git a/examples/official-site/sqlpage/migrations/02_hero_component.sql b/examples/official-site/sqlpage/migrations/02_hero_component.sql index b8e4e59e..47dbcd42 100644 --- a/examples/official-site/sqlpage/migrations/02_hero_component.sql +++ b/examples/official-site/sqlpage/migrations/02_hero_component.sql @@ -122,7 +122,7 @@ FROM ( 'icon', 'Icon of the feature section.', - 'TEXT', + 'ICON', FALSE, TRUE ), diff --git a/examples/official-site/sqlpage/migrations/31_card_docs_update.sql b/examples/official-site/sqlpage/migrations/31_card_docs_update.sql index c0cd6b93..901f7c76 100644 --- a/examples/official-site/sqlpage/migrations/31_card_docs_update.sql +++ b/examples/official-site/sqlpage/migrations/31_card_docs_update.sql @@ -7,7 +7,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('title', 'Text header at the top of the list of cards.', 'TEXT', TRUE, TRUE), ('description', 'A short paragraph displayed below the title.', 'TEXT', TRUE, TRUE), ('description_md', 'A short paragraph displayed below the title - formatted using markdown.', 'TEXT', TRUE, TRUE), - ('columns', 'The number of columns in the grid of cards. This is just a hint, the grid will adjust dynamically to the user''s screen size, rendering fewer columns if needed to fit the contents.', 'INTEGER', TRUE, TRUE), + ('columns', 'The number of columns in the grid of cards. This is just a hint, the grid will adjust dynamically to the user''s screen size, rendering fewer columns if needed to fit the contents. To control the size of cards individually, use the `width` row-level property instead.', 'INTEGER', TRUE, TRUE), -- item level ('title', 'Name of the card, displayed at the top.', 'TEXT', FALSE, FALSE), ('description', 'The body of the card, where you put the main text contents of the card. @@ -26,7 +26,8 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('icon', 'Name of an icon to display on the left side of the card.', 'ICON', FALSE, TRUE), ('color', 'The name of a color, to be displayed on the left of the card to highlight it.', 'COLOR', FALSE, TRUE), ('background_color', 'The background color of the card.', 'COLOR', FALSE, TRUE), - ('active', 'Whether this item in the grid is considered "active". Active items are displayed more prominently.', 'BOOLEAN', FALSE, TRUE) + ('active', 'Whether this item in the grid is considered "active". Active items are displayed more prominently.', 'BOOLEAN', FALSE, TRUE), + ('width', 'The width of the card, between 1 (smallest) and 12 (full-width). The default width is 3, resulting in 4 cards per line.', 'INTEGER', FALSE, TRUE) ) x; INSERT INTO parameter(component, name, description_md, type, top_level, optional) SELECT 'card', * FROM (VALUES ('embed', 'A url whose contents will be fetched and injected into the body of this card. @@ -41,20 +42,18 @@ INSERT INTO parameter(component, name, description_md, type, top_level, optional ) x; INSERT INTO example(component, description, properties) VALUES - ('card', 'The most basic card', json('[{"component":"card"},{"description":"A"},{"description":"B"},{"description":"C"}]')), - ('card', 'A card with a Markdown description', - json('[{"component":"card", "columns": 2}, {"title":"A card with a Markdown description", "description_md": "This is a card with a **Markdown** description. \n\n'|| - 'This is useful if you want to display a lot of text in the card, with many options for formatting, such as '|| - '\n - **bold**, \n - *italics*, \n - [links](index.sql), \n - etc."}]')), ('card', 'A beautiful card grid with bells and whistles, showing examples of SQLPage features.', json('[{"component":"card", "title":"Popular SQLPage features", "columns": 2}, {"title": "Download as spreadsheet", "link": "?component=csv#component", "description": "Using the CSV component, you can download your data as a spreadsheet.", "icon":"file-plus", "color": "green", "footer_md": "SQLPage can both [read](?component=form#component) and [write](?component=csv#component) **CSV** files."}, {"title": "Custom components", "link": "/custom_components.sql", "description": "If you know some HTML, you can create your own components for your application.", "icon":"code", "color": "orange", "footer_md": "You can look at the [source of the official components](https://github.com/lovasoa/SQLpage/tree/main/sqlpage/templates) for inspiration."} ]')), - ('card', 'Short information notices', + ('card', 'You can use cards to display a dashboard with quick access to important information. Use [markdown](https://www.markdownguide.org/basic-syntax) to format the text.', json('[ - {"component": "card"}, - {"description_md": "This post is also available in [german](?lang=de).", "active": true, "icon": "language"} + {"component": "card", "columns": 4}, + {"description_md": "**152** sales today", "active": true, "icon": "currency-euro"}, + {"description_md": "**13** new users", "icon": "user-plus", "color": "green"}, + {"description_md": "**2** complaints", "icon": "alert-circle", "color": "danger", "link": "?view_complaints", "background_color": "red-lt"}, + {"description_md": "**1** pending support request", "icon": "mail-question", "color": "warning"} ]')), ('card', 'A gallery of images.', json('[ @@ -63,12 +62,12 @@ INSERT INTO example(component, description, properties) VALUES {"title": "Squirrel", "description_md": "The **chipmunk** is a small, striped rodent of the family Sciuridae. Chipmunks are found in North America, with the exception of the Siberian chipmunk which is found primarily in Asia.", "top_image": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/be/Tamias-rufus-001.jpg/640px-Tamias-rufus-001.jpg" }, {"title": "Spider", "description_md": "The **jumping spider family** (_Salticidae_) contains more than 600 described genera and about *6000 described species*, making it the largest family of spiders with about 13% of all species.", "top_image": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Jumping_spiders_%28Salticidae%29.jpg/640px-Jumping_spiders_%28Salticidae%29.jpg" } ]')), - ('card', 'Beautifully colored cards', + ('card', 'Beautifully colored cards with variable width. The blue card (width 6) takes half the screen, whereas of the red and green cards have the default width of 3', json('[ - {"component":"card", "title":"Beautifully colored cards", "columns": 3}, + {"component":"card", "title":"Beautifully colored cards" }, {"title": "Red card", "color": "red", "background_color": "red-lt", "description": "Penalty! You are out!", "icon":"play-football" }, - {"title": "Green card", "color": "green", "background_color": "green-lt", "description": "Welcome to the United States of America !", "icon":"user-dollar" }, - {"title": "Blue card", "color": "blue", "background_color": "blue-lt", "description": "The Blue Card facilitates migration of foreigners to Europe.", "icon":"currency-euro" } + {"title": "Blue card", "color": "blue", "width": 6, "background_color": "blue-lt", "description": "The Blue Card facilitates migration of foreigners to Europe.", "icon":"currency-euro" }, + {"title": "Green card", "color": "green", "background_color": "green-lt", "description": "Welcome to the United States of America !", "icon":"user-dollar" } ]')), ('card', 'Cards with remote content', json('[ diff --git a/examples/using react and other custom scripts and styles/README.md b/examples/using react and other custom scripts and styles/README.md index cdc6fe39..7cda84c9 100644 --- a/examples/using react and other custom scripts and styles/README.md +++ b/examples/using react and other custom scripts and styles/README.md @@ -11,6 +11,9 @@ It integrates a simple [react](https://reactjs.org/) component and loads it with ![example client-side reactive SQLPage application with React](screenshot-react.png) +![example physics equations](screenshot-latex-math-equations.png) + + ## Notes This example relies on a CDN to load the react library, and the example component is written in plain Javscript, not JSX. diff --git a/examples/using react and other custom scripts and styles/equations.sql b/examples/using react and other custom scripts and styles/equations.sql new file mode 100644 index 00000000..575faf77 --- /dev/null +++ b/examples/using react and other custom scripts and styles/equations.sql @@ -0,0 +1,32 @@ +select 'shell' as component, + 'Equations' as title, + 'style.css' as css, + 'settings' as icon, + 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js' as javascript; + +select 'text' as component, ' +Newton''s laws of motion are three physical laws that describe the relationship between the forces \( \overrightarrow{F} \) acting on a body, +the resulting motion \( \overrightarrow{a} \) of the body, and the body''s mass \( m \). +' as contents; +select + 'card' as component, + 3 as columns; +select + 'Inertia' as title, + 'The natural behavior of a body is to move in a straight line at constant speed \( \overrightarrow{v} \) unless acted upon by a force \( \overrightarrow{F} \).' as description, + TRUE as active, + 'arrow-right' as icon; +select + 'Force' as title, + 'The acceleration \( \overrightarrow{a} \) of a body is directly proportional to the net force \( \overrightarrow{F_{\text{net}}} \) acting on the it, and inversely proportional to its mass \( m \): +\( \overrightarrow{F_{\text{net}}} = m \overrightarrow{a} \), or +\( \sum \overrightarrow F = m \frac{\mathrm d \overrightarrow v }{\mathrm d t} \).' as description, + 'rocket' as icon, + 'red' as color; +select + 'Action and reaction' as title, + 'For every action, there is an equal and opposite reaction. +If body A exerts a force \( \overrightarrow{F_{\text{A on B}}} \) on body B, +then body B exerts a force \( \overrightarrow{F_{\text{B on A}}} = -\overrightarrow{F_{\text{A on B}}} \) on body A.' as description, + 'arrows-exchange' as icon, + 'orange' as color; \ No newline at end of file diff --git a/examples/using react and other custom scripts and styles/index.sql b/examples/using react and other custom scripts and styles/index.sql index 203c5dd4..c7159f51 100644 --- a/examples/using react and other custom scripts and styles/index.sql +++ b/examples/using react and other custom scripts and styles/index.sql @@ -1,7 +1,8 @@ SELECT 'shell' AS component, 'SQLPage with a frontend component' as title, 'style.css' as css, - 'settings' as icon; + 'settings' as icon, + 'equations' as menu_item; -SELECT 'text' AS component, 'funky_text' AS id; -SELECT 'Try my react component !' AS contents, 'react.sql' AS link; +SELECT 'button' AS component, 'center' as justify; +SELECT 'Try my react component !' AS title, 'react.sql' AS link, 'funky_text' AS id; diff --git a/examples/using react and other custom scripts and styles/screenshot-latex-math-equations.png b/examples/using react and other custom scripts and styles/screenshot-latex-math-equations.png new file mode 100644 index 00000000..500485c0 Binary files /dev/null and b/examples/using react and other custom scripts and styles/screenshot-latex-math-equations.png differ diff --git a/examples/using react and other custom scripts and styles/screenshot-math-equations.png b/examples/using react and other custom scripts and styles/screenshot-math-equations.png new file mode 100644 index 00000000..13c772d7 Binary files /dev/null and b/examples/using react and other custom scripts and styles/screenshot-math-equations.png differ diff --git a/lambda.Dockerfile b/lambda.Dockerfile index f1586ba0..27cef7f7 100644 --- a/lambda.Dockerfile +++ b/lambda.Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.79-alpine as builder +FROM rust:1.80-alpine as builder RUN rustup component add clippy rustfmt RUN apk add --no-cache musl-dev zip WORKDIR /usr/src/sqlpage diff --git a/sqlpage/apexcharts.js b/sqlpage/apexcharts.js index cfd40180..285548c3 100644 --- a/sqlpage/apexcharts.js +++ b/sqlpage/apexcharts.js @@ -196,4 +196,4 @@ function bubbleTooltip({ series, seriesIndex, dataPointIndex, w }) { return tooltip.outerHTML; } -add_init_function(sqlpage_chart); \ No newline at end of file +add_init_fn(sqlpage_chart); \ No newline at end of file diff --git a/sqlpage/sqlpage.js b/sqlpage/sqlpage.js index d1496919..1115b302 100644 --- a/sqlpage/sqlpage.js +++ b/sqlpage/sqlpage.js @@ -1,6 +1,8 @@ /* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta20/dist/js/tabler.min.js */ /* !include https://cdn.jsdelivr.net/npm/list.js-fixed@2.3.4/dist/list.min.js */ +const nonce = document.currentScript.nonce; + function sqlpage_card() { for (const c of document.querySelectorAll("[data-pre-init=card]")) { const source = c.dataset.embed; @@ -41,6 +43,9 @@ function sqlpage_select_dropdown(){ if (!window.TomSelect) { const script = document.createElement("script"); script.src= src; + script.integrity = "sha384-aAqv9vleUwO75zAk1sGKd5VvRqXamBXwdxhtihEUPSeq1HtxwmZqQG/HxQnq7zaE"; + script.crossOrigin = "anonymous"; + script.nonce = nonce; script.onload = sqlpage_select_dropdown; document.head.appendChild(script); return; @@ -69,6 +74,7 @@ function sqlpage_map() { 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; document.head.appendChild(leaflet_js); is_leaflet_injected = true; @@ -182,16 +188,15 @@ function load_scripts() { } } -function add_init_function(f) { +function add_init_fn(f) { document.addEventListener('DOMContentLoaded', f); document.addEventListener('fragment-loaded', f); - if (document.readyState !== "loading") f(); + if (document.readyState !== "loading") setTimeout(f, 0); } -add_init_function(function init_components() { - sqlpage_table(); - sqlpage_map(); - sqlpage_card(); - sqlpage_form(); - load_scripts(); -}); \ No newline at end of file + +add_init_fn(sqlpage_table); +add_init_fn(sqlpage_map); +add_init_fn(sqlpage_card); +add_init_fn(sqlpage_form); +add_init_fn(load_scripts); \ No newline at end of file diff --git a/sqlpage/templates/card.handlebars b/sqlpage/templates/card.handlebars index 615f6691..db21c086 100644 --- a/sqlpage/templates/card.handlebars +++ b/sqlpage/templates/card.handlebars @@ -7,22 +7,22 @@ {{#if description_md}} {{{markdown description_md}}} {{/if}} -
+ {{/if}}"> {{#each_row}} -
+
{{#if color}} - + {{/if}} {{#if image_url}} {{title}} {{/if}} {{#if icon}} - {{~icon_img icon~}} + {{~icon_img icon~}} {{/if}} {{#if description}} {{description}} diff --git a/sqlpage/templates/debug.handlebars b/sqlpage/templates/debug.handlebars index ddda74f2..9b7d1cb7 100644 --- a/sqlpage/templates/debug.handlebars +++ b/sqlpage/templates/debug.handlebars @@ -1,5 +1,5 @@

Debug output

-
{{stringify this}}
-{{#each_row}} -
{{stringify this}}
+
{{stringify this}}
+{{#each_row}}{{stringify this}}
 {{/each_row}}
+
\ No newline at end of file diff --git a/sqlpage/templates/form.handlebars b/sqlpage/templates/form.handlebars index 184c2e08..a80bce1c 100644 --- a/sqlpage/templates/form.handlebars +++ b/sqlpage/templates/form.handlebars @@ -1,6 +1,6 @@
-
+
{{#if title}} -

{{title}}

+

{{title}}

{{/if}}
{{#each_row}} @@ -50,7 +50,7 @@ placeholder="{{placeholder}}" rows="{{default rows 3}}" {{#if id}}id="{{id}}" {{/if~}} - {{~#if value}}value="{{value}}" {{/if~}} + {{~#if value includeZero=true}}value="{{value}}" {{/if~}} {{~#if minlength}}minlength="{{minlength}}" {{/if~}} {{~#if maxlength}}maxlength="{{maxlength}}" {{/if~}} {{~#if required}}required="required" {{/if~}} @@ -58,7 +58,7 @@ {{~#if disabled}}disabled {{/if~}} {{~#if readonly}}readonly {{/if~}} > - {{~#if value}}{{value}}{{/if~}} + {{~#if value includeZero=true}}{{value}}{{/if~}} {{else}}{{#if (eq type 'select')}}