From 5dd536ad2329f7a1b47f9b1b67f96f5301e07325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Sat, 19 Oct 2024 14:32:29 +0200 Subject: [PATCH 01/10] Air quality chart --- src/components/Root.js | 1 + src/components/Routes.js | 5 +++++ src/utils/chart.js | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/src/components/Root.js b/src/components/Root.js index c5d6e18c..decc54a9 100644 --- a/src/components/Root.js +++ b/src/components/Root.js @@ -10,6 +10,7 @@ import useAppSettings from "../hooks/useAppSettings"; export const appPages = [ { path: "/", name: i18n.t("Home") }, { path: "/explore", name: i18n.t("Explore data") }, + { path: "/station", name: i18n.t("Station data") }, { path: "/import", name: i18n.t("Import data") }, { path: "/setup", name: i18n.t("Setup guide") }, { path: "/settings", name: i18n.t("Settings") }, diff --git a/src/components/Routes.js b/src/components/Routes.js index dc80c7ce..297a1233 100644 --- a/src/components/Routes.js +++ b/src/components/Routes.js @@ -3,6 +3,7 @@ import { useDataEngine } from "@dhis2/app-runtime"; import Root from "./Root"; import AboutPage from "./AboutPage"; import ExplorePage from "./explore/ExplorePage"; +import StationPage from "./station/StationPage"; import OrgUnit from "./explore/OrgUnit"; import Tabs from "./explore/Tabs"; import Forecast from "./explore/forecast/Forecast"; @@ -149,6 +150,10 @@ const Routes = () => { }, ], }, + { + path: "station", + element: , + }, { path: "import", element: , diff --git a/src/utils/chart.js b/src/utils/chart.js index 3497c817..4b483af4 100644 --- a/src/utils/chart.js +++ b/src/utils/chart.js @@ -65,3 +65,8 @@ export const heatCredits = { "ERA5-HEAT / Copernicus Climate Change Service / Google Earth Engine" ), }; + +export const airQoCredits = { + href: "https://airqo.net", + text: i18n.t("AirQo"), +}; From 1a58786ca86f231c26d7ce097649e2e7ccab92e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Sat, 19 Oct 2024 14:32:44 +0200 Subject: [PATCH 02/10] Air quality chart --- src/components/station/StationPage.js | 10 ++ src/components/station/airqo/DeviceSelect.js | 10 ++ .../station/airqo/SiteMeasurements.js | 121 ++++++++++++++++++ .../airqo/styles/SiteMeasurements.module.css | 19 +++ .../station/charts/AirQualityGauge.js | 86 +++++++++++++ .../station/styles/StationPage.module.css | 0 src/data/pm2.5-legend.js | 70 ++++++++++ 7 files changed, 316 insertions(+) create mode 100644 src/components/station/StationPage.js create mode 100644 src/components/station/airqo/DeviceSelect.js create mode 100644 src/components/station/airqo/SiteMeasurements.js create mode 100644 src/components/station/airqo/styles/SiteMeasurements.module.css create mode 100644 src/components/station/charts/AirQualityGauge.js create mode 100644 src/components/station/styles/StationPage.module.css create mode 100644 src/data/pm2.5-legend.js diff --git a/src/components/station/StationPage.js b/src/components/station/StationPage.js new file mode 100644 index 00000000..74131cd3 --- /dev/null +++ b/src/components/station/StationPage.js @@ -0,0 +1,10 @@ +import { useState, useEffect } from "react"; +import i18n from "@dhis2/d2-i18n"; +import SiteMeasurements from "./airqo/SiteMeasurements"; +import styles from "./styles/StationPage.module.css"; + +const StationPage = () => { + return ; +}; + +export default StationPage; diff --git a/src/components/station/airqo/DeviceSelect.js b/src/components/station/airqo/DeviceSelect.js new file mode 100644 index 00000000..912831e2 --- /dev/null +++ b/src/components/station/airqo/DeviceSelect.js @@ -0,0 +1,10 @@ +import { useState, useEffect } from "react"; +import i18n from "@dhis2/d2-i18n"; +// import styles from "./styles/StationPage.module.css"; + +const MonitorSelect = () => { + console.log("MonitorSelect"); + return null; +}; + +export default MonitorSelect; diff --git a/src/components/station/airqo/SiteMeasurements.js b/src/components/station/airqo/SiteMeasurements.js new file mode 100644 index 00000000..e3bc27d0 --- /dev/null +++ b/src/components/station/airqo/SiteMeasurements.js @@ -0,0 +1,121 @@ +import { useState, useEffect } from "react"; +import i18n from "@dhis2/d2-i18n"; +import Chart from "../../explore/Chart"; +import getChartConfig from "../charts/AirQualityGauge"; +import legend from "../../../data/pm2.5-legend"; +import styles from "./styles/SiteMeasurements.module.css"; + +const token = "4ZFXXVHKFJYNAMYV"; // TODO: Change + +const testData = { + device: "airqo-g5139", + device_id: "63e20c993d2d33001e5266b4", + is_reading_primary: true, + health_tips: [ + { + _id: "64283ec79c7eaf001ea5e9c9", + title: "For Everyone", + description: + "Avoid activities that make you breathe more rapidly. Today is the perfect day to spend indoors reading.", + image: + "https://firebasestorage.googleapis.com/v0/b/airqo-250220.appspot.com/o/health-tip-images%2Ffamily.png?alt=media&token=8cdc93c0-9f3f-42db-b9a1-7dbdf73c5519", + }, + ], + site_id: "6461df90dab86000293ba49f", + time: "2024-10-19T08:00:00.000Z", + pm2_5: { + value: 57.4929, + }, + pm10: { + value: 85.1324089769792, + }, + frequency: "hourly", + no2: {}, + siteDetails: { + _id: "6461df90dab86000293ba49f", + formatted_name: "8H7V+JXC, Kampala, Uganda", + parish: "Makindye Division", + village: "Makindye Division", + sub_county: "Makindye Division", + town: "Kampala", + city: "Kampala", + district: "Kampala", + county: "Kampala", + region: "Central Region", + country: "Uganda", + name: "Mukwano Road device 5", + approximate_latitude: 0.310194643492371, + approximate_longitude: 32.5972135856642, + bearing_in_radians: 2.616, + description: "Mukwano Road device 5", + location_name: "Kampala, Uganda", + search_name: "Makindye Division", + data_provider: "AirQo", + lastActive: "2024-10-19T07:00:00.000Z", + isOnline: true, + }, + timeDifferenceHours: 1.29829083333333, + aqi_color: "ff0000", + aqi_category: "Unhealthy", + aqi_color_name: "Red", +}; + +const SiteMeasurements = ({ siteId }) => { + const [data, setData] = useState(testData); + + /* + useEffect(() => { + fetch( + `https://api.airqo.net/api/v2/devices/measurements/sites/${siteId}/recent?token=${token}` + ) + .then((response) => { + if (response.ok) { + response.json().then((data) => { + setData(data?.measurements?.[0]); + }); + } else { + console.error( + "Error fetching data:", + response.status, + response.statusText + ); + } + }) + .catch((error) => { + console.error("Error fetching data:", error); + }); + }, []); + */ + + if (!data) { + return
{i18n.t("Loading...")}
; + } + + const { + siteDetails: { name, city }, + aqi_category: category, + aqi_color: color, + health_tips, + no2: { value: no2 }, + pm2_5: { value: pm2_5 }, + pm10: { value: pm10 }, + time, + } = data; + + // console.log(name, city, category, color, health_tips, no2, pm2_5, pm10, time); + + const description = legend.items.find( + (item) => item.category === category + )?.description; + + console.log("time", new Date(time)); + + return data ? ( +
+ + {description &&
{description}
} +
+ ) : null; +}; + +export default SiteMeasurements; diff --git a/src/components/station/airqo/styles/SiteMeasurements.module.css b/src/components/station/airqo/styles/SiteMeasurements.module.css new file mode 100644 index 00000000..eb5ae910 --- /dev/null +++ b/src/components/station/airqo/styles/SiteMeasurements.module.css @@ -0,0 +1,19 @@ +.container { + border: 1px solid #ccc; + width: 400px; + height: 400px; +} + +.container :global(.airquality-label) span span { + display: block; + font-weight: normal; + font-size: 14px; + line-height: 28px; + text-align: center; +} + +.description { + padding: 10px 20px; + font-size: 16px; + line-height: 20px; +} diff --git a/src/components/station/charts/AirQualityGauge.js b/src/components/station/charts/AirQualityGauge.js new file mode 100644 index 00000000..63670b58 --- /dev/null +++ b/src/components/station/charts/AirQualityGauge.js @@ -0,0 +1,86 @@ +import i18n from "@dhis2/d2-i18n"; +import legend from "../../../data/pm2.5-legend"; +import { roundOneDecimal } from "../../../utils/calc"; +import { airQoCredits } from "../../../utils/chart"; + +const thickness = 50; + +const getChartConfig = (name, value, category) => ({ + title: { + text: i18n.t("Air Quality PM 2.5"), + }, + subtitle: { + text: name, + }, + credits: airQoCredits, + chart: { + type: "gauge", + plotBackgroundColor: null, + plotBackgroundImage: null, + plotBorderWidth: 0, + plotShadow: false, + height: "80%", + }, + pane: { + startAngle: -90, + endAngle: 90, + background: null, + center: ["50%", "75%"], + size: "130%", + }, + yAxis: { + min: 0, + max: 350, + tickPosition: "inside", + tickColor: "#FFFFFF", + tickLength: thickness, + tickWidth: 1, + minorTickInterval: null, + labels: { + distance: 20, + style: { + fontSize: "14px", + }, + }, + + tickPositions: [9, 35.5, 55.5, 125.5, 225.5], + lineWidth: 0, + plotBands: legend.items.map((item) => ({ + ...item, + thickness, + borderRadius: "50%", + })), + }, + series: [ + { + name: "PM2.5", + data: [value], + tooltip: { + valueSuffix: " μg/m3", + }, + dataLabels: { + useHTML: true, + format: `${category}${roundOneDecimal(value)} μg/m3`, + borderWidth: 0, + className: "airquality-label", + color: "#333333", + style: { + fontSize: "16px", + }, + }, + dial: { + radius: "80%", + backgroundColor: "#333", + baseWidth: 12, + baseLength: "0%", + rearLength: "0%", + }, + pivot: { + backgroundColor: "#333", + radius: 6, + }, + }, + ], +}); + +export default getChartConfig; diff --git a/src/components/station/styles/StationPage.module.css b/src/components/station/styles/StationPage.module.css new file mode 100644 index 00000000..e69de29b diff --git a/src/data/pm2.5-legend.js b/src/data/pm2.5-legend.js new file mode 100644 index 00000000..b5e5cfdd --- /dev/null +++ b/src/data/pm2.5-legend.js @@ -0,0 +1,70 @@ +import i18n from "@dhis2/d2-i18n"; + +// https://www.iqair.com/newsroom/what-is-aqi +// https://aqicn.org/scale/ +export default { + name: i18n.t("Air Quality Index (AQI"), + description: i18n.t(""), + items: [ + { + category: "Good", + name: i18n.t("Good"), + description: i18n.t( + "Air quality is considered satisfactory, and air pollution poses little or no risk" + ), + color: "#abd162", + from: 0, + to: 9, + }, + { + category: "Moderate", + name: i18n.t("Moderate"), + description: i18n.t( + "Active children and adults, and people with respiratory disease, such as asthma, should limit prolonged outdoor exertion." + ), + color: "#f8d461", + from: 9, + to: 35.5, + }, + { + category: "Unhealthy for Sensitive Groups", + name: i18n.t("Unhealthy for Sensitive Groups"), + description: i18n.t( + "Members of sensitive groups may experience health effects. Active children and adults, and people with respiratory disease, such as asthma, should limit prolonged outdoor exertion." + ), + color: "#fb9956", + from: 35.5, + to: 55.5, + }, + { + category: "Unhealthy", + name: i18n.t("Unhealthy"), + description: i18n.t( + "Everyone may begin to experience health effects; members of sensitive groups may experience more serious health effects." + ), + color: "#f6686a", + from: 55.5, + to: 125.5, + }, + { + category: "Very unhealthy", + name: i18n.t("Very unhealthy"), + description: i18n.t( + "Health warnings of emergency conditions. The entire population is more likely to be affected." + ), + color: "#a47db8", + from: 125.5, + to: 225.5, + }, + { + category: "Hazardous", + name: i18n.t("Hazardous"), + description: i18n.t( + "Health alert: everyone may experience more serious health effects" + ), + color: "#a07785", + from: 225.5, + to: 400, + }, + ], +}; From e3212ca878dbcfa30b848157a1ca73586df67694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Sun, 20 Oct 2024 18:24:15 +0200 Subject: [PATCH 03/10] Code cleaning --- .../station/airqo/SiteMeasurements.js | 21 ++++++++++--- .../airqo/styles/SiteMeasurements.module.css | 2 ++ .../station/charts/AirQualityGauge.js | 31 ++++++++++++++----- src/utils/chart.js | 5 --- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/components/station/airqo/SiteMeasurements.js b/src/components/station/airqo/SiteMeasurements.js index e3bc27d0..262300f9 100644 --- a/src/components/station/airqo/SiteMeasurements.js +++ b/src/components/station/airqo/SiteMeasurements.js @@ -5,7 +5,7 @@ import getChartConfig from "../charts/AirQualityGauge"; import legend from "../../../data/pm2.5-legend"; import styles from "./styles/SiteMeasurements.module.css"; -const token = "4ZFXXVHKFJYNAMYV"; // TODO: Change +const token = ""; // TODO: Read from settings / use Routes API const testData = { device: "airqo-g5139", @@ -94,8 +94,6 @@ const SiteMeasurements = ({ siteId }) => { const { siteDetails: { name, city }, aqi_category: category, - aqi_color: color, - health_tips, no2: { value: no2 }, pm2_5: { value: pm2_5 }, pm10: { value: pm10 }, @@ -108,11 +106,24 @@ const SiteMeasurements = ({ siteId }) => { (item) => item.category === category )?.description; - console.log("time", new Date(time)); + const formattedTime = new Date(time).toLocaleTimeString([], { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); return data ? (
- + {description &&
{description}
}
) : null; diff --git a/src/components/station/airqo/styles/SiteMeasurements.module.css b/src/components/station/airqo/styles/SiteMeasurements.module.css index eb5ae910..c3a4e30d 100644 --- a/src/components/station/airqo/styles/SiteMeasurements.module.css +++ b/src/components/station/airqo/styles/SiteMeasurements.module.css @@ -1,4 +1,5 @@ .container { + margin-top: 20px; border: 1px solid #ccc; width: 400px; height: 400px; @@ -16,4 +17,5 @@ padding: 10px 20px; font-size: 16px; line-height: 20px; + color: #333; } diff --git a/src/components/station/charts/AirQualityGauge.js b/src/components/station/charts/AirQualityGauge.js index 63670b58..14193020 100644 --- a/src/components/station/charts/AirQualityGauge.js +++ b/src/components/station/charts/AirQualityGauge.js @@ -1,18 +1,28 @@ import i18n from "@dhis2/d2-i18n"; import legend from "../../../data/pm2.5-legend"; import { roundOneDecimal } from "../../../utils/calc"; -import { airQoCredits } from "../../../utils/chart"; const thickness = 50; -const getChartConfig = (name, value, category) => ({ +const getChartConfig = (name, value, category, time) => ({ title: { - text: i18n.t("Air Quality PM 2.5"), + text: i18n.t("Air Quality PM2.5"), + useHTML: true, }, subtitle: { - text: name, + text: `${name}
${time}`, + style: { + fontSize: "14px", + lineHeight: "20px", + }, + }, + credits: { + href: "https://airqo.net", + text: i18n.t("AirQo"), + style: { + fontSize: "12px", + }, }, - credits: airQoCredits, chart: { type: "gauge", plotBackgroundColor: null, @@ -51,16 +61,21 @@ const getChartConfig = (name, value, category) => ({ borderRadius: "50%", })), }, + tooltip: { + useHTML: true, + }, series: [ { - name: "PM2.5", + name: "PM2.5", data: [value], tooltip: { - valueSuffix: " μg/m3", + valueSuffix: " μg/m3", }, dataLabels: { useHTML: true, - format: `${category}${roundOneDecimal(value)} μg/m3`, + format: `${category}${roundOneDecimal( + value + )} μg/m3`, borderWidth: 0, className: "airquality-label", color: "#333333", diff --git a/src/utils/chart.js b/src/utils/chart.js index 4b483af4..3497c817 100644 --- a/src/utils/chart.js +++ b/src/utils/chart.js @@ -65,8 +65,3 @@ export const heatCredits = { "ERA5-HEAT / Copernicus Climate Change Service / Google Earth Engine" ), }; - -export const airQoCredits = { - href: "https://airqo.net", - text: i18n.t("AirQo"), -}; From 15a33f1012db09a23dae3adeea4ad1885f04c80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Wed, 6 Nov 2024 12:11:17 +0100 Subject: [PATCH 04/10] AirQo testing --- src/components/settings/ChartSettings.js | 1 - src/components/settings/DataSettings.js | 28 ++++ src/components/settings/SettingsPage.js | 2 + src/components/station/StationPage.js | 4 +- src/components/station/airqo/AirQoPlugin.js | 81 +++++++++++ src/components/station/airqo/DeviceSelect.js | 6 +- .../station/airqo/SiteMeasurements.js | 132 ------------------ ...ents.module.css => AirQoPlugin.module.css} | 0 8 files changed, 116 insertions(+), 138 deletions(-) create mode 100644 src/components/settings/DataSettings.js create mode 100644 src/components/station/airqo/AirQoPlugin.js delete mode 100644 src/components/station/airqo/SiteMeasurements.js rename src/components/station/airqo/styles/{SiteMeasurements.module.css => AirQoPlugin.module.css} (100%) diff --git a/src/components/settings/ChartSettings.js b/src/components/settings/ChartSettings.js index 109b1a65..e5e89b8f 100644 --- a/src/components/settings/ChartSettings.js +++ b/src/components/settings/ChartSettings.js @@ -33,7 +33,6 @@ const ChartSettings = () => { label={i18n.t("Max temperature in °C")} value={tempMax} onChange={changeSetting} - inputWidth="100px" /> { + const { settings, changeSetting } = useAppSettings(); + + if (!settings) { + return null; + } + + const { airqoToken } = settings; + + return ( + <> +

Data settings

+ + changeSetting("airqoToken", value !== "" ? value : undefined) + } + /> + + ); +}; + +export default ChartSettings; diff --git a/src/components/settings/SettingsPage.js b/src/components/settings/SettingsPage.js index 454fadea..5c7a3097 100644 --- a/src/components/settings/SettingsPage.js +++ b/src/components/settings/SettingsPage.js @@ -2,6 +2,7 @@ import i18n from "@dhis2/d2-i18n"; import StartPageSelect from "./StartPageSelect"; import TimeZoneSelect from "./TimeZoneSelect"; import ChartSettings from "./ChartSettings"; +import DataSettings from "./DataSettings"; import useAppSettings from "../../hooks/useAppSettings"; import styles from "./styles/SettingsPage.module.css"; @@ -21,6 +22,7 @@ const SettingsPage = () => { + ); }; diff --git a/src/components/station/StationPage.js b/src/components/station/StationPage.js index 74131cd3..0e2560f7 100644 --- a/src/components/station/StationPage.js +++ b/src/components/station/StationPage.js @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; import i18n from "@dhis2/d2-i18n"; -import SiteMeasurements from "./airqo/SiteMeasurements"; +import AirQoPlugin from "./airqo/AirQoPlugin"; import styles from "./styles/StationPage.module.css"; const StationPage = () => { - return ; + return ; }; export default StationPage; diff --git a/src/components/station/airqo/AirQoPlugin.js b/src/components/station/airqo/AirQoPlugin.js new file mode 100644 index 00000000..b44a7909 --- /dev/null +++ b/src/components/station/airqo/AirQoPlugin.js @@ -0,0 +1,81 @@ +import { useState, useEffect } from "react"; +import i18n from "@dhis2/d2-i18n"; +import Chart from "../../explore/Chart"; +import getChartConfig from "../charts/AirQualityGauge"; +import legend from "../../../data/pm2.5-legend"; +import useAppSettings from "../../../hooks/useAppSettings"; +import styles from "./styles/AirQoPlugin.module.css"; + +const AirQoPlugin = ({ siteId }) => { + const [data, setData] = useState(); + const { settings = {} } = useAppSettings(); + const { airqoToken } = settings; + + useEffect(() => { + if (airqoToken && !data) { + fetch( + `https://api.airqo.net/api/v2/devices/measurements/sites/${siteId}/recent?token=${airqoToken}` + ) + .then((response) => { + if (response.ok) { + response.json().then((data) => { + setData(data?.measurements?.[0]); + }); + } else { + console.error( + "Error fetching data:", + response.status, + response.statusText + ); + } + }) + .catch((error) => { + console.error("Error fetching data:", error); + }); + } + }, [airqoToken, data]); + + if (!airqoToken) { + return
{i18n.t("AirQo token not set")}
; + } + + if (!data) { + return
{i18n.t("Loading...")}
; + } + + const { + siteDetails: { name, city }, + aqi_category: category, + pm2_5: { value: pm2_5 }, + pm10: { value: pm10 }, + time, + } = data; + + const description = legend.items.find( + (item) => item.category === category + )?.description; + + const formattedTime = new Date(time).toLocaleTimeString([], { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + + return data ? ( +
+ + {description &&
{description}
} +
+ ) : null; +}; + +export default AirQoPlugin; diff --git a/src/components/station/airqo/DeviceSelect.js b/src/components/station/airqo/DeviceSelect.js index 912831e2..04c5710e 100644 --- a/src/components/station/airqo/DeviceSelect.js +++ b/src/components/station/airqo/DeviceSelect.js @@ -2,9 +2,9 @@ import { useState, useEffect } from "react"; import i18n from "@dhis2/d2-i18n"; // import styles from "./styles/StationPage.module.css"; -const MonitorSelect = () => { - console.log("MonitorSelect"); +const DeviceSelect = () => { + console.log("DeviceSelect"); return null; }; -export default MonitorSelect; +export default DeviceSelect; diff --git a/src/components/station/airqo/SiteMeasurements.js b/src/components/station/airqo/SiteMeasurements.js deleted file mode 100644 index 262300f9..00000000 --- a/src/components/station/airqo/SiteMeasurements.js +++ /dev/null @@ -1,132 +0,0 @@ -import { useState, useEffect } from "react"; -import i18n from "@dhis2/d2-i18n"; -import Chart from "../../explore/Chart"; -import getChartConfig from "../charts/AirQualityGauge"; -import legend from "../../../data/pm2.5-legend"; -import styles from "./styles/SiteMeasurements.module.css"; - -const token = ""; // TODO: Read from settings / use Routes API - -const testData = { - device: "airqo-g5139", - device_id: "63e20c993d2d33001e5266b4", - is_reading_primary: true, - health_tips: [ - { - _id: "64283ec79c7eaf001ea5e9c9", - title: "For Everyone", - description: - "Avoid activities that make you breathe more rapidly. Today is the perfect day to spend indoors reading.", - image: - "https://firebasestorage.googleapis.com/v0/b/airqo-250220.appspot.com/o/health-tip-images%2Ffamily.png?alt=media&token=8cdc93c0-9f3f-42db-b9a1-7dbdf73c5519", - }, - ], - site_id: "6461df90dab86000293ba49f", - time: "2024-10-19T08:00:00.000Z", - pm2_5: { - value: 57.4929, - }, - pm10: { - value: 85.1324089769792, - }, - frequency: "hourly", - no2: {}, - siteDetails: { - _id: "6461df90dab86000293ba49f", - formatted_name: "8H7V+JXC, Kampala, Uganda", - parish: "Makindye Division", - village: "Makindye Division", - sub_county: "Makindye Division", - town: "Kampala", - city: "Kampala", - district: "Kampala", - county: "Kampala", - region: "Central Region", - country: "Uganda", - name: "Mukwano Road device 5", - approximate_latitude: 0.310194643492371, - approximate_longitude: 32.5972135856642, - bearing_in_radians: 2.616, - description: "Mukwano Road device 5", - location_name: "Kampala, Uganda", - search_name: "Makindye Division", - data_provider: "AirQo", - lastActive: "2024-10-19T07:00:00.000Z", - isOnline: true, - }, - timeDifferenceHours: 1.29829083333333, - aqi_color: "ff0000", - aqi_category: "Unhealthy", - aqi_color_name: "Red", -}; - -const SiteMeasurements = ({ siteId }) => { - const [data, setData] = useState(testData); - - /* - useEffect(() => { - fetch( - `https://api.airqo.net/api/v2/devices/measurements/sites/${siteId}/recent?token=${token}` - ) - .then((response) => { - if (response.ok) { - response.json().then((data) => { - setData(data?.measurements?.[0]); - }); - } else { - console.error( - "Error fetching data:", - response.status, - response.statusText - ); - } - }) - .catch((error) => { - console.error("Error fetching data:", error); - }); - }, []); - */ - - if (!data) { - return
{i18n.t("Loading...")}
; - } - - const { - siteDetails: { name, city }, - aqi_category: category, - no2: { value: no2 }, - pm2_5: { value: pm2_5 }, - pm10: { value: pm10 }, - time, - } = data; - - // console.log(name, city, category, color, health_tips, no2, pm2_5, pm10, time); - - const description = legend.items.find( - (item) => item.category === category - )?.description; - - const formattedTime = new Date(time).toLocaleTimeString([], { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - - return data ? ( -
- - {description &&
{description}
} -
- ) : null; -}; - -export default SiteMeasurements; diff --git a/src/components/station/airqo/styles/SiteMeasurements.module.css b/src/components/station/airqo/styles/AirQoPlugin.module.css similarity index 100% rename from src/components/station/airqo/styles/SiteMeasurements.module.css rename to src/components/station/airqo/styles/AirQoPlugin.module.css From ddc333c0249d5b4f3f8dabcbac5bb83f4819954c Mon Sep 17 00:00:00 2001 From: Abyot Asalefew Gizaw Date: Wed, 12 Feb 2025 15:09:38 +0100 Subject: [PATCH 05/10] feat: Use route api to fetch AirQo data --- i18n/en.pot | 80 +++++++++++++++++++- src/components/station/airqo/AirQoPlugin.jsx | 35 ++++----- src/hooks/useGetRoute.js | 24 ++++++ 3 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 src/hooks/useGetRoute.js diff --git a/i18n/en.pot b/i18n/en.pot index c5448a05..20a1466c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-01-23T16:19:51.054Z\n" -"PO-Revision-Date: 2025-01-23T16:19:51.055Z\n" +"POT-Creation-Date: 2025-02-11T11:10:48.583Z\n" +"PO-Revision-Date: 2025-02-11T11:10:48.583Z\n" msgid "About this app" msgstr "About this app" @@ -111,6 +111,9 @@ msgstr "Home" msgid "Explore data" msgstr "Explore data" +msgid "Station data" +msgstr "Station data" + msgid "Import data" msgstr "Import data" @@ -696,6 +699,9 @@ msgstr "Heat stress upper category" msgid "Heat stress lower category" msgstr "Heat stress lower category" +msgid "AirQo token" +msgstr "AirQo token" + msgid "Calculate from data" msgstr "Calculate from data" @@ -863,6 +869,18 @@ msgstr "" msgid "Time zone" msgstr "Time zone" +msgid "AirQo token not set" +msgstr "AirQo token not set" + +msgid "Loading..." +msgstr "Loading..." + +msgid "Air Quality PM2.5" +msgstr "Air Quality PM2.5" + +msgid "AirQo" +msgstr "AirQo" + msgid "Approximately 31 km (0.25°)" msgstr "Approximately 31 km (0.25°)" @@ -1038,6 +1056,64 @@ msgstr "Extreme heat stress" msgid "Extreme
heat stress" msgstr "Extreme
heat stress" +msgid "Air Quality Index (AQI" +msgstr "Air Quality Index (AQI" + +msgid "Good" +msgstr "Good" + +msgid "" +"Air quality is considered satisfactory, and air pollution poses little or " +"no risk" +msgstr "" +"Air quality is considered satisfactory, and air pollution poses little or " +"no risk" + +msgid "Moderate" +msgstr "Moderate" + +msgid "" +"Active children and adults, and people with respiratory disease, such as " +"asthma, should limit prolonged outdoor exertion." +msgstr "" +"Active children and adults, and people with respiratory disease, such as " +"asthma, should limit prolonged outdoor exertion." + +msgid "Unhealthy for Sensitive Groups" +msgstr "Unhealthy for Sensitive Groups" + +msgid "" +"Members of sensitive groups may experience health effects. Active children " +"and adults, and people with respiratory disease, such as asthma, should " +"limit prolonged outdoor exertion." +msgstr "" +"Members of sensitive groups may experience health effects. Active children " +"and adults, and people with respiratory disease, such as asthma, should " +"limit prolonged outdoor exertion." + +msgid "Unhealthy" +msgstr "Unhealthy" + +msgid "" +"Everyone may begin to experience health effects; members of sensitive " +"groups may experience more serious health effects." +msgstr "" +"Everyone may begin to experience health effects; members of sensitive " +"groups may experience more serious health effects." + +msgid "Very unhealthy" +msgstr "Very unhealthy" + +msgid "" +"Health warnings of emergency conditions. The entire population is more " +"likely to be affected." +msgstr "" +"Health warnings of emergency conditions. The entire population is more " +"likely to be affected." + +msgid "Hazardous" +msgstr "Hazardous" + msgid "ERA5-Land / Copernicus Climate Change Service / Google Earth Engine" msgstr "ERA5-Land / Copernicus Climate Change Service / Google Earth Engine" diff --git a/src/components/station/airqo/AirQoPlugin.jsx b/src/components/station/airqo/AirQoPlugin.jsx index e45db575..105bd44e 100644 --- a/src/components/station/airqo/AirQoPlugin.jsx +++ b/src/components/station/airqo/AirQoPlugin.jsx @@ -1,3 +1,4 @@ +import { useDataEngine } from '@dhis2/app-runtime' import { useState, useEffect } from 'react' import i18n from '@dhis2/d2-i18n' import Chart from '../../explore/Chart.jsx' @@ -5,37 +6,31 @@ import getChartConfig from '../charts/AirQualityGauge.js' import legend from '../../../data/pm2.5-legend.js' import useAppSettings from '../../../hooks/useAppSettings.js' import styles from './styles/AirQoPlugin.module.css' +import useGetRoute from '../../../hooks/useGetRoute.js' const AirQoPlugin = ({ siteId }) => { + const engine = useDataEngine() const [data, setData] = useState() const { settings = {} } = useAppSettings() - const { airqoToken } = settings - + const { route } = useGetRoute() useEffect(() => { - if (airqoToken && !data) { - fetch( - `https://api.airqo.net/api/v2/devices/measurements/sites/${siteId}/recent?token=${airqoToken}` - ) + if (route && route.id && !data) { + const resource = `routes/${route.id}/run/${siteId}/recent` + + engine + .query({ + routes: { resource }, + }) .then((response) => { - if (response.ok) { - response.json().then((data) => { - setData(data?.measurements?.[0]) - }) - } else { - console.error( - 'Error fetching data:', - response.status, - response.statusText - ) - } + setData(response?.routes?.measurements?.[0]) }) .catch((error) => { - console.error('Error fetching data:', error) + console.error('Error fetching airqo data:', error) }) } - }, [airqoToken, data]) + }, [route, data]) - if (!airqoToken) { + if (!route || !route.id) { return
{i18n.t('AirQo token not set')}
} diff --git a/src/hooks/useGetRoute.js b/src/hooks/useGetRoute.js new file mode 100644 index 00000000..c5c8072b --- /dev/null +++ b/src/hooks/useGetRoute.js @@ -0,0 +1,24 @@ +import { useDataQuery } from '@dhis2/app-runtime' + +const ROUTE_QUERY = { + routes: { + resource: 'routes', + params: { + paging: false, + filter: `code:eq:airqo`, + fields: '*', + }, + }, +} + +const useGetRoute = () => { + const { loading, error, data } = useDataQuery(ROUTE_QUERY) + + return { + route: data?.routes?.routes[0], + error, + loading, + } +} + +export default useGetRoute From 312531df835867da76d17110dd3d526bbcbeb652 Mon Sep 17 00:00:00 2001 From: Abyot Asalefew Gizaw Date: Wed, 12 Feb 2025 15:22:17 +0100 Subject: [PATCH 06/10] chore: code style --- src/components/Routes.jsx | 2 +- src/components/settings/SettingsPage.jsx | 2 +- src/components/station/airqo/AirQoPlugin.jsx | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/Routes.jsx b/src/components/Routes.jsx index 7358ea86..6796791b 100644 --- a/src/components/Routes.jsx +++ b/src/components/Routes.jsx @@ -7,7 +7,6 @@ import CheckPage from './check/CheckPage.jsx' import ErrorPage from './ErrorPage.jsx' import ClimateChange from './explore/climateChange/ClimateChange.jsx' import ExplorePage from './explore/ExplorePage.jsx' -import StationPage from './station/StationPage.jsx' import Forecast from './explore/forecast/Forecast.jsx' import HeatDaily from './explore/heat/HeatDaily.jsx' import HeatMonthly from './explore/heat/HeatMonthly.jsx' @@ -23,6 +22,7 @@ import ImportPage from './import/ImportPage.jsx' import Root from './Root.jsx' import SettingsPage from './settings/SettingsPage.jsx' import SetupPage from './setup/SetupPage.jsx' +import StationPage from './station/StationPage.jsx' const monthlyPath = 'monthly/:startTime/:endTime/:referencePeriodId' const dailyPath = 'daily/:startTime/:endTime' diff --git a/src/components/settings/SettingsPage.jsx b/src/components/settings/SettingsPage.jsx index daf25397..ace16633 100644 --- a/src/components/settings/SettingsPage.jsx +++ b/src/components/settings/SettingsPage.jsx @@ -1,8 +1,8 @@ import i18n from '@dhis2/d2-i18n' import useAppSettings from '../../hooks/useAppSettings.js' import ChartSettings from './ChartSettings.jsx' -import StartPageSelect from './StartPageSelect.jsx' import DataSettings from './DataSettings.jsx' +import StartPageSelect from './StartPageSelect.jsx' import styles from './styles/SettingsPage.module.css' import TimeZoneSelect from './TimeZoneSelect.jsx' diff --git a/src/components/station/airqo/AirQoPlugin.jsx b/src/components/station/airqo/AirQoPlugin.jsx index 105bd44e..15cbebe0 100644 --- a/src/components/station/airqo/AirQoPlugin.jsx +++ b/src/components/station/airqo/AirQoPlugin.jsx @@ -1,12 +1,13 @@ import { useDataEngine } from '@dhis2/app-runtime' -import { useState, useEffect } from 'react' import i18n from '@dhis2/d2-i18n' -import Chart from '../../explore/Chart.jsx' -import getChartConfig from '../charts/AirQualityGauge.js' +import PropTypes from 'prop-types' +import { useState, useEffect } from 'react' import legend from '../../../data/pm2.5-legend.js' import useAppSettings from '../../../hooks/useAppSettings.js' -import styles from './styles/AirQoPlugin.module.css' import useGetRoute from '../../../hooks/useGetRoute.js' +import Chart from '../../explore/Chart.jsx' +import getChartConfig from '../charts/AirQualityGauge.js' +import styles from './styles/AirQoPlugin.module.css' const AirQoPlugin = ({ siteId }) => { const engine = useDataEngine() @@ -75,4 +76,8 @@ const AirQoPlugin = ({ siteId }) => { ) : null } +AirQoPlugin.propTypes = { + siteId: PropTypes.string.isRequired, +} + export default AirQoPlugin From ef5c80660425bdcff0edef9031e27c671700e01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Mon, 17 Feb 2025 09:37:59 +0100 Subject: [PATCH 07/10] chore: merge in main --- src/components/settings/DataSettings.jsx | 47 +++++++++++--------- src/components/station/airqo/AirQoPlugin.jsx | 2 +- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/components/settings/DataSettings.jsx b/src/components/settings/DataSettings.jsx index 8b816b40..e36c410f 100644 --- a/src/components/settings/DataSettings.jsx +++ b/src/components/settings/DataSettings.jsx @@ -1,28 +1,31 @@ -import i18n from "@dhis2/d2-i18n"; -import { InputField } from "@dhis2/ui"; -import useAppSettings from "../../hooks/useAppSettings"; +import i18n from '@dhis2/d2-i18n' +import { InputField } from '@dhis2/ui' +import useAppSettings from '../../hooks/useAppSettings.js' const ChartSettings = () => { - const { settings, changeSetting } = useAppSettings(); + const { settings, changeSetting } = useAppSettings() - if (!settings) { - return null; - } + if (!settings) { + return null + } - const { airqoToken } = settings; + const { airqoToken } = settings - return ( - <> -

Data settings

- - changeSetting("airqoToken", value !== "" ? value : undefined) - } - /> - - ); -}; + return ( + <> +

Data settings

+ + changeSetting( + 'airqoToken', + value !== '' ? value : undefined + ) + } + /> + + ) +} -export default ChartSettings; +export default ChartSettings diff --git a/src/components/station/airqo/AirQoPlugin.jsx b/src/components/station/airqo/AirQoPlugin.jsx index 15cbebe0..d5ed6e20 100644 --- a/src/components/station/airqo/AirQoPlugin.jsx +++ b/src/components/station/airqo/AirQoPlugin.jsx @@ -12,7 +12,7 @@ import styles from './styles/AirQoPlugin.module.css' const AirQoPlugin = ({ siteId }) => { const engine = useDataEngine() const [data, setData] = useState() - const { settings = {} } = useAppSettings() + // const { settings = {} } = useAppSettings() const { route } = useGetRoute() useEffect(() => { if (route && route.id && !data) { From 72e133b77ec2fa40463eda2e2e570911892dad6f Mon Sep 17 00:00:00 2001 From: Abyot Asalefew Gizaw Date: Tue, 25 Mar 2025 10:18:51 +0100 Subject: [PATCH 08/10] feat: AirQo route creation --- i18n/en.pot | 42 ++++++-- src/components/setup/RouteSetup.jsx | 95 +++++++++++++++++++ src/components/station/airqo/AirQoPlugin.jsx | 43 ++++++++- .../airqo/styles/AirQoPlugin.module.css | 4 + 4 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 src/components/setup/RouteSetup.jsx diff --git a/i18n/en.pot b/i18n/en.pot index bb9df086..ffe2ff52 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-03-07T10:47:32.346Z\n" -"PO-Revision-Date: 2025-03-07T10:47:32.346Z\n" +"POT-Creation-Date: 2025-03-21T10:46:10.062Z\n" +"PO-Revision-Date: 2025-03-21T10:46:10.062Z\n" msgid "About this app" msgstr "About this app" @@ -508,6 +508,19 @@ msgstr "{{name}}: Monthly temperatures {{period}}" msgid "Normal temperature" msgstr "Normal temperature" +msgid "Resample" +msgstr "Resample" + +msgid "Weekly" +msgstr "Weekly" + +msgid "" +"Change from 16-days to weekly or monthly periods using linear " +"interpolation. Weekly periods are recommended when importing to DHIS2." +msgstr "" +"Change from 16-days to weekly or monthly periods using linear " +"interpolation. Weekly periods are recommended when importing to DHIS2." + msgid "{{name}}: {{band}} vegetation index 2024" msgstr "{{name}}: {{band}} vegetation index 2024" @@ -791,6 +804,21 @@ msgstr "End value" msgid "Color" msgstr "Color" +msgid "Route set up" +msgstr "Route set up" + +msgid "Your route will be created with the following information ..." +msgstr "Your route will be created with the following information ..." + +msgid "Token" +msgstr "Token" + +msgid "Cancel" +msgstr "Cancel" + +msgid "Save" +msgstr "Save" + msgid "How to configure this app" msgstr "How to configure this app" @@ -906,11 +934,14 @@ msgstr "" msgid "Time zone" msgstr "Time zone" +msgid "Missing route" +msgstr "Missing route" + msgid "AirQo token not set" msgstr "AirQo token not set" -msgid "Loading..." -msgstr "Loading..." +msgid "Create route" +msgstr "Create route" msgid "Air Quality PM2.5" msgstr "Air Quality PM2.5" @@ -1216,6 +1247,3 @@ msgstr "NASA LP DAAC at the USGS EROS Center / Google Earth Engine" msgid "No data found for the selected period" msgstr "No data found for the selected period" - -msgid "Weekly" -msgstr "Weekly" diff --git a/src/components/setup/RouteSetup.jsx b/src/components/setup/RouteSetup.jsx new file mode 100644 index 00000000..104a876b --- /dev/null +++ b/src/components/setup/RouteSetup.jsx @@ -0,0 +1,95 @@ +import { useDataMutation } from '@dhis2/app-runtime' +import i18n from '@dhis2/d2-i18n' +import { + Modal, + ModalTitle, + ModalContent, + ModalActions, + ButtonStrip, + Button, + InputField, +} from '@dhis2/ui' +import PropTypes from 'prop-types' +import React, { useState } from 'react' + +const mutation = { + resource: 'routes', + type: 'create', + data: ({ data }) => data, +} + +const RouteSetup = ({ + routeInfo, + isRouteSetupModalOpen, + setIsRouteSetupModalOpen, +}) => { + const [token, setToken] = useState('') + //token = CTBMDUGQBQD9SCHA + if (routeInfo.auth.type == 'api-query-params') { + routeInfo.auth.queryParams = { + token: token, + } + } else if (routeInfo.auth.type == 'api-headers') { + routeInfo.auth.headers = { + 'X-API-KEY': token, + } + } + const [saveRoute] = useDataMutation(mutation, { + variables: { + data: routeInfo, + }, + onComplete: () => { + setIsRouteSetupModalOpen(!isRouteSetupModalOpen) + }, + }) + + return ( + <> + + {i18n.t('Route set up')} + +

+ {i18n.t( + 'Your route will be created with the following information ...' + )} +

+
+
{JSON.stringify(routeInfo, null, 2)}
+
+ setToken(value)} + /> +
+ + + + + + +
+ + ) +} + +RouteSetup.propTypes = { + isRouteSetupModalOpen: PropTypes.bool.isRequired, + routeInfo: PropTypes.object.isRequired, + setIsRouteSetupModalOpen: PropTypes.func.isRequired, +} + +export default RouteSetup diff --git a/src/components/station/airqo/AirQoPlugin.jsx b/src/components/station/airqo/AirQoPlugin.jsx index d5ed6e20..4d7f769f 100644 --- a/src/components/station/airqo/AirQoPlugin.jsx +++ b/src/components/station/airqo/AirQoPlugin.jsx @@ -1,19 +1,32 @@ import { useDataEngine } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' +import { CircularLoader, NoticeBox, Button } from '@dhis2/ui' import PropTypes from 'prop-types' import { useState, useEffect } from 'react' import legend from '../../../data/pm2.5-legend.js' -import useAppSettings from '../../../hooks/useAppSettings.js' import useGetRoute from '../../../hooks/useGetRoute.js' import Chart from '../../explore/Chart.jsx' +import RouteSetup from '../../setup/RouteSetup.jsx' import getChartConfig from '../charts/AirQualityGauge.js' import styles from './styles/AirQoPlugin.module.css' const AirQoPlugin = ({ siteId }) => { const engine = useDataEngine() const [data, setData] = useState() - // const { settings = {} } = useAppSettings() const { route } = useGetRoute() + + const [isRouteSetupModalOpen, setIsRouteSetupModalOpen] = useState(false) + + const routeInfo = { + name: 'AirQo', + code: 'airqo', + disabled: false, + url: 'https://api.airqo.net/api/v2/devices/measurements/sites/**', + auth: { + type: 'api-query-params', + }, + } + useEffect(() => { if (route && route.id && !data) { const resource = `routes/${route.id}/run/${siteId}/recent` @@ -32,11 +45,33 @@ const AirQoPlugin = ({ siteId }) => { }, [route, data]) if (!route || !route.id) { - return
{i18n.t('AirQo token not set')}
+ return ( +
+ + {i18n.t('AirQo token not set')} + + +
+ + {isRouteSetupModalOpen && ( + + )} +
+
+ ) } if (!data) { - return
{i18n.t('Loading...')}
+ return } const { diff --git a/src/components/station/airqo/styles/AirQoPlugin.module.css b/src/components/station/airqo/styles/AirQoPlugin.module.css index c3a4e30d..674c45a8 100644 --- a/src/components/station/airqo/styles/AirQoPlugin.module.css +++ b/src/components/station/airqo/styles/AirQoPlugin.module.css @@ -19,3 +19,7 @@ line-height: 20px; color: #333; } + +.margin { + margin-top: 20px; +} \ No newline at end of file From 5d2c9635a2e60b67fc1f2f564d89fbd359a54360 Mon Sep 17 00:00:00 2001 From: Abyot Asalefew Gizaw Date: Tue, 25 Mar 2025 10:18:51 +0100 Subject: [PATCH 09/10] feat: airqo route creation --- i18n/en.pot | 42 ++++++-- src/components/setup/RouteSetup.jsx | 95 +++++++++++++++++++ src/components/station/airqo/AirQoPlugin.jsx | 43 ++++++++- .../airqo/styles/AirQoPlugin.module.css | 4 + 4 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 src/components/setup/RouteSetup.jsx diff --git a/i18n/en.pot b/i18n/en.pot index bb9df086..ffe2ff52 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-03-07T10:47:32.346Z\n" -"PO-Revision-Date: 2025-03-07T10:47:32.346Z\n" +"POT-Creation-Date: 2025-03-21T10:46:10.062Z\n" +"PO-Revision-Date: 2025-03-21T10:46:10.062Z\n" msgid "About this app" msgstr "About this app" @@ -508,6 +508,19 @@ msgstr "{{name}}: Monthly temperatures {{period}}" msgid "Normal temperature" msgstr "Normal temperature" +msgid "Resample" +msgstr "Resample" + +msgid "Weekly" +msgstr "Weekly" + +msgid "" +"Change from 16-days to weekly or monthly periods using linear " +"interpolation. Weekly periods are recommended when importing to DHIS2." +msgstr "" +"Change from 16-days to weekly or monthly periods using linear " +"interpolation. Weekly periods are recommended when importing to DHIS2." + msgid "{{name}}: {{band}} vegetation index 2024" msgstr "{{name}}: {{band}} vegetation index 2024" @@ -791,6 +804,21 @@ msgstr "End value" msgid "Color" msgstr "Color" +msgid "Route set up" +msgstr "Route set up" + +msgid "Your route will be created with the following information ..." +msgstr "Your route will be created with the following information ..." + +msgid "Token" +msgstr "Token" + +msgid "Cancel" +msgstr "Cancel" + +msgid "Save" +msgstr "Save" + msgid "How to configure this app" msgstr "How to configure this app" @@ -906,11 +934,14 @@ msgstr "" msgid "Time zone" msgstr "Time zone" +msgid "Missing route" +msgstr "Missing route" + msgid "AirQo token not set" msgstr "AirQo token not set" -msgid "Loading..." -msgstr "Loading..." +msgid "Create route" +msgstr "Create route" msgid "Air Quality PM2.5" msgstr "Air Quality PM2.5" @@ -1216,6 +1247,3 @@ msgstr "NASA LP DAAC at the USGS EROS Center / Google Earth Engine" msgid "No data found for the selected period" msgstr "No data found for the selected period" - -msgid "Weekly" -msgstr "Weekly" diff --git a/src/components/setup/RouteSetup.jsx b/src/components/setup/RouteSetup.jsx new file mode 100644 index 00000000..104a876b --- /dev/null +++ b/src/components/setup/RouteSetup.jsx @@ -0,0 +1,95 @@ +import { useDataMutation } from '@dhis2/app-runtime' +import i18n from '@dhis2/d2-i18n' +import { + Modal, + ModalTitle, + ModalContent, + ModalActions, + ButtonStrip, + Button, + InputField, +} from '@dhis2/ui' +import PropTypes from 'prop-types' +import React, { useState } from 'react' + +const mutation = { + resource: 'routes', + type: 'create', + data: ({ data }) => data, +} + +const RouteSetup = ({ + routeInfo, + isRouteSetupModalOpen, + setIsRouteSetupModalOpen, +}) => { + const [token, setToken] = useState('') + //token = CTBMDUGQBQD9SCHA + if (routeInfo.auth.type == 'api-query-params') { + routeInfo.auth.queryParams = { + token: token, + } + } else if (routeInfo.auth.type == 'api-headers') { + routeInfo.auth.headers = { + 'X-API-KEY': token, + } + } + const [saveRoute] = useDataMutation(mutation, { + variables: { + data: routeInfo, + }, + onComplete: () => { + setIsRouteSetupModalOpen(!isRouteSetupModalOpen) + }, + }) + + return ( + <> + + {i18n.t('Route set up')} + +

+ {i18n.t( + 'Your route will be created with the following information ...' + )} +

+
+
{JSON.stringify(routeInfo, null, 2)}
+
+ setToken(value)} + /> +
+ + + + + + +
+ + ) +} + +RouteSetup.propTypes = { + isRouteSetupModalOpen: PropTypes.bool.isRequired, + routeInfo: PropTypes.object.isRequired, + setIsRouteSetupModalOpen: PropTypes.func.isRequired, +} + +export default RouteSetup diff --git a/src/components/station/airqo/AirQoPlugin.jsx b/src/components/station/airqo/AirQoPlugin.jsx index d5ed6e20..4d7f769f 100644 --- a/src/components/station/airqo/AirQoPlugin.jsx +++ b/src/components/station/airqo/AirQoPlugin.jsx @@ -1,19 +1,32 @@ import { useDataEngine } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' +import { CircularLoader, NoticeBox, Button } from '@dhis2/ui' import PropTypes from 'prop-types' import { useState, useEffect } from 'react' import legend from '../../../data/pm2.5-legend.js' -import useAppSettings from '../../../hooks/useAppSettings.js' import useGetRoute from '../../../hooks/useGetRoute.js' import Chart from '../../explore/Chart.jsx' +import RouteSetup from '../../setup/RouteSetup.jsx' import getChartConfig from '../charts/AirQualityGauge.js' import styles from './styles/AirQoPlugin.module.css' const AirQoPlugin = ({ siteId }) => { const engine = useDataEngine() const [data, setData] = useState() - // const { settings = {} } = useAppSettings() const { route } = useGetRoute() + + const [isRouteSetupModalOpen, setIsRouteSetupModalOpen] = useState(false) + + const routeInfo = { + name: 'AirQo', + code: 'airqo', + disabled: false, + url: 'https://api.airqo.net/api/v2/devices/measurements/sites/**', + auth: { + type: 'api-query-params', + }, + } + useEffect(() => { if (route && route.id && !data) { const resource = `routes/${route.id}/run/${siteId}/recent` @@ -32,11 +45,33 @@ const AirQoPlugin = ({ siteId }) => { }, [route, data]) if (!route || !route.id) { - return
{i18n.t('AirQo token not set')}
+ return ( +
+ + {i18n.t('AirQo token not set')} + + +
+ + {isRouteSetupModalOpen && ( + + )} +
+
+ ) } if (!data) { - return
{i18n.t('Loading...')}
+ return } const { diff --git a/src/components/station/airqo/styles/AirQoPlugin.module.css b/src/components/station/airqo/styles/AirQoPlugin.module.css index c3a4e30d..674c45a8 100644 --- a/src/components/station/airqo/styles/AirQoPlugin.module.css +++ b/src/components/station/airqo/styles/AirQoPlugin.module.css @@ -19,3 +19,7 @@ line-height: 20px; color: #333; } + +.margin { + margin-top: 20px; +} \ No newline at end of file From 78bded1fe17cc502ed66da5b895c1e244b662a81 Mon Sep 17 00:00:00 2001 From: Abyot Asalefew Gizaw Date: Tue, 25 Mar 2025 14:26:02 +0100 Subject: [PATCH 10/10] chore: cleanup --- src/components/setup/RouteSetup.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/setup/RouteSetup.jsx b/src/components/setup/RouteSetup.jsx index 104a876b..887599ac 100644 --- a/src/components/setup/RouteSetup.jsx +++ b/src/components/setup/RouteSetup.jsx @@ -24,7 +24,6 @@ const RouteSetup = ({ setIsRouteSetupModalOpen, }) => { const [token, setToken] = useState('') - //token = CTBMDUGQBQD9SCHA if (routeInfo.auth.type == 'api-query-params') { routeInfo.auth.queryParams = { token: token,