diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41066910a..d709fddd7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + uses: docker/metadata-action@v5 with: images: joepmeneer/atomic-server github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2b0f37c7f..df9483cdc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,8 @@ "vadimcn.vscode-lldb", "rust-lang.rust-analyzer", "styled-components.vscode-styled-components", - "svelte.svelte-vscode" + "svelte.svelte-vscode", + "vadimcn.vscode-lldb", + "rust-lang.rust-analyzer" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6ef8e7ba9..04efc0c7c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,8 @@ { + // This is used to prevent `build.rs` from running every time you make a change to a file. + "rust-analyzer.check.extraEnv": { + "IS_RUST_ANALYZER": "true" + }, // The linter in the CI is quite strict, so running `cargo fmt` on save is probably a good idea! "editor.formatOnSave": true, "files.autoSave": "onFocusChange", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 223a84826..1e14f6722 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -15,6 +15,23 @@ "group": "build", "problemMatcher": [] }, + { + "label": "watch atomic-server (cargo watch)", + "type": "shell", + "command": "~/.cargo/bin/cargo-watch", + "args": [ + "--", + "cargo", + "run", + "--bin", + "atomic-server", + "--", + "--env-file", + "server/.env", + ], + "group": "build", + "problemMatcher": [] + }, { "label": "test atomic-server (cargo nextest run)", "type": "shell", @@ -26,9 +43,9 @@ "group": "test" }, { - "label": "test end-to-end / E2E (npm playwright)", + "label": "test end-to-end / E2E (pnpm playwright)", "type": "shell", - "command": "cd server/e2e_tests/ && npm i && npm run test", + "command": "cd server/e2e_tests/ && pnpm i && pnpm run test", "group": "test" }, { diff --git a/CHANGELOG.md b/CHANGELOG.md index 55defc6a0..79591ae07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,17 +86,25 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain - Refactor `Endpoint` handlers, uses a Context now #592 - Re-build store + invite when adjusting server url #607 - Use local atomic-server for properties and classes, improves atomic-server #604 +- New sign up / register flow. Add `/register` Endpoint #489 #254 +- New sign up / register flow. Add `/register`, `/confirm-email`, `/add-public-key` endpoints #489 #254 +- Add multi-tenancy support. Users can create their own `Drives` on subdomains. #288 +- Refactor URLs. `store.self_url()` returns an `AtomicUrl`, which provides methods to easily add paths, find subdomains and more. +- Add support for subdomains, use a Wildcard TLS certificate #502 ## [v0.34.1] - 2023-02-11 - - Improve query performance, refactor indexes. The `.tpf` API is deprecated in favor of the more powerful `.query`. #529 - Replace `acme_lib` with `instant-acme`, drop OpenSSL dependency, add DNS verification for TLS option with `--https-dns` #192 - Improved error handling for HTTPS initialization #530 - Add `--force` to `atomic-server import` #536 +- Email support. Connect to external SMTP servers. #276 +- Basic plugin support through Endpoints. For now only works if you use `**Atomic**-Lib` as a library. Add your plugins by calling `Db::register_endpoint`. +- Allow parsing `.env` files from custom locations using the `--env-file` flag. +- Plugins support `tokio`, so you can spawn async tasks from plugins. +- Add JWT token support, used for emails and registration #544 - Fix index issue happening when deleting a single property in a sorted collection #545 - Update JS assets & playwright - Fix initial indexing bug #560 -- Fix errors on succesful export / import #565 - Fix envs for store path, change `ATOMIC_STORE_DIR` to `ATOMIC_DATA_DIR` #567 - Refactor static file asset hosting #578 - Meta tags server side #577 @@ -104,6 +112,10 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain - Remove feature to index external RDF files and search them #579 - Add staging environment #588 - Add systemd instructions to readme #271 +- Improve check_append error #558 +- Fix errors on successful export / import #565 +- Most Collection routes now live under `/collections`, e.g. `/collections/agents` instead of `/agents`. #556 +- Constrain new URLs. Commits for new Resources are now only valid if their parent is part of the current URL. So it's no longer possible to create `/some-path/new-resource` if `new-resource` is its parent is not in its URL. #556 ## [v0.34.0] - 2022-10-31 @@ -114,6 +126,7 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain - `Store::all_resources` returns `Iterator` instead of `Vec` #522 #487 - Change authentication order #525 - Fix cookie subject check #525 +- Refactored Subject URLs to use `AtomicUrl` ## [v0.33.1] - 2022-09-25 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53fa38035..ce41983eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,8 @@ TL;DR Clone the repo and run `cargo run` from each folder (e.g. `cli` or `server - Go to `browser`, run `pnpm install` (if you haven't already), and run `pnpm dev` to start the browser - Visit your `localhost` in your locally running `atomic-data-browser` instance: (e.g. `http://localhost:5173/app/show?subject=http%3A%2F%2Flocalhost`) - use `cargo watch -- cargo run` to automatically recompile `atomic-server` when you update JS assets in `browser` +- use `cargo watch -- cargo run --bin atomic-server -- --env-file server/.env` to automatically recompile `atomic-server` when you update code or JS assets. +- If you want to debug emails: `brew install mailhog` => `mailhog` => `http://localhost:8025` and add `ATOMIC_SMTP_HOST=localhost` `ATOMIC_SMTP_PORT=1025` to your `.env`. ### IDE setup (VSCode) diff --git a/Cargo.lock b/Cargo.lock index 1ede40ce3..0082432cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -541,6 +541,15 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -617,6 +626,7 @@ dependencies = [ "tracing-opentelemetry", "tracing-subscriber", "ureq", + "url", "urlencoding", "walkdir", "webp", @@ -626,15 +636,18 @@ dependencies = [ name = "atomic_lib" version = "0.40.0" dependencies = [ + "async-mutex", "base64 0.21.7", "bincode", "criterion", "directories", "html2md", "iai", + "jwt-simple", "kuchikiki", "lazy_static", "lol_html", + "mail-send", "ntest", "rand 0.8.5", "regex", @@ -645,6 +658,7 @@ dependencies = [ "serde_jcs", "serde_json", "sled", + "tokio", "toml", "tracing", "ulid", @@ -697,6 +711,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -709,6 +735,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bincode" version = "1.3.3" @@ -718,6 +750,12 @@ dependencies = [ "serde", ] +[[package]] +name = "binstring" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0d60973d9320722cb1206f412740e162a33b8547ea8d6be75d7cff237c7a85" + [[package]] name = "bit_field" version = "0.10.2" @@ -985,6 +1023,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "coarsetime" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b3839cf01bb7960114be3ccf2340f541b6d0c81f8690b007b2b39f750f7e5d" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1030,6 +1079,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" @@ -1179,6 +1240,28 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1216,6 +1299,12 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "ct-codecs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "026ac6ceace6298d2c557ef5ed798894962296469ec7842288ea64674201a2d1" + [[package]] name = "darling" version = "0.20.10" @@ -1251,6 +1340,39 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid 0.7.1", + "crypto-bigint 0.3.2", + "pem-rfc7468 0.3.1", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.6.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1300,7 +1422,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid 0.9.6", "crypto-common", + "subtle", ] [[package]] @@ -1397,6 +1521,30 @@ dependencies = [ "dtoa", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.9", + "digest", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.15", +] + [[package]] name = "edit" version = "0.1.5" @@ -1413,6 +1561,27 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint 0.5.5", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1460,6 +1629,12 @@ dependencies = [ "str-buf", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "exr" version = "1.72.0" @@ -1508,6 +1683,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "flate2" version = "1.0.34" @@ -1678,6 +1863,27 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", ] [[package]] @@ -1726,6 +1932,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -1810,6 +2027,48 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha1-compact" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9d405ec732fa3fcde87264e54a32a84956a377b3e3107de96e59b798c84a7" + +[[package]] +name = "hmac-sha256" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ce1f4656bae589a3fab938f9f09bf58645b7ed01a2c5f8a3c238e01a4ef78a" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -2190,6 +2449,46 @@ dependencies = [ "rayon", ] +[[package]] +name = "jwt-simple" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357892bb32159d763abdea50733fadcb9a8e1c319a9aa77592db8555d05af83e" +dependencies = [ + "anyhow", + "binstring", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "rsa 0.7.2", + "serde", + "serde_json", + "spki 0.6.0", + "thiserror", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature 2.2.0", +] + [[package]] name = "kuchikiki" version = "0.8.2" @@ -2214,6 +2513,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] [[package]] name = "lazycell" @@ -2364,6 +2666,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mail-builder" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8390bb0f68168cabc59999706c2199c9b556d7dfd94e65a26e8840fa1d58b238" +dependencies = [ + "gethostname 0.4.3", +] + +[[package]] +name = "mail-send" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17009bb6737cd4b3e2422a0046dc18a07b8ca8f3fa87e59ea3e8408cb0d2e8f" +dependencies = [ + "base64 0.13.1", + "gethostname 0.2.3", + "mail-builder", + "md5", + "rand 0.8.5", + "rsa 0.6.1", + "rustls 0.20.9", + "sha2", + "tokio", + "tokio-rustls 0.23.4", + "webpki-roots 0.22.6", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -2415,6 +2745,12 @@ dependencies = [ "rayon", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "measure_time" version = "0.8.3" @@ -2627,6 +2963,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2653,6 +3006,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -2825,6 +3189,30 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1584efc0f222d0fc8f31e6c229b8fd93f69280aab398828f0dbce3957178a1ac" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -2910,6 +3298,33 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3040,6 +3455,60 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" +dependencies = [ + "der 0.5.1", + "pkcs8 0.8.0", + "zeroize", +] + +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der 0.6.1", + "pkcs8 0.9.0", + "spki 0.6.0", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der 0.5.1", + "spki 0.5.4", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.9", + "spki 0.7.3", +] + [[package]] name = "pkg-config" version = "0.3.31" @@ -3135,6 +3604,15 @@ dependencies = [ "termtree", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -3475,6 +3953,16 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.50" @@ -3531,6 +4019,47 @@ dependencies = [ "rio_api", ] +[[package]] +name = "rsa" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" +dependencies = [ + "byteorder", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1 0.3.3", + "pkcs8 0.8.0", + "rand_core 0.6.4", + "smallvec", + "subtle", + "zeroize", +] + +[[package]] +name = "rsa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1 0.4.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "signature 1.6.4", + "smallvec", + "subtle", + "zeroize", +] + [[package]] name = "rust-stemmers" version = "1.2.0" @@ -3741,6 +4270,20 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.9", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -3914,6 +4457,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3944,6 +4498,26 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -4036,6 +4610,36 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der 0.5.1", +] + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.9", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -4448,9 +5052,21 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "tokio-rustls" version = "0.23.4" @@ -4842,6 +5458,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "wasm-bindgen" version = "0.2.93" diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 41b45e0f5..7204e5cfe 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -243,11 +243,18 @@ This changelog covers all five packages, as they are (for now) updated as a whol - Add `store.getResourceAncestry` method, which returns the ancestry of a resource, including the resource itself. - Add `resource.title` property, which returns the name of a resource, or the first property that is can be used to name the resource. - `store.createSubject` now accepts a `parent` argument, which allows creating nested subjects. +- Add `store.getServerSupports` to know which features a Server supports + +### @tomic/react + +- Add `useServerSupports` hook to see supported features of the server ## v0.35.0 ### @tomic/browser +- Let users register using e-mail address, improve sign-up UX. +- Add `Store.parseMetaTags` to load JSON-AD objects stored in the DOM. Speeds up initial page load by allowing server to set JSON-AD objects in the initial HTML response. - Move static assets around, align build with server and fix PWA #292 - Add `useChildren` hook and `Store.getChildren` method - Add new file preview UI for images, audio, text and PDF files. @@ -255,8 +262,14 @@ This changelog covers all five packages, as they are (for now) updated as a whol - Fix Dialogue form #308 - Refactor search, escape query strings for Tantivy - Add `import` context menu, allows importing anywhere +- Let users register using e-mail address, improve sign-up UX. ### @tomic/react +- `store.createSubject` allows creating nested paths +- `store.createSubject` allows creating nested paths +- Add `useChildren` hook and `Store.getChildren` method +- Add `Store.postToServer` method, add `endpoints`, `import_json_ad_string` +- Add `store.preloadClassesAndProperties` and remove `urls.properties.getAll` and `urls.classes.getAll`. This enables using `atomic-data-browser` without relying on `atomicdata.dev` being available. - Add more options to `useSearch` diff --git a/browser/data-browser/src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx b/browser/data-browser/src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx index 5ce5d8298..919ecd766 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +++ b/browser/data-browser/src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx @@ -14,6 +14,7 @@ import { ToggleButton } from './ToggleButton'; import { SlashCommands, suggestion } from './SlashMenu/CommandsExtension'; import { ExtendedImage } from './ImagePicker'; import { transition } from '../../helpers/transition'; +import { markdownStyles } from '../../components/datatypes/Markdown'; export type AsyncMarkdownEditorProps = { placeholder?: string; @@ -112,7 +113,7 @@ export default function AsyncMarkdownEditor({ /> )} - + Type '/' for options @@ -139,6 +140,7 @@ const calcHeight = (value: string) => { return `calc(${lines * LINE_HEIGHT}em + 5px)`; }; +// WARNING: Only add styles specific to editing. For other styles, add to `MarkdownWrapper` const EditorWrapper = styled.div<{ hideEditor: boolean }>` position: relative; background-color: ${p => p.theme.colors.bg}; @@ -173,27 +175,7 @@ const EditorWrapper = styled.div<{ hideEditor: boolean }>` height: auto; } - pre { - padding: 0.75rem 1rem; - background-color: ${p => p.theme.colors.bg1}; - border-radius: ${p => p.theme.radius}; - font-family: monospace; - - code { - white-space: pre; - color: inherit; - padding: 0; - background: none; - font-size: 0.8rem; - } - } - - blockquote { - margin-inline-start: 0; - border-inline-start: 3px solid ${p => p.theme.colors.textLight2}; - color: ${p => p.theme.colors.textLight}; - padding-inline-start: 1rem; - } + ${markdownStyles} } `; diff --git a/browser/data-browser/src/components/Card.tsx b/browser/data-browser/src/components/Card.tsx index 00c678ef3..31a3a48c5 100644 --- a/browser/data-browser/src/components/Card.tsx +++ b/browser/data-browser/src/components/Card.tsx @@ -42,6 +42,7 @@ export const CardRow = styled.div` display: block; border-top: ${p => (p.noBorder ? 'none' : 'var(--border)')}; padding: ${p => p.theme.size(2)} ${p => p.theme.size()}; + overflow-wrap: break-word; `; /** A block inside a Card which has full width */ diff --git a/browser/data-browser/src/components/CodeBlock.tsx b/browser/data-browser/src/components/CodeBlock.tsx index aafe0fa56..293014011 100644 --- a/browser/data-browser/src/components/CodeBlock.tsx +++ b/browser/data-browser/src/components/CodeBlock.tsx @@ -7,9 +7,11 @@ import { Button } from './Button'; interface CodeBlockProps { content?: string; loading?: boolean; + wrapContent?: boolean; } -export function CodeBlock({ content, loading }: CodeBlockProps) { +/** Codeblock with copy feature */ +export function CodeBlock({ content, loading, wrapContent }: CodeBlockProps) { const [isCopied, setIsCopied] = useState(undefined); function copyToClipboard() { @@ -19,7 +21,7 @@ export function CodeBlock({ content, loading }: CodeBlockProps) { } return ( - + {loading ? ( 'loading...' ) : ( @@ -46,13 +48,22 @@ export function CodeBlock({ content, loading }: CodeBlockProps) { ); } -export const CodeBlockStyled = styled.pre` +interface Props { + /** Renders all in a single line */ + wrapContent?: boolean; +} + +export const CodeBlockStyled = styled.pre` position: relative; background-color: ${p => p.theme.colors.bg1}; border-radius: ${p => p.theme.radius}; border: solid 1px ${p => p.theme.colors.bg2}; padding: 0.3rem; + min-height: 2.2rem; + font-size: 0.8rem; font-family: monospace; width: 100%; overflow-x: auto; + word-wrap: ${p => (p.wrapContent ? 'break-word' : 'initial')}; + white-space: ${p => (p.wrapContent ? 'pre-wrap' : 'pre')}; `; diff --git a/browser/data-browser/src/components/ConfirmationDialog.tsx b/browser/data-browser/src/components/ConfirmationDialog.tsx index b48a7b8f1..4b25bd197 100644 --- a/browser/data-browser/src/components/ConfirmationDialog.tsx +++ b/browser/data-browser/src/components/ConfirmationDialog.tsx @@ -33,7 +33,11 @@ export function ConfirmationDialog({ bindShow, theme = ConfirmationDialogTheme.Default, }: React.PropsWithChildren): JSX.Element { - const [dialogProps, showDialog, hideDialog] = useDialog({ + const { + dialogProps, + show: showDialog, + close: hideDialog, + } = useDialog({ bindShow, onCancel, onSuccess: onConfirm, diff --git a/browser/data-browser/src/components/Dialog/index.tsx b/browser/data-browser/src/components/Dialog/index.tsx index 9e70549b3..350225f56 100644 --- a/browser/data-browser/src/components/Dialog/index.tsx +++ b/browser/data-browser/src/components/Dialog/index.tsx @@ -19,7 +19,8 @@ import { useDialogGlobalContext } from './DialogGlobalContextProvider'; import { DIALOG_CONTENT_CONTAINER } from '../../helpers/containers'; export interface InternalDialogProps { - show: boolean; + /** Is the Dialog visible */ + isVisible: boolean; onClose: (success: boolean) => void; onClosed: () => void; width?: CSS.Property.Width; @@ -81,7 +82,7 @@ export function Dialog(props: React.PropsWithChildren) { const InnerDialog: React.FC> = ({ children, - show, + isVisible, width, onClose, onClosed, @@ -89,9 +90,9 @@ const InnerDialog: React.FC> = ({ const dialogRef = useRef(null); const innerDialogRef = useRef(null); const { hasOpenInnerPopup } = useDialogTreeContext(); - const { isTopLevel } = useDialogGlobalContext(show); + const { isTopLevel } = useDialogGlobalContext(isVisible); - useControlLock(show); + useControlLock(isVisible); const cancelDialog = useCallback(() => { onClose(false); @@ -123,22 +124,26 @@ const InnerDialog: React.FC> = ({ () => { cancelDialog(); }, - { enabled: show && !hasOpenInnerPopup && isTopLevel }, + { enabled: isVisible && !hasOpenInnerPopup && isTopLevel }, ); // When closing the `data-closing` attribute must be set before rendering so the animation has started when the regular useEffect is called. useLayoutEffect(() => { - if (!show && dialogRef.current && dialogRef.current.hasAttribute('open')) { + if ( + !isVisible && + dialogRef.current && + dialogRef.current.hasAttribute('open') + ) { dialogRef.current.setAttribute('data-closing', 'true'); } - }, [show]); + }, [isVisible]); useEffect(() => { if (!dialogRef.current) { return; } - if (show) { + if (isVisible) { if (!dialogRef.current.hasAttribute('open')) // @ts-ignore dialogRef.current.showModal(); @@ -153,7 +158,7 @@ const InnerDialog: React.FC> = ({ onClosed(); }, ANIM_MS); } - }, [show, onClosed]); + }, [isVisible, onClosed]); return ( void, + show: () => void; /** Function to close the dialog */ - close: (success?: boolean) => void, - /** Boolean indicating wether the dialog is currently open */ - isOpen: boolean, -]; + close: (success?: boolean) => void; + /** Whether the dialog is currently open */ + isOpen: boolean; +}; export type UseDialogOptions = { bindShow?: React.Dispatch; @@ -25,26 +25,27 @@ export function useDialog( ): UseDialogReturnType { const { bindShow, onCancel, onSuccess, triggerRef } = options ?? {}; - const [showDialog, setShowDialog] = useState(false); - const [visible, setVisible] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [isVisible, setIsVisible] = useState(false); const [wasSuccess, setWasSuccess] = useState(false); const show = useCallback(() => { document.body.setAttribute('inert', ''); - setShowDialog(true); - setVisible(true); + setIsVisible(true); + setIsOpen(true); bindShow?.(true); }, []); const close = useCallback((success = false) => { setWasSuccess(success); - setShowDialog(false); + setIsVisible(false); + // The 'isOpen' will be set to false by handleClosed after the animation }, []); const handleClosed = useCallback(() => { document.body.removeAttribute('inert'); bindShow?.(false); - setVisible(false); + setIsOpen(false); if (wasSuccess) { onSuccess?.(); @@ -60,12 +61,12 @@ export function useDialog( /** Props that should be passed to a {@link Dialog} component. */ const dialogProps = useMemo( () => ({ - show: showDialog, + isVisible, onClose: close, onClosed: handleClosed, }), - [showDialog, close, handleClosed], + [isVisible, close, handleClosed], ); - return [dialogProps, show, close, visible]; + return { dialogProps, show, close, isOpen }; } diff --git a/browser/data-browser/src/components/ErrorLook.tsx b/browser/data-browser/src/components/ErrorLook.tsx index 71c1469dd..0733fc6c7 100644 --- a/browser/data-browser/src/components/ErrorLook.tsx +++ b/browser/data-browser/src/components/ErrorLook.tsx @@ -2,6 +2,9 @@ import { lighten } from 'polished'; import { styled, css } from 'styled-components'; import { FaExclamationTriangle } from 'react-icons/fa'; +import { Column } from './Row'; +import { CodeBlock } from './CodeBlock'; +import { Button } from './Button'; export const errorLookStyle = css` color: ${props => props.theme.colors.alert}; @@ -18,6 +21,43 @@ export interface ErrorBlockProps { showTrace?: boolean; } +const githubIssueTemplate = ( + message, + stack, +) => `**Describe what you did to produce the bug** + +## Error message +\`\`\` +${message} +\`\`\` + +## Stack trace +\`\`\` +${stack} +\`\`\` +`; + +/** Returns github URL for new bugs */ +export function createGithubIssueLink(error: Error): string { + const url = new URL( + 'https://github.com/atomicdata-dev/atomic-server/issues/new', + ); + url.searchParams.set('body', githubIssueTemplate(error.message, error.stack)); + url.searchParams.set('labels', 'bug'); + + console.log('opening', url); + + return url.href; +} + +export function GitHubIssueButton({ error }) { + return ( + + ); +} + export function ErrorBlock({ error, showTrace }: ErrorBlockProps): JSX.Element { return ( @@ -25,40 +65,29 @@ export function ErrorBlock({ error, showTrace }: ErrorBlockProps): JSX.Element { Something went wrong -
-        {error.message}
+      
+        
         {showTrace && (
           <>
-            
-
- Stack trace: -
- {error.stack} + Stack trace: + )} -
+
); } const ErrorLookBig = styled.div` - color: ${p => p.theme.colors.alert}; font-size: 1rem; padding: ${p => p.theme.margin}rem; border-radius: ${p => p.theme.radius}; border: 1px solid ${p => lighten(0.2, p.theme.colors.alert)}; - background-color: ${p => p.theme.colors.bg1}; -`; - -const Pre = styled.pre` - white-space: pre-wrap; - border-radius: ${p => p.theme.radius}; - padding: ${p => p.theme.margin}rem; background-color: ${p => p.theme.colors.bg}; - font-size: 0.9rem; `; const BiggerText = styled.p` + color: ${p => p.theme.colors.alert}; font-size: 1.3rem; display: flex; align-items: center; diff --git a/browser/data-browser/src/components/Guard.tsx b/browser/data-browser/src/components/Guard.tsx new file mode 100644 index 000000000..60554e845 --- /dev/null +++ b/browser/data-browser/src/components/Guard.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { useSettings } from '../helpers/AppSettings'; +import { RegisterSignIn } from './RegisterSignIn'; + +/** + * The Guard can be wrapped around a Component that depends on a user being logged in. + * If the user is not logged in, it will show a button to sign up / sign in. + * Show to users after a new Agent has been created. + * Instructs them to save their secret somewhere safe + */ +export function Guard({ children }: React.PropsWithChildren): JSX.Element { + const { agent } = useSettings(); + + if (agent) { + return <>{children}; + } else return ; +} diff --git a/browser/data-browser/src/components/ParentPicker/ParentPickerDialog.tsx b/browser/data-browser/src/components/ParentPicker/ParentPickerDialog.tsx index efbc4a85c..7edac75e7 100644 --- a/browser/data-browser/src/components/ParentPicker/ParentPickerDialog.tsx +++ b/browser/data-browser/src/components/ParentPicker/ParentPickerDialog.tsx @@ -29,7 +29,7 @@ export function ParentPickerDialog({ }: ParentPickerDialogProps): React.JSX.Element { const [selected, setSelected] = useState(); - const [dialogProps, show, close, isOpen] = useDialog({ + const { dialogProps, show, close, isOpen } = useDialog({ onCancel, bindShow: onOpenChange, }); diff --git a/browser/data-browser/src/components/RegisterSignIn.tsx b/browser/data-browser/src/components/RegisterSignIn.tsx new file mode 100644 index 000000000..99533c710 --- /dev/null +++ b/browser/data-browser/src/components/RegisterSignIn.tsx @@ -0,0 +1,297 @@ +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + useDialog, +} from './Dialog'; +import { FormEvent, useCallback, useEffect, useState } from 'react'; +import { Button } from './Button'; +import { + addPublicKey, + nameRegex, + register as createRegistration, + useServerSupports, + useServerURL, + useStore, +} from '@tomic/react'; +import Field from './forms/Field'; +import { InputWrapper, InputStyled } from './forms/InputStyles'; +import { Row } from './Row'; +import { ErrorLook } from './ErrorLook'; +import { SettingsAgent } from './SettingsAgent'; + +/** What is currently showing */ +enum PageStateOpts { + /** Start state, select register or sign in */ + none, + /** Enter Secret*/ + signIn, + /** Register new Email address */ + register, + /** Reset existing email, send Secret reset email link */ + reset, + mailSentRegistration, + mailSentAddPubkey, +} + +/** + * Two buttons: Register / Sign in. + * Opens a Dialog / Modal with the appropriate form. + */ +export function RegisterSignIn(): JSX.Element { + const { dialogProps, show, close } = useDialog(); + const [pageState, setPageState] = useState(PageStateOpts.none); + const [email, setEmail] = useState(''); + const { emailRegister } = useServerSupports(); + + if (!emailRegister) { + return ( + <> + + No email support on this server... + + ); + } + + return ( + <> + + + + + + {pageState === PageStateOpts.register && ( + + )} + {pageState === PageStateOpts.signIn && ( + + )} + {pageState === PageStateOpts.reset && ( + + )} + {pageState === PageStateOpts.mailSentRegistration && ( + + )} + {pageState === PageStateOpts.mailSentAddPubkey && ( + + )} + + + ); +} + +function Reset({ email, setEmail, setPageState }) { + const store = useStore(); + const [err, setErr] = useState(undefined); + + const handleRequestReset = useCallback(async () => { + try { + await addPublicKey(store, email); + setPageState(PageStateOpts.mailSentAddPubkey); + } catch (e) { + setErr(e); + } + }, [email]); + + return ( + <> + +

Reset your Secret

+
+ +

+ { + "Lost it? No worries, we'll send a link that let's you create a new one." + } +

+ { + setErr(undefined); + setEmail(e); + }} + /> + {err && {err.message}} +
+ + + + + ); +} + +function MailSentConfirm({ email, close, message }) { + return ( + <> + +

Go to your email inbox

+
+ +

+ {"We've sent a confirmation link to "} + {email} + {'.'} +

+

{message}

+
+ + + + + ); +} + +function Register({ setPageState, email, setEmail }) { + const [name, setName] = useState(''); + const [serverUrlStr] = useServerURL(); + const [nameErr, setErr] = useState(undefined); + const store = useStore(); + + const serverUrl = new URL(serverUrlStr); + serverUrl.host = `${name}.${serverUrl.host}`; + + useEffect(() => { + // check regex of name, set error + if (!name.match(nameRegex)) { + setErr(new Error('Name must be lowercase and only contain numbers')); + } else { + setErr(undefined); + } + }, [name, email]); + + const handleSubmit = useCallback( + async (event: FormEvent) => { + event.preventDefault(); + + if (!name) { + setErr(new Error('Name is required')); + + return; + } + + try { + await createRegistration(store, name, email); + setPageState(PageStateOpts.mailSentRegistration); + } catch (er) { + setErr(er); + } + }, + [name, email], + ); + + return ( + <> + +

Register

+
+ +
+ + + { + setName(e.target.value); + }} + /> + + + + {name && nameErr && {nameErr.message}} + +
+ + + + + + ); +} + +function SignIn({ setPageState }) { + return ( + <> + +

Sign in

+
+ + + + + + + + + ); +} + +function EmailField({ setEmail, email }) { + return ( + + + { + setEmail(e.target.value); + }} + /> + + + ); +} diff --git a/browser/data-browser/src/components/SearchFilter.tsx b/browser/data-browser/src/components/SearchFilter.tsx index 6cc278ec1..c3c22812e 100644 --- a/browser/data-browser/src/components/SearchFilter.tsx +++ b/browser/data-browser/src/components/SearchFilter.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { urls, useArray, useProperty, useResource } from '@tomic/react'; +import { core, urls, useArray, useProperty, useResource } from '@tomic/react'; import { ResourceSelector } from '../components/forms/ResourceSelector'; /** @@ -18,7 +18,7 @@ export function ClassFilter({ filters, setFilters }): JSX.Element { // Set the filters to the default values of the properties setFilters({ ...filters, - [urls.properties.isA]: klass, + [core.properties.isA]: klass, }); }, [klass, JSON.stringify(filters)]); @@ -27,7 +27,7 @@ export function ClassFilter({ filters, setFilters }): JSX.Element { {allProps?.map(propertySubject => ( { + const { agent, setAgent } = useSettings(); + const [subject, setSubject] = useState(undefined); + const [privateKey, setPrivateKey] = useState(undefined); + const [error, setError] = useState(undefined); + const [showPrivateKey, setShowPrivateKey] = useState(false); + const [advanced, setAdvanced] = useState(false); + const [secret, setSecret] = useState(undefined); + + // When there is an agent, set the advanced values + // Otherwise, reset the secret value + React.useEffect(() => { + if (agent !== undefined) { + fillAdvanced(); + } else { + setSecret(''); + } + }, [agent]); + + // When the key or subject changes, update the secret + React.useEffect(() => { + renewSecret(); + }, [subject, privateKey]); + + function renewSecret() { + if (agent) { + setSecret(agent.buildSecret()); + } + } + + function fillAdvanced() { + try { + if (!agent) { + throw new Error('No agent set'); + } + + setSubject(agent.subject); + setPrivateKey(agent.privateKey); + } catch (e) { + const err = new Error('Cannot fill subject and privatekey fields.' + e); + setError(err); + setSubject(''); + } + } + + function setAgentIfChanged(oldAgent: Agent | undefined, newAgent: Agent) { + if (JSON.stringify(oldAgent) !== JSON.stringify(newAgent)) { + setAgent(newAgent); + } + } + + /** Called when the secret or the subject is updated manually */ + async function handleUpdateSubjectAndKey() { + renewSecret(); + setError(undefined); + + try { + const newAgent = new Agent(privateKey!, subject); + await newAgent.getPublicKey(); + await newAgent.verifyPublicKeyWithServer(); + + setAgentIfChanged(agent, newAgent); + } catch (e) { + const err = new Error('Invalid Agent' + e); + setError(err); + } + } + + function handleCopy() { + secret && navigator.clipboard.writeText(secret); + } + + /** When the Secret updates, parse it and try if the */ + async function handleUpdateSecret(updateSecret: string) { + setSecret(updateSecret); + + if (updateSecret === '') { + setSecret(''); + setError(undefined); + + return; + } + + setError(undefined); + + try { + const newAgent = Agent.fromSecret(updateSecret); + setAgentIfChanged(agent, newAgent); + setPrivateKey(newAgent.privateKey); + setSubject(newAgent.subject); + // This will fail and throw if the agent is not public, which is by default + // await newAgent.checkPublicKey(); + } catch (e) { + const err = new Error('Invalid secret. ' + e); + setError(err); + } + } + + return ( +
+ + + handleUpdateSecret(e.target.value)} + type={showPrivateKey ? 'text' : 'password'} + disabled={agent !== undefined} + name='secret' + id='current-password' + autoComplete='current-password' + spellCheck='false' + placeholder='Paste your Secret' + /> + setShowPrivateKey(!showPrivateKey)} + > + {showPrivateKey ? : } + + setAdvanced(!advanced)} + > + + + {agent && ( + + copy + + )} + + + {advanced ? ( + + + + { + setSubject(e.target.value); + handleUpdateSubjectAndKey(); + }} + /> + + + + + { + setPrivateKey(e.target.value); + handleUpdateSubjectAndKey(); + }} + /> + setShowPrivateKey(!showPrivateKey)} + > + {showPrivateKey ? : } + + + + + ) : null} +
+ ); +}; diff --git a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx index 6d4ba73c7..27089ecae 100644 --- a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx +++ b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx @@ -1,4 +1,10 @@ -import { Resource, core, server, useResources } from '@tomic/react'; +import { + Resource, + server, + truncateUrl, + urls, + useResources, +} from '@tomic/react'; import { useMemo } from 'react'; import { FaCog, @@ -21,7 +27,8 @@ const Trigger = buildDefaultTrigger(, 'Open Drive Settings'); function getTitle(resource: Resource): string { return ( - (resource.get(core.properties.name) as string) ?? resource.getSubject() + (resource.get(urls.properties.name) as string) ?? + truncateUrl(resource.getSubject(), 20) ); } @@ -45,6 +52,7 @@ export function DriveSwitcher() { }; const createNewResource = useNewResourceUI(); + // const createNewDrive = useDefaultNewInstanceHandler(classes.drive); const items = useMemo( () => [ diff --git a/browser/data-browser/src/components/datatypes/Markdown.tsx b/browser/data-browser/src/components/datatypes/Markdown.tsx index 3889493f2..8bb4419b8 100644 --- a/browser/data-browser/src/components/datatypes/Markdown.tsx +++ b/browser/data-browser/src/components/datatypes/Markdown.tsx @@ -1,5 +1,5 @@ import ReactMarkdown from 'react-markdown'; -import { styled } from 'styled-components'; +import { css, styled } from 'styled-components'; import remarkGFM from 'remark-gfm'; import { Button } from '../Button'; import { truncateMarkdown } from '../../helpers/markdown'; @@ -52,16 +52,15 @@ const Markdown: FC = ({ ); }; -const MarkdownWrapper = styled.div` - width: 100%; - overflow-x: hidden; - img { - max-width: 100%; - } +Markdown.defaultProps = { + renderGFM: true, +}; - * { +/** Styles shared in Markdown View & Edit renderers */ +export const markdownStyles = css` + /* * { white-space: unset; - } + } */ p, h1, @@ -77,6 +76,10 @@ const MarkdownWrapper = styled.div` margin-bottom: 0; } + ul li { + margin-bottom: 1rem; + } + blockquote { margin-inline-start: 0rem; padding-inline-start: 1rem; @@ -124,4 +127,14 @@ const MarkdownWrapper = styled.div` } `; +const MarkdownWrapper = styled.div` + width: 100%; + overflow-x: hidden; + img { + max-width: 100%; + } + + ${markdownStyles} +`; + export default Markdown; diff --git a/browser/data-browser/src/components/forms/FileDropzone/FileDropzoneInput.tsx b/browser/data-browser/src/components/forms/FileDropzone/FileDropzoneInput.tsx index 25e5a2f48..3d542b842 100644 --- a/browser/data-browser/src/components/forms/FileDropzone/FileDropzoneInput.tsx +++ b/browser/data-browser/src/components/forms/FileDropzone/FileDropzoneInput.tsx @@ -8,11 +8,11 @@ import { useUpload } from '../../../hooks/useUpload'; export interface FileDropzoneInputProps { parentResource: Resource; - onFilesUploaded?: (files: string[]) => void; text?: string; maxFiles?: number; className?: string; accept?: string[]; + onFilesUploaded?: (fileSubjects: string[]) => void; } /** diff --git a/browser/data-browser/src/components/forms/FilePicker/FilePickerDialog.tsx b/browser/data-browser/src/components/forms/FilePicker/FilePickerDialog.tsx index b73162173..6a1802e30 100644 --- a/browser/data-browser/src/components/forms/FilePicker/FilePickerDialog.tsx +++ b/browser/data-browser/src/components/forms/FilePicker/FilePickerDialog.tsx @@ -34,7 +34,11 @@ export function FilePickerDialog({ noUpload = false, }: FilePickerProps): React.JSX.Element { const { drive } = useSettings(); - const [dialogProps, showDialog, closeDialog] = useDialog({ + const { + dialogProps, + show: showDialog, + close: closeDialog, + } = useDialog({ bindShow: onShowChange, }); diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx index 35d8293b4..9154be958 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx @@ -44,7 +44,10 @@ export const NewArticleDialog: FC = ({ onClose(); }, [name, createResourceAndNavigate, onClose, parent]); - const [dialogProps, show, hide] = useDialog({ onSuccess, onCancel: onClose }); + const { dialogProps, show, close } = useDialog({ + onSuccess, + onCancel: onClose, + }); const onNameChange = (e: React.ChangeEvent) => { setName(e.target.value); @@ -69,7 +72,7 @@ export const NewArticleDialog: FC = ({
{ e.preventDefault(); - hide(true); + close(true); }} > @@ -89,10 +92,10 @@ export const NewArticleDialog: FC = ({ - - diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx index f2b7b4811..482f56bbf 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx @@ -27,7 +27,7 @@ export const NewBookmarkDialog: FC = ({ }) => { const [url, setUrl] = useState(''); - const [dialogProps, show, hide] = useDialog({ onCancel: onClose }); + const { dialogProps, show, close } = useDialog({ onCancel: onClose }); const createResourceAndNavigate = useCreateAndNavigate(); @@ -58,32 +58,34 @@ export const NewBookmarkDialog: FC = ({ }, []); return ( - - -

New Bookmark

-
- -
- - - setUrl(e.target.value)} - /> - - -
-
- - - - -
+ <> + + +

New Bookmark

+
+ +
+ + + setUrl(e.target.value)} + /> + + +
+
+ + + + +
+ ); }; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx index 349866c84..09f2ccd54 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx @@ -22,7 +22,7 @@ export const NewCollectionDialog: FC = ({ const [valueFilter, setValue] = useState(); const [propertyFilter, setProperty] = useState(); - const [dialogProps, show, hide] = useDialog({ onCancel: onClose }); + const { dialogProps, show, close } = useDialog({ onCancel: onClose }); const createResourceAndNavigate = useCreateAndNavigate(); @@ -91,7 +91,7 @@ export const NewCollectionDialog: FC = ({ - + )} + + ); +} + +export default ConfirmEmail; diff --git a/browser/data-browser/src/routes/DataRoute.tsx b/browser/data-browser/src/routes/DataRoute.tsx index 1aa41f022..3134e6fba 100644 --- a/browser/data-browser/src/routes/DataRoute.tsx +++ b/browser/data-browser/src/routes/DataRoute.tsx @@ -59,7 +59,7 @@ function Data(): JSX.Element { setTextResponseLoading(true); try { - const resp = await window.fetch(subject!, { headers: headers }); + const resp = await fetch(subject!, { headers: headers }); const body = await resp.text(); setTextResponseLoading(false); setTextResponse(body); diff --git a/browser/data-browser/src/routes/History/HistoryMobileView.tsx b/browser/data-browser/src/routes/History/HistoryMobileView.tsx index b69ac8f6b..df0b57466 100644 --- a/browser/data-browser/src/routes/History/HistoryMobileView.tsx +++ b/browser/data-browser/src/routes/History/HistoryMobileView.tsx @@ -23,7 +23,7 @@ export function HistoryMobileView({ onSelectVersion, onVersionAccept, }: HistoryViewProps) { - const [dialogProps, showDialog, closeDialog] = useDialog(); + const { dialogProps, show: showDialog, close: closeDialog } = useDialog(); const handleVersionSelect = useCallback((version: Version) => { onSelectVersion(version); diff --git a/browser/data-browser/src/routes/NewResource/NewRoute.tsx b/browser/data-browser/src/routes/NewResource/NewRoute.tsx index edb9e307c..b4b27a1fc 100644 --- a/browser/data-browser/src/routes/NewResource/NewRoute.tsx +++ b/browser/data-browser/src/routes/NewResource/NewRoute.tsx @@ -62,11 +62,13 @@ function NewResourceSelector() { } const onUploadComplete = useCallback( - (files: string[]) => { - toast.success(`Uploaded ${files.length} files.`); + (fileSubjects: string[]) => { + toast.success(`Uploaded ${fileSubjects.length} files.`); - if (calculatedParent) { - navigate(constructOpenURL(calculatedParent)); + if (fileSubjects.length > 1 && parentSubject) { + navigate(constructOpenURL(parentSubject)); + } else { + navigate(constructOpenURL(fileSubjects[0])); } }, [calculatedParent, navigate], diff --git a/browser/data-browser/src/routes/Routes.tsx b/browser/data-browser/src/routes/Routes.tsx index 3c4cc7d40..c1e11f927 100644 --- a/browser/data-browser/src/routes/Routes.tsx +++ b/browser/data-browser/src/routes/Routes.tsx @@ -11,7 +11,7 @@ import Data from './DataRoute'; import { Shortcuts } from './ShortcutsRoute'; import { About as About } from './AboutRoute'; import Local from './LocalRoute'; -import SettingsAgent from './SettingsAgent'; +import { SettingsAgentRoute } from './SettingsAgent'; import { SettingsServer } from './SettingsServer'; import { paths } from './paths'; import ResourcePage from '../views/ResourcePage'; @@ -21,8 +21,10 @@ import { TokenRoute } from './TokenRoute'; import { ImporterPage } from '../views/ImporterPage'; import { History } from './History'; import { PruneTestsRoute } from './PruneTestsRoute'; +import ConfirmEmail from './ConfirmEmail'; -const homeURL = window.location.origin; +/** Server URLs should have a `/` at the end */ +const homeURL = window.location.origin + '/'; const isDev = import.meta.env.MODE === 'development'; @@ -37,7 +39,7 @@ export function AppRoutes(): JSX.Element { } /> } /> - } /> + } /> } /> } /> } /> @@ -51,6 +53,7 @@ export function AppRoutes(): JSX.Element { } /> {isDev && } />} {isDev && } />} + } /> } /> } /> diff --git a/browser/data-browser/src/routes/Sandbox.tsx b/browser/data-browser/src/routes/Sandbox.tsx index 8685fc42f..f7ffb6e45 100644 --- a/browser/data-browser/src/routes/Sandbox.tsx +++ b/browser/data-browser/src/routes/Sandbox.tsx @@ -1,6 +1,15 @@ +import { Button } from '../components/Button'; import { ContainerFull } from '../components/Containers'; +import { + Dialog, + DialogContent, + DialogTitle, + useDialog, +} from '../components/Dialog'; export function Sandbox(): JSX.Element { + const { dialogProps, show, isOpen } = useDialog(); + return (
@@ -9,6 +18,12 @@ export function Sandbox(): JSX.Element { Welcome to the sandbox. This is a place to test components in isolation.

+

{isOpen ? 'TRUE' : 'FALSE'}

+ + + Title + Content +
); diff --git a/browser/data-browser/src/routes/SearchRoute.tsx b/browser/data-browser/src/routes/SearchRoute.tsx index b39175f96..958667526 100644 --- a/browser/data-browser/src/routes/SearchRoute.tsx +++ b/browser/data-browser/src/routes/SearchRoute.tsx @@ -95,7 +95,7 @@ export function Search(): JSX.Element { } if (loading) { - message = 'Loading results...'; + message = 'Loading results for'; } if (results.length > 0) { @@ -142,6 +142,7 @@ export function Search(): JSX.Element { {results.map((subject, index) => ( { +export function SettingsAgentRoute() { const { agent, setAgent } = useSettings(); - const [subject, setSubject] = useState(undefined); - const [privateKey, setPrivateKey] = useState(undefined); - const [error, setError] = useState(undefined); - const [showPrivateKey, setShowPrivateKey] = useState(false); - const [advanced, setAdvanced] = useState(false); - const [secret, setSecret] = useState(undefined); const navigate = useNavigate(); - // When there is an agent, set the advanced values - // Otherwise, reset the secret value - React.useEffect(() => { - if (agent !== undefined) { - fillAdvanced(); - } else { - setSecret(''); - } - }, [agent]); - - // When the key or subject changes, update the secret - React.useEffect(() => { - renewSecret(); - }, [subject, privateKey]); - - function renewSecret() { - if (agent) { - setSecret(agent.buildSecret()); - } - } - - function fillAdvanced() { - try { - if (!agent) { - throw new Error('No agent set'); - } - - setSubject(agent.subject); - setPrivateKey(agent.privateKey); - } catch (e) { - const err = new Error('Cannot fill subject and privatekey fields.' + e); - setError(err); - setSubject(''); - } - } - function handleSignOut() { if ( window.confirm( @@ -74,196 +19,27 @@ const SettingsAgent: React.FunctionComponent = () => { ) ) { setAgent(undefined); - setError(undefined); - setSubject(''); - setPrivateKey(''); - } - } - - function setAgentIfChanged(oldAgent: Agent | undefined, newAgent: Agent) { - if (JSON.stringify(oldAgent) !== JSON.stringify(newAgent)) { - setAgent(newAgent); - } - } - - /** Called when the secret or the subject is updated manually */ - async function handleUpdateSubjectAndKey() { - renewSecret(); - setError(undefined); - - try { - const newAgent = new Agent(privateKey!, subject); - await newAgent.getPublicKey(); - await newAgent.verifyPublicKeyWithServer(); - - setAgentIfChanged(agent, newAgent); - } catch (e) { - const err = new Error('Invalid Agent' + e); - setError(err); - } - } - - function handleCopy() { - secret && navigator.clipboard.writeText(secret); - } - - /** When the Secret updates, parse it and try if the */ - async function handleUpdateSecret(updateSecret: string) { - setSecret(updateSecret); - - if (updateSecret === '') { - setSecret(''); - setError(undefined); - - return; - } - - setError(undefined); - - try { - const newAgent = Agent.fromSecret(updateSecret); - setAgentIfChanged(agent, newAgent); - setPrivateKey(newAgent.privateKey); - setSubject(newAgent.subject); - // This will fail and throw if the agent is not public, which is by default - // await newAgent.checkPublicKey(); - } catch (e) { - const err = new Error('Invalid secret. ' + e); - setError(err); } } return ( -
- -
-

User Settings

-

- An Agent is a user, consisting of a Subject (its URL) and Private - Key. Together, these can be used to edit data and sign Commits. -

- {agent ? ( - - {agent.subject?.startsWith('http://localhost') && ( - - Warning: - { - "You're using a local Agent, which cannot authenticate on other domains, because its URL does not resolve." - } - - )} -
- - You{"'"}re signed in as - - -
- - -
- ) : ( + +

User Settings

+

+ An Agent is a user, consisting of a Subject (its URL) and Private Key. + Together, these can be used to edit data and sign Commits. +

+ + {agent && ( + <>

- You can create your own Agent by hosting an{' '} - - atomic-server - - . Alternatively, you can use{' '} - - an Invite - {' '} - to get a guest Agent on someone else{"'s"} Atomic Server. +

- )} - - - handleUpdateSecret(e.target.value)} - type={showPrivateKey ? 'text' : 'password'} - disabled={agent !== undefined} - name='secret' - id='current-password' - autoComplete='current-password' - spellCheck='false' - /> - setShowPrivateKey(!showPrivateKey)} - > - {showPrivateKey ? : } - - setAdvanced(!advanced)} - > - - - {agent && ( - - copy - - )} - - - {advanced ? ( - <> - - - { - setSubject(e.target.value); - handleUpdateSubjectAndKey(); - }} - /> - - - - - { - setPrivateKey(e.target.value); - handleUpdateSubjectAndKey(); - }} - /> - setShowPrivateKey(!showPrivateKey)} - > - {showPrivateKey ? : } - - - - - ) : null} - {agent && ( + + + - )} - -
-
+ + )} + + ); -}; - -export default SettingsAgent; +} diff --git a/browser/data-browser/src/routes/paths.tsx b/browser/data-browser/src/routes/paths.tsx index 66531d2a7..bd6b27b3e 100644 --- a/browser/data-browser/src/routes/paths.tsx +++ b/browser/data-browser/src/routes/paths.tsx @@ -15,6 +15,7 @@ export const paths = { history: '/app/history', allVersions: '/all-versions', sandbox: '/sandbox', + confirmEmail: '/confirm-email', fetchBookmark: '/fetch-bookmark', pruneTests: '/prunetests', }; diff --git a/browser/data-browser/src/views/Card/CollectionCard.tsx b/browser/data-browser/src/views/Card/CollectionCard.tsx index 754999b5c..353d61cdf 100644 --- a/browser/data-browser/src/views/Card/CollectionCard.tsx +++ b/browser/data-browser/src/views/Card/CollectionCard.tsx @@ -16,7 +16,7 @@ const MAX_COUNT = 5; * Renders a Resource and all its Properties in a random order. Title * (shortname) is rendered prominently at the top. */ -function CollectionCard({ resource, small }: CardViewProps): JSX.Element { +function CollectionCard({ resource }: CardViewProps): JSX.Element { const [description] = useString(resource, core.properties.description); const [members] = useArray(resource, collections.properties.members); const [showAll, setShowMore] = useState(false); @@ -32,38 +32,32 @@ function CollectionCard({ resource, small }: CardViewProps): JSX.Element { {description && } - - {subjects.length === 0 ? ( - No resources - ) : ( - - {subjects.map(member => { - return ( - - - - ); - })} - {tooMany && ( - - + {subjects.length === 0 ? ( + No resources + ) : ( + + {subjects.map(member => { + return ( + + - )} - - )} - + ); + })} + {tooMany && ( + + + + )} + + )} ); } -const Show: FC> = ({ show, children }) => { - return show ? children : null; -}; - const Empty = styled.span` color: ${({ theme }) => theme.colors.textLight}; `; diff --git a/browser/data-browser/src/views/Card/ResourceCard.tsx b/browser/data-browser/src/views/Card/ResourceCard.tsx index a0a57d188..e5d6ea7e0 100644 --- a/browser/data-browser/src/views/Card/ResourceCard.tsx +++ b/browser/data-browser/src/views/Card/ResourceCard.tsx @@ -127,20 +127,22 @@ export function ResourceCardDefault({ {isAResource.title} - + - {!small && ( - - )} +
); } @@ -148,8 +150,7 @@ export function ResourceCardDefault({ export default ResourceCard; const DescriptionWrapper = styled.div` - max-height: 10rem; - overflow: hidden; + overflow: auto; `; const ClassName = styled.span` diff --git a/browser/data-browser/src/views/ChatRoomPage.tsx b/browser/data-browser/src/views/ChatRoomPage.tsx index ebd8eb281..562e30011 100644 --- a/browser/data-browser/src/views/ChatRoomPage.tsx +++ b/browser/data-browser/src/views/ChatRoomPage.tsx @@ -22,6 +22,7 @@ import { CommitDetail } from '../components/CommitDetail'; import Markdown from '../components/datatypes/Markdown'; import { Detail } from '../components/Detail'; import { EditableTitle } from '../components/EditableTitle'; +import { Guard } from '../components/Guard'; import { NavBarSpacer } from '../components/NavBarSpacer'; import { editURL } from '../helpers/navigation'; import { ResourceInline } from './ResourceInline'; @@ -147,25 +148,28 @@ export function ChatRoomPage({ resource }: ResourcePageProps) { )} - - - - Send - - + + + + + Send + + + + ); diff --git a/browser/data-browser/src/views/CodeUsage/ResourceCodeUsageDialog.tsx b/browser/data-browser/src/views/CodeUsage/ResourceCodeUsageDialog.tsx index a00b091ce..f903aba5c 100644 --- a/browser/data-browser/src/views/CodeUsage/ResourceCodeUsageDialog.tsx +++ b/browser/data-browser/src/views/CodeUsage/ResourceCodeUsageDialog.tsx @@ -21,7 +21,7 @@ export function ResourceCodeUsageDialog({ bindShow, }: ResourceCodeUsageDialogProps): React.JSX.Element { const resource = useResource(subject); - const [dialogProps, show, hide, isOpen] = useDialog({ bindShow }); + const { dialogProps, show, close: hide, isOpen } = useDialog({ bindShow }); useEffect(() => { if (open) { diff --git a/browser/data-browser/src/views/CollectionPage.tsx b/browser/data-browser/src/views/CollectionPage.tsx index e467a39c6..a83d7a85c 100644 --- a/browser/data-browser/src/views/CollectionPage.tsx +++ b/browser/data-browser/src/views/CollectionPage.tsx @@ -13,6 +13,7 @@ import { FaArrowLeft, FaArrowRight, FaInfo, + FaPlus, FaTable, FaThLarge, } from 'react-icons/fa'; @@ -171,8 +172,9 @@ function Collection({ resource }: ResourcePageProps): JSX.Element { {isClass && ( diff --git a/browser/data-browser/src/views/CrashPage.tsx b/browser/data-browser/src/views/CrashPage.tsx index 520f82586..e8e88fa04 100644 --- a/browser/data-browser/src/views/CrashPage.tsx +++ b/browser/data-browser/src/views/CrashPage.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import { Resource } from '@tomic/react'; import { ContainerWide } from '../components/Containers'; -import { ErrorBlock } from '../components/ErrorLook'; +import { + createGithubIssueLink, + ErrorBlock, + GitHubIssueButton, +} from '../components/ErrorLook'; import { Button } from '../components/Button'; import { Column, Row } from '../components/Row'; @@ -26,6 +30,8 @@ function CrashPage({ {children ? children : } + Create Github issue + {clearError && } diff --git a/browser/data-browser/src/views/DrivePage.tsx b/browser/data-browser/src/views/DrivePage.tsx index 66843c550..b94b64b1e 100644 --- a/browser/data-browser/src/views/DrivePage.tsx +++ b/browser/data-browser/src/views/DrivePage.tsx @@ -5,7 +5,7 @@ import { Button } from '../components/Button'; import { useSettings } from '../helpers/AppSettings'; import { ResourcePageProps } from './ResourcePage'; import { EditableTitle } from '../components/EditableTitle'; -import { Column, Row } from '../components/Row'; +import { Row } from '../components/Row'; import { styled } from 'styled-components'; import InputSwitcher from '../components/forms/InputSwitcher'; import { WarningBlock } from '../components/WarningBlock'; @@ -23,38 +23,43 @@ function DrivePage({ resource }: ResourcePageProps): JSX.Element { return ( - - - - {baseURL !== resource.subject && ( - - )} - - + + {baseURL !== resource.subject && ( + + )} + + + Default Ontology + +
+ Default Ontology + -
- Default Ontology - -
- {baseURL.startsWith('http://localhost') && ( - - You are running Atomic-Server on `localhost`, which means that it - will not be available from any other machine than your current local - device. If you want your Atomic-Server to be available from the web, - you should set this up at a Domain on a server. - - )} - +
+ {baseURL.startsWith('http://localhost') && ( + + You are running Atomic-Server on `localhost`, which means that it will + not be available from any other machine than your current local + device. If you want your Atomic-Server to be available from the web, + you should set this up at a Domain on a server. + + )}
); } diff --git a/browser/data-browser/src/views/ErrorPage.tsx b/browser/data-browser/src/views/ErrorPage.tsx index 4be11ec79..b336d1e2a 100644 --- a/browser/data-browser/src/views/ErrorPage.tsx +++ b/browser/data-browser/src/views/ErrorPage.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; import { isUnauthorized, useStore } from '@tomic/react'; import { ContainerWide } from '../components/Containers'; -import { ErrorBlock } from '../components/ErrorLook'; +import { ErrorBlock, GitHubIssueButton } from '../components/ErrorLook'; import { Button } from '../components/Button'; -import { SignInButton } from '../components/SignInButton'; import { useSettings } from '../helpers/AppSettings'; import { ResourcePageProps } from './ResourcePage'; import { Column, Row } from '../components/Row'; import CrashPage from './CrashPage'; import { clearAllLocalData } from '../helpers/clearData'; +import { Guard } from '../components/Guard'; /** * A View for Resource Errors. Not to be confused with the CrashPage, which is @@ -17,15 +17,28 @@ import { clearAllLocalData } from '../helpers/clearData'; function ErrorPage({ resource }: ResourcePageProps): JSX.Element { const { agent } = useSettings(); const store = useStore(); - const subject = resource.getSubject(); + const subject = resource.subject; + + React.useEffect(() => { + // Try again when agent changes + store.fetchResourceFromServer(subject); + }, [agent]); if (isUnauthorized(resource.error)) { + // This might be a bit too aggressive, but it fixes 'Unauthorized' messages after signing in to a new drive. + store.fetchResourceFromServer(subject); + return (

Unauthorized

{agent ? ( <> +

+ { + "You don't have access to this. Try asking for access, or sign in with a different account." + } +