Implement aggregation frontend, also http on prod
This commit is contained in:
@@ -8,7 +8,7 @@ primary_region = "waw"
|
|||||||
|
|
||||||
[http_service]
|
[http_service]
|
||||||
internal_port = 8080
|
internal_port = 8080
|
||||||
force_https = true
|
force_https = false
|
||||||
auto_stop_machines = false
|
auto_stop_machines = false
|
||||||
auto_start_machines = true
|
auto_start_machines = true
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
VITE_API_HOST=https://weather-tool.fly.dev
|
VITE_API_HOST=http://weather-tool.fly.dev
|
||||||
Generated
+16
-3
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-template-solid",
|
"name": "weather-tool-web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vite-template-solid",
|
"name": "weather-tool-web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"moment": "^2.29.4",
|
||||||
"solid-js": "^1.6.10"
|
"solid-js": "^1.6.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1279,6 +1279,14 @@
|
|||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
@@ -2390,6 +2398,11 @@
|
|||||||
"is-what": "^4.1.8"
|
"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": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"vite-plugin-solid": "^2.5.0"
|
"vite-plugin-solid": "^2.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"moment": "^2.29.4",
|
||||||
"solid-js": "^1.6.10"
|
"solid-js": "^1.6.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-13
@@ -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 styles from './css/App.module.css';
|
||||||
import { FileManager } from './fileManager/FileManager';
|
import { FileManager } from './fileManager/FileManager';
|
||||||
|
|
||||||
console.log("env:", import.meta.env.MODE);
|
console.log("env:", import.meta.env.MODE);
|
||||||
console.log("api host:", import.meta.env.VITE_API_HOST);
|
console.log("api host:", import.meta.env.VITE_API_HOST);
|
||||||
|
|
||||||
|
type Section = "aggregator" | "fileManager";
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
return (
|
const [getSection, setSection] = createSignal<Section>("aggregator");
|
||||||
<div class={styles.App}>
|
|
||||||
<FileManager />
|
const section = () => getSection() === "aggregator" ? <Aggregator /> : <FileManager />;
|
||||||
<header class={styles.header}>
|
|
||||||
<img src={logo} class={styles.logo} alt="logo" />
|
return (
|
||||||
<p>
|
<div class={styles.App}>
|
||||||
Weather tool web
|
<div class={styles.sections}>
|
||||||
</p>
|
<span onClick={() => setSection("aggregator")}>aggregator</span> |
|
||||||
</header>
|
<span onClick={() => setSection("fileManager")}>file manager</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
{ section() }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -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<Set<string>>(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 (
|
||||||
|
<div class="aggregator">
|
||||||
|
<h2>Aggregator</h2>
|
||||||
|
<div class="container">
|
||||||
|
<div class="column">
|
||||||
|
<SelectCity getCities={getCities} setCities={setCities} />
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div>
|
||||||
|
<SelectTimeRange
|
||||||
|
getStart={getStart}
|
||||||
|
setStart={setStart}
|
||||||
|
getEnd={getEnd}
|
||||||
|
setEnd={setEnd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<SelectAggregator
|
||||||
|
getField={getField}
|
||||||
|
setField={setField}
|
||||||
|
getKey={getKey}
|
||||||
|
setKey={setKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<Result
|
||||||
|
getCities={getCities}
|
||||||
|
getStart={getStart}
|
||||||
|
getEnd={getEnd}
|
||||||
|
getField={getField}
|
||||||
|
getKey={getKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<Set<string>>,
|
||||||
|
getStart: Accessor<Date>,
|
||||||
|
getEnd: Accessor<Date>,
|
||||||
|
getField: Accessor<string>,
|
||||||
|
getKey: Accessor<string>,
|
||||||
|
}> = (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<number>(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 (
|
||||||
|
<div>
|
||||||
|
<h3>Aggregate by:</h3>
|
||||||
|
<input type="button" value="Query Data" onClick={() => setTimestamp(Date.now())} />
|
||||||
|
<p>cities: { cities() }</p>
|
||||||
|
<p>time range: { startStr() } - { endStr() }</p>
|
||||||
|
<p>weather field: { props.getField() }</p>
|
||||||
|
<p>aggregation field: { props.getKey() }</p>
|
||||||
|
<h3>Result:</h3>
|
||||||
|
<p>
|
||||||
|
{ queryResource.loading && (
|
||||||
|
<div>
|
||||||
|
<span class="spinner"></span>
|
||||||
|
<span style={{ "padding-left": "16px" }}>Loading query</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ queryResource.error && (
|
||||||
|
<div>Error while querying: ${queryResource.error}</div>
|
||||||
|
)}
|
||||||
|
{ queryResource() && (
|
||||||
|
<span>{JSON.stringify(queryResource())}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Accessor, Component, Setter } from "solid-js";
|
||||||
|
import { aggregateKey, weatherField } from "../consts";
|
||||||
|
|
||||||
|
export const SelectAggregator: Component<{
|
||||||
|
getField: Accessor<string>,
|
||||||
|
setField: Setter<string>,
|
||||||
|
getKey: Accessor<string>,
|
||||||
|
setKey: Setter<string>,
|
||||||
|
}> = (props) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Select aggregator</h3>
|
||||||
|
<select onChange={(e) => props.setField(e.target.value)}>
|
||||||
|
{ weatherField.map(field =>
|
||||||
|
<option
|
||||||
|
value={field}
|
||||||
|
selected={field === props.getField() ? true : false}
|
||||||
|
>{field}</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
<ul>
|
||||||
|
{ aggregateKey.map(key =>
|
||||||
|
<label><li><input
|
||||||
|
type="checkbox"
|
||||||
|
name="aggregateKey"
|
||||||
|
value={key}
|
||||||
|
checked={key === props.getKey() ? true : false}
|
||||||
|
onClick={() => props.setKey(key)}
|
||||||
|
/>{key}</li></label>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Accessor, Component, Setter } from "solid-js";
|
||||||
|
import { cityList } from "../consts";
|
||||||
|
|
||||||
|
export const SelectCity: Component<{
|
||||||
|
getCities: Accessor<Set<string>>,
|
||||||
|
setCities: Setter<Set<string>>,
|
||||||
|
}> = (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 (
|
||||||
|
<ul>{cityList.map(city =>
|
||||||
|
<label>
|
||||||
|
<li>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="city"
|
||||||
|
value={city}
|
||||||
|
onClick={handleSelect}
|
||||||
|
/>
|
||||||
|
{city}
|
||||||
|
</li>
|
||||||
|
</label>
|
||||||
|
)}</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import { Accessor, Component, Setter } from "solid-js";
|
||||||
|
|
||||||
|
export const SelectTimeRange: Component<{
|
||||||
|
getStart: Accessor<Date>,
|
||||||
|
setStart: Setter<Date>,
|
||||||
|
getEnd: Accessor<Date>,
|
||||||
|
setEnd: Setter<Date>,
|
||||||
|
}> = (props) => {
|
||||||
|
const startStr = () => moment(props.getStart()).format("YYYY-MM-DDTHH:mm");
|
||||||
|
const endStr = () => moment(props.getEnd()).format("YYYY-MM-DDTHH:mm");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Select time range</h3>
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={startStr()}
|
||||||
|
onChange={e => props.setStart(new Date(e.target.value))}
|
||||||
|
/> start
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={endStr()}
|
||||||
|
onChange={e => props.setEnd(new Date(e.target.value))}
|
||||||
|
/> end
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +1,7 @@
|
|||||||
export const apiHost = import.meta.env.VITE_API_HOST;
|
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"];
|
||||||
|
|||||||
@@ -23,6 +23,15 @@
|
|||||||
color: #b318f0;
|
color: #b318f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sections {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
padding-left: 20px;
|
||||||
|
line-height: 70px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes logo-spin {
|
@keyframes logo-spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|||||||
Reference in New Issue
Block a user