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 {
+
+ }
+ }
+}
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! {
+ <>
+
+ { label }
+
+ {
+ 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! {
+
+ {
+ select_options.as_ref().unwrap().iter().map(|option| {
+ let selected = option == input_value;
+ html! {
+ { option.to_title_case() }
+ }
+ }).collect::()
+ }
+
+ }
+ }
+ }
+ }
+
+ >
+ }
+}
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! {
- <>
-
- { label }
-
- {
- 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! {
-
- {
- select_options.as_ref().unwrap().iter().map(|option| {
- let selected = &option == &input_value;
- html! {
- { option }
- }
- }).collect::()
- }
-
- }
- }
- }
- }
-
- >
- }
-}
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! {