diff --git a/fly.toml b/fly.toml
index eb80183..74e3344 100644
--- a/fly.toml
+++ b/fly.toml
@@ -8,7 +8,7 @@ primary_region = "waw"
[http_service]
internal_port = 8080
- force_https = true
+ force_https = false
auto_stop_machines = false
auto_start_machines = true
diff --git a/web/.env.production b/web/.env.production
index 602ea08..bf69970 100644
--- a/web/.env.production
+++ b/web/.env.production
@@ -1 +1 @@
-VITE_API_HOST=https://weather-tool.fly.dev
\ No newline at end of file
+VITE_API_HOST=http://weather-tool.fly.dev
\ No newline at end of file
diff --git a/web/package-lock.json b/web/package-lock.json
index ccb841d..5a4a4da 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,14 +1,14 @@
{
- "name": "vite-template-solid",
+ "name": "weather-tool-web",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "vite-template-solid",
+ "name": "weather-tool-web",
"version": "0.0.0",
- "license": "MIT",
"dependencies": {
+ "moment": "^2.29.4",
"solid-js": "^1.6.10"
},
"devDependencies": {
@@ -1279,6 +1279,14 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
+ "node_modules/moment": {
+ "version": "2.29.4",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -2390,6 +2398,11 @@
"is-what": "^4.1.8"
}
},
+ "moment": {
+ "version": "2.29.4",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
+ },
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
diff --git a/web/package.json b/web/package.json
index 38ef122..86933af 100644
--- a/web/package.json
+++ b/web/package.json
@@ -14,6 +14,7 @@
"vite-plugin-solid": "^2.5.0"
},
"dependencies": {
+ "moment": "^2.29.4",
"solid-js": "^1.6.10"
}
}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 00cd43c..10b293d 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,24 +1,28 @@
-import type { Component } from 'solid-js';
+import { Component, createSignal } from 'solid-js';
+import { Aggregator } from './aggregator/Aggregator';
-import logo from './assets/logo.svg';
import styles from './css/App.module.css';
import { FileManager } from './fileManager/FileManager';
console.log("env:", import.meta.env.MODE);
console.log("api host:", import.meta.env.VITE_API_HOST);
+type Section = "aggregator" | "fileManager";
+
const App: Component = () => {
- return (
-
-
-
-
- );
+ const [getSection, setSection] = createSignal("aggregator");
+
+ const section = () => getSection() === "aggregator" ? : ;
+
+ return (
+
+
+ setSection("aggregator")}>aggregator |
+ setSection("fileManager")}>file manager
+
+ { section() }
+
+ );
};
export default App;
diff --git a/web/src/aggregator/Aggregator.tsx b/web/src/aggregator/Aggregator.tsx
new file mode 100644
index 0000000..a75f8e4
--- /dev/null
+++ b/web/src/aggregator/Aggregator.tsx
@@ -0,0 +1,53 @@
+import { createSignal } from "solid-js";
+import { Result } from "./Result";
+import { SelectAggregator } from "./SelectAggregator";
+import { SelectCity } from "./SelectCity";
+import { SelectTimeRange } from "./SelectTimeRange";
+
+const dayAgoMs = Date.now() - 24 * 60 * 60 * 1000;
+
+export function Aggregator() {
+ const [getCities, setCities] = createSignal>(new Set([]));
+ const [getStart, setStart] = createSignal(new Date(dayAgoMs));
+ const [getEnd, setEnd] = createSignal(new Date());
+ const [getField, setField] = createSignal("tempMax");
+ const [getKey, setKey] = createSignal("max");
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/aggregator/Result.tsx b/web/src/aggregator/Result.tsx
new file mode 100644
index 0000000..a00ab74
--- /dev/null
+++ b/web/src/aggregator/Result.tsx
@@ -0,0 +1,55 @@
+import moment from "moment";
+import { Accessor, Component, createResource, createSignal } from "solid-js";
+import { apiHost } from "../consts";
+
+export const Result: Component<{
+ getCities: Accessor>,
+ getStart: Accessor,
+ getEnd: Accessor,
+ getField: Accessor,
+ getKey: Accessor,
+}> = (props) => {
+ const cities = () => [...props.getCities()].join(",")
+ const startStr = () => moment(props.getStart()).format("YYYY/MM/DD, HH:mm");
+ const endStr = () => moment(props.getEnd()).format("YYYY/MM/DD, HH:mm");
+ const queryStart = () => moment(props.getStart()).format("YYYYMMDD_HHmm");
+ const queryEnd = () => moment(props.getEnd()).format("YYYYMMDD_HHmm");
+ const [getTimestamp, setTimestamp] = createSignal(0);
+
+ const fetchQuery = async (timestamp: number) => {
+ console.log("query:", new Date(timestamp));
+ if (cities() === "") return "ERROR: Select cities!";
+ await new Promise(resolve => setTimeout(resolve, 500))
+ const response = await fetch(`${apiHost}/api/query/${queryStart()}-${queryEnd()}/${cities()}/${props.getField()}/${props.getKey()}`);
+ const json = await response.json();
+ return json;
+ }
+
+ const [queryResource] = createResource(getTimestamp, fetchQuery);
+
+ return (
+
+
Aggregate by:
+
setTimestamp(Date.now())} />
+
cities: { cities() }
+
time range: { startStr() } - { endStr() }
+
weather field: { props.getField() }
+
aggregation field: { props.getKey() }
+
Result:
+
+ { queryResource.loading && (
+
+
+ Loading query
+
+ )}
+ { queryResource.error && (
+
Error while querying: ${queryResource.error}
+ )}
+ { queryResource() && (
+
{JSON.stringify(queryResource())}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/aggregator/SelectAggregator.tsx b/web/src/aggregator/SelectAggregator.tsx
new file mode 100644
index 0000000..9cdaea2
--- /dev/null
+++ b/web/src/aggregator/SelectAggregator.tsx
@@ -0,0 +1,34 @@
+import { Accessor, Component, Setter } from "solid-js";
+import { aggregateKey, weatherField } from "../consts";
+
+export const SelectAggregator: Component<{
+ getField: Accessor,
+ setField: Setter,
+ getKey: Accessor,
+ setKey: Setter,
+}> = (props) => {
+ return (
+
+
Select aggregator
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/aggregator/SelectCity.tsx b/web/src/aggregator/SelectCity.tsx
new file mode 100644
index 0000000..a5ad5fe
--- /dev/null
+++ b/web/src/aggregator/SelectCity.tsx
@@ -0,0 +1,34 @@
+import { Accessor, Component, Setter } from "solid-js";
+import { cityList } from "../consts";
+
+export const SelectCity: Component<{
+ getCities: Accessor>,
+ setCities: Setter>,
+}> = (props) => {
+ function handleSelect(e: MouseEvent) {
+ if (!e.target) return;
+ const target = e.target as HTMLInputElement;
+ const { checked: selected, value: city } = target;
+ const cityList = new Set([...props.getCities()]);
+ if (selected) cityList.add(city);
+ else cityList.delete(city);
+
+ props.setCities(cityList);
+ }
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/web/src/aggregator/SelectTimeRange.tsx b/web/src/aggregator/SelectTimeRange.tsx
new file mode 100644
index 0000000..7f0088f
--- /dev/null
+++ b/web/src/aggregator/SelectTimeRange.tsx
@@ -0,0 +1,32 @@
+import moment from 'moment';
+import { Accessor, Component, Setter } from "solid-js";
+
+export const SelectTimeRange: Component<{
+ getStart: Accessor,
+ setStart: Setter,
+ getEnd: Accessor,
+ setEnd: Setter,
+}> = (props) => {
+ const startStr = () => moment(props.getStart()).format("YYYY-MM-DDTHH:mm");
+ const endStr = () => moment(props.getEnd()).format("YYYY-MM-DDTHH:mm");
+
+ return (
+
+
Select time range
+
+ props.setStart(new Date(e.target.value))}
+ /> start
+
+
+ props.setEnd(new Date(e.target.value))}
+ /> end
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/consts.ts b/web/src/consts.ts
index 239a460..3564d3b 100644
--- a/web/src/consts.ts
+++ b/web/src/consts.ts
@@ -1 +1,7 @@
export const apiHost = import.meta.env.VITE_API_HOST;
+
+export const cityList = ["Ainaži", "Alūksne", "Bauska", "Dagda", "Daugavgrīva", "Daugavpils", "Dobele", "Gulbene", "Jelgava", "Kalnciems", "Kolka", "Kuldīga", "Lielpēči", "Liepāja", "Madona", "Mērsrags", "Pāvilosta", "Piedruja", "Priekuļi", "Rēzekne", "Rīga", "Rucava", "Rūjiena", "Saldus", "Sigulda", "Sīļi", "Skrīveri", "Skulte", "Stende", "Ventspils", "Vičaki", "Zīlāni", "Zosēni", "Ainaži", "Alūksne", "Bauska", "Dagda", "Daugavgrīva", "Daugavpils", "Dobele", "Gulbene", "Jelgava", "Kalnciems", "Kolka", "Kuldīga", "Lielpēči", "Liepāja", "Madona", "Mērsrags", "Pāvilosta", "Piedruja", "Priekuļi", "Rēzekne", "Rīga", "Rucava", "Rūjiena", "Saldus", "Sigulda", "Sīļi", "Skrīveri", "Skulte", "Stende", "Ventspils", "Vičaki", "Zīlāni", "Zosēni"];
+
+export const weatherField = ["tempMax", "tempMin", "tempAvg", "precipitation", "windAvg", "windMax", "visibilityMin", "visibilityAvg", "snowAvg", "atmPressire", "dewPoint", "humidity", "sunDuration", "phenomena"];
+
+export const aggregateKey = ["min", "max", "avg", "sum", "list", "distinct"];
diff --git a/web/src/css/App.module.css b/web/src/css/App.module.css
index 48308b2..d156436 100644
--- a/web/src/css/App.module.css
+++ b/web/src/css/App.module.css
@@ -23,6 +23,15 @@
color: #b318f0;
}
+.sections {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ padding-left: 20px;
+ line-height: 70px;
+ text-align: left;
+}
+
@keyframes logo-spin {
from {
transform: rotate(0deg);