diff --git a/.github/workflows/ngafid.yaml b/.github/workflows/ngafid.yaml index c41ec430d..7c9fbb343 100644 --- a/.github/workflows/ngafid.yaml +++ b/.github/workflows/ngafid.yaml @@ -10,10 +10,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Setup JDK 17 + - name: Setup JDK 21 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'adopt' - name: Cache Maven packages diff --git a/.gitignore b/.gitignore index 9e6912288..478e7a55e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ db/db_info.php db/db_info email_info.txt package-lock.json +data/archive/* \ No newline at end of file diff --git a/db/create_tables.php b/db/create_tables.php index 814fa0c29..bed8e851c 100755 --- a/db/create_tables.php +++ b/db/create_tables.php @@ -573,6 +573,16 @@ );"; query_ngafid_db($query); + + $query = "CREATE TABLE `email_unsubscribe_tokens` ( + `token` VARCHAR(64) NOT NULL, + `user_id` INT(11) NOT NULL, + `expiration_date` DATETIME NOT NULL, + PRIMARY KEY (`token`), + FOREIGN KEY (`user_id`) REFERENCES user(`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1;"; + query_ngafid_db($query); + $query = "CREATE TABLE `stored_filters` ( `fleet_id` INT(11) NOT NULL, @@ -603,11 +613,13 @@ `api_secret` varchar(64) NOT NULL, `last_upload_time` timestamp ON UPDATE CURRENT_TIMESTAMP, `timeout` int(11) DEFAULT NULL, - `mutex` TINYINT DEFAULT 0, - KEY `airsync_fleet_id_fk` (`fleet_id`), - CONSTRAINT `airsync_fleet_id_fk` FOREIGN KEY (`fleet_id`) REFERENCES `fleet` (`id`) - );"; + `mutex` TINYINT DEFAULT 0, + PRIMARY KEY(`fleet_id`), + FOREIGN KEY(`fleet_id`) REFERENCES `fleet`(`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1;"; + + # CONSTRAINT `airsync_fleet_id_fk` FOREIGN KEY (`fleet_id`) REFERENCES `fleet` (`id`) query_ngafid_db($query); $query = "CREATE TABLE `airsync_imports` ( @@ -725,4 +737,5 @@ query_ngafid_db($query); } -?> \ No newline at end of file + +?> diff --git a/pom.xml b/pom.xml index 55fd749a9..408302827 100644 --- a/pom.xml +++ b/pom.xml @@ -136,8 +136,10 @@ maven-compiler-plugin 3.8.1 - 16 - 16 + + 20 + 20 + -Xlint:all -Xmaxwarns @@ -152,8 +154,8 @@ org.apache.maven.plugins maven-compiler-plugin - 16 - 16 + 20 + 20 diff --git a/src/main/java/org/ngafid/SendEmail.java b/src/main/java/org/ngafid/SendEmail.java index a9787cc0f..7046ae043 100644 --- a/src/main/java/org/ngafid/SendEmail.java +++ b/src/main/java/org/ngafid/SendEmail.java @@ -23,7 +23,6 @@ import javax.mail.internet.*; import javax.activation.*; - public class SendEmail { private static String password; @@ -33,6 +32,16 @@ public class SendEmail { private static final Logger LOG = Logger.getLogger(SendEmail.class.getName()); + private static final String baseURL = "https://ngafid.org"; + //private static final String baseURL = "https://ngafidbeta.rit.edu"; + private static final String unsubscribeURLTemplate = (baseURL + "/email_unsubscribe?id=__ID__&token=__TOKEN__"); + private static final java.sql.Date lastTokenFree = new java.sql.Date(0); + private static final int EMAIL_UNSUBSCRIBE_TOKEN_EXPIRATION_MONTHS = 3; + private static final int MS_PER_DAY = 86_400_000; + private static final int EXPIRATION_POLL_THRESHOLD_MS = (MS_PER_DAY); //Minimum number of milliseconds needed before trying to free old tokens + + + static { String enabled = System.getenv("NGAFID_EMAIL_ENABLED"); @@ -145,6 +154,75 @@ public boolean isValid() { } + + public static void freeExpiredUnsubscribeTokens() { + + Calendar calendar = Calendar.getInstance(); + java.sql.Date currentDate = new java.sql.Date(calendar.getTimeInMillis()); + + //Wait at least 24 hours before trying to free tokens again + long tokenDeltaTime = (currentDate.getTime() - lastTokenFree.getTime()); + LOG.info("Token timings: DELTA/CURRENT/LAST: " + tokenDeltaTime + " " + currentDate.getTime() + " " + lastTokenFree.getTime()); + if (tokenDeltaTime < EXPIRATION_POLL_THRESHOLD_MS) { + LOG.info("Not attempting to free expired tokens (only " + tokenDeltaTime + " / " + EXPIRATION_POLL_THRESHOLD_MS + " milliseconds have passed)"); + return; + } + lastTokenFree.setTime(currentDate.getTime()); + + try (Connection connection = Database.getConnection()) { + + String query = "DELETE FROM email_unsubscribe_tokens WHERE expiration_date < ?"; + PreparedStatement preparedStatement = connection.prepareStatement(query); + preparedStatement.setDate(1, currentDate); + preparedStatement.execute(); + + LOG.info("Freed expired email unsubscribe tokens"); + + } catch (Exception e) { + LOG.severe("Failed to free expired email unsubscribe tokens"); + } + + } + + private static String generateUnsubscribeToken(String recipientEmail, int userID) { + + //Generate a random string + String token = UUID.randomUUID().toString().replace("-", ""); + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, EMAIL_UNSUBSCRIBE_TOKEN_EXPIRATION_MONTHS); + java.sql.Date expirationDate = new java.sql.Date(calendar.getTimeInMillis()); + + try (Connection connection = Database.getConnection()) { + + String query = "INSERT INTO email_unsubscribe_tokens (token, user_id, expiration_date) VALUES (?, ?, ?)"; + PreparedStatement preparedStatement = connection.prepareStatement(query); + preparedStatement.setString(1, token); + preparedStatement.setInt(2, userID); + preparedStatement.setDate(3, expirationDate); + preparedStatement.execute(); + + } catch (Exception e) { + LOG.severe("Failed to generate token for email recipient: "+recipientEmail); + } + + //Log the token's expiration date + Calendar expirationCalendar = Calendar.getInstance(); + expirationCalendar.setTime(expirationDate); + String expirationDateString = + expirationCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.US) + + " " + expirationCalendar.get(Calendar.DAY_OF_MONTH) + + ", " + expirationCalendar.get(Calendar.YEAR) + + " " + expirationCalendar.get(Calendar.HOUR_OF_DAY) + + ":" + expirationCalendar.get(Calendar.MINUTE) + + ":" + expirationCalendar.get(Calendar.SECOND); + + LOG.info("Generated email unsubscribe token for " + recipientEmail + ": " + token + " (Expires at " + expirationDateString + ")"); + + return token; + + } + /** * Wrapper for sending an email to NGAFID admins * @param subject - subject of the email @@ -163,6 +241,9 @@ public static void sendEmail(ArrayList toRecipients, ArrayList b return; } + //Attempt to free expired tokens + freeExpiredUnsubscribeTokens(); + //System.out.println(String.format("Username: %s, PW: %s", username, password)); if (auth.isValid()) { @@ -195,6 +276,8 @@ public static void sendEmail(ArrayList toRecipients, ArrayList b try { + /* SEND TO toRecipients */ + // Create a default MimeMessage object. MimeMessage message = new MimeMessage(session); @@ -210,9 +293,11 @@ public static void sendEmail(ArrayList toRecipients, ArrayList b } //Check if the emailType is forced + boolean embedUnsubscribeURL = true; if (EmailType.isForced(emailType)) { System.out.println("Delivering FORCED email type: " + emailType); + embedUnsubscribeURL = false; } else if (!UserEmailPreferences.getEmailTypeUserState(toRecipient, emailType)) { //Check whether or not the emailType is enabled for the user @@ -220,26 +305,62 @@ public static void sendEmail(ArrayList toRecipients, ArrayList b } - System.out.println("EMAILING TO: " + toRecipient); + String bodyPersonalized = body; + if (embedUnsubscribeURL) { + + try { + int userID = UserEmailPreferences.getUserIDFromEmail(toRecipient); + + //Generate a token for the user to unsubscribe + String token = generateUnsubscribeToken(toRecipient, userID); + String unsubscribeURL = unsubscribeURLTemplate.replace("__ID__", Integer.toString(userID)).replace("__TOKEN__", token); - message.addRecipient(Message.RecipientType.TO, new InternetAddress(toRecipient)); + //Embed the Unsubscribe URL at the top of the email body + bodyPersonalized = ("Unsubscribe from non-critical NGAFID emails

" + body); + + } + catch(Exception e) { + LOG.severe("Recipient email "+toRecipient+" is not mapped in UserEmailPreferences, skipping unsubscribe URL"); + } } - for (String bccRecipient : bccRecipients) { - message.addRecipient(Message.RecipientType.BCC, new InternetAddress(bccRecipient)); + // Set Subject: header field + message.setSubject(subject); + message.setContent(bodyPersonalized, "text/html; charset=utf-8"); + + message.setRecipient(Message.RecipientType.TO, new InternetAddress(toRecipient)); + + // Send the message to the current recipient + System.out.println("sending message!"); + Transport.send(message); + + } + + /* SEND TO bccRecipients */ + if (!bccRecipients.isEmpty()) { + + // Create a default MimeMessage object. + message = new MimeMessage(session); + + // Set From: header field of the header. + message.setFrom(new InternetAddress(from)); + + for (String bccRecipient : bccRecipients) { + message.addRecipient(Message.RecipientType.BCC, new InternetAddress(bccRecipient)); } - // Set Subject: header field - message.setSubject(subject); + // Set Subject: header field + message.setSubject(subject); + message.setContent(body, "text/html; charset=utf-8"); - // Now set the actual message - message.setContent(body, "text/html; charset=utf-8"); + // Send the message to the current BCC recipients + System.out.println("sending message (BCC)!"); + Transport.send(message); + + } - // Send message - System.out.println("sending message!"); - Transport.send(message); - System.out.println("Sent message successfully...."); + System.out.println("Sent messages successfully...."); } catch (MessagingException mex) { mex.printStackTrace(); diff --git a/src/main/java/org/ngafid/WebServer.java b/src/main/java/org/ngafid/WebServer.java index b4682d1f6..7e3e603a1 100755 --- a/src/main/java/org/ngafid/WebServer.java +++ b/src/main/java/org/ngafid/WebServer.java @@ -13,15 +13,20 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.io.IOException; + +import java.time.*; +import java.io.PrintWriter; +import java.io.StringWriter; -import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.logging.LogManager; import java.util.logging.Logger; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.stream.*; +import com.google.gson.TypeAdapter; import static org.ngafid.SendEmail.sendAdminEmails; @@ -33,12 +38,33 @@ */ public final class WebServer { private static final Logger LOG = Logger.getLogger(WebServer.class.getName()); - public static final Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); public static final String NGAFID_UPLOAD_DIR; public static final String NGAFID_ARCHIVE_DIR; public static final String MUSTACHE_TEMPLATE_DIR; + public static class LocalDateTimeTypeAdapter extends TypeAdapter { + @Override + public void write(final JsonWriter jsonWriter, final LocalDateTime localDate) throws IOException { + if (localDate == null) { + jsonWriter.nullValue(); + return; + } + jsonWriter.value(localDate.toString()); + } + + @Override + public LocalDateTime read(final JsonReader jsonReader) throws IOException { + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.nextNull(); + return null; + } + return ZonedDateTime.parse(jsonReader.nextString()).toLocalDateTime(); + } + } + + public static final Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter()).create(); + static { if (System.getenv("NGAFID_UPLOAD_DIR") == null) { @@ -207,6 +233,13 @@ public static void main(String[] args) { Spark.get("/reset_password", new GetResetPassword(gson)); Spark.post("/reset_password", new PostResetPassword(gson)); + //to unsubscribe from emails + Spark.get("/email_unsubscribe", new GetEmailUnsubscribe(gson)); + Spark.after("/email_unsubscribe", (request, response) -> { + response.redirect("/"); + }); + + Spark.get("/protected/welcome", new GetWelcome(gson)); Spark.get("/protected/aggregate", new GetAggregate(gson)); Spark.post("/protected/event_counts", new PostEventCounts(gson, false)); @@ -217,6 +250,7 @@ public static void main(String[] args) { Spark.post("/protected/monthly_event_counts", new PostMonthlyEventCounts(gson)); Spark.get("/protected/severities", new GetSeverities(gson)); Spark.post("/protected/severities", new PostSeverities(gson)); + Spark.post("/protected/all_severities", new PostAllSeverities(gson)); Spark.get("/protected/event_statistics", new GetEventStatistics(gson)); Spark.get("/protected/waiting", new GetWaiting(gson)); @@ -325,6 +359,7 @@ public static void main(String[] args) { Spark.post("/protected/preferences_metric", new PostUserPreferencesMetric(gson)); Spark.post("/protected/update_tail", new PostUpdateTail(gson)); Spark.post("/protected/update_email_preferences", new PostUpdateUserEmailPreferences(gson)); + // Event Definition Management Spark.get("/protected/manage_event_definitions", new GetAllEventDefinitions(gson)); @@ -363,4 +398,4 @@ public static void main(String[] args) { LOG.info("NGAFID WebServer initialization complete."); } -} \ No newline at end of file +} diff --git a/src/main/java/org/ngafid/accounts/AirSyncFleet.java b/src/main/java/org/ngafid/accounts/AirSyncFleet.java index 66cc1ce22..8d808054b 100644 --- a/src/main/java/org/ngafid/accounts/AirSyncFleet.java +++ b/src/main/java/org/ngafid/accounts/AirSyncFleet.java @@ -30,7 +30,7 @@ public class AirSyncFleet extends Fleet { private AirSyncAuth authCreds; private List aircraft; - private LocalDateTime lastQueryTime; + private transient LocalDateTime lastQueryTime; //timeout in minutes private int timeout = -1; @@ -89,6 +89,59 @@ private AirSyncFleet(ResultSet resultSet) throws SQLException { } } + public boolean getOverride(Connection connection) throws SQLException { + String query = """ + SELECT override from airsync_fleet_info WHERE + fleet_id = ? + """; + + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setInt(1, getId()); + + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + return rs.getInt("override") != 0; + } else { + return false; + } + } + } + } + + public void setOverride(Connection connection, boolean value) throws SQLException { + String query = """ + UPDATE airsync_fleet_info SET override = ? WHERE fleet_id = ? + """; + + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setInt(1, value ? 1 : 0); + statement.setInt(2, getId()); + + statement.executeUpdate(); + } + } + + public boolean compareAndSetMutex(Connection connection, int expected, int newValue) throws SQLException { + String queryText = """ + SELECT fleet_id, mutex FROM airsync_fleet_info WHERE + fleet_id = ? + AND mutex = ? FOR UPDATE + """; + + PreparedStatement query = connection.prepareStatement(queryText, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); + query.setInt(1, getId()); + query.setInt(2, expected); + + ResultSet rs = query.executeQuery(); + if (rs.next()) { + rs.updateInt("mutex", newValue); + rs.updateRow(); + return true; + } else { + return false; + } + } + /** * Semaphore-style (P) mutex that allows for an entity (i.e. the daemon or user) to * ask AirSync for updates @@ -103,25 +156,7 @@ private AirSyncFleet(ResultSet resultSet) throws SQLException { * @throws SQLException if the DMBS has an issue */ public boolean lock(Connection connection) throws SQLException { - String sql = "SELECT mutex FROM airsync_fleet_info WHERE fleet_id = ?"; - PreparedStatement query = connection.prepareStatement(sql); - - query.setInt(1, super.getId()); - ResultSet resultSet = query.executeQuery(); - - if (resultSet.next() && !resultSet.getBoolean(1)) { - sql = "UPDATE airsync_fleet_info SET mutex = 1 WHERE fleet_id = ?"; - query = connection.prepareStatement(sql); - - query.setInt(1, super.getId()); - query.executeUpdate(); - query.close(); - - return true; - } - - query.close(); - return false; + return compareAndSetMutex(connection, 0, 1); } /** @@ -134,14 +169,8 @@ public boolean lock(Connection connection) throws SQLException { * * @throws SQLException if the DMBS has an issue */ - public void unlock(Connection connection) throws SQLException { - String sql = "UPDATE airsync_fleet_info SET mutex = 0 WHERE fleet_id = ?"; - PreparedStatement query = connection.prepareStatement(sql); - - query.setInt(1, super.getId()); - query.executeUpdate(); - - query.close(); + public boolean unlock(Connection connection) throws SQLException { + return compareAndSetMutex(connection, 1, 0); } private LocalDateTime getLastQueryTime(Connection connection) throws SQLException { @@ -244,6 +273,8 @@ public static String getTimeout(Connection connection, int fleetId) throws SQLEx * @throws SQLException if there is a DBMS issue */ public boolean isQueryOutdated(Connection connection) throws SQLException { + LOG.info("dur " + Duration.between(getLastQueryTime(connection), LocalDateTime.now()).toMinutes()); + LOG.info("dur " + getTimeout(connection)); return (Duration.between(getLastQueryTime(connection), LocalDateTime.now()).toMinutes() >= getTimeout(connection)); } @@ -371,27 +402,24 @@ public AirSyncAuth getAuth() { * * @return a {@link List} of the {@link AirSyncAircraft} in this fleet. */ - public List getAircraft() { + public List getAircraft() throws IOException { if (aircraft == null) { - try { - HttpsURLConnection connection = (HttpsURLConnection) new URL(AirSyncEndpoints.AIRCRAFT).openConnection(); - - connection.setRequestMethod("GET"); - connection.setDoOutput(true); - connection.setRequestProperty("Authorization", this.authCreds.bearerString()); - - InputStream is = connection.getInputStream(); - byte [] respRaw = is.readAllBytes(); - - String resp = new String(respRaw).replaceAll("tail_number", "tailNumber"); - - Type target = new TypeToken>(){}.getType(); - this.aircraft = gson.fromJson(resp, target); - - for (AirSyncAircraft a : aircraft) a.initialize(this); - } catch (IOException ie) { - AirSync.handleAirSyncAPIException(ie, this.authCreds); - } + HttpsURLConnection connection = (HttpsURLConnection) new URL(AirSyncEndpoints.AIRCRAFT).openConnection(); + + connection.setRequestMethod("GET"); + connection.setDoOutput(true); + connection.setRequestProperty("Authorization", this.authCreds.bearerString()); + + InputStream is = connection.getInputStream(); + byte [] respRaw = is.readAllBytes(); + + String resp = new String(respRaw).replaceAll("tail_number", "tailNumber"); + + Type target = new TypeToken>(){}.getType(); + System.out.println(resp); + this.aircraft = gson.fromJson(resp, target); + + for (AirSyncAircraft a : aircraft) a.initialize(this); } return this.aircraft; diff --git a/src/main/java/org/ngafid/accounts/EmailType.java b/src/main/java/org/ngafid/accounts/EmailType.java index c8e78a1a5..999c4ce79 100644 --- a/src/main/java/org/ngafid/accounts/EmailType.java +++ b/src/main/java/org/ngafid/accounts/EmailType.java @@ -1,11 +1,5 @@ package org.ngafid.accounts; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.FileWriter; -import java.io.IOException; - import java.util.*; import java.util.logging.Logger; @@ -75,10 +69,20 @@ public enum EmailType { //Store number of email types private final static int emailTypeCount = values().length; + private final static int emailTypeNonForcedCount; private static Logger LOG = Logger.getLogger(EmailType.class.getName()); static { + + int emailTypeNonForcedCounter = 0; + for (EmailType emailType : EmailType.values()) { + if (!isForced(emailType)) { + emailTypeNonForcedCounter++; + } + } + emailTypeNonForcedCount = emailTypeNonForcedCounter; + LOG.info("EmailType class loaded..."); LOG.info("Detected " + emailTypeCount + " email types"); } @@ -97,6 +101,9 @@ public String getType() { public static int getEmailTypeCount() { return emailTypeCount; } + public static int getEmailTypeCountNonForced() { + return emailTypeNonForcedCount; + } public static EmailType[] getAllTypes() { return values(); @@ -115,6 +122,9 @@ public static String[] getEmailTypeKeysRecent(boolean doRefresh) { public static boolean isForced(EmailType emailType) { return emailType.getType().contains("FORCED"); } + public static boolean isForced(String emailTypeName) { + return emailTypeName.contains("FORCED"); + } //PHP Execution diff --git a/src/main/java/org/ngafid/accounts/User.java b/src/main/java/org/ngafid/accounts/User.java index 3d15a0e34..67a723618 100644 --- a/src/main/java/org/ngafid/accounts/User.java +++ b/src/main/java/org/ngafid/accounts/User.java @@ -393,7 +393,7 @@ public static UserEmailPreferences getUserEmailPreferences(Connection connection } - int emailTypeCountExpected = EmailType.getEmailTypeCount(); + int emailTypeCountExpected = EmailType.getEmailTypeCountNonForced(); LOG.info("Checking email preferences for user with ID (" + userId + ")... Expected: " + emailTypeCountExpected + " / Actual: " + emailPreferencesCount); diff --git a/src/main/java/org/ngafid/accounts/UserEmailPreferences.java b/src/main/java/org/ngafid/accounts/UserEmailPreferences.java index 7f7416b6f..5c6040fae 100644 --- a/src/main/java/org/ngafid/accounts/UserEmailPreferences.java +++ b/src/main/java/org/ngafid/accounts/UserEmailPreferences.java @@ -25,8 +25,9 @@ public class UserEmailPreferences { private HashMap emailTypesUser; private String[] emailTypesKeys; - private static HashMap users = new HashMap(); - private static HashMap> emailTypesUsers = new HashMap>(); + private static HashMap users = new HashMap<>(); + private static HashMap userIDs = new HashMap<>(); + private static HashMap> emailTypesUsers = new HashMap<>(); private static final Logger LOG = Logger.getLogger(UserEmailPreferences.class.getName()); @@ -48,7 +49,14 @@ public UserEmailPreferences(int userId, HashMap emailTypesUser) public static void addUser(Connection connection, User user) throws SQLException { - users.put(user.getId(), user); + int userID = user.getId(); + String userEmail = user.getEmail(); + + //user email --> userId + userIDs.put(userEmail, userID); + + //userId --> User + users.put(userID, user); emailTypesUsers.put( user.getEmail(), user.getUserEmailPreferences(connection).getEmailTypesUser() ); @@ -58,6 +66,10 @@ public static User getUser(int userId) { return users.get(userId); } + public static int getUserIDFromEmail(String userEmail) { + return userIDs.get(userEmail); + } + public HashMap getEmailTypesUser() { return emailTypesUser; } diff --git a/src/main/java/org/ngafid/events/Event.java b/src/main/java/org/ngafid/events/Event.java index 2847bca98..0f5f96f26 100644 --- a/src/main/java/org/ngafid/events/Event.java +++ b/src/main/java/org/ngafid/events/Event.java @@ -369,11 +369,24 @@ public static HashMap> getEvents(Connection connection, eventsByAirframe.put(airframe, new ArrayList()); } - String query = "SELECT id FROM event_definitions WHERE (fleet_id = 0 OR fleet_id = ?) AND name LIKE ? ORDER BY name"; + String query; + PreparedStatement preparedStatement; + + if (eventName.equals("ANY Event")) { + + LOG.info("[EX] Getting ALL events for fleet with ID: " + fleetId); + + query = "SELECT id FROM event_definitions WHERE fleet_id = 0 OR fleet_id = ? ORDER BY name"; + preparedStatement = connection.prepareStatement(query); + preparedStatement.setInt(1, fleetId); + + } else { + query = "SELECT id FROM event_definitions WHERE (fleet_id = 0 OR fleet_id = ?) AND name LIKE ? ORDER BY name"; + preparedStatement = connection.prepareStatement(query); + preparedStatement.setInt(1, fleetId); + preparedStatement.setString(2, eventName); + } - PreparedStatement preparedStatement = connection.prepareStatement(query); - preparedStatement.setInt(1, fleetId); - preparedStatement.setString(2, eventName); LOG.info(preparedStatement.toString()); ResultSet resultSet = preparedStatement.executeQuery(); diff --git a/src/main/java/org/ngafid/flights/AirSync.java b/src/main/java/org/ngafid/flights/AirSync.java index a5361fd7b..981c878d1 100644 --- a/src/main/java/org/ngafid/flights/AirSync.java +++ b/src/main/java/org/ngafid/flights/AirSync.java @@ -87,33 +87,11 @@ public static void crashGracefully(Exception e) { String message = sb.toString(); System.err.println(message); - sendAdminCrashNotification(message); + // sendAdminCrashNotification(message); System.exit(1); } - /** - * Gets the shortest wait time of all fleets so the daemon can sleep "smartly" - * - * @param connection the DBMS connection - * - * @throws SQLException if there is a DBMS issue - */ - private static long getWaitTime(Connection connection) throws SQLException { - String sql = "SELECT MIN(timeout - TIMESTAMPDIFF(MINUTE, last_upload_time, CURRENT_TIMESTAMP)) AS remaining_time FROM airsync_fleet_info"; - PreparedStatement query = connection.prepareStatement(sql); - - ResultSet resultSet = query.executeQuery(); - - long waitTime = DEFAULT_WAIT_TIME; - if (resultSet.next()) { - waitTime = 1000 * 60 * resultSet.getLong(1); - } - - return Math.max(waitTime, 0); - } - - /** * This daemon's entry point. * This is where the logic for how the daemon operates will be defined. @@ -123,7 +101,6 @@ private static long getWaitTime(Connection connection) throws SQLException { public static void main(String [] args) { LOG.info("AirSync daemon started"); - try { LocalDateTime now = LocalDateTime.now(); String timeStamp = new String() + now.getYear() + now.getMonthValue() + now.getDayOfMonth() + "-" + now.getHour() + now.getMinute() + now.getSecond(); @@ -141,27 +118,27 @@ public static void main(String [] args) { for (AirSyncFleet fleet : airSyncFleets) { String logMessage = "Fleet " + fleet.getName() + ": %s"; + LOG.info("Override = " + fleet.getOverride(connection)); - //if (fleet.isQueryOutdated(connection)) { + if (fleet.getOverride(connection) || fleet.isQueryOutdated(connection)) { LOG.info(String.format(logMessage, "past timeout! Checking with the AirSync servers now.")); + fleet.setOverride(connection, false); + + String status = fleet.update(connection); - if (fleet.lock(connection)) { - String status = fleet.update(connection); - fleet.unlock(connection); - - LOG.info("Update status: " + status); - } else { - LOG.info("Unable to lock fleet " + fleet.toString() + ", will skip for now. This usually means a user has requested to manually update the fleet."); - } - //} else { - //LOG.info(String.format(logMessage, "does not need to be updated, will skip.")); - //} + LOG.info("Update status: " + status); + } } - long waitTime = getWaitTime(connection); - LOG.info("Sleeping for " + waitTime + "ms."); + long waitTime = 30000; + LOG.info("Sleeping for " + waitTime / 1000 + "s."); Thread.sleep(waitTime); } + } catch (IOException e) { + String message = e.getMessage(); + LOG.info("Got exception: " + e.getMessage()); + if (message.contains("HTTP response code: 40")) + LOG.info("HINT: Your bearer token is either expired, or you are rate limited"); } catch (Exception e) { crashGracefully(e); } diff --git a/src/main/java/org/ngafid/flights/AirSyncEndpoints.java b/src/main/java/org/ngafid/flights/AirSyncEndpoints.java index 49b9ad00f..9956da4ec 100644 --- a/src/main/java/org/ngafid/flights/AirSyncEndpoints.java +++ b/src/main/java/org/ngafid/flights/AirSyncEndpoints.java @@ -10,28 +10,23 @@ public interface AirSyncEndpoints { //NOTE: DEV endpoints //comment this block out and uncomment the below for prod endpoints - //Authentication - //public static final String AUTH = "https://service-dev.air-sync.com/partner_api/v1/auth/"; - - // Logs with format arguments (aircraft_id, page_num, num_results) - //public static final String SINGLE_LOG = "https://service-dev.air-sync.com/partner_api/v1/logs/%d"; - //public static final String ALL_LOGS = "https://service-dev.air-sync.com/partner_api/v1/aircraft/%d/logs?page=%d&number_of_results=%d"; - //public static final String ALL_LOGS_BY_TIME = "https://service-dev.air-sync.com/partner_api/v1/aircraft/%d/logs?page=%d&number_of_results=%d×tamp_uploaded=%s,%s"; - // - //Aircraft info - //public static final String AIRCRAFT = "https://service-dev.air-sync.com/partner_api/v1/aircraft/"; - - - //NOTE: PROD endpoints (default) - //comment this block out and uncomment the above for dev endpoints + // Use this to swap to sandbox / dev api + public static final String AIRSYNC_ROOT = "https://api.air-sync.com/partner_api/v1"; + // public static final String AIRSYNC_ROOT = "https://service-dev.air-sync.com/partner_api/v1"; //Authentication - public static final String AUTH = "https://api.air-sync.com/partner_api/v1/auth/"; + public static final String AUTH = + AIRSYNC_ROOT + "/auth/"; // Logs with format arguments (aircraft_id, page_num, num_results) - public static final String SINGLE_LOG = "https://api.air-sync.com/partner_api/v1/logs/%d"; - public static final String ALL_LOGS = "https://api.air-sync.com/partner_api/v1/aircraft/%d/logs?page=%d&number_of_results=%d"; - public static final String ALL_LOGS_BY_TIME = "https://api.air-sync.com/partner_api/v1/aircraft/%d/logs?page=%d&number_of_results=%d×tamp_uploaded=%s,%s"; - ////Aircraft info - public static final String AIRCRAFT = "https://api.air-sync.com/partner_api/v1/aircraft/"; + public static final String SINGLE_LOG = + AIRSYNC_ROOT + "/logs/%d"; + public static final String ALL_LOGS = + AIRSYNC_ROOT + "/aircraft/%d/logs?page=%d&number_of_results=%d"; + public static final String ALL_LOGS_BY_TIME = + AIRSYNC_ROOT + "/aircraft/%d/logs?page=%d&number_of_results=%d×tamp_uploaded=%s,%s"; + + //Aircraft info + public static final String AIRCRAFT = + AIRSYNC_ROOT + "/aircraft/"; } diff --git a/src/main/java/org/ngafid/flights/Flight.java b/src/main/java/org/ngafid/flights/Flight.java index f1a4bb2e0..321207128 100644 --- a/src/main/java/org/ngafid/flights/Flight.java +++ b/src/main/java/org/ngafid/flights/Flight.java @@ -1658,6 +1658,40 @@ public void calculateScanEagleAltMSL(Connection connection, String outAltMSLColu doubleTimeSeries.put(outAltMSLColumnName, outAltMSL); } + public void calculateBeechcraftAltMSL(Connection connection, String outAltMSLColumnName, String inAltMSLColumnName) throws SQLException { + DoubleTimeSeries inAltMSL = doubleTimeSeries.get(inAltMSLColumnName); + DoubleTimeSeries outAltMSL = new DoubleTimeSeries(connection, outAltMSLColumnName, "feet"); + + for (int i = 0; i < inAltMSL.size(); i++) { + outAltMSL.add(inAltMSL.get(i) * 32.8084); //convert decameters to feet + } + doubleTimeSeries.put(outAltMSLColumnName, outAltMSL); + } + + public void calculateBeechcraftLatLon(Connection connection, String inLatColumnName, String inLonColumnName, String outLatColumnName, String outLonColumnName) throws SQLException { + DoubleTimeSeries inLatitudes = doubleTimeSeries.get(inLatColumnName); + DoubleTimeSeries inLongitudes = doubleTimeSeries.get(inLonColumnName); + + DoubleTimeSeries latitude = new DoubleTimeSeries(connection, outLatColumnName, "degrees"); + DoubleTimeSeries longitude = new DoubleTimeSeries(connection, outLonColumnName, "degrees"); + + for (int i = 0; i < inLatitudes.size(); i++) { + double inLat = inLatitudes.get(i); + double inLon = inLongitudes.get(i); + double outLat = inLat; + double outLon = inLon; + + if (inLat == 0) outLat = Double.NaN; + if (inLon == 0) outLon = Double.NaN; + + latitude.add(outLat); + longitude.add(outLon); + } + + doubleTimeSeries.put(outLatColumnName, latitude); + doubleTimeSeries.put(outLonColumnName, longitude); + } + public void calculateScanEagleStartEndTime(String timeColumnName, String latColumnName, String lonColumnName) throws MalformedFlightFileException { StringTimeSeries times = stringTimeSeries.get(timeColumnName); DoubleTimeSeries latitudes = doubleTimeSeries.get(latColumnName); @@ -1856,12 +1890,15 @@ private void initialize(Connection connection, InputStream inputStream) throws F airframeName = "ScanEagle"; airframeType = "UAS Fixed Wing"; + } else if(fileInformation.startsWith("Aircraft ID")) { + airframeName = "Beechcraft C90A King Air"; + airframeType = "Fixed Wing"; } else { throw new FatalFlightFileException("First line of the flight file should begin with a '#' and contain flight recorder information."); } } - if (airframeName != null && airframeName.equals("ScanEagle")) { + if (airframeName != null && (airframeName.equals("ScanEagle") || airframeName.equals("Beechcraft C90A King Air"))) { //need a custom method to process ScanEagle data because the column //names are different and there is no header info @@ -1872,8 +1909,13 @@ private void initialize(Connection connection, InputStream inputStream) throws F System.out.println("end date: '" + startDateTime + "'"); //UND doesn't have the systemId for UAS anywhere in the filename or file (sigh) - suggestedTailNumber = "N" + filenameParts[1] + "ND"; - systemId = suggestedTailNumber; + if (airframeName.equals("Beechcraft C90A King Air")){ + systemId = "N709EA"; + tailNumber = "N709EA"; + } else { + suggestedTailNumber = "N" + filenameParts[1] + "ND"; + systemId = suggestedTailNumber; + } System.out.println("suggested tail number: '" + suggestedTailNumber + "'"); System.out.println("system id: '" + systemId + "'"); @@ -1950,7 +1992,8 @@ private void initialize(Connection connection, InputStream inputStream) throws F airframeName.equals("Quest Kodiak 100") || airframeName.equals("Cessna 400") || airframeName.equals("Beechcraft A36/G36") || - airframeName.equals("Beechcraft G58")) { + airframeName.equals("Beechcraft G58") || + airframeName.equals("Beechcraft C90A King Air")) { airframeType = "Fixed Wing"; } else if (airframeName.equals("R44") || airframeName.equals("Robinson R44")) { airframeName = "R44"; @@ -1980,7 +2023,7 @@ private void initialize(Connection connection, InputStream inputStream) throws F throw new FatalFlightFileException("Flight information (first line of flight file) does not contain an 'system_id' key/value pair."); System.err.println("detected airframe type: '" + systemId + "'"); - if (airframeName.equals("ScanEagle")) { + if (airframeName.equals("ScanEagle") || airframeName.equals("Beechcraft C90A King Air")) { //for the ScanEagle, the first line is the headers of the columns String headersLine = fileInformation; //System.out.println("Headers line is: " + headersLine); @@ -2197,8 +2240,9 @@ private void process(Connection connection) throws IOException, FatalFlightFileE //this is all we can do with the scan eagle data until we //get better lat/lon info hasCoords = true; - } else if (airframeName.equals("")) { - + } else if (airframeName.equals("Beechcraft C90A King Air")) { + calculateBeechcraftAltMSL(connection, "AltMSL", "Altitude(decameters)"); + calculateBeechcraftLatLon(connection, "Latitude(DD)", "Longitude(DD)", "Latitude", "Longitude"); } else { calculateStartEndTime("Lcl Date", "Lcl Time", "UTCOfst"); } diff --git a/src/main/java/org/ngafid/routes/GetAirSyncUploads.java b/src/main/java/org/ngafid/routes/GetAirSyncUploads.java index 11a4962e7..33a594205 100644 --- a/src/main/java/org/ngafid/routes/GetAirSyncUploads.java +++ b/src/main/java/org/ngafid/routes/GetAirSyncUploads.java @@ -71,6 +71,8 @@ public Object handle(Request request, Response response) { int pageSize = 10; String timestamp = fleet.getLastUpdateTime(connection); + if (fleet.getOverride(connection)) + timestamp = "Pending"; int totalUploads = AirSyncImport.getNumUploads(connection, fleet.getId(), null); List uploads = AirSyncImport.getUploads(connection, fleet.getId(), " LIMIT "+ (currentPage * pageSize) + "," + pageSize); int numberPages = totalUploads / pageSize; diff --git a/src/main/java/org/ngafid/routes/GetEmailUnsubscribe.java b/src/main/java/org/ngafid/routes/GetEmailUnsubscribe.java new file mode 100644 index 000000000..910f57075 --- /dev/null +++ b/src/main/java/org/ngafid/routes/GetEmailUnsubscribe.java @@ -0,0 +1,105 @@ +package org.ngafid.routes; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.lang.reflect.Type; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + + +import spark.Route; +import spark.Request; +import spark.Response; +import spark.Session; +import spark.Spark; + +import org.ngafid.Database; +import org.ngafid.accounts.User; +import org.ngafid.accounts.UserPreferences; +import org.ngafid.accounts.UserEmailPreferences; +import org.ngafid.accounts.EmailType; +import org.ngafid.flights.DoubleTimeSeries; + +import static org.ngafid.flights.calculations.Parameters.*; + + + +public class GetEmailUnsubscribe implements Route { + + private static final Logger LOG = Logger.getLogger(GetEmailUnsubscribe.class.getName()); + private Gson gson; + private static Connection connection = Database.getConnection(); + + public GetEmailUnsubscribe(Gson gson) { + this.gson = gson; + LOG.info("email unsubscribe route initialized."); + } + + @Override + public Object handle(Request request, Response response) throws SQLException { + + + int id = Integer.parseInt( request.queryParams("id") ); + String token = request.queryParams("token"); + + LOG.info("Attempting to unsubscribe from emails... (id: "+id+", token: "+token+")"); + + + //Check if the token is valid + try { + + PreparedStatement query = connection.prepareStatement("SELECT * FROM email_unsubscribe_tokens WHERE token=? AND user_id=?"); + query.setString(1, token); + query.setInt(2, id); + ResultSet resultSet = query.executeQuery(); + if (!resultSet.next()) { + String exceptionMessage = "Provided token/id pairing was not found: ("+token+", "+id+"), may have already expired or been used"; + LOG.severe(exceptionMessage); + throw new Exception(exceptionMessage); + } + + } catch (Exception e) { + e.printStackTrace(); + return gson.toJson(new ErrorResponse(e)); + } + + //Remove the token from the database + PreparedStatement queryTokenRemoval; + queryTokenRemoval = connection.prepareStatement("DELETE FROM email_unsubscribe_tokens WHERE token=? AND user_id=?"); + queryTokenRemoval.setString(1, token); + queryTokenRemoval.setInt(2, id); + queryTokenRemoval.executeUpdate(); + + //Set all non-forced email preferences to 0 in the database + PreparedStatement queryClearPreferences; + queryClearPreferences = connection.prepareStatement("SELECT * FROM email_preferences WHERE user_id=?"); + queryClearPreferences.setInt(1, id); + ResultSet resultSet = queryClearPreferences.executeQuery(); + + while (resultSet.next()) { + + String emailType = resultSet.getString("email_type"); + if (EmailType.isForced(emailType)) { + continue; + } + + PreparedStatement update = connection.prepareStatement("UPDATE email_preferences SET enabled=0 WHERE user_id=? AND email_type=?"); + update.setInt(1, id); + update.setString(2, emailType); + update.executeUpdate(); + } + + return "Successfully unsubscribed from emails..."; + + } + +} \ No newline at end of file diff --git a/src/main/java/org/ngafid/routes/PostAllSeverities.java b/src/main/java/org/ngafid/routes/PostAllSeverities.java new file mode 100644 index 000000000..0622604d1 --- /dev/null +++ b/src/main/java/org/ngafid/routes/PostAllSeverities.java @@ -0,0 +1,89 @@ +package org.ngafid.routes; + +import java.time.LocalDate; + +import java.util.List; +import java.util.ArrayList; +import java.util.logging.Logger; +import java.util.HashMap; + + +import java.sql.Connection; +import java.sql.SQLException; + +import com.google.gson.Gson; + +import spark.Route; +import spark.Request; +import spark.Response; +import spark.Session; +import spark.Spark; + +import org.ngafid.Database; +import org.ngafid.WebServer; +import org.ngafid.accounts.User; +import org.ngafid.events.Event; +import org.ngafid.events.EventStatistics; +import org.ngafid.common.*; + +public class PostAllSeverities implements Route { + private static final Logger LOG = Logger.getLogger(PostAllSeverities.class.getName()); + private Gson gson; + + public PostAllSeverities(Gson gson) { + this.gson = gson; + LOG.info("post " + this.getClass().getName() + " initalized"); + } + + + @Override + public Object handle(Request request, Response response) { + LOG.info("handling " + this.getClass().getName() + " route"); + + String startDate = request.queryParams("startDate"); + String endDate = request.queryParams("endDate"); + String eventNames = request.queryParams("eventNames"); + String tagName = request.queryParams("tagName"); + final Session session = request.session(); + User user = session.attribute("user"); + int fleetId = user.getFleetId(); + + //check to see if the user has upload access for this fleet. + if (!user.hasViewAccess(fleetId)) { + LOG.severe("INVALID ACCESS: user did not have access view imports for this fleet."); + Spark.halt(401, "User did not have access to view imports for this fleet."); + return null; + } + + try { + + Connection connection = Database.getConnection(); + String[] eventNamesArray = eventNames.split(","); + HashMap> > eventMap = new HashMap<>(); + + for (String eventName : eventNamesArray) { + + //Remove leading and trailing quotes + eventName = eventName.replace("\"", ""); + + //Remove brackets + eventName = eventName.replace("[", ""); + eventName = eventName.replace("]", ""); + + //Remove trailing spaces + eventName = eventName.trim(); + + if (eventName.equals("ANY Event")) + continue; + + HashMap> events = Event.getEvents(connection, fleetId, eventName, LocalDate.parse(startDate), LocalDate.parse(endDate), tagName); + eventMap.put(eventName, events); + } + + return gson.toJson(eventMap); + + } catch (SQLException e) { + return gson.toJson(new ErrorResponse(e)); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/ngafid/routes/PostManualAirSyncUpdate.java b/src/main/java/org/ngafid/routes/PostManualAirSyncUpdate.java index 0f2887c94..03e9c6131 100644 --- a/src/main/java/org/ngafid/routes/PostManualAirSyncUpdate.java +++ b/src/main/java/org/ngafid/routes/PostManualAirSyncUpdate.java @@ -58,30 +58,13 @@ public Object handle(Request request, Response response) { try { AirSyncFleet fleet = AirSyncFleet.getAirSyncFleet(connection, fleetId); - if (fleet.lock(connection)) { - LOG.info("Beginning AirSync update process!"); - String status = fleet.update(connection); - LOG.info("AirSync update process complete! Status: " + status); - - fleet.unlock(connection); - - StringBuilder sb = new StringBuilder("

NGAFID Manual AirSync Update Report

"); - sb.append("

"); - sb.append(status); - sb.append("

"); - - //Send email - ArrayList emailList = new ArrayList<>(); - emailList.add(user.getEmail()); - - SendEmail.sendEmail(emailList, new ArrayList(), "NGAFID AirSync Update Report", sb.toString(), EmailType.AIRSYNC_UPDATE_REPORT); - String lastUpdateTime = fleet.getLastUpdateTime(connection); - - return gson.toJson(lastUpdateTime); - } else { - LOG.info("Unable to enter critical section!"); - return gson.toJson("UNABLE_MUTEX"); - } + LOG.info("Beginning AirSync update process!"); + String status = fleet.update(connection); + LOG.info("AirSync update process complete! Status: " + status); + + fleet.setOverride(connection, true); + + return gson.toJson("OK"); } catch (Exception e) { return gson.toJson(new ErrorResponse(e)); } diff --git a/src/main/java/org/ngafid/routes/PostUploads.java b/src/main/java/org/ngafid/routes/PostUploads.java index 87632d965..7cdaa5f64 100644 --- a/src/main/java/org/ngafid/routes/PostUploads.java +++ b/src/main/java/org/ngafid/routes/PostUploads.java @@ -51,10 +51,10 @@ public Object handle(Request request, Response response) { int fleetId = user.getFleetId(); - //check to see if the user has upload access for this fleet. - if (!user.hasUploadAccess(fleetId)) { - LOG.severe("INVALID ACCESS: user did not have access to upload flights for this fleet."); - Spark.halt(401, "User did not have access to upload flights for this fleet."); + //check to see if the user has view access for this fleet. + if (!user.hasViewAccess(fleetId)) { + LOG.severe("INVALID ACCESS: user did not have access to view flights for this fleet."); + Spark.halt(401, "User did not have access to view flights for this fleet."); return null; } diff --git a/src/main/javascript/airsync_uploads.js b/src/main/javascript/airsync_uploads.js index 16cc2ee75..1ce0055a2 100644 --- a/src/main/javascript/airsync_uploads.js +++ b/src/main/javascript/airsync_uploads.js @@ -219,13 +219,8 @@ class AirSyncUploadsCard extends React.Component { //data : submissionData, dataType : 'json', success : function(response) { - if (response == "UNABLE_MUTEX") { - console.log("displaying error modal!"); - errorModal.show("Unable to manually sync!", "This could be because your fleet is already being updated, or someone else has already requested an update!"); - } else { - theseUploads.state.lastUpdateTime = response; - theseUploads.setState(theseUploads.state); - } + theseUploads.state.lastUpdateTime = "Pending"; + theseUploads.setState(theseUploads.state); }, error : function(jqXHR, textStatus, errorThrown) { errorModal.show("Error Updating:", errorThrown); diff --git a/src/main/javascript/create_event.js b/src/main/javascript/create_event.js index 684ca3a47..d53b7cc8a 100644 --- a/src/main/javascript/create_event.js +++ b/src/main/javascript/create_event.js @@ -50,12 +50,15 @@ class CreateEventCard extends React.Component { stopBuffer : "", severityType : "min", severityColumnNames : [], + severityColumn : doubleTimeSeriesNames[0], filters : { type : "GROUP", condition : "AND", filters : [] - } + }, + } + } validateEventName(event) { diff --git a/src/main/javascript/flights.js b/src/main/javascript/flights.js index 8dbd55a1c..52102a994 100644 --- a/src/main/javascript/flights.js +++ b/src/main/javascript/flights.js @@ -32,6 +32,18 @@ var visitedAirports = [ "GFK", "FAR", "ALB", "ROC" ]; */ // var tagNames = ["Tag A", "Tag B"]; var rules = [ + + { + name: "Has Any Event(s)", + conditions: [ + { + type: "select", + name: "airframes", + options: airframes, + } + ] + }, + { name: "Airframe", conditions: [ @@ -620,6 +632,52 @@ class FlightsPage extends React.Component { return storedFilters; } + + transformHasAnyEvent(filters) { + + let newFilters = []; + filters.forEach((filter) => { + + if (filter.inputs && filter.inputs[0] === "Has Any Event(s)") { + + console.log("Rebuilding filter for 'Has Any Event(s)' as 'Event Count' > 0 for all events for the given airframe..."); + + let airframe = filter.inputs[1]; + newFilters.push({ + type: "GROUP", + condition: "AND", + filters: [ + { + type: "RULE", + inputs: ["Airframe", "is", airframe] + }, + { + type: "GROUP", + condition: "OR", + filters: eventNames.map((eventName) => ({ + type: "RULE", + inputs: ["Event Count", eventName, ">", "0"] + })) + } + ] + }); + + //Attempt to recursively transform nested filters... + } else if (filter.filters) { + newFilters.push({ + ...filter, + filters: transformAirframeEventsCountFilter(filter.filters) + }); + } else { + newFilters.push(filter); + } + + }); + + return newFilters; + + }; + submitFilter(resetCurrentPage = false) { console.log( "submitting filter! currentPage: " + @@ -630,8 +688,7 @@ class FlightsPage extends React.Component { this.state.sortColumn ); - console.log("Submitting filters:"); - console.log(this.state.filters); + console.log("Submitting filters: ", this.state.filters); $("#loading").show(); @@ -642,6 +699,10 @@ class FlightsPage extends React.Component { currentPage = 0; } + //Transform the 'Has Any Event(s)' filter + let originalFilters = this.state.filters.filters; + this.state.filters.filters = this.transformHasAnyEvent(this.state.filters.filters); + var submissionData = { filterQuery: JSON.stringify(this.state.filters), currentPage: currentPage, @@ -652,6 +713,9 @@ class FlightsPage extends React.Component { console.log(submissionData); + //Undo the transformation + this.state.filters.filters = originalFilters; + let flightsPage = this; $.ajax({ @@ -689,6 +753,7 @@ class FlightsPage extends React.Component { }, error: function (jqXHR, textStatus, errorThrown) { errorModal.show("Error Loading Flights", errorThrown); + $("#loading").hide(); }, async: true, }); @@ -1271,4 +1336,4 @@ $(document).ready(function () { } }); }); -}); +}); \ No newline at end of file diff --git a/src/main/javascript/manage_events.js b/src/main/javascript/manage_events.js index ad0385fc7..5aadcf001 100644 --- a/src/main/javascript/manage_events.js +++ b/src/main/javascript/manage_events.js @@ -87,6 +87,7 @@ class CreateEventCard extends React.Component { stopBuffer : "", severityType : "min", severityColumnNames : [], + severityColumn : doubleTimeSeriesNames[0], filters : { type : "GROUP", condition : "AND", @@ -291,6 +292,7 @@ class UpdateEventDefinitionModal extends React.Component { stopBuffer: "", severityType: "min", severityColumnNames: [], + severityColumn: doubleTimeSeriesNames[0], filters: { type: "GROUP", condition: "AND", diff --git a/src/main/javascript/paginator_component.js b/src/main/javascript/paginator_component.js index 6fe368a7c..6f897f0a0 100644 --- a/src/main/javascript/paginator_component.js +++ b/src/main/javascript/paginator_component.js @@ -151,8 +151,8 @@ class Paginator extends React.Component { {pages} - this.nextPage()} /> - this.jumpPage(this.props.numberPages - 1)} /> + = (this.props.numberPages-1)} onClick={() => this.nextPage()} /> + = (this.props.numberPages-1)} onClick={() => this.jumpPage(this.props.numberPages - 1)} />
diff --git a/src/main/javascript/severities.js b/src/main/javascript/severities.js index 5a64e7999..be5697583 100644 --- a/src/main/javascript/severities.js +++ b/src/main/javascript/severities.js @@ -1,6 +1,6 @@ import 'bootstrap'; -import React, { Component } from "react"; +import React, { Component, useEffect } from "react"; import ReactDOM from "react-dom"; import { errorModal } from "./error_modal.js"; @@ -12,6 +12,7 @@ import GetDescription from "./get_description.js"; import Plotly from 'plotly.js'; import {OverlayTrigger} from "react-bootstrap"; import Tooltip from "react-bootstrap/Tooltip"; +import { set } from 'ol/transform.js'; airframes.unshift("All Airframes"); @@ -51,12 +52,20 @@ var eventFleetPercents = {}; var eventNGAFIDPercents = {}; class SeveritiesPage extends React.Component { + constructor(props) { + super(props); let eventChecked = {}; + let eventsEmpty = {}; + + eventNames.unshift("ANY Event"); for (let i = 0; i < eventNames.length; i++) { - eventChecked[eventNames[i]] = false; + + let eventNameCur = eventNames[i]; + eventChecked[eventNameCur] = false; + eventsEmpty[eventNameCur] = false; } var date = new Date(); @@ -70,9 +79,18 @@ class SeveritiesPage extends React.Component { datesChanged : false, eventMetaData : {}, eventChecked : eventChecked, + eventsEmpty : eventsEmpty, metaDataChecked: false, // severityTraces: [], }; + + //Fetch all event severities + this.fetchAllEventSeverities(); + + } + + componentDidMount() { + this.displayPlot(this.state.airframe); } exportCSV() { @@ -161,18 +179,36 @@ class SeveritiesPage extends React.Component { } displayPlot(selectedAirframe) { - console.log("displaying plots with airframe: '" + selectedAirframe + "'"); + + console.log("Displaying plots with airframe: '" + selectedAirframe + "'"); + // console.log("Event Severities: ", eventSeverities); let severityTraces = []; + var airframeNames = {}; + for (let [eventName, countsMap] of Object.entries(eventSeverities)) { - //console.log("checking to plot event: '" + eventName + "', checked? '" + this.state.eventChecked[eventName] + "'"); - if (!this.state.eventChecked[eventName]) continue; + + if (!this.state.eventChecked[eventName]) + continue; for (let [airframe, counts] of Object.entries(countsMap)) { - if (airframe === "Garmin Flight Display") continue; - if (selectedAirframe !== airframe && selectedAirframe !== "All Airframes") continue; + if (airframe === "Garmin Flight Display") + continue; + + if (selectedAirframe !== airframe && selectedAirframe !== "All Airframes") + continue; + + //Map airframe names to a consistent marker symbol + const markerSymbolList = ["circle", "diamond", "square", "x", "pentagon", "hexagon", "octagon"]; + // console.log("Marker Symbol List: ", markerSymbolList); + + airframeNames[airframe] ??= Object.keys(airframeNames).length; + // console.log(`airframeNames[${airframe}]: `, airframeNames[airframe]); + let markerSymbol = markerSymbolList[airframeNames[airframe] % markerSymbolList.length]; + // console.log("Marker Symbol: ", markerSymbol); + let markerSymbolAny = (markerSymbol + "-open-dot"); let severityTrace = { name : eventName + ' - ' + airframe, @@ -191,6 +227,36 @@ class SeveritiesPage extends React.Component { tagName: [] }; + //Use a hollow circle for the "ANY Event" event + if (eventName === "ANY Event") { + + severityTrace.marker = { + color: 'gray', + size: 14, + symbol: markerSymbolAny, + opacity: 0.65 + }; + + } else { + + let eventNameIndex = eventNames.indexOf(eventName); + severityTrace.marker = { + + //Consistent rainbow colors for each event + color: + 'hsl(' + + parseInt(360.0 * eventNameIndex / eventNames.length) + + ',100%' + + ',75%)', + symbol: markerSymbol, + size: 8, + line: { + color:'black', + width:1 + } + }; + } + for (let i = 0; i < counts.length; i++) { severityTrace.id.push( counts[i].id ); severityTrace.y.push( counts[i].severity ); @@ -207,7 +273,7 @@ class SeveritiesPage extends React.Component { severityTrace.flightIds.push( counts[i].flightId); } - let hovertext = "Flight #" + counts[i].flightId + ", System ID: " + counts[i].systemId + ", Tail: " + counts[i].tail + ", severity: " + (Math.round(counts[i].severity * 100) / 100).toFixed(2) + ", event start time: " + counts[i].startTime + ", event end time: " + counts[i].endTime; + let hovertext = "Flight #" + counts[i].flightId + ", System ID: " + counts[i].systemId + ", Tail: " + counts[i].tail + ", severity: " + (Math.round(counts[i].severity * 100) / 100).toFixed(2) + ", event name: " + eventName + ", event start time: " + counts[i].startTime + ", event end time: " + counts[i].endTime; if(counts[i].tagName !== ""){ hovertext += ", Tag: " + counts[i].tagName; } @@ -219,16 +285,25 @@ class SeveritiesPage extends React.Component { severityTrace.hovertext.push(hovertext); //+ ", severity: " + counts[i].severity); } - severityTraces.push(severityTrace); + + //Display the "ANY Event" markers under the other ones + if (eventName === "ANY Event") { + severityTraces.unshift(severityTrace); + } else { + severityTraces.push(severityTrace); + } this.setState(this.state); } } + let styles = getComputedStyle(document.documentElement); + let plotBgColor = styles.getPropertyValue("--c_plotly_bg").trim(); + let plotTextColor = styles.getPropertyValue("--c_plotly_text").trim(); + let plotGridColor = styles.getPropertyValue("--c_plotly_grid").trim(); + var severityLayout = { title : 'Severity of Events', hovermode : "closest", - //autosize: false, - //width: 500, height: 700, margin: { l: 50, @@ -236,6 +311,17 @@ class SeveritiesPage extends React.Component { b: 50, t: 50, pad: 4 + }, + plot_bgcolor : "transparent", + paper_bgcolor : plotBgColor, + font : { + color : plotTextColor + }, + xaxis : { + gridcolor : plotGridColor + }, + yaxis : { + gridcolor : plotGridColor } }; @@ -322,11 +408,11 @@ class SeveritiesPage extends React.Component { } + fetchAllEventSeverities() { + + $('#loading').show(); + console.log("showing loading spinner!"); - checkEvent(eventName) { - console.log("checking event: '" + eventName + "'"); - this.state.eventChecked[eventName] = !this.state.eventChecked[eventName]; - this.setState(this.state); let startDate = this.state.startYear + "-"; let endDate = this.state.endYear + "-"; @@ -337,6 +423,101 @@ class SeveritiesPage extends React.Component { if (parseInt(this.state.endMonth) < 10) endDate += "0" + parseInt(this.state.endMonth); else endDate += this.state.endMonth; + let severitiesPage = this; + + var submission_data = { + startDate : startDate + "-01", + endDate : endDate + "-28", + eventNames : JSON.stringify(eventNames), + tagName: this.state.tagName + }; + + $.ajax({ + type: 'POST', + url: '/protected/all_severities', + data : submission_data, + dataType : 'json', + success : function(response) { + console.log("Received response : ", this.data, response); + + $('#loading').hide(); + + if (response.err_msg) { + errorModal.show(response.err_title, response.err_msg); + return; + } + + //Check if the response is empty for each event + for (let [eventName, countsMap] of Object.entries(response)) { + + let eventSeverityCounts = response[eventName]; + + let isEmpty = true; + for(let [airframe, counts] of Object.entries(eventSeverityCounts)) { + + if (counts.length > 0) { + isEmpty = false; + break; + } + } + if (isEmpty) { + severitiesPage.state.eventsEmpty[eventName] = true; + eventSeverities[eventName] = {}; + } else { + + //Mark severities for event + severitiesPage.state.eventsEmpty[eventName] = false; + eventSeverities[eventName] = eventSeverityCounts; + + //Concatenate the counts for the "ANY Event" event + if (eventSeverities["ANY Event"] == null) + eventSeverities["ANY Event"] = {}; + console.log("Merging counts for event: '" + eventName + "'"); + for(let [airframe, counts] of Object.entries(eventSeverityCounts)) { + + if (eventSeverities["ANY Event"][airframe] == null) + eventSeverities["ANY Event"][airframe] = eventSeverityCounts[airframe]; + else + eventSeverities["ANY Event"][airframe] = eventSeverities["ANY Event"][airframe].concat(eventSeverityCounts[airframe]); + + } + + } + + } + + severitiesPage.setState(severitiesPage.state); + severitiesPage.displayPlot(severitiesPage.state.airframe); + + }, + + error : function(jqXHR, textStatus, errorThrown) { + errorModal.show("Error Loading Uploads", errorThrown); + } + + }); + + } + + fetchEventSeverities(eventName) { + + $('#loading').show(); + console.log("showing loading spinner!"); + + + let startDate = this.state.startYear + "-"; + let endDate = this.state.endYear + "-"; + + //0 pad the months on the front + if (parseInt(this.state.startMonth) < 10) startDate += "0" + parseInt(this.state.startMonth); + else startDate += this.state.startMonth; + if (parseInt(this.state.endMonth) < 10) endDate += "0" + parseInt(this.state.endMonth); + else endDate += this.state.endMonth; + + + let severitiesPage = this; + + var submission_data = { startDate : startDate + "-01", endDate : endDate + "-28", @@ -344,40 +525,59 @@ class SeveritiesPage extends React.Component { tagName: this.state.tagName }; + $.ajax({ + type: 'POST', + url: '/protected/severities', + data : submission_data, + dataType : 'json', + success : function(response) { + console.log("Received response : ", this.data, response); + + $('#loading').hide(); + + if (response.err_msg) { + errorModal.show(response.err_title, response.err_msg); + return; + } + + //Check if the response is empty + for(let [airframe, counts] of Object.entries(response)) { + + if (counts.length != 0) + continue; + + console.log("No counts for event: '" + eventName + "' and airframe: '" + airframe + "'"); + + severitiesPage.state.eventsEmpty[eventName] = true; + eventSeverities[eventName] = {}; + severitiesPage.setState(severitiesPage.state); + return; + } + + eventSeverities[eventName] = response; + severitiesPage.setState(severitiesPage.state); + severitiesPage.displayPlot(severitiesPage.state.airframe); + + }, + error : function(jqXHR, textStatus, errorThrown) { + errorModal.show("Error Loading Uploads", errorThrown); + }, + async: true + }); + + } + + checkEvent(eventName) { + + console.log("Checking event: '" + eventName + "'"); + this.state.eventChecked[eventName] = !this.state.eventChecked[eventName]; + this.setState(this.state); + if (eventName in eventSeverities) { console.log("already loaded counts for event: '" + eventName + "'"); severitiesPage.displayPlot(severitiesPage.state.airframe); - } else { - $('#loading').show(); - console.log("showing loading spinner!"); - - let severitiesPage = this; - - $.ajax({ - type: 'POST', - url: '/protected/severities', - data : submission_data, - dataType : 'json', - success : function(response) { - console.log("received response: "); - console.log(response); - - $('#loading').hide(); - - if (response.err_msg) { - errorModal.show(response.err_title, response.err_msg); - return; - } - - eventSeverities[eventName] = response; - severitiesPage.displayPlot(severitiesPage.state.airframe); - }, - error : function(jqXHR, textStatus, errorThrown) { - errorModal.show("Error Loading Uploads", errorThrown); - }, - async: true - }); + this.fetchEventSeverities(eventName); } } @@ -416,6 +616,8 @@ class SeveritiesPage extends React.Component { eventSeverities = {}; this.displayPlot(this.state.airframe); + + this.fetchAllEventSeverities(); } airframeChange(airframe) { @@ -449,14 +651,17 @@ class SeveritiesPage extends React.Component { }; return ( -
- +
-
+
+ {this.displayPlot(this.state.airframe);}} waitingUserCount={waitingUserCount} fleetManager={fleetManager} unconfirmedTailsCount={unconfirmedTailsCount} modifyTailsAccess={modifyTailsAccess} plotMapHidden={plotMapHidden}/> +
+ +
-
+
{ + + //Don't show a description for the "ANY Event" event + if (eventName === "ANY Event") return ( +
+ this.checkEvent(eventName)} style={{border:"1px solid red"}}/> + +
+ ); + return (
- this.checkEvent(eventName)}> + this.checkEvent(eventName)}> ( {GetDescription(eventName)})} @@ -507,7 +723,7 @@ class SeveritiesPage extends React.Component {
-
+
@@ -527,4 +743,4 @@ var severitiesPage = ReactDOM.render( document.querySelector('#severities-page') ); -severitiesPage.displayPlot("All Airframes"); +severitiesPage.displayPlot("All Airframes"); \ No newline at end of file diff --git a/src/main/javascript/time_header.js b/src/main/javascript/time_header.js index 28fc21579..03ff1a17c 100644 --- a/src/main/javascript/time_header.js +++ b/src/main/javascript/time_header.js @@ -85,6 +85,8 @@ export default class TimeHeader extends React.Component { return (
+ { exportButton } +
{ tags }
@@ -121,8 +123,6 @@ export default class TimeHeader extends React.Component {
- { exportButton } -
@@ -216,4 +216,4 @@ class TurnToFinalHeaderComponents extends React.Component { } }; -export { TimeHeader, TurnToFinalHeaderComponents }; +export { TimeHeader, TurnToFinalHeaderComponents }; \ No newline at end of file diff --git a/src/main/javascript/trends.js b/src/main/javascript/trends.js index e1c410d6e..0287a4561 100644 --- a/src/main/javascript/trends.js +++ b/src/main/javascript/trends.js @@ -47,9 +47,16 @@ class TrendsPage extends React.Component { constructor(props) { super(props); let eventChecked = {}; + let eventsEmpty = {}; + + eventNames.unshift("ANY Event"); for (let i = 0; i < eventNames.length; i++) { - eventChecked[eventNames[i]] = false; + + let eventNameCur = eventNames[i]; + eventChecked[eventNameCur] = false; + eventsEmpty[eventNameCur] = true; } + eventsEmpty["ANY Event"] = false; var date = new Date(); this.state = { @@ -60,7 +67,8 @@ class TrendsPage extends React.Component { endMonth : date.getMonth() + 1, datesChanged : false, aggregatePage : props.aggregate_page, - eventChecked : eventChecked + eventChecked : eventChecked, + eventsEmpty : eventsEmpty }; this.fetchMonthlyEventCounts(); @@ -91,26 +99,84 @@ class TrendsPage extends React.Component { }; let trendsPage = this; - $.ajax({ - type: 'POST', - url: '/protected/monthly_event_counts', - data : submission_data, - dataType : 'json', - success : function(response) { - $('#loading').hide(); - - if (response.err_msg) { - errorModal.show(response.err_title, response.err_msg); - return; - } - - eventCounts = response; - trendsPage.displayPlots(trendsPage.state.airframe); - }, - error : function(jqXHR, textStatus, errorThrown) { - errorModal.show("Error Loading Uploads", errorThrown); - }, - async: true + + $('#loading').hide(); + + return new Promise((resolve, reject) => { + $.ajax({ + type: 'POST', + url: '/protected/monthly_event_counts', + data : submission_data, + dataType : 'json', + success : function(response) { + + if (response.err_msg) { + errorModal.show(response.err_title, response.err_msg); + return; + } + + eventCounts = response; + + let countsMerged = {}; + for(let [eventName, countsObject] of Object.entries(eventCounts)) { + + for(let [airframeName] of Object.entries(countsObject)) { + + if (airframeName === "Garmin Flight Display") { + continue; + } + + let countsAirframe = countsObject[airframeName]; + + //Airframe name is not in the merged counts object yet, add it + if (!(airframeName in countsMerged)) { + + countsMerged[airframeName] = { + airframeName: airframeName, + eventName: "ANY Event", + dates: [...countsAirframe.dates], + aggregateFlightsWithEventCounts: [...countsAirframe.aggregateFlightsWithEventCounts], + aggregateTotalEventsCounts: [...countsAirframe.aggregateTotalEventsCounts], + aggregateTotalFlightsCounts: [...countsAirframe.aggregateTotalFlightsCounts], + flightsWithEventCounts: [...countsAirframe.flightsWithEventCounts], + totalEventsCounts: [...countsAirframe.totalEventsCounts], + totalFlightsCounts: [...countsAirframe.totalFlightsCounts] + }; + + //Airframe name is already in the merged counts object, add the counts + } else { + + for (let i = 0 ; i < countsAirframe.dates.length ; i++) { + + if (countsAirframe.totalEventsCounts[i] === 0) + continue; + + countsMerged[airframeName].aggregateFlightsWithEventCounts[i] += countsAirframe.aggregateFlightsWithEventCounts[i]; + countsMerged[airframeName].aggregateTotalEventsCounts[i] += countsAirframe.aggregateTotalEventsCounts[i]; + countsMerged[airframeName].aggregateTotalFlightsCounts[i] += countsAirframe.aggregateTotalFlightsCounts[i]; + countsMerged[airframeName].flightsWithEventCounts[i] += countsAirframe.flightsWithEventCounts[i]; + countsMerged[airframeName].totalEventsCounts[i] += countsAirframe.totalEventsCounts[i]; + countsMerged[airframeName].totalFlightsCounts[i] += countsAirframe.totalFlightsCounts[i]; + } + + } + + } + + } + + eventCounts["ANY Event"] = countsMerged; + + trendsPage.displayPlots(trendsPage.state.airframe); + + resolve(response); + }, + error : function(jqXHR, textStatus, errorThrown) { + errorModal.show("Error Loading Uploads", errorThrown); + reject(errorThrown); + }, + async: true + }); }); } @@ -311,15 +377,29 @@ class TrendsPage extends React.Component { countData = []; percentData = []; + + let counts = eventCounts == null ? {} : eventCounts; + let airframeNames = []; + for (let [eventName, countsObject] of Object.entries(counts)) { + for (let [airframe, value] of Object.entries(countsObject)) { + if (value.airframeName === "Garmin Flight Display") continue; + if (!airframeNames.includes(value.airframeName)) { + airframeNames.push(value.airframeName); + } + } + } + for (let [eventName, countsObject] of Object.entries(counts)) { //console.log("checking to plot event: '" + eventName + "', checked? '" + this.state.eventChecked[eventName] + "'"); if (!this.state.eventChecked[eventName]) continue; + let fleetPercents = null; let ngafidPercents = null; + if (eventName in eventFleetPercents) { console.log('getting existing fleetPercents!'); @@ -373,18 +453,69 @@ class TrendsPage extends React.Component { console.log(value); */ - value.name = value.eventName + " - " + value.airframeName; value.x = value.dates; value.type = 'scatter'; value.hoverinfo = 'x+text'; + + let airframeIndex = airframes.indexOf(value.airframeName); + let eventNameIndex = eventNames.indexOf(eventName); + + let indexCur = (airframeIndex + eventNameIndex); + let indicesMax = (airframes.length + eventNames.length); + + //Dashed lines for ANY Event + if (eventName === "ANY Event") { + + value = { + ...value, + legendgroup: value.name, + + //Consistent rainbow colors for each airframe + line : { + width: 1.0, + dash: 'dot', + color : 'hsl(' + parseInt(360.0 * airframeIndex / airframeNames.length) + ', 50%, 50%)' + } + + }; + + //'Glowing' rainbow lines for other events + } else { + + value = { + ...value, + legendgroup: value.name, + //showlegend: false, + + mode : 'lines', + line : { + width : 2, + // color : 'hsl(' + // + parseInt(360.0 * indexCur / indicesMax) + ',' + // + parseInt(50.0 + 50.0 * airframeIndex / airframeNames.length) + '%,' + // + parseInt(25.0 + 25.0) + '%)' + } + + }; + } + //don't add airframes to the count plot that the fleet doesn't have - if (airframes.indexOf(value.airframeName) >= 0) countData.push(value); + if (airframes.indexOf(value.airframeName) >= 0) { + + //Display the "ANY Event" lines under the other ones + if (eventName === "ANY Event") { + countData.unshift(value); + } else { + countData.push(value); + } + + } + if (this.state.aggregatePage) { value.y = value.aggregateTotalEventsCounts; - } - else { + } else { value.y = value.totalEventsCounts; } value.hovertext = []; @@ -427,15 +558,45 @@ class TrendsPage extends React.Component { if (this.state.aggregatePage) { flightsWithEventCount = value.aggregateFlightsWithEventCounts[i]; totalFlightsCount = value.aggregateTotalFlightsCounts[i]; - } - else { + } else { flightsWithEventCount = value.flightsWithEventCounts[i]; totalFlightsCount = value.totalFlightsCounts[i]; } value.hovertext.push(value.y[i] + " events in " + flightsWithEventCount + " of " + totalFlightsCount + " flights : " + value.eventName + " - " + value.airframeName); } - + } + + } + + for (const airframeName of airframeNames) { + + let airframeIndex = airframeNames.indexOf(airframeName); + let airframeLegendHighlight = { + + name : airframeName, + + x : [0], + y : [0], + + //visible: 'legendonly', + visible: false, + showlegend: true, + + mode : 'markers', + marker : { + width : 2.0, + opacity: 1.0, + // color : 'hsl(' + // + parseInt(360.0 * airframeIndex / airframeNames.length) + ',' + // + parseInt(50.0) + '%,' + // + parseInt(50.0) + '%)' + } + + }; + + countData.push(airframeLegendHighlight); + } /* @@ -472,6 +633,58 @@ class TrendsPage extends React.Component { fleetValue.hovertext.push(fixedText + " (" + fleetValue.flightsWithEventCounts[date] + " of " + fleetValue.totalFlightsCounts[date] + " flights) : " + fleetValue.name); } } + + + + let airframeIndex = airframes.indexOf(ngafidValue.airframeName); + let eventNameIndex = eventNames.indexOf(eventName); + + let indexCur = (airframeIndex + eventNameIndex); + let indicesMax = (airframes.length + eventNames.length); + + //... + if (eventName === "ANY Event") { + + ngafidValue = { + ...ngafidValue, + + legendgroup: ngafidValue.name, + + //Consistent rainbow colors for each event + line : { + width: 1, + dash: 'dot', + color : 'hsl(' + parseInt(360.0 * airframeIndex / airframeNames.length) + ', 50%, 50%)' + } + + }; + + //... + } else { + + ngafidValue = { + ...ngafidValue, + + legendgroup: ngafidValue.name, + mode : 'lines', + + //Consistent rainbow colors for each event + line : { + width : 2, + // color : 'hsl(' + // + parseInt(360.0 * eventNameIndex / eventNames.length) + // + parseInt(50.0 + 50.0 * airframeIndex / airframeNames.length) + '%,' + // + parseInt(25.0 + 25.0 * airframeIndex / airframeNames.length) + '%)' + color : 'hsl(' + + parseInt(360.0 * indexCur / indicesMax) + ',' + + parseInt(50.0 + 50.0 * airframeIndex / airframeNames.length) + '%,' + + parseInt(25.0 + 25.0) + '%)' +// + parseInt(25.0 + 25.0 * (indexCur%2)) + '%)' + } + + }; + } + percentData.push(ngafidValue); ngafidValue.x = []; ngafidValue.y = []; @@ -483,6 +696,7 @@ class TrendsPage extends React.Component { ngafidValue.y.push(v); + //console.log(date + " :: " + ngafidValue.flightsWithEventCounts[date] + " / " + ngafidValue.totalFlightsCounts[date] + " : " + v); //this will give 2 significant figures (and leading 0s if it is quite small) @@ -545,39 +759,20 @@ class TrendsPage extends React.Component { Plotly.newPlot('count-trends-plot', countData, countLayout, config); Plotly.newPlot('percent-trends-plot', percentData, percentLayout, config); + console.log("Hiding loading spinner"); + $('#loading').hide(); } checkEvent(eventName) { + console.log("checking event: '" + eventName + "'"); - console.log("Changing event state : " + eventName + " from " + this.state.eventChecked[eventName] + " to " + !this.state.eventChecked[eventName]); this.state.eventChecked[eventName] = !this.state.eventChecked[eventName]; this.setState(this.state); - let startDate = this.state.startYear + "-"; - let endDate = this.state.endYear + "-"; - - //0 pad the months on the front - if (parseInt(this.state.startMonth) < 10) startDate += "0" + parseInt(this.state.startMonth); - else startDate += this.state.startMonth; - if (parseInt(this.state.endMonth) < 10) endDate += "0" + parseInt(this.state.endMonth); - else endDate += this.state.endMonth; + this.displayPlots(this.state.airframe); - var submission_data = { - startDate : startDate + "-01", - endDate : endDate + "-28", - eventName : eventName, - aggregatePage : this.props.aggregate_page - }; - console.log(eventCounts); - if (eventCounts != null) { - console.log("already loaded counts for event: '" + eventName + "'"); - this.displayPlots(this.state.airframe); - } else { - $('#loading').show(); - console.log("showing loading spinner!"); - } } updateStartYear(newStartYear) { @@ -611,11 +806,25 @@ class TrendsPage extends React.Component { this.state.eventChecked[eventName] = false; } this.state.datesChanged = false; - this.setState(this.state); + $('#loading').hide(); + + this.fetchMonthlyEventCounts().then((data) => { + + //Set all events to empty initially + for (let i = 0; i < eventNames.length; i++) { + let eventNameCur = eventNames[i]; + this.state.eventsEmpty[eventNameCur] = true; + } + + for (let [eventName, countsObject] of Object.entries(data)) { + this.state.eventsEmpty[eventName] = false; + } + + this.setState(this.state); + this.displayPlots(this.state.airframe); + + }); - eventCounts = null; - this.fetchMonthlyEventCounts(); - this.displayPlots(this.state.airframe); } airframeChange(airframe) { @@ -624,7 +833,6 @@ class TrendsPage extends React.Component { } render() { - //console.log(systemIds); const numberOptions = { minimumFractionDigits: 2, @@ -656,29 +864,38 @@ class TrendsPage extends React.Component { updateEndYear={(newEndYear) => this.updateEndYear(newEndYear)} updateEndMonth={(newEndMonth) => this.updateEndMonth(newEndMonth)} exportCSV={() => this.exportCSV()} - - /> -
{ eventNames.map((eventName, index) => { + + //Don't show a description for the "ANY Event" event + if (eventName === "ANY Event") return ( +
+ this.checkEvent(eventName)}> + +
+ ); + return (
- this.checkEvent(eventName)}> - + onChange={() => this.checkEvent(eventName)}> + ( {GetDescription(eventName)})} placement="bottom"> - diff --git a/src/main/javascript/uploads.js b/src/main/javascript/uploads.js index 74d5c57fd..d71f46132 100644 --- a/src/main/javascript/uploads.js +++ b/src/main/javascript/uploads.js @@ -20,6 +20,7 @@ var chunkSize = 2 * 1024 * 1024; //2MB class Upload extends React.Component { constructor(props) { super(props); + this.isFleetManager = this.props.isFleetManager; } componentDidMount() { @@ -153,8 +154,8 @@ class Upload extends React.Component { console.log("uploadInfo:"); console.log(uploadInfo); - //Disable Download/Delete buttons while Upload HASHING / UPLOADING - let doButtonDisplay = (status!="HASHING" && status!="UPLOADING"); + //Disable Download/Delete buttons while Hashing/Uploading or if not a Fleet Manager + let doButtonDisplay = (this.isFleetManager && status!="HASHING" && status!="UPLOADING"); return (
@@ -651,6 +652,9 @@ class UploadsPage extends React.Component { display : "none" }; + //Disable Upload Flights button if not a Fleet Manager + let doButtonDisplay = (fleetManager); + return (
@@ -666,7 +670,12 @@ class UploadsPage extends React.Component { ? ( ) : "" } -
@@ -682,6 +691,7 @@ class UploadsPage extends React.Component { //uploadInfo.position = index; return ( { this.removePendingUpload(uploadInfo); } } @@ -709,7 +719,7 @@ class UploadsPage extends React.Component { this.state.uploads.map((uploadInfo, index) => { uploadInfo.position = index; return ( - {this.removeUpload(uploadInfo);}} /> + {this.removeUpload(uploadInfo);}} /> ); }) }