Skip to content

Commit

Permalink
Merge branch 'main' into 186866310-station-distance-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
eireland committed Jan 31, 2024
2 parents 065e591 + ebf636a commit 8ef01fa
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 144 deletions.
6 changes: 6 additions & 0 deletions src/assets/images/icon-done.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/images/icon-progress-indicator.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
81 changes: 63 additions & 18 deletions src/components/App.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import "vars.scss";

.App {
padding: 12px;
box-sizing: border-box;
Expand Down Expand Up @@ -31,32 +33,75 @@

.divider {
width: 313px;
border: solid 1px #72bfca;
border: solid 1px $teal-light;
}

.footer {
display: flex;
align-items: center;
width: 100%;
justify-content: right;
justify-content: space-between;
margin: 21px 0;

.clear-data-button {
border: none;
color: #72bfca;
background-color: white;
cursor: pointer;
}
.get-data-button {
width: 82px;
height: 31px;
margin: 0 0 0 13px;
border-radius: 3px;
border: solid 1px #000;
background-color: #fff;
color: #000;
margin-right: 15px;
cursor: pointer;
.status-update {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 5px;
.status-icon {
.progress {
animation-name: spin;
animation-duration: 1000ms;
animation-iteration-count: infinite;

}
}
.status-message {
width: 120px;
&.success {
color: $teal-dark;
}
}
}

.clear-data-button {
border: none;
color: #72bfca;
background-color: white;
cursor: pointer;
font-family: 'Montserrat', sans-serif;
padding: 0px;
}
.get-data-button {
width: 82px;
height: 31px;
margin-left: 13px;
border-radius: 3px;
border: solid 1px #000;
background-color: #fff;
color: #000;
font-family: 'Montserrat', sans-serif;
cursor: pointer;
&:hover {
background-color: rgba(198, 198, 198, 0.25);
}
&:disabled {
cursor: default;
color: #a2a2a2;
border: solid 1px #a2a2a2;
}
}


}
}

@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
126 changes: 99 additions & 27 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import dayjs from "dayjs";
import { ClientNotification, addComponentListener, initializePlugin } from "@concord-consortium/codap-plugin-api";
import { LocationPicker } from "./location-picker";
import { DateRange } from "./date-range/date-range";
Expand All @@ -8,13 +9,16 @@ import { InfoModal } from "./info-modal";
import { useStateContext } from "../hooks/use-state";
import { adjustStationDataset, calculateDistance, getWeatherStations } from "../utils/getWeatherStations";
import { addNotificationHandler, createStationsDataset, guaranteeGlobal } from "../utils/codapHelpers";
import InfoIcon from "../assets/images/icon-info.svg";
import { useCODAPApi } from "../hooks/use-codap-api";
import { composeURL, formatData } from "../utils/noaaApiHelper";
// import { IDataType } from "../types";
import { StationDSName, globalMaxDate, globalMinDate } from "../constants";
import { geoLocSearch, geoNameSearch } from "../utils/geonameSearch";
import { DataReturnWarning } from "./data-return-warning";
import { IState } from "../types";
import InfoIcon from "../assets/images/icon-info.svg";
import ProgressIcon from "../assets/images/icon-progress-indicator.svg";
import DoneIcon from "../assets/images/icon-done.svg";
import WarningIcon from "../assets/images/icon-warning.svg";

import "./App.scss";

Expand All @@ -25,16 +29,51 @@ const kInitialDimensions = {
height: 670
};

interface IStatus {
status: "success" | "error" | "fetching";
message: string;
icon: JSX.Element;
}

export const App = () => {
const { state, setState } = useStateContext();
const { showModal, location, weatherStation, startDate, endDate, timezone, units, frequencies, selectedFrequency } = state;
const { filterItems, createNOAAItems } = useCODAPApi();
const [statusMessage, setStatusMessage] = useState("");
const [isFetching, setIsFetching] = useState(false);
const { showModal } = state;
const [disableGetData, setDisableGetData] = useState(true);
const [status, setStatus] = useState<IStatus>();
const weatherStations = getWeatherStations();

useEffect(() => {
initializePlugin({pluginName: kPluginName, version: kVersion, dimensions: kInitialDimensions});
const init = async () => {
const newState = await initializePlugin({pluginName: kPluginName, version: kVersion, dimensions: kInitialDimensions}) as IState;
// plugins in new documents return an empty object for the interactive state
// so ignore the new state and keep the default starting state in that case
if (Object.keys(newState || {}).length > 0) {
setState((draft) => {
draft.location = newState.location;
draft.selectedFrequency = newState.selectedFrequency;
draft.units = newState.units;
draft.timezone = newState.timezone;
draft.weatherStation = newState.weatherStation;
draft.weatherStationDistance = newState.weatherStationDistance;
draft.zoomMap = newState.zoomMap;
draft.frequencies = newState.frequencies;
draft.didUserSelectDate = newState.didUserSelectDate;
draft.isMapOpen = newState.isMapOpen;

const startDateStr = newState.startDate;
const endDateStr = newState.endDate;
if (startDateStr) {
draft.startDate = dayjs(startDateStr).toDate();
}
if (endDateStr) {
draft.endDate = dayjs(endDateStr).toDate();
}
});
}
};
init();

const stationSelectionHandler = async(req: any) =>{
if (req.values.operation === "selectCases") {
Expand All @@ -56,11 +95,11 @@ export const App = () => {
}
}
};

addNotificationHandler("notify",
`dataContextChangeNotice[${StationDSName}]`, async (req: any) => {
stationSelectionHandler(req);
});

const createMapListener = (listenerRes: ClientNotification) => {
const { values } = listenerRes;
if (values.operation === "delete" && values.type === "DG.MapView" && values.name === "US Weather Stations") {
Expand All @@ -75,13 +114,18 @@ export const App = () => {
}, []);

useEffect(() => {
const minDate = state.startDate || new Date( -5364662060);
const maxDate = state.endDate || new Date(Date.now());
const allDefined = (startDate && endDate && location && weatherStation);
setDisableGetData(!allDefined);
}, [location, endDate, startDate, weatherStation, timezone, units]);

useEffect(() => {
const minDate = startDate || new Date( -5364662060);
const maxDate = endDate || new Date(Date.now());
adjustStationDataset(weatherStations); //change max data to "present"
createStationsDataset(weatherStations); //send weather station data to CODAP
guaranteeGlobal(globalMinDate, Number(minDate)/1000);
guaranteeGlobal(globalMaxDate, Number(maxDate)/1000);
},[state.endDate, state.startDate, weatherStations]);
}, [endDate, startDate, weatherStations]);

const handleOpenInfo = () => {
setState(draft => {
Expand All @@ -90,8 +134,6 @@ export const App = () => {
};

const fetchSuccessHandler = async (data: any) => {
const {startDate, endDate, units, selectedFrequency,
weatherStation, timezone} = state;
const allDefined = (startDate && endDate && units && selectedFrequency &&
weatherStation && timezone);

Expand All @@ -108,21 +150,37 @@ export const App = () => {
const dataRecords = formatData(formatDataProps);
const items = Array.isArray(dataRecords) ? dataRecords : [dataRecords];
const filteredItems = filterItems(items);
setStatusMessage("Sending weather records to CODAP");
setStatus({
status: "fetching",
message: "Sending weather records to CODAP",
icon: <ProgressIcon className="status-icon progress"/>
});
await createNOAAItems(filteredItems).then(
function (result: any) {
setIsFetching(false);
setStatusMessage(`Retrieved ${filteredItems.length} cases`);
setStatus({
status: "success",
message: `Retrieved ${filteredItems.length} cases`,
icon: <DoneIcon/>
});
return result;
},
function (msg: string) {
setIsFetching(false);
setStatusMessage(msg);
setStatus({
status: "error",
message: msg,
icon: <WarningIcon/>
});
}
);
} else {
setIsFetching(false);
setStatusMessage("No data retrieved");
setStatus({
status: "error",
message: "No data retrieved",
icon: <WarningIcon/>
});
}
};

Expand All @@ -140,19 +198,24 @@ export const App = () => {
console.warn("fetchErrorHandler: " + resultText);
console.warn("fetchErrorHandler error: " + message);
setIsFetching(false);
setStatusMessage(message);
setStatus({
status: "error",
message,
icon: <WarningIcon/>
});
};

const handleGetData = async () => {
const { location, startDate, endDate, weatherStation, frequencies,
selectedFrequency, timezone, units } = state;
const attributes = frequencies[selectedFrequency].attrs.map(attr => attr.name);
const allDefined = (startDate && endDate && location && weatherStation && timezone);

if (allDefined) {
const attributes = frequencies[selectedFrequency].attrs.map(attr => attr.name);
const isEndDateAfterStartDate = endDate.getTime() >= startDate.getTime();
if (isEndDateAfterStartDate) {
setStatusMessage("Fetching weather records from NOAA");
setStatus({
status: "fetching",
message: "Fetching weather records from NOAA",
icon: <ProgressIcon className="status-icon progress"/>
});
const tURL = composeURL({
startDate,
endDate,
Expand Down Expand Up @@ -184,7 +247,11 @@ export const App = () => {
fetchErrorHandler(error, msg);
}
} else {
setStatusMessage("End date must be on or after start date");
setStatus({
status: "error",
message: "End date must be on or after start date",
icon: <WarningIcon/>
});
}
}
};
Expand All @@ -201,12 +268,17 @@ export const App = () => {
<DateRange />
<div className="divider" />
<AttributesSelector />
{state.frequencies[state.selectedFrequency].attrs.length > 0 && <AttributeFilter />}
{frequencies[selectedFrequency].attrs.length > 0 && <AttributeFilter />}
<div className="divider" />
<div className="footer">
{statusMessage && <div>{statusMessage}</div>}
<button className="clear-data-button">Clear Data</button>
<button className="get-data-button" disabled={isFetching} onClick={handleGetData}>Get Data</button>
<div className={"footer"}>
<div className="status-update">
<div className="status-icon">{status ? status.icon : ""}</div>
<div className={`status-message ${status?.status}`}>{status ? status.message : ""}</div>
</div>
<div>
<button className="clear-data-button">Clear Data</button>
<button className="get-data-button" disabled={isFetching || disableGetData} onClick={handleGetData}>Get Data</button>
</div>
</div>
{showModal === "info" && <InfoModal />}
{showModal === "data-return-warning" && <DataReturnWarning />}
Expand Down
Loading

0 comments on commit 8ef01fa

Please sign in to comment.