From 9a3227bd36050d81fa8bc0195be4192576b5c9c5 Mon Sep 17 00:00:00 2001 From: Fred Clausen <43556888+fredclausen@users.noreply.github.com> Date: Sat, 15 Jun 2024 06:59:03 -0600 Subject: [PATCH] Websocket communicate with backend (#41) --- .github/workflows/deploy.yml | 20 +- .github/workflows/markdownlint.yml | 2 +- .github/workflows/on_pr.yaml | 24 +- .github/workflows/pre-commit-updates.yaml | 2 +- .github/workflows/yamllint.yml | 2 +- Cargo.lock | 35 +- Cargo.toml | 1 + Dockerfile.build_binary | 2 +- Dockerfile.build_frontend | 2 +- Dockerfile.local | 2 +- README.md | 1 + sh-frontend/Cargo.lock | 45 +-- sh-frontend/Cargo.toml | 56 +-- sh-frontend/Trunk.toml | 1 + sh-frontend/css/components/_navbar.scss | 2 +- sh-frontend/css/components/_settings.scss | 97 ++++- sh-frontend/css/utility/_alerts.scss | 16 + sh-frontend/css/utility/_base.scss | 10 +- sh-frontend/src/app/webapp.rs | 322 +++++++++++++++- .../mod.rs => common/alert_boxes.rs} | 12 +- sh-frontend/src/common/mod.rs | 2 + sh-frontend/src/common/wssprops.rs | 16 + .../src/components/alerts/alert_error.rs | 73 ---- sh-frontend/src/components/alerts/base.rs | 348 ++++++++++++++++++ sh-frontend/src/components/alerts/error.rs | 52 +++ sh-frontend/src/components/alerts/mod.rs | 195 +++++++++- sh-frontend/src/components/alerts/notice.rs | 52 +++ .../connected.rs | 4 +- .../{footer_components => footer}/mod.rs | 0 sh-frontend/src/components/input/field.rs | 296 +++++++++++++++ .../src/components/input/input_field.rs | 108 ------ sh-frontend/src/components/input/mod.rs | 2 +- .../{layout_components => layout}/footer.rs | 4 +- .../{layout_components => layout}/live.rs | 19 +- .../{layout_components => layout}/mod.rs | 0 .../{layout_components => layout}/nav.rs | 6 +- .../{map_components => map}/control.rs | 0 .../{map_components => map}/map_display.rs | 4 +- .../components/{map_components => map}/mod.rs | 0 sh-frontend/src/components/mod.rs | 10 +- .../components/{nav_components => nav}/mod.rs | 0 .../{nav_components => nav}/search.rs | 2 + .../src/components/pages/acars_messages.rs | 2 + sh-frontend/src/components/pages/adsb.rs | 2 + sh-frontend/src/components/pages/help.rs | 2 + sh-frontend/src/components/pages/settings.rs | 17 +- sh-frontend/src/components/pages/stats.rs | 2 + sh-frontend/src/components/setting/mod.rs | 23 ++ .../sh_app_config.rs | 110 ++---- .../sh_data_sources.rs | 2 + .../sh_enabled_data_sources.rs | 2 + sh-frontend/src/components/setting/sh_map.rs | 197 ++++++++++ .../components/setting_components/sh_map.rs | 25 -- sh-frontend/src/services/mod.rs | 1 - sh-frontend/src/services/websocket.rs | 182 --------- src/bin/sh-tauri/Cargo.toml | 2 +- src/libraries/sdrehub/Cargo.toml | 1 + src/libraries/sdrehub/src/lib.rs | 32 +- src/libraries/sh-api/Cargo.toml | 1 + src/libraries/sh-api/src/lib.rs | 113 +++++- src/libraries/sh-common-server/Cargo.toml | 22 ++ src/libraries/sh-common-server/src/lib.rs | 23 ++ src/libraries/sh-common/Cargo.toml | 1 - src/libraries/sh-common/src/lib.rs | 28 +- src/libraries/sh-config/Cargo.toml | 2 +- src/libraries/sh-config/src/lib.rs | 11 +- src/libraries/sh-config/src/map.rs | 4 +- 67 files changed, 1988 insertions(+), 666 deletions(-) rename sh-frontend/src/{components/setting_components/mod.rs => common/alert_boxes.rs} (55%) create mode 100644 sh-frontend/src/common/wssprops.rs delete mode 100644 sh-frontend/src/components/alerts/alert_error.rs create mode 100644 sh-frontend/src/components/alerts/base.rs create mode 100644 sh-frontend/src/components/alerts/error.rs create mode 100644 sh-frontend/src/components/alerts/notice.rs rename sh-frontend/src/components/{footer_components => footer}/connected.rs (90%) rename sh-frontend/src/components/{footer_components => footer}/mod.rs (100%) create mode 100644 sh-frontend/src/components/input/field.rs delete mode 100644 sh-frontend/src/components/input/input_field.rs rename sh-frontend/src/components/{layout_components => layout}/footer.rs (79%) rename sh-frontend/src/components/{layout_components => layout}/live.rs (88%) rename sh-frontend/src/components/{layout_components => layout}/mod.rs (100%) rename sh-frontend/src/components/{layout_components => layout}/nav.rs (99%) rename sh-frontend/src/components/{map_components => map}/control.rs (100%) rename sh-frontend/src/components/{map_components => map}/map_display.rs (95%) rename sh-frontend/src/components/{map_components => map}/mod.rs (100%) rename sh-frontend/src/components/{nav_components => nav}/mod.rs (100%) rename sh-frontend/src/components/{nav_components => nav}/search.rs (90%) create mode 100644 sh-frontend/src/components/setting/mod.rs rename sh-frontend/src/components/{setting_components => setting}/sh_app_config.rs (72%) rename sh-frontend/src/components/{setting_components => setting}/sh_data_sources.rs (93%) rename sh-frontend/src/components/{setting_components => setting}/sh_enabled_data_sources.rs (93%) create mode 100644 sh-frontend/src/components/setting/sh_map.rs delete mode 100644 sh-frontend/src/components/setting_components/sh_map.rs delete mode 100644 sh-frontend/src/services/websocket.rs create mode 100644 src/libraries/sh-common-server/Cargo.toml create mode 100644 src/libraries/sh-common-server/src/lib.rs diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b52371c..d787cf1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -55,7 +55,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -73,7 +73,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Build armv7 - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: false @@ -95,7 +95,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -113,7 +113,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Build arm64 - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: false @@ -134,7 +134,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -152,7 +152,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Build amd64 - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: false @@ -172,7 +172,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -190,7 +190,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Build amd64 - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: false @@ -269,7 +269,7 @@ jobs: github.event.inputs.build_latest_as_test == '' ) runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: setup node uses: actions/setup-node@v4.0.2 @@ -298,7 +298,7 @@ jobs: # - name: install frontend dependencies # run: yarn install # change this to npm, pnpm or bun depending on which one you use. - - uses: tauri-apps/tauri-action@v0.5.5 + - uses: tauri-apps/tauri-action@v0.5.6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/markdownlint.yml b/.github/workflows/markdownlint.yml index 225ff0a..5573ac2 100644 --- a/.github/workflows/markdownlint.yml +++ b/.github/workflows/markdownlint.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Pull markdownlint/markdownlint:latest Image run: docker pull markdownlint/markdownlint:latest - name: Run markdownlint against *.md files diff --git a/.github/workflows/on_pr.yaml b/.github/workflows/on_pr.yaml index a00d37d..585eb04 100644 --- a/.github/workflows/on_pr.yaml +++ b/.github/workflows/on_pr.yaml @@ -23,7 +23,7 @@ jobs: name: "Linting: hadolint" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Pull hadolint/hadolint:latest Image run: docker pull hadolint/hadolint:latest - name: Run hadolint against Dockerfiles @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -59,7 +59,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -77,7 +77,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Build armv7 - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: false @@ -99,7 +99,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -117,7 +117,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Build arm64 - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: false @@ -139,7 +139,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -157,7 +157,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Build amd64 - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: false @@ -178,7 +178,7 @@ jobs: needs: [test_rust_functionality] steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -196,7 +196,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Build amd64 - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: false @@ -283,7 +283,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: setup node uses: actions/setup-node@v4.0.2 @@ -312,7 +312,7 @@ jobs: # - name: install frontend dependencies # run: yarn install # change this to npm, pnpm or bun depending on which one you use. - - uses: tauri-apps/tauri-action@v0.5.5 + - uses: tauri-apps/tauri-action@v0.5.6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/pre-commit-updates.yaml b/.github/workflows/pre-commit-updates.yaml index c92a984..bfc1ca8 100644 --- a/.github/workflows/pre-commit-updates.yaml +++ b/.github/workflows/pre-commit-updates.yaml @@ -9,7 +9,7 @@ jobs: update: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 with: fetch-depth: 0 - uses: vrslev/pre-commit-autoupdate@v1.0.0 diff --git a/.github/workflows/yamllint.yml b/.github/workflows/yamllint.yml index 784ea5c..13866b2 100644 --- a/.github/workflows/yamllint.yml +++ b/.github/workflows/yamllint.yml @@ -15,7 +15,7 @@ jobs: name: Run yamllint against YAML files runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: yaml-lint uses: ibiqlik/action-yamllint@v3.1.1 with: diff --git a/Cargo.lock b/Cargo.lock index a89de20..351248e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1026,7 +1026,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.13", + "toml 0.8.14", "vswhom", "winreg 0.52.0", ] @@ -1194,7 +1194,7 @@ dependencies = [ "atomic", "pear", "serde", - "toml 0.8.13", + "toml 0.8.14", "uncased", "version_check", ] @@ -3423,6 +3423,7 @@ dependencies = [ "log", "sh-api", "sh-common", + "sh-common-server", "sh-config", "tokio", ] @@ -3636,6 +3637,7 @@ dependencies = [ "serde", "serde_json", "sh-common", + "sh-common-server", "sh-config", "tokio", ] @@ -3644,12 +3646,21 @@ dependencies = [ name = "sh-common" version = "4.0.0-alpha.1" dependencies = [ - "async-trait", "serde", "serde_json", "sh-config", ] +[[package]] +name = "sh-common-server" +version = "4.0.0-alpha.1" +dependencies = [ + "async-trait", + "sh-common", + "sh-config", + "tokio", +] + [[package]] name = "sh-config" version = "4.0.0-alpha.1" @@ -3660,7 +3671,7 @@ dependencies = [ "sdre-rust-logging", "serde", "serde-inline-default", - "toml 0.8.13", + "toml 0.8.14", "void", ] @@ -3935,7 +3946,7 @@ dependencies = [ "cfg-expr 0.15.8", "heck 0.5.0", "pkg-config", - "toml 0.8.13", + "toml 0.8.14", "version-compare 0.2.0", ] @@ -4016,9 +4027,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tauri" -version = "1.6.7" +version = "1.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c7177b6be45bbb875aa239578f5adc982a1b3d5ea5b315ccd100aeb0043374" +checksum = "77567d2b3b74de4588d544147142d02297f3eaa171a25a065252141d8597a516" dependencies = [ "anyhow", "bytes", @@ -4431,14 +4442,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.13", + "toml_edit 0.22.14", ] [[package]] @@ -4476,9 +4487,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap 2.2.6", "serde", diff --git a/Cargo.toml b/Cargo.toml index af37a85..afa0d8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "src/libraries/sh-api", "src/libraries/sh-config", "src/libraries/sh-common", + "src/libraries/sh-common-server", ] exclude = ["sh-frontend"] diff --git a/Dockerfile.build_binary b/Dockerfile.build_binary index b527d8b..381b9c9 100644 --- a/Dockerfile.build_binary +++ b/Dockerfile.build_binary @@ -1,4 +1,4 @@ -FROM rust:1.78.0 as builder +FROM rust:1.79.0 as builder ENV CARGO_NET_GIT_FETCH_WITH_CLI=true WORKDIR /tmp/sdre-hub # hadolint ignore=DL3008,DL3003,SC1091,DL4006,DL3009 diff --git a/Dockerfile.build_frontend b/Dockerfile.build_frontend index 6d60405..bab14b1 100644 --- a/Dockerfile.build_frontend +++ b/Dockerfile.build_frontend @@ -1,4 +1,4 @@ -FROM rust:1.78.0 as builder +FROM rust:1.79.0 as builder WORKDIR /tmp/sdre-hub # hadolint ignore=DL3008,DL3003,SC1091,DL4006,DL3009 RUN set -x && \ diff --git a/Dockerfile.local b/Dockerfile.local index 54f50a2..e35ba33 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -1,4 +1,4 @@ -FROM rust:1.78.0 as builder +FROM rust:1.79.0 as builder WORKDIR /tmp/sdre-hub # hadolint ignore=DL3008,DL3003,SC1091,DL4006 RUN set -x && \ diff --git a/README.md b/README.md index 227164e..e3f35b4 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Sorry, there is no migration path from ACARS Hub to SDR-E Hub. The data that ACA - [x] Application build - [ ] Manage settings from the web interface - [x] GitHub CI + - [ ] Custom Error handling - [ ] FIXME/TODO Cleanup - [ ] Code documentation - [ ] Unit Tests diff --git a/sh-frontend/Cargo.lock b/sh-frontend/Cargo.lock index 69ac98a..2c1b35d 100644 --- a/sh-frontend/Cargo.lock +++ b/sh-frontend/Cargo.lock @@ -108,17 +108,6 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" -[[package]] -name = "async-trait" -version = "0.1.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.60", -] - [[package]] name = "atomic" version = "0.6.0" @@ -913,6 +902,12 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1472,7 +1467,6 @@ dependencies = [ name = "sh-common" version = "4.0.0-alpha.1" dependencies = [ - "async-trait", "serde", "serde_json", "sh-config", @@ -1500,6 +1494,7 @@ dependencies = [ "futures", "gloo 0.11.0", "gloo-utils 0.2.0", + "heck", "leaflet", "log", "reqwasm", @@ -1514,7 +1509,6 @@ dependencies = [ "wasm-logger", "web-sys", "yew", - "yew-alert", "yew-hooks", "yew-router", "yew-websocket", @@ -1594,9 +1588,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "pin-project-lite", @@ -1615,14 +1609,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.13", + "toml_edit 0.22.14", ] [[package]] @@ -1647,9 +1641,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", @@ -2071,17 +2065,6 @@ dependencies = [ "yew-macro", ] -[[package]] -name = "yew-alert" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4e321496da4fe631ad3985b2d89e35e1cb4a9ce276bbd89f64721ee630b078" -dependencies = [ - "gloo 0.11.0", - "web-sys", - "yew", -] - [[package]] name = "yew-hooks" version = "0.3.2" diff --git a/sh-frontend/Cargo.toml b/sh-frontend/Cargo.toml index 7fda0d3..d52fbb7 100644 --- a/sh-frontend/Cargo.toml +++ b/sh-frontend/Cargo.toml @@ -11,27 +11,27 @@ readme = "README.MD" license = "MIT" rust-version = "1.78.0" keywords = [ - "acars", - "vdlm", - "vdlm2", - "vdl-m2", - "hfdl", - "adsb", - "docker", - "web", - "sdr", - "rtl-sdr", - "aircraft", - "airplane", - "airline", - "flight", - "tracking", - "hub", - "server", - "client", - "frontend", - "backend", - "api", + "acars", + "vdlm", + "vdlm2", + "vdl-m2", + "hfdl", + "adsb", + "docker", + "web", + "sdr", + "rtl-sdr", + "aircraft", + "airplane", + "airline", + "flight", + "tracking", + "hub", + "server", + "client", + "frontend", + "backend", + "api", ] categories = ["aerospace"] resolver = "2" @@ -43,6 +43,7 @@ anyhow = "1.0.86" futures = "0.3.30" gloo = "0.11.0" gloo-utils = "0.2.0" +heck = "0.5.0" leaflet = "0.4.1" log = "0.4.21" serde = { version = "1.0.203", features = ["derive"] } @@ -50,7 +51,6 @@ serde_derive = "1.0.203" serde_json = "1.0.117" reqwasm = "0.5.0" yew = { version = "0.21.0", features = ["csr"] } -yew-alert = "0.1.0" yewdux = "0.10.0" yew-websocket = "1.21.0" yew-router = "0.18.0" @@ -59,12 +59,12 @@ wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.42" wasm-logger = "0.2.0" web-sys = { version = "0.3.69", features = [ - "Document", - "Element", - "HtmlCollection", - "KeyboardEvent", - "EventTarget", - "HtmlFormElement", + "Document", + "Element", + "HtmlCollection", + "KeyboardEvent", + "EventTarget", + "HtmlFormElement", ] } sh-common = { path = "../src/libraries/sh-common" } diff --git a/sh-frontend/Trunk.toml b/sh-frontend/Trunk.toml index ebd5e2c..fb33f55 100644 --- a/sh-frontend/Trunk.toml +++ b/sh-frontend/Trunk.toml @@ -6,4 +6,5 @@ dist = "dist" # FIXME: hard coding this Feels Bad Man. As of 4/22/24, the latest version of wasm-opt is 117 but # fucking trunk hard codes 116 as the default. This should be fine but the docker build fails on # macs because.....*****reasons***** +# https://github.com/trunk-rs/trunk/blob/c160ed3ff7c98a94ab00f14acbbd198268a92525/Trunk.toml#L67 wasm_opt = "version_117" diff --git a/sh-frontend/css/components/_navbar.scss b/sh-frontend/css/components/_navbar.scss index 710da75..f91a63d 100644 --- a/sh-frontend/css/components/_navbar.scss +++ b/sh-frontend/css/components/_navbar.scss @@ -28,7 +28,7 @@ $menu-offset: 2rem; /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ color: colors.$text-color; padding: config.$double-padding; - z-index: 1000; + z-index: 9; border-radius: config.$border-radius; .menu-right { diff --git a/sh-frontend/css/components/_settings.scss b/sh-frontend/css/components/_settings.scss index f0ba524..cc2081c 100644 --- a/sh-frontend/css/components/_settings.scss +++ b/sh-frontend/css/components/_settings.scss @@ -105,8 +105,8 @@ padding-left: config.$normal-padding; } - input:read-only, - select:disabled { + .data-quantity input:read-only, + .data-quantity select:disabled { background: colors.$grey; } @@ -115,6 +115,98 @@ padding-left: config.$normal-padding; } + .data-quantity { + position: relative; + padding: 0; + margin: 0; + border: 0; + } + + .data-quantity legend { + display: none; + } + + .data-quantity input, + .data-quantity select { + font-size: 18px; + height: 100%; + padding: 0rem; + border-radius: 2rem; + border: 0; + background: #fff; + color: #222; + box-shadow: 0 10px 65px -10px rgba(0, 0, 0, 0.25); + text-align: center; + width: 100%; + box-sizing: border-box; + } + + .data-quantity select, + .data-quantity select option { + text-transform: capitalize; + } + + .data-quantity input:focus { + outline: none; + box-shadow: 0 5px 55px -10px rgba(0, 0, 0, 0.2), 0 0 4px #3fb0ff; /* Allows border radius on focus */ + } + + .data-quantity input[type=number]::-webkit-inner-spin-button, + .data-quantity input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .data-quantity input[type=number] { + -moz-appearance: textfield; + appearance: textfield; + } + + .data-quantity button { + position: absolute; + width: 3.0rem; + height: 100%; + top: 0rem; + display: block; + padding: 0; + margin: 0; + border: 0; + background: #fff + url('data:image/svg+xml;utf8,') + no-repeat 0 0; + background-position-y: -0.5rem; + background-size: 5.6rem 2.8rem; + overflow: hidden; + white-space: nowrap; + text-indent: 100%; + border-radius: 1.4rem; + cursor: pointer; + transition: opacity 0.15s; + opacity: 0.5; + } + + .data-quantity button:active { + background-position-y: 1px; + box-shadow: inset 0 2px 12px -4px #c5d1d9; + } + + .data-quantity button:focus { + outline: none; + } + + .data-quantity button:hover { + opacity: 1; + } + + .data-quantity button.sub { + left: 0.0rem; + } + + .data-quantity button.add { + right: 0.6rem; + background-position-x: -1.8rem; + } + .settings-item { display: flex; flex-direction: row; @@ -144,6 +236,7 @@ & > *:nth-child(2) { width: 100%; + max-width: 20rem; } & > *:only-child { diff --git a/sh-frontend/css/utility/_alerts.scss b/sh-frontend/css/utility/_alerts.scss index 9371fc4..6bab204 100644 --- a/sh-frontend/css/utility/_alerts.scss +++ b/sh-frontend/css/utility/_alerts.scss @@ -18,6 +18,22 @@ max-width: 400px; width: fit-content; height: fit-content; + position: absolute; + align-items: center; +} + +.alert-notification { + text-align: center; + color: colors.$text-color; + background-color: colors.$sdre-yellow; + border-radius: config.$border-radius; + border-width: config.$border-size; + border-color: colors.$sdre-green; + padding: config.$double-padding; + min-width: 250px; + max-width: 400px; + width: fit-content; + height: fit-content; } .alert-icon { diff --git a/sh-frontend/css/utility/_base.scss b/sh-frontend/css/utility/_base.scss index 64af0f8..1155da7 100644 --- a/sh-frontend/css/utility/_base.scss +++ b/sh-frontend/css/utility/_base.scss @@ -8,14 +8,16 @@ html, body { - height: 100%; - width: 100%; - max-width: 100%; - max-height: 100%; + height: 100vh; + width: 100vw; + max-width: 100vw; + max-height: 100vh; overflow: hidden; color: colors.$text-color; background-color: colors.$background-color; font-size: config.$text-size; + margin: 0; + padding: 0; } // to make the map fill the screen diff --git a/sh-frontend/src/app/webapp.rs b/sh-frontend/src/app/webapp.rs index d5d18ab..5781fe8 100644 --- a/sh-frontend/src/app/webapp.rs +++ b/sh-frontend/src/app/webapp.rs @@ -3,24 +3,312 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use crate::components::layout_components::footer::Footer; -use crate::components::layout_components::live::Live; -use crate::components::layout_components::nav::Nav; -use crate::services::websocket::ShWebSocketComponent; +use crate::common::alert_boxes::AlertBoxToShow; +use crate::components::alerts::error::ShAlertErrorBox; +use crate::components::alerts::AlertPropsTrait; +use crate::components::alerts::{AlertType, ShAlert}; +use crate::components::layout::footer::Footer; +use crate::components::layout::live::Live; +use crate::components::layout::nav::Nav; +use crate::services::temp_state::WebAppStateTemp; +use anyhow::Error; +use sh_common::{ + MessageData, ServerMessageTypes, ServerWssMessage, UserMessageTypes, UserWssMessage, +}; use yew::prelude::*; +use yew_websocket::websocket::{WebSocketService, WebSocketStatus, WebSocketTask}; +use yewdux::Dispatch; -#[function_component(App)] -pub fn app() -> Html { - html! { - <> - -
-
- +// https://github.com/security-union/yew-websocket/ + +#[derive(Debug)] +pub enum WsAction { + Connect, + SendData(UserWssMessage), + Disconnect, + Lost, +} + +#[derive(Debug)] +pub enum Msg { + WsAction(WsAction), + WsReady(Result), + ShowAlert(AlertBoxToShow), + HideAlert, +} + +impl From for Msg { + fn from(action: WsAction) -> Self { + Self::WsAction(action) + } +} + +pub struct App { + pub fetching: bool, + pub ws: Option, + pub dispatch: Dispatch, + pub alert_box_type: AlertBoxToShow, +} + +impl App { + fn handle_wsaction_connect(&mut self, ctx: &Context) { + let callback = ctx.link().callback(Msg::WsReady); + let notification = ctx.link().batch_callback(|status| match status { + WebSocketStatus::Opened => { + let initial_message = + UserWssMessage::new(UserMessageTypes::UserRequestConfig, MessageData::NoData); + Some(WsAction::SendData(initial_message).into()) + } + WebSocketStatus::Closed | WebSocketStatus::Error => Some(WsAction::Lost.into()), + }); + let task = + WebSocketService::connect_text("ws://127.0.0.1:3000/sdre-hub", callback, notification) + .unwrap(); + self.ws = Some(task); + } + + fn handle_wsaction_send_data(&mut self, data: &UserWssMessage) { + log::debug!("Sending data: {:?}", data); + let serialized_data = serde_json::to_string(&data).unwrap(); + self.ws + .as_mut() + .unwrap() + .send(serde_json::to_string(&serialized_data).unwrap()); + + if !self.fetching { + self.fetching = true; + self.dispatch.reduce_mut(|state| { + state.websocket_connected = true; + }); + } + } + + fn handle_wsaction_disconnect(&mut self) { + self.ws.take(); + log::info!("WebSocket connection disconnected. Why? This should be unreachable"); + self.fetching = false; + self.dispatch.reduce_mut(|state| { + state.config = None; + state.websocket_connected = false; + }); + } + + fn handle_wsaction_lost(&mut self, ctx: &Context) { + self.ws = None; + log::error!("WebSocket connection lost. Reconnecting"); + self.fetching = false; + // reconnect + ctx.link().send_message(WsAction::Connect); + self.dispatch.reduce_mut(|state| { + state.config = None; + state.websocket_connected = false; + }); + } + + fn handle_wsaction_ready(&mut self, ctx: &Context, response: Result) { + log::debug!("Received data: {:?}", response); + + if response.is_err() { + log::error!("Error: {:?}", response.err().unwrap()); + return; + } + + let data = response.unwrap(); + // remove the first and last characters + + let data_deserialized: ServerWssMessage = match serde_json::from_str(&data) { + Ok(message) => message, + Err(e) => { + log::error!("Error deserializing message: {:?}", e); + return; + } + }; + + match data_deserialized.get_message_type() { + ServerMessageTypes::ServerResponseConfig => { + log::debug!("Received config message"); + self.dispatch + .reduce_mut(|state| match data_deserialized.get_data() { + MessageData::ShConfig(config) => { + state.config = Some(config.clone()); + } + _ => { + log::error!("Received invalid data type"); + } + }); + } + + ServerMessageTypes::ServerWriteConfigFailure => { + match data_deserialized.get_data() { + MessageData::ShConfigFailure(data) => { + log::error!("Failed to write config: {}", data); + } + _ => { + log::error!("Invalid response type"); + } + } + log::error!("Failed to write config"); + + // lets regrab the config + + let initial_message = + UserWssMessage::new(UserMessageTypes::UserRequestConfig, MessageData::NoData); + + ctx.link().send_message(WsAction::SendData(initial_message)); + + // show alert + ctx.link() + .send_message(Msg::ShowAlert(AlertBoxToShow::ConfigWriteFailure)); + } + + ServerMessageTypes::ServerWriteConfigSuccess => { + // see if there is any data + match data_deserialized.get_data() { + MessageData::ShConfigSuccess(data) => { + log::info!("Config written successfully: {}", data); + } + MessageData::ShConfigFailure(_) => { + log::error!("Invalid response type"); + } + _ => (), + } + log::info!("Config written successfully"); + + // lets regrab the config + + let initial_message = + UserWssMessage::new(UserMessageTypes::UserRequestConfig, MessageData::NoData); + + ctx.link().send_message(WsAction::SendData(initial_message)); + + // show alert + ctx.link() + .send_message(Msg::ShowAlert(AlertBoxToShow::ConfigWriteSuccess)); + } + } + } + + fn handle_msg_show_alert(&mut self, alert_box_type: &AlertBoxToShow) { + log::debug!("Showing alert box: {:?}", alert_box_type); + match alert_box_type { + AlertBoxToShow::ConfigWriteSuccess => { + self.alert_box_type = AlertBoxToShow::ConfigWriteSuccess; + } + AlertBoxToShow::ConfigWriteFailure => { + self.alert_box_type = AlertBoxToShow::ConfigWriteFailure; + } + AlertBoxToShow::UnsavedChanges => { + self.alert_box_type = AlertBoxToShow::UnsavedChanges; + } + AlertBoxToShow::None => { + self.alert_box_type = AlertBoxToShow::None; + } + } + } + + fn handle_msg_hide_alert(&mut self) { + self.alert_box_type = AlertBoxToShow::None; + } +} + +impl Component for App { + type Message = Msg; + type Properties = (); + + fn create(_ctx: &Context) -> Self { + let dispatch = Dispatch::::global(); + + Self { + fetching: false, + ws: None, + dispatch, + alert_box_type: AlertBoxToShow::None, + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::WsAction(action) => match action { + WsAction::Connect => { + self.handle_wsaction_connect(ctx); + false + } + WsAction::SendData(data) => { + self.handle_wsaction_send_data(&data); + false + } + WsAction::Disconnect => { + self.handle_wsaction_disconnect(); + false + } + WsAction::Lost => { + self.handle_wsaction_lost(ctx); + false + } + }, + Msg::WsReady(response) => { + self.handle_wsaction_ready(ctx, response); + false + } + + Msg::ShowAlert(alert_box_type) => { + self.handle_msg_show_alert(&alert_box_type); + true + } + + Msg::HideAlert => { + self.handle_msg_hide_alert(); + true + } + } + } + + fn view(&self, ctx: &Context) -> Html { + if self.ws.is_none() { + ctx.link().send_message(WsAction::Connect); + log::info!("Connecting to WebSocket"); + } + + let send_data_to_wss = ctx.link().callback(move |input: UserWssMessage| { + log::debug!("Got a message. Sending up the chain to the websocket!"); + Msg::WsAction(WsAction::SendData(input)) + }); + + let hide_alert_box = ctx.link().callback(|()| Msg::HideAlert); + let show_alert_box = ctx.link().callback(Msg::ShowAlert); + + html! { + <> +
+ { + match self.alert_box_type { + AlertBoxToShow::ConfigWriteSuccess => { + html! { + + } + } + AlertBoxToShow::ConfigWriteFailure => { + html! { + + } + } + AlertBoxToShow::UnsavedChanges => { + html! { + + } + } + AlertBoxToShow::None => { + html! {} + } + } + } +
+ + } } } diff --git a/sh-frontend/src/components/setting_components/mod.rs b/sh-frontend/src/common/alert_boxes.rs similarity index 55% rename from sh-frontend/src/components/setting_components/mod.rs rename to sh-frontend/src/common/alert_boxes.rs index 53c0f7e..c70beae 100644 --- a/sh-frontend/src/components/setting_components/mod.rs +++ b/sh-frontend/src/common/alert_boxes.rs @@ -3,7 +3,11 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -pub mod sh_app_config; -pub mod sh_data_sources; -pub mod sh_enabled_data_sources; -pub mod sh_map; +#[derive(Debug, Default)] +pub enum AlertBoxToShow { + ConfigWriteSuccess, + ConfigWriteFailure, + UnsavedChanges, + #[default] + None, +} diff --git a/sh-frontend/src/common/mod.rs b/sh-frontend/src/common/mod.rs index f8c1bcf..0cbb3d8 100644 --- a/sh-frontend/src/common/mod.rs +++ b/sh-frontend/src/common/mod.rs @@ -3,4 +3,6 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +pub mod alert_boxes; pub mod panels; +pub mod wssprops; diff --git a/sh-frontend/src/common/wssprops.rs b/sh-frontend/src/common/wssprops.rs new file mode 100644 index 0000000..741b3a6 --- /dev/null +++ b/sh-frontend/src/common/wssprops.rs @@ -0,0 +1,16 @@ +// Copyright (C) 2024 Fred Clausen +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use sh_common::UserWssMessage; +use yew::prelude::*; + +use super::alert_boxes::AlertBoxToShow; + +// FIXME: This should probably be renamed and refactored to a different file name +#[derive(Properties, Clone, PartialEq)] +pub struct WssCommunicationProps { + pub send_message: Callback, + pub request_alert_box: Callback, +} diff --git a/sh-frontend/src/components/alerts/alert_error.rs b/sh-frontend/src/components/alerts/alert_error.rs deleted file mode 100644 index b9556d7..0000000 --- a/sh-frontend/src/components/alerts/alert_error.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (C) 2024 Fred Clausen -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -use yew::prelude::*; -use yew_alert::Alert; - -#[derive(Clone, PartialEq, Properties)] -pub struct AlertErrorProps { - #[prop_or_default] - pub message: &'static str, - #[prop_or_default] - pub title: &'static str, - pub show_alert: UseStateHandle, - #[prop_or(Callback::noop())] - pub on_confirm: Callback<()>, - #[prop_or(Callback::noop())] - pub on_cancel: Callback<()>, - #[prop_or(5000)] - pub timeout: u32, - #[prop_or("button")] - pub cancel_button_class: &'static str, - #[prop_or("button")] - pub confirm_button_class: &'static str, - #[prop_or_default] - pub show_cancel_button: bool, - #[prop_or(true)] - pub show_confirm_button: bool, -} - -// FIXME: Before ridding of tailwind the "position" part of this prop needs us to implement some more CSS classes. See: https://github.com/next-rs/yew-alert/blob/37da6d37d10cb32dc778d4f7a642800eb8188175/src/lib.rs#L233 - -#[function_component(AlertError)] -pub fn alert_error(props: &AlertErrorProps) -> Html { - let show_alert = props.show_alert.clone(); - let on_confirm = props.on_confirm.clone(); - let on_cancel = props.on_cancel.clone(); - let show_cancel = props.show_cancel_button; - let show_confirm = props.show_confirm_button; - let title = props.title; - let message = props.message; - let timeout = props.timeout; - let cancel_button_class = props.cancel_button_class; - let confirm_button_class = props.confirm_button_class; - - html! { - - } -} diff --git a/sh-frontend/src/components/alerts/base.rs b/sh-frontend/src/components/alerts/base.rs new file mode 100644 index 0000000..81ec3b9 --- /dev/null +++ b/sh-frontend/src/components/alerts/base.rs @@ -0,0 +1,348 @@ +// Copyright (C) 2024 Fred Clausen +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +// https://github.com/next-rs/yew-alert + +use gloo::timers::callback::Timeout; +use yew::prelude::*; + +const TITLE: &str = "Info"; +const ALERT_CLASS: &str = "w-96 h-48 text-white"; +const ICON_CLASS: &str = "flex justify-center"; +const CONFIRM_BUTTON_TEXT: &str = "Okay"; +const CANCEL_BUTTON_TEXT: &str = "Cancel"; +const CONFIRM_BUTTON_CLASS: &str = + "mt-2 mx-2 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 focus:outline-none focus:border-blue-700 focus:ring focus:ring-blue-200"; +const CANCEL_BUTTON_CLASS: &str = + "mt-2 mx-2 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 focus:outline-none focus:border-gray-700 focus:ring focus:ring-gray-200"; +const CONTAINER_CLASS: &str = + "flex items-center text-center justify-center bg-gray-800 text-white border border-gray-600"; +const TITLE_CLASS: &str = "dark:text-white"; +const MESSAGE_CLASS: &str = "dark:text-gray-300"; +const ICON_COLOR: &str = ""; +const ICON_WIDTH: &str = "50"; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub enum Position { + TopLeft, + TopRight, + Middle, + Bottom, + Top, + #[default] + BottomRight, + BottomLeft, +} + +impl Position { + #[must_use] + pub const fn get_class(&self) -> &'static str { + match self { + Self::TopLeft => "top-0 left-0", + Self::TopRight => "top-0 right-0", + Self::Middle => "top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2", + Self::Bottom => "bottom-0 left-1/2 transform -translate-x-1/2", + Self::Top => "top-0 left-1/2 transform -translate-x-1/2", + Self::BottomRight => "bottom-0 right-0", + Self::BottomLeft => "bottom-0 left-0", + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Default)] +pub enum IconType { + Warning, + Error, + Success, + Info, + #[default] + Question, +} + +impl IconType { + #[must_use] + pub const fn get_default_color(&self) -> &'static str { + match self { + Self::Warning => "#CC5500", // Burnt Orange + Self::Error => "#EE4B2B", // Bright Red + Self::Success => "lightgreen", + Self::Info => "lightblue", + Self::Question => "lightgray", + } + } + + pub fn get_icon(&self, icon_width: &'static str, icon_color: &'static str) -> Html { + match self { + Self::Warning => { + html! { + + + + } + } + Self::Error => { + html! { + + + + } + } + Self::Success => { + html! { + + + + } + } + Self::Info => { + html! { + + + + + } + } + Self::Question => { + html! { + + + + + } + } + } + } +} + +/// Props for the Alert component. +#[derive(Debug, PartialEq, Properties, Clone)] +pub struct AlertProps { + /// The message to be displayed in the alert. + #[prop_or_default] + pub message: &'static str, + + /// State handle to control the visibility of the alert. + pub show_alert: UseStateHandle, + + /// Time duration in milliseconds before the alert automatically closes. + #[prop_or(2500)] + pub timeout: u32, + + /// The title of the alert. + #[prop_or(TITLE)] + pub title: &'static str, + + /// CSS class for styling the alert. + #[prop_or(ALERT_CLASS)] + pub alert_class: &'static str, + + /// CSS class for styling the icon in the alert. + #[prop_or(ICON_CLASS)] + pub icon_class: &'static str, + + /// Text for the confirm button in the alert. + #[prop_or(CONFIRM_BUTTON_TEXT)] + pub confirm_button_text: &'static str, + + /// Text for the cancel button in the alert. + #[prop_or(CANCEL_BUTTON_TEXT)] + pub cancel_button_text: &'static str, + + /// CSS class for styling the confirm button. + #[prop_or(CONFIRM_BUTTON_CLASS)] + pub confirm_button_class: &'static str, + + /// CSS class for styling the cancel button. + #[prop_or(CANCEL_BUTTON_CLASS)] + pub cancel_button_class: &'static str, + + /// Flag to determine if the confirm button should be displayed. + #[prop_or(true)] + pub show_confirm_button: bool, + + /// Flag to determine if the cancel button should be displayed. + #[prop_or(true)] + pub show_cancel_button: bool, + + /// Flag to determine if the close button should be displayed. + #[prop_or(false)] + pub show_close_button: bool, + + /// Callback function triggered on confirm button click. + #[prop_or_default] + pub on_confirm: Callback<()>, + + /// Callback function triggered on cancel button click. + #[prop_or_default] + pub on_cancel: Callback<()>, + + /// Position of the alert on the screen (e.g., "top-left", "middle", "bottom-right"). + #[prop_or_default] + pub position: Position, + + /// CSS class for styling the alert container. + #[prop_or(CONTAINER_CLASS)] + pub container_class: &'static str, + + /// CSS class for styling the title in the alert. + #[prop_or(TITLE_CLASS)] + pub title_class: &'static str, + + /// CSS class for styling the message in the alert. + #[prop_or(MESSAGE_CLASS)] + pub message_class: &'static str, + + /// Type of the icon to be displayed in the alert (e.g., "warning", "error", "success"). + #[prop_or_default] + pub icon_type: IconType, + + /// Color of the icon in the alert. + #[prop_or(ICON_COLOR)] + pub icon_color: &'static str, + + /// Width of the icon in the alert. + #[prop_or(ICON_WIDTH)] + pub icon_width: &'static str, +} + +/// Alert Component +#[function_component] +pub fn Alert(props: &AlertProps) -> Html { + log::debug!("Rendering Alert component"); + + let show = *props.show_alert; + let timeout = props.timeout; + let show_alert = props.show_alert.clone(); + + use_effect_with(show_alert.clone(), move |show_alert| { + if **show_alert { + let show_alert = show_alert.clone(); + let handle = Timeout::new(timeout, move || show_alert.set(false)).forget(); + let clear_handle = move || { + web_sys::Window::clear_timeout_with_handle( + &web_sys::window().unwrap(), + handle.as_f64().unwrap() as i32, + ); + }; + + Box::new(clear_handle) as Box + } else { + Box::new(|| {}) as Box + } + }); + + let on_cancel = { + let on_cancel = props.on_cancel.clone(); + + Callback::from(move |_| { + on_cancel.emit(()); + show_alert.set(false); + }) + }; + let on_confirm = { + let on_confirm = props.on_confirm.clone(); + + Callback::from(move |_| { + on_confirm.emit(()); + }) + }; + + let position_class = props.position.get_class(); + + let icon_color = if props.icon_color.is_empty() { + props.icon_type.get_default_color() + } else { + props.icon_color + }; + + // SVGs taken from: https://fontawesome.com/icons + let icon_path = props.icon_type.get_icon(props.icon_width, icon_color); + + html! { + if show { +
+
+ { if props.show_close_button { + html! { + + } + } else { + html! {} + } } +
{ icon_path }
+ { props.title } +
+

{ props.message }

+ { if props.show_confirm_button { + html! { + + } + } else { + html! {} + } } + { if props.show_cancel_button { + html! { + + } + } else { + html! {} + } } +
+
+ } + } +} diff --git a/sh-frontend/src/components/alerts/error.rs b/sh-frontend/src/components/alerts/error.rs new file mode 100644 index 0000000..ea283fc --- /dev/null +++ b/sh-frontend/src/components/alerts/error.rs @@ -0,0 +1,52 @@ +// Copyright (C) 2024 Fred Clausen +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use super::base::{IconType, Position}; +use super::AlertPropsTrait; + +#[derive(Default, Clone, PartialEq, Eq)] +pub struct ShAlertErrorBox {} + +impl AlertPropsTrait for ShAlertErrorBox { + fn new() -> Self { + Self {} + } + + fn get_position(&self) -> Position { + Position::Middle + } + + fn get_icon_type(&self) -> IconType { + IconType::Error + } + + fn get_alert_class(&self) -> &'static str { + "alert-error top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" + } + + fn get_title_class(&self) -> &'static str { + "text-background-color" + } + + fn get_message_class(&self) -> &'static str { + "text-background-color" + } + + fn get_icon_class(&self) -> &'static str { + "alert-icon" + } + + fn get_confirm_button_text(&self) -> &'static str { + "Dismiss" + } + + fn get_cancel_button_text(&self) -> &'static str { + "Cancel" + } + + fn get_icon_color(&self) -> &'static str { + "text-background-color" + } +} diff --git a/sh-frontend/src/components/alerts/mod.rs b/sh-frontend/src/components/alerts/mod.rs index 90e3dd9..3dcdd95 100644 --- a/sh-frontend/src/components/alerts/mod.rs +++ b/sh-frontend/src/components/alerts/mod.rs @@ -5,4 +5,197 @@ // https://github.com/next-rs/yew-alert -pub mod alert_error; +pub mod base; +pub mod error; +pub mod notice; + +use crate::components::alerts::base::Alert; +use base::IconType; +use base::Position; +use error::ShAlertErrorBox; +use notice::ShAlertNoticeBox; +use yew::prelude::*; + +// FIXME: Before ridding of tailwind the "position" part of this prop needs us to implement some more CSS classes. See: https://github.com/next-rs/yew-alert/blob/37da6d37d10cb32dc778d4f7a642800eb8188175/src/lib.rs#L233 + +#[derive(Clone, PartialEq, Eq)] +pub enum AlertType { + Error(ShAlertErrorBox), + Notice(ShAlertNoticeBox), +} + +impl Default for AlertType { + fn default() -> Self { + Self::Notice(ShAlertNoticeBox::new()) + } +} + +impl AlertPropsTrait for AlertType { + fn new() -> Self { + Self::Notice(ShAlertNoticeBox::new()) + } + + fn get_position(&self) -> Position { + match self { + Self::Error(details) => details.get_position(), + Self::Notice(details) => details.get_position(), + } + } + + fn get_icon_type(&self) -> IconType { + match self { + Self::Error(details) => details.get_icon_type(), + Self::Notice(details) => details.get_icon_type(), + } + } + + fn get_alert_class(&self) -> &'static str { + match self { + Self::Error(details) => details.get_alert_class(), + Self::Notice(details) => details.get_alert_class(), + } + } + + fn get_title_class(&self) -> &'static str { + match self { + Self::Error(details) => details.get_title_class(), + Self::Notice(details) => details.get_title_class(), + } + } + + fn get_message_class(&self) -> &'static str { + match self { + Self::Error(details) => details.get_message_class(), + Self::Notice(details) => details.get_message_class(), + } + } + + fn get_icon_class(&self) -> &'static str { + match self { + Self::Error(details) => details.get_icon_class(), + Self::Notice(details) => details.get_icon_class(), + } + } + + fn get_confirm_button_text(&self) -> &'static str { + match self { + Self::Error(details) => details.get_confirm_button_text(), + Self::Notice(details) => details.get_confirm_button_text(), + } + } + + fn get_cancel_button_text(&self) -> &'static str { + match self { + Self::Error(details) => details.get_cancel_button_text(), + Self::Notice(details) => details.get_cancel_button_text(), + } + } + + fn get_icon_color(&self) -> &'static str { + match self { + Self::Error(details) => details.get_icon_color(), + Self::Notice(details) => details.get_icon_color(), + } + } +} + +pub trait AlertPropsTrait { + fn new() -> Self; + fn get_position(&self) -> Position; + fn get_icon_type(&self) -> IconType; + fn get_alert_class(&self) -> &'static str; + fn get_title_class(&self) -> &'static str; + fn get_message_class(&self) -> &'static str; + fn get_icon_class(&self) -> &'static str; + fn get_confirm_button_text(&self) -> &'static str; + fn get_cancel_button_text(&self) -> &'static str; + fn get_icon_color(&self) -> &'static str; +} + +#[derive(Clone, PartialEq, Properties)] +pub struct ShAlertProps { + #[prop_or_default] + pub alert_type: AlertType, + #[prop_or_default] + pub message: &'static str, + #[prop_or_default] + pub title: &'static str, + pub show_alert: bool, + #[prop_or(Callback::noop())] + pub on_confirm: Callback<()>, + #[prop_or(Callback::noop())] + pub on_cancel: Callback<()>, + #[prop_or(5000)] + pub timeout: u32, + #[prop_or("button")] + pub cancel_button_class: &'static str, + #[prop_or("button")] + pub confirm_button_class: &'static str, + #[prop_or_default] + pub show_cancel_button: bool, + #[prop_or(true)] + pub show_confirm_button: bool, +} + +#[function_component(ShAlert)] +pub fn sh_alert(props: &ShAlertProps) -> Html { + log::debug!("Rendering ShAlert"); + + let show_alert_as_state = use_state_eq(|| props.show_alert); + let on_confirm = props.on_confirm.clone(); + let on_cancel = props.on_cancel.clone(); + let show_cancel = props.show_cancel_button; + let show_confirm = props.show_confirm_button; + let title = props.title; + let message = props.message; + let cancel_button_class = props.cancel_button_class; + let confirm_button_class = props.confirm_button_class; + let timeout = props.timeout; + let new_show_alert = show_alert_as_state.clone(); + + let alert_type = props.alert_type.clone(); + let position = alert_type.get_position(); + let icon_type = alert_type.get_icon_type(); + let alert_class = alert_type.get_alert_class(); + let title_class = alert_type.get_title_class(); + let message_class = alert_type.get_message_class(); + let icon_class = alert_type.get_icon_class(); + let confirm_button_text = alert_type.get_confirm_button_text(); + let cancel_button_text = alert_type.get_cancel_button_text(); + let icon_color = alert_type.get_icon_color(); + + use_effect_with(props.show_alert, move |show_alert| { + show_alert_as_state.set(*show_alert); + }); + + if *new_show_alert != props.show_alert { + on_confirm.emit(()); + } + + html! { + + } +} diff --git a/sh-frontend/src/components/alerts/notice.rs b/sh-frontend/src/components/alerts/notice.rs new file mode 100644 index 0000000..5e9d6e3 --- /dev/null +++ b/sh-frontend/src/components/alerts/notice.rs @@ -0,0 +1,52 @@ +// Copyright (C) 2024 Fred Clausen +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use super::base::{IconType, Position}; +use super::AlertPropsTrait; + +#[derive(Default, Clone, Eq, PartialEq)] +pub struct ShAlertNoticeBox {} + +impl AlertPropsTrait for ShAlertNoticeBox { + fn new() -> Self { + Self {} + } + + fn get_position(&self) -> Position { + Position::BottomRight + } + + fn get_icon_type(&self) -> IconType { + IconType::Info + } + + fn get_alert_class(&self) -> &'static str { + "alert-notification bottom-0 right-0" + } + + fn get_title_class(&self) -> &'static str { + "text-background-color" + } + + fn get_message_class(&self) -> &'static str { + "text-background-color" + } + + fn get_icon_class(&self) -> &'static str { + "alert-icon" + } + + fn get_confirm_button_text(&self) -> &'static str { + "Dismiss" + } + + fn get_cancel_button_text(&self) -> &'static str { + "Cancel" + } + + fn get_icon_color(&self) -> &'static str { + "text-background-color" + } +} diff --git a/sh-frontend/src/components/footer_components/connected.rs b/sh-frontend/src/components/footer/connected.rs similarity index 90% rename from sh-frontend/src/components/footer_components/connected.rs rename to sh-frontend/src/components/footer/connected.rs index 7c8f795..028ec3f 100644 --- a/sh-frontend/src/components/footer_components/connected.rs +++ b/sh-frontend/src/components/footer/connected.rs @@ -9,7 +9,9 @@ use yewdux::prelude::*; #[function_component(Connected)] pub fn connected() -> Html { - let connected = use_selector(|state: &WebAppStateTemp| state.websocket_connected.clone()); + log::debug!("Connected component rendered"); + + let connected = use_selector(|state: &WebAppStateTemp| state.websocket_connected); html! {
{ diff --git a/sh-frontend/src/components/footer_components/mod.rs b/sh-frontend/src/components/footer/mod.rs similarity index 100% rename from sh-frontend/src/components/footer_components/mod.rs rename to sh-frontend/src/components/footer/mod.rs diff --git a/sh-frontend/src/components/input/field.rs b/sh-frontend/src/components/input/field.rs new file mode 100644 index 0000000..4a086e9 --- /dev/null +++ b/sh-frontend/src/components/input/field.rs @@ -0,0 +1,296 @@ +// Copyright (C) 2024 Fred Clausen +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use heck::ToTitleCase; +use std::{f64, fmt::Display}; +use wasm_bindgen::JsCast; +use web_sys::HtmlInputElement; +use yew::prelude::*; + +#[derive(Clone, PartialEq, Eq)] +pub enum InputFieldType { + Text, + Number, + Select, +} + +#[derive(Clone, PartialEq, Eq)] +pub enum CoordinateType { + Latitude, + Longitude, +} + +impl Display for InputFieldType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text => write!(f, "text"), + Self::Select => write!(f, "select"), + Self::Number => write!(f, "number"), + } + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct NumberProperties { + pub max: String, + pub min: String, + pub step: String, + pub value: String, +} + +impl NumberProperties { + #[must_use] + pub fn new(coordinate_type: &CoordinateType, initial_value: String) -> Self { + let max = match coordinate_type { + CoordinateType::Latitude => "90", + CoordinateType::Longitude => "180", + } + .to_string(); + + let min = match coordinate_type { + CoordinateType::Latitude => "-90", + CoordinateType::Longitude => "-180", + } + .to_string(); + + Self { + max, + min, + step: "0.00001".to_string(), + value: initial_value, + } + } +} + +impl Default for NumberProperties { + fn default() -> Self { + Self { + max: "180".to_string(), + min: "180".to_string(), + step: "0.00001".to_string(), + value: "0.0".to_string(), + } + } +} + +#[must_use] +pub fn make_sure_string_has_five_digits(value: &String) -> String { + let Ok(value) = value.parse::() else { + log::error!("Could not parse value as f64: {}", value); + return "0.0".to_string(); + }; + + format!("{value:.5}") +} + +#[derive(Clone, PartialEq, Properties)] +pub struct InputFieldProps { + pub input_value: String, + // pub on_cautious_change: Callback, + pub label: String, + pub field_type: InputFieldType, + #[prop_or_default] + pub select_options: Option>, + #[prop_or_default] + pub number_properties: Option, + pub name: String, + pub input_node_ref: NodeRef, + #[prop_or(false)] + pub read_only: bool, +} + +#[function_component(InputField)] +pub fn input_field(props: &InputFieldProps) -> Html { + log::debug!("Rendering input field"); + + let InputFieldProps { + input_value, + // on_cautious_change, + label, + field_type, + select_options, + name, + input_node_ref, + read_only, + number_properties, + } = props; + + let onchange = { + // format the number to always have 5 decimal places + let field_type = field_type.clone(); + + Callback::from(move |event: Event| { + event.prevent_default(); + + let target = event + .target() + .unwrap() + .dyn_into::() + .unwrap(); + + let value = target.value(); + let value = match field_type { + InputFieldType::Number => make_sure_string_has_five_digits(&value), + _ => value, + }; + + target.set_value(&value); + }) + }; + + let on_increase = { + let input_node_ref = input_node_ref.clone(); + let number_properties = number_properties.clone(); + let step = number_properties + .as_ref() + .unwrap_or(&NumberProperties::default()) + .step + .clone() + .parse::() + .unwrap(); + + Callback::from(move |event: MouseEvent| { + event.prevent_default(); + + // get the value from input_node_ref + + let current_value = input_node_ref.cast::().unwrap().value(); + + let Ok(current_value) = current_value.parse::() else { + log::error!("Could not parse value as f64: {}", current_value); + return; + }; + + let new_value = current_value + step; + let formatted_new_value = make_sure_string_has_five_digits(&new_value.to_string()); + input_node_ref + .cast::() + .unwrap() + .set_value(&formatted_new_value); + }) + }; + + let on_decrease = { + let input_node_ref = input_node_ref.clone(); + let number_properties = number_properties.clone(); + let step = number_properties + .as_ref() + .unwrap_or(&NumberProperties::default()) + .step + .clone() + .parse::() + .unwrap(); + + Callback::from(move |event: MouseEvent| { + event.prevent_default(); + + // get the value from input_node_ref + + let current_value = input_node_ref.cast::().unwrap().value(); + + let Ok(current_value) = current_value.parse::() else { + log::error!("Could not parse value as f64: {}", current_value); + return; + }; + + let new_value = current_value - step; + let formatted_new_value = make_sure_string_has_five_digits(&new_value.to_string()); + input_node_ref + .cast::() + .unwrap() + .set_value(&formatted_new_value); + }) + }; + + html! { + <> +
+
+ { + match field_type { + InputFieldType::Text => { + if select_options.is_some() { + log::warn!("Select options were provided for a text field. Ignoring them."); + } + + html! { + + } + }, + InputFieldType::Number => { + if number_properties.is_none() { + log::error!("Number properties must be provided for a number field"); + + return html! { + { "Field type is number but no number properties were provided" } + } + } + + let number_properties = number_properties.as_ref().unwrap(); + + html! { + <> + + + + + } + }, + InputFieldType::Select => { + if select_options.is_none() { + log::error!("Select options must be provided for a select field"); + + return html! { + { "Field type is select but no select options were provided" } + } + } + + html! { + + } + } + } + } +
+ + } +} diff --git a/sh-frontend/src/components/input/input_field.rs b/sh-frontend/src/components/input/input_field.rs deleted file mode 100644 index 4f79097..0000000 --- a/sh-frontend/src/components/input/input_field.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (C) 2024 Fred Clausen -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -use yew::prelude::*; - -#[derive(Clone, PartialEq)] -pub enum InputFieldType { - Text, - Select, -} - -impl ToString for InputFieldType { - fn to_string(&self) -> String { - match self { - InputFieldType::Text => "text".to_string(), - InputFieldType::Select => "select".to_string(), - } - } -} - -#[derive(Clone, PartialEq, Properties)] -pub struct InputFieldProps { - pub input_value: String, - // pub on_cautious_change: Callback, - pub label: String, - pub field_type: InputFieldType, - pub select_options: Option>, - pub name: String, - pub input_node_ref: NodeRef, - #[prop_or(false)] - pub read_only: bool, -} - -#[function_component(InputField)] -pub fn input_field(props: &InputFieldProps) -> Html { - let InputFieldProps { - input_value, - // on_cautious_change, - label, - field_type, - select_options, - name, - input_node_ref, - read_only, - } = props; - - log::debug!("InputField: {:?}", input_value); - html! { - <> -
-
- { - match field_type { - InputFieldType::Text => { - if select_options.is_some() { - log::warn!("Select options were provided for a text field. Ignoring them."); - } - - html! { - - } - }, - InputFieldType::Select => { - if select_options.is_none() { - log::error!("Select options must be provided for a select field"); - - return html! { - { "Field type is select but no select options were provided" } - } - } - - html! { - - } - } - } - } -
- - } -} diff --git a/sh-frontend/src/components/input/mod.rs b/sh-frontend/src/components/input/mod.rs index 2a14ca3..b427700 100644 --- a/sh-frontend/src/components/input/mod.rs +++ b/sh-frontend/src/components/input/mod.rs @@ -3,4 +3,4 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -pub mod input_field; +pub mod field; diff --git a/sh-frontend/src/components/layout_components/footer.rs b/sh-frontend/src/components/layout/footer.rs similarity index 79% rename from sh-frontend/src/components/layout_components/footer.rs rename to sh-frontend/src/components/layout/footer.rs index ff56654..73a1c30 100644 --- a/sh-frontend/src/components/layout_components/footer.rs +++ b/sh-frontend/src/components/layout/footer.rs @@ -3,11 +3,13 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use crate::components::footer_components::connected::Connected; +use crate::components::footer::connected::Connected; use yew::prelude::*; #[function_component(Footer)] pub fn footer() -> Html { + log::debug!("Rendering footer."); + html! {