From 5e2b5bd33d7a1b3ef3b714224a22ebdb274c0d8e Mon Sep 17 00:00:00 2001 From: ianLewis8 Date: Tue, 30 Jul 2024 15:18:06 -0400 Subject: [PATCH 01/22] Developing new database model Initial commit towards making a database to store logs. --- src/server/models/LogBase.js | 76 +++++++++++++++++++ .../sql/logbase/create_logbase_table.sql | 7 ++ src/server/sql/logbase/get_all_logs.sql | 4 + .../sql/logbase/get_logs_from_dates.sql | 10 +++ src/server/sql/logbase/get_logs_from_type.sql | 9 +++ src/server/sql/logbase/insert_new_log.sql | 5 ++ 6 files changed, 111 insertions(+) create mode 100644 src/server/models/LogBase.js create mode 100644 src/server/sql/logbase/create_logbase_table.sql create mode 100644 src/server/sql/logbase/get_all_logs.sql create mode 100644 src/server/sql/logbase/get_logs_from_dates.sql create mode 100644 src/server/sql/logbase/get_logs_from_type.sql create mode 100644 src/server/sql/logbase/insert_new_log.sql diff --git a/src/server/models/LogBase.js b/src/server/models/LogBase.js new file mode 100644 index 000000000..dcc54d6ee --- /dev/null +++ b/src/server/models/LogBase.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const database = require('./database'); +const { mapToObject, threeDHoleAlgorithm } = require('../util'); +const determineMaxPoints = require('../util/determineMaxPoints'); +const log = require('../log'); +const { isReadonlyKeywordOrPlusOrMinusToken } = require('typescript'); +const LogEmail = require('./LogEmail'); + +const sqlFile = database.sqlFile; + +class LogBase { + /** + * Creates a new log + * @param logType + * @param logMessage + * @param {Moment} logTime + */ + constructor(logType, logMessage, logTime) { + this.logType = logType; + this.logMessage = logMessage; + this.logTime = logTime; + } + + /** + * Returns a promise to create the logging table + * @param conn the database connection to use + * @returns {Promise.<>} + */ + static createTable(conn) { + return conn.none(sqlFile('logbase/create_logbase_table.sql')); + } + + /** + * Returns a promise to insert this log into the database + * @param conn the database connection to use + * @returns {Promise.<>} + */ + async insert(conn) { + const logBase = this; + await conn.none(sqlFile('logbase/insert_new_log.sql'), logBase); + } + + /** + * Returns a promise to get all of the logs from the database + * @param conn the connection to be used. + * @returns {Promise.>} + */ + static async getAll(conn) { + const rows = await conn.any(sqlFile('logbase/get_all_logs.sql')); + if (rows.length > 0) { + return rows.map(row => new LogEmail(row.logType, row.logMessage, row.logTime)); + } + } + + /** + * Returns a promise to get all of the logs in between two dates. + * If no startDate is specified, all logs before the endDate are returned. + * If no endDate is specified, all logs after the startDate are returned. + * @param {Date} startDate + * @param {Date} endDate + * @param conn is the connection to use. + * @returns {Promise.>} + */ + static async getLogsByDateRange(startDate, endDate, conn) { + const rows = await conn.any(sqlFile('logbase/get_logs_from_dates.sql'), { + startDate: startDate, + endDate: endDate + }); + + return rows; + } +} +module.exports = LogBase; \ No newline at end of file diff --git a/src/server/sql/logbase/create_logbase_table.sql b/src/server/sql/logbase/create_logbase_table.sql new file mode 100644 index 000000000..db13bd1e4 --- /dev/null +++ b/src/server/sql/logbase/create_logbase_table.sql @@ -0,0 +1,7 @@ + +--create logbase table +CREATE TABLE IF NOT EXISTS logbase ( + log_type VARCHAR(5) NOT NULL, + log_message VARCHAR(100) NOT NULL, + log_time TIMESTAMP NOT NULL +); \ No newline at end of file diff --git a/src/server/sql/logbase/get_all_logs.sql b/src/server/sql/logbase/get_all_logs.sql new file mode 100644 index 000000000..3e3e55bc4 --- /dev/null +++ b/src/server/sql/logbase/get_all_logs.sql @@ -0,0 +1,4 @@ + + +-- gets all logs in the database +SELECT * FROM logbase; \ No newline at end of file diff --git a/src/server/sql/logbase/get_logs_from_dates.sql b/src/server/sql/logbase/get_logs_from_dates.sql new file mode 100644 index 000000000..3294a0f2f --- /dev/null +++ b/src/server/sql/logbase/get_logs_from_dates.sql @@ -0,0 +1,10 @@ + + +-- Gets logs in table by date range. This is then ordered by time ascending. +SELECT + -- Short column names for smaller data. + log_type as p, log_message as m, log_time as i +FROM logbase +WHERE log_time >= COALESCE(${startDate}, '-infinity'::TIMESTAMP) + AND log_time <= COALESCE(${endDate}, 'infinity'::TIMESTAMP) +ORDER BY log_time ASC; \ No newline at end of file diff --git a/src/server/sql/logbase/get_logs_from_type.sql b/src/server/sql/logbase/get_logs_from_type.sql new file mode 100644 index 000000000..8d453ab5e --- /dev/null +++ b/src/server/sql/logbase/get_logs_from_type.sql @@ -0,0 +1,9 @@ + + +-- Gets logs in table by date range. This is then ordered by time ascending. +SELECT + -- Short column names for smaller data. + log_type as p, log_message as m, log_time as i +FROM logbase +WHERE log_type = ${logType} +ORDER BY log_time ASC; \ No newline at end of file diff --git a/src/server/sql/logbase/insert_new_log.sql b/src/server/sql/logbase/insert_new_log.sql new file mode 100644 index 000000000..e8e20caa0 --- /dev/null +++ b/src/server/sql/logbase/insert_new_log.sql @@ -0,0 +1,5 @@ + + +--Inserts a new log into the table +INSERT INTO logbase (log_type, log_message, log_time) +VALUES (${logType}, ${logMessage}, ${logTime}); From b4dae28791c64b69d4669ab66a7061b57ee86107 Mon Sep 17 00:00:00 2001 From: ianLewis8 Date: Fri, 2 Aug 2024 15:10:02 -0400 Subject: [PATCH 02/22] LogMsg Model Almost finished LogMsg database model --- src/server/models/LogBase.js | 76 ----------- src/server/models/LogMsg.js | 126 ++++++++++++++++++ .../sql/logbase/create_logbase_table.sql | 7 - src/server/sql/logbase/get_all_logs.sql | 4 - src/server/sql/logbase/get_logs_from_type.sql | 9 -- src/server/sql/logbase/insert_new_log.sql | 5 - .../sql/logmsg/create_log_types_enum.sql | 9 ++ src/server/sql/logmsg/create_logmsg_table.sql | 13 ++ src/server/sql/logmsg/get_all_logs.sql | 6 + .../get_logs_from_dates.sql | 8 +- .../logmsg/get_logs_from_dates_and_type.sql | 13 ++ src/server/sql/logmsg/get_logs_from_type.sql | 11 ++ src/server/sql/logmsg/insert_new_log.sql | 7 + 13 files changed, 189 insertions(+), 105 deletions(-) delete mode 100644 src/server/models/LogBase.js create mode 100644 src/server/models/LogMsg.js delete mode 100644 src/server/sql/logbase/create_logbase_table.sql delete mode 100644 src/server/sql/logbase/get_all_logs.sql delete mode 100644 src/server/sql/logbase/get_logs_from_type.sql delete mode 100644 src/server/sql/logbase/insert_new_log.sql create mode 100644 src/server/sql/logmsg/create_log_types_enum.sql create mode 100644 src/server/sql/logmsg/create_logmsg_table.sql create mode 100644 src/server/sql/logmsg/get_all_logs.sql rename src/server/sql/{logbase => logmsg}/get_logs_from_dates.sql (51%) create mode 100644 src/server/sql/logmsg/get_logs_from_dates_and_type.sql create mode 100644 src/server/sql/logmsg/get_logs_from_type.sql create mode 100644 src/server/sql/logmsg/insert_new_log.sql diff --git a/src/server/models/LogBase.js b/src/server/models/LogBase.js deleted file mode 100644 index dcc54d6ee..000000000 --- a/src/server/models/LogBase.js +++ /dev/null @@ -1,76 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const database = require('./database'); -const { mapToObject, threeDHoleAlgorithm } = require('../util'); -const determineMaxPoints = require('../util/determineMaxPoints'); -const log = require('../log'); -const { isReadonlyKeywordOrPlusOrMinusToken } = require('typescript'); -const LogEmail = require('./LogEmail'); - -const sqlFile = database.sqlFile; - -class LogBase { - /** - * Creates a new log - * @param logType - * @param logMessage - * @param {Moment} logTime - */ - constructor(logType, logMessage, logTime) { - this.logType = logType; - this.logMessage = logMessage; - this.logTime = logTime; - } - - /** - * Returns a promise to create the logging table - * @param conn the database connection to use - * @returns {Promise.<>} - */ - static createTable(conn) { - return conn.none(sqlFile('logbase/create_logbase_table.sql')); - } - - /** - * Returns a promise to insert this log into the database - * @param conn the database connection to use - * @returns {Promise.<>} - */ - async insert(conn) { - const logBase = this; - await conn.none(sqlFile('logbase/insert_new_log.sql'), logBase); - } - - /** - * Returns a promise to get all of the logs from the database - * @param conn the connection to be used. - * @returns {Promise.>} - */ - static async getAll(conn) { - const rows = await conn.any(sqlFile('logbase/get_all_logs.sql')); - if (rows.length > 0) { - return rows.map(row => new LogEmail(row.logType, row.logMessage, row.logTime)); - } - } - - /** - * Returns a promise to get all of the logs in between two dates. - * If no startDate is specified, all logs before the endDate are returned. - * If no endDate is specified, all logs after the startDate are returned. - * @param {Date} startDate - * @param {Date} endDate - * @param conn is the connection to use. - * @returns {Promise.>} - */ - static async getLogsByDateRange(startDate, endDate, conn) { - const rows = await conn.any(sqlFile('logbase/get_logs_from_dates.sql'), { - startDate: startDate, - endDate: endDate - }); - - return rows; - } -} -module.exports = LogBase; \ No newline at end of file diff --git a/src/server/models/LogMsg.js b/src/server/models/LogMsg.js new file mode 100644 index 000000000..b0f67007f --- /dev/null +++ b/src/server/models/LogMsg.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const database = require('./database'); +const { mapToObject, threeDHoleAlgorithm } = require('../util'); +const determineMaxPoints = require('../util/determineMaxPoints'); +const log = require('../log'); +const { isReadonlyKeywordOrPlusOrMinusToken } = require('typescript'); +const LogEmail = require('./LogEmail'); + +const sqlFile = database.sqlFile; + +class LogMsg { + /** + * Creates a new log + * @param logType + * @param logMessage + * @param {Moment} logTime + */ + constructor(logType, logMessage, logTime) { + this.logType = logType; + this.logMessage = logMessage; + this.logTime = logTime; + } + + /** + * Creates a new log from data in the row + * @param {*} row The row from which the log is to be created. + * @returns The new log object. + */ + static mapRow(row) { + return new LogMsg(row.logType, row.logMessage, row.logTime); + } + + /** + * Returns a promise to create the logging table + * @param conn the database connection to use + * @returns {Promise.<>} + */ + static createTable(conn) { + return conn.none(sqlFile('logmsg/create_logmsg_table.sql')); + } + + /** + * Returns a promise to create the logMsgType enum. + * @param {*} conn The connection to use. + * @returns {Promise.<>} + */ + static createLogMsgTypeEnum(conn) { + return conn.none(sqlFile('logmsg/create_log_types_enum.sql')); + } + + /** + * Returns a promise to insert this log into the database + * @param conn the database connection to use + * @returns {Promise.<>} + */ + async insert(conn) { + const logMsg = this; + await conn.none(sqlFile('logmsg/insert_new_log.sql'), logMsg); + } + + /** + * Returns a promise to get all of the logs from the database + * @param conn the connection to be used. + * @returns {Promise.>} + */ + static async getAll(conn) { + const rows = await conn.any(sqlFile('logmsg/get_all_logs.sql')); + if (rows.length > 0) { + return rows.map(LogMsg.mapRow); + } + } + + /** + * Returns a promise to get all of the logs in between two dates. + * If no startDate is specified, all logs before the endDate are returned. + * If no endDate is specified, all logs after the startDate are returned. + * @param {Date} startDate + * @param {Date} endDate + * @param conn is the connection to use. + * @returns {Promise.>} + */ + static async getLogsByDateRange(startDate, endDate, conn) { + const rows = await conn.any(sqlFile('logmsg/get_logs_from_dates.sql'), { + startDate: startDate, + endDate: endDate + }); + + return rows.map(LogMsg.mapRow); + } + + /** + * Returns a promise to get all of the logs of a certain type + * @param logType + * @param conn is the connection to use. + * @returns {Promise.>} + */ + static async getLogsByType(logType, conn){ + const rows = await conn.any(sqlFile('logmsg/get_logs_from_type.sql'), {logType: logType}); + + return rows.map(LogMsg.mapRow); + } + + /** + * Returns a promise to get all of the logs in between two dates. + * If no startDate is specified, all logs before the endDate are returned. + * If no endDate is specified, all logs after the startDate are returned. + * @param {Date} startDate + * @param {Date} endDate + * @param logType + * @param conn is the connection to use. + * @returns {Promise.>} + */ + static async getLogsByDateRangeAndType(startDate, endDate, logType, conn) { + const rows = await conn.any(sqlFile('logmsg/get_logs_from_dates_and_type.sql'), { + startDate: startDate, + endDate: endDate, + logType: logType + }); + + return rows.map(LogMsg.mapRow); + } +} +module.exports = LogMsg; \ No newline at end of file diff --git a/src/server/sql/logbase/create_logbase_table.sql b/src/server/sql/logbase/create_logbase_table.sql deleted file mode 100644 index db13bd1e4..000000000 --- a/src/server/sql/logbase/create_logbase_table.sql +++ /dev/null @@ -1,7 +0,0 @@ - ---create logbase table -CREATE TABLE IF NOT EXISTS logbase ( - log_type VARCHAR(5) NOT NULL, - log_message VARCHAR(100) NOT NULL, - log_time TIMESTAMP NOT NULL -); \ No newline at end of file diff --git a/src/server/sql/logbase/get_all_logs.sql b/src/server/sql/logbase/get_all_logs.sql deleted file mode 100644 index 3e3e55bc4..000000000 --- a/src/server/sql/logbase/get_all_logs.sql +++ /dev/null @@ -1,4 +0,0 @@ - - --- gets all logs in the database -SELECT * FROM logbase; \ No newline at end of file diff --git a/src/server/sql/logbase/get_logs_from_type.sql b/src/server/sql/logbase/get_logs_from_type.sql deleted file mode 100644 index 8d453ab5e..000000000 --- a/src/server/sql/logbase/get_logs_from_type.sql +++ /dev/null @@ -1,9 +0,0 @@ - - --- Gets logs in table by date range. This is then ordered by time ascending. -SELECT - -- Short column names for smaller data. - log_type as p, log_message as m, log_time as i -FROM logbase -WHERE log_type = ${logType} -ORDER BY log_time ASC; \ No newline at end of file diff --git a/src/server/sql/logbase/insert_new_log.sql b/src/server/sql/logbase/insert_new_log.sql deleted file mode 100644 index e8e20caa0..000000000 --- a/src/server/sql/logbase/insert_new_log.sql +++ /dev/null @@ -1,5 +0,0 @@ - - ---Inserts a new log into the table -INSERT INTO logbase (log_type, log_message, log_time) -VALUES (${logType}, ${logMessage}, ${logTime}); diff --git a/src/server/sql/logmsg/create_log_types_enum.sql b/src/server/sql/logmsg/create_log_types_enum.sql new file mode 100644 index 000000000..64df5b2ab --- /dev/null +++ b/src/server/sql/logmsg/create_log_types_enum.sql @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +DO $$ BEGIN + CREATE TYPE log_msg_type AS ENUM('INFO', 'WARN', 'ERROR', 'DEBUG', 'SILENT'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; \ No newline at end of file diff --git a/src/server/sql/logmsg/create_logmsg_table.sql b/src/server/sql/logmsg/create_logmsg_table.sql new file mode 100644 index 000000000..1e6cfc5d8 --- /dev/null +++ b/src/server/sql/logmsg/create_logmsg_table.sql @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +--create logmsg table +CREATE TABLE IF NOT EXISTS logmsg ( + id SERIAL PRIMARY KEY, + log_type log_msg_type NOT NULL, + log_message TEXT NOT NULL, + log_time TIMESTAMP NOT NULL +); + +-- TODO Consider index optimization for queries \ No newline at end of file diff --git a/src/server/sql/logmsg/get_all_logs.sql b/src/server/sql/logmsg/get_all_logs.sql new file mode 100644 index 000000000..b44910172 --- /dev/null +++ b/src/server/sql/logmsg/get_all_logs.sql @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +-- gets all logs in the database +SELECT * FROM logmsg; \ No newline at end of file diff --git a/src/server/sql/logbase/get_logs_from_dates.sql b/src/server/sql/logmsg/get_logs_from_dates.sql similarity index 51% rename from src/server/sql/logbase/get_logs_from_dates.sql rename to src/server/sql/logmsg/get_logs_from_dates.sql index 3294a0f2f..17780ef5a 100644 --- a/src/server/sql/logbase/get_logs_from_dates.sql +++ b/src/server/sql/logmsg/get_logs_from_dates.sql @@ -2,9 +2,9 @@ -- Gets logs in table by date range. This is then ordered by time ascending. SELECT - -- Short column names for smaller data. - log_type as p, log_message as m, log_time as i -FROM logbase + -- Short column names for smaller data. + log_type, log_message as log_msg, log_time +FROM logmsg WHERE log_time >= COALESCE(${startDate}, '-infinity'::TIMESTAMP) - AND log_time <= COALESCE(${endDate}, 'infinity'::TIMESTAMP) + AND log_time <= COALESCE(${endDate}, 'infinity'::TIMESTAMP) ORDER BY log_time ASC; \ No newline at end of file diff --git a/src/server/sql/logmsg/get_logs_from_dates_and_type.sql b/src/server/sql/logmsg/get_logs_from_dates_and_type.sql new file mode 100644 index 000000000..90fe43b1f --- /dev/null +++ b/src/server/sql/logmsg/get_logs_from_dates_and_type.sql @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +-- Gets logs in table by date range. This is then ordered by time ascending. +SELECT + -- Short column names for smaller data. + log_type as p, log_message as m, log_time as i +FROM logmsg +WHERE log_type = ${logType} + AND log_time >= COALESCE(${startDate}, '-infinity'::TIMESTAMP) + AND log_time <= COALESCE(${endDate}, 'infinity'::TIMESTAMP) +ORDER BY log_time ASC; \ No newline at end of file diff --git a/src/server/sql/logmsg/get_logs_from_type.sql b/src/server/sql/logmsg/get_logs_from_type.sql new file mode 100644 index 000000000..2997292ff --- /dev/null +++ b/src/server/sql/logmsg/get_logs_from_type.sql @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +-- Gets logs in table by date range. This is then ordered by time ascending. +SELECT + -- Short column names for smaller data. + log_type, log_message as log_msg, log_time +FROM logmsg +WHERE log_type = ${logType} +ORDER BY log_time ASC; \ No newline at end of file diff --git a/src/server/sql/logmsg/insert_new_log.sql b/src/server/sql/logmsg/insert_new_log.sql new file mode 100644 index 000000000..2c5726ca7 --- /dev/null +++ b/src/server/sql/logmsg/insert_new_log.sql @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +--Inserts a new log into the table +INSERT INTO logmsg (log_type, log_message, log_time) +VALUES (${logType}, ${logMessage}, ${logTime}); From 195396cdd8a822923b655dd018a095a13dc7f5be Mon Sep 17 00:00:00 2001 From: ianLewis8 Date: Fri, 2 Aug 2024 16:19:33 -0400 Subject: [PATCH 03/22] slight changes forgot to include these in push --- src/server/sql/logmsg/get_logs_from_dates_and_type.sql | 3 +-- src/server/sql/logmsg/get_logs_from_type.sql | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/server/sql/logmsg/get_logs_from_dates_and_type.sql b/src/server/sql/logmsg/get_logs_from_dates_and_type.sql index 90fe43b1f..48450804a 100644 --- a/src/server/sql/logmsg/get_logs_from_dates_and_type.sql +++ b/src/server/sql/logmsg/get_logs_from_dates_and_type.sql @@ -4,8 +4,7 @@ -- Gets logs in table by date range. This is then ordered by time ascending. SELECT - -- Short column names for smaller data. - log_type as p, log_message as m, log_time as i + log_type, log_message, log_time FROM logmsg WHERE log_type = ${logType} AND log_time >= COALESCE(${startDate}, '-infinity'::TIMESTAMP) diff --git a/src/server/sql/logmsg/get_logs_from_type.sql b/src/server/sql/logmsg/get_logs_from_type.sql index 2997292ff..0e72fd0d1 100644 --- a/src/server/sql/logmsg/get_logs_from_type.sql +++ b/src/server/sql/logmsg/get_logs_from_type.sql @@ -4,7 +4,6 @@ -- Gets logs in table by date range. This is then ordered by time ascending. SELECT - -- Short column names for smaller data. log_type, log_message as log_msg, log_time FROM logmsg WHERE log_type = ${logType} From 07584018e18c138a20eb3b86d4869ecbf70265eb Mon Sep 17 00:00:00 2001 From: ianLewis8 Date: Wed, 7 Aug 2024 16:34:59 -0400 Subject: [PATCH 04/22] Update get logs from dates added header file --- src/server/sql/logmsg/get_logs_from_dates.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/sql/logmsg/get_logs_from_dates.sql b/src/server/sql/logmsg/get_logs_from_dates.sql index 17780ef5a..7e20b1350 100644 --- a/src/server/sql/logmsg/get_logs_from_dates.sql +++ b/src/server/sql/logmsg/get_logs_from_dates.sql @@ -1,4 +1,6 @@ - +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -- Gets logs in table by date range. This is then ordered by time ascending. SELECT From d0a4fc5fb11114b66f9ac60e052f829314245982 Mon Sep 17 00:00:00 2001 From: nmqng Date: Mon, 14 Oct 2024 18:52:33 -0400 Subject: [PATCH 05/22] create logmsg table in database --- src/server/models/database.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/models/database.js b/src/server/models/database.js index 50ef1de89..8bb3f3ebc 100644 --- a/src/server/models/database.js +++ b/src/server/models/database.js @@ -79,6 +79,7 @@ async function createSchema(conn) { const Configfile = require('./obvius/Configfile'); const Migration = require('./Migration'); const LogEmail = require('./LogEmail'); + const LogMsg = require('./LogMsg'); const Baseline = require('./Baseline'); const { Map } = require('./Map'); const Unit = require('./Unit'); @@ -106,10 +107,12 @@ async function createSchema(conn) { await Group.createTables(conn); await Migration.createTable(conn); await LogEmail.createTable(conn); + await LogMsg.createLogMsgTypeEnum(conn); + await LogMsg.createTable(conn); await Reading.createReadingsMaterializedViews(conn); await Reading.createCompareReadingsFunction(conn); // For 3D reading - await Reading.create3DReadingsFunction(conn); + await Reading.create3DReadingsFunction(conn); await Baseline.createTable(conn); await Map.createTable(conn); await conn.none(sqlFile('baseline/create_function_get_average_reading.sql')); From c72148b3aa1749bd214a3be5dbdb9ccdbb2afb3f Mon Sep 17 00:00:00 2001 From: nmqng Date: Wed, 30 Oct 2024 16:43:01 -0400 Subject: [PATCH 06/22] upload for fetching logs data from server question --- .../app/components/HeaderButtonsComponent.tsx | 11 +- src/client/app/components/RouteComponent.tsx | 4 +- .../app/components/admin/LogMsgComponent.tsx | 330 ++++++++++++++++++ src/client/app/translations/data.ts | 9 +- src/client/app/utils/api/LogsApi.ts | 7 +- src/server/log.js | 25 +- src/server/routes/logs.js | 17 + 7 files changed, 396 insertions(+), 7 deletions(-) create mode 100644 src/client/app/components/admin/LogMsgComponent.tsx diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 39fda6430..b4955184c 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -61,6 +61,7 @@ export default function HeaderButtonsComponent() { shouldCSVButtonDisabled: true, shouldUnitsButtonDisabled: true, shouldConversionsButtonDisabled: true, + shouldLogMsgButtonDisabled: true, // Translated menu title that depend on whether logged in. menuTitle: '', // link to help page for page choices. Should not see default but use general help URL. @@ -95,7 +96,8 @@ export default function HeaderButtonsComponent() { shouldMapsButtonDisabled: pathname === '/maps', shouldCSVButtonDisabled: pathname === '/csv', shouldUnitsButtonDisabled: pathname === '/units', - shouldConversionsButtonDisabled: pathname === '/conversions' + shouldConversionsButtonDisabled: pathname === '/conversions', + shouldLogMsgButtonDisabled: pathname === '/logmsg' })); }, [pathname]); @@ -213,6 +215,13 @@ export default function HeaderButtonsComponent() { to="/admin"> + + + }, { path: 'units', element: }, { path: 'conversions', element: }, - { path: 'users', element: } + { path: 'users', element: }, + { path: 'logmsg', element: } ] }, { diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx new file mode 100644 index 000000000..b01150426 --- /dev/null +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -0,0 +1,330 @@ +import * as React from 'react'; +import { Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, FormGroup, Input, Label, Table } from 'reactstrap'; +import DateRangePicker from '@wojtekmaj/react-daterange-picker'; +import { useAppSelector } from '../../redux/reduxHooks'; +import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; +import { showWarnNotification } from '../../utils/notifications'; +import { logsApi } from '../../utils/api'; +// import translate from '../../utils/translate'; + +const initialLogs = [ + { + id: 1, + log_type: 'ERROR', + // eslint-disable-next-line max-len + log_message: 'Unhandled Promise Rejection: AggregateError Stacktrace: AggregateError [ECONNREFUSED]: at internalConnectMultiple (node:net:1118:18) at afterConnectMultiple (node:net:1685:7)', + log_time: '2024-09-11 16:58:51.303' + }, + { + id: 2, + log_type: 'ERROR', + // eslint-disable-next-line max-len + log_message: 'Unhandled Promise Rejection: AggregateError Stacktrace: AggregateError [ECONNREFUSED]: at internalConnectMultiple(node:net:1118:18) at afterConnectMultiple (node:net:1685:7)', + log_time: '2024-09-11 16:59:12.524' + }, + { + id: 3, + log_type: 'WARN', + log_message: 'Automatically set identifier of the unit "kg" to "kg"', + log_time: '2024-09-12 17:35:29.953' + }, + { + id: 4, + log_type: 'WARN', + log_message: 'Automatically set identifier of the unit "liter" to "liter"', + log_time: '2024-09-12 17:35:29.954' + }, + { + id: 5, + log_type: 'WARN', + log_message: 'Automatically set identifier of the unit "Fahrenheit" to "Fahrenheit"', + log_time: '2024-09-12 17:35:29.954' + }, + { + id: 6, + log_type: 'INFO', + log_message: 'Schema created', + log_time: '2024-10-01 21:53:13.505' + }, + { + id: 7, + log_type: 'INFO', + log_message: 'Listening on port 3000', + log_time: '2024-10-01 21:53:35.09' + }, + { + id: 8, + log_type: 'INFO', + // eslint-disable-next-line max-len + log_message: 'The uploaded file /usr/src/app/src/server/tmp/uploads/csvPipeline/2024-10-01_22:49:55.358-29ac-3yrLeap.csv was created to upload readings csv data', + log_time: '2024-10-01 22:49:55.374' + }, + { + id: 9, + log_type: 'ERROR', + // eslint-disable-next-line max-len + log_message: 'Unhandled Promise Rejection: AggregateError Stacktrace: AggregateError [ECONNREFUSED]: at internalConnectMultiple (node:net:1118:18) at afterConnectMultiple (node:net:1685:7)', + log_time: '2024-09-11T16:58:51.303-04:00' + }, + { + id: 10, + log_type: 'ERROR', + // eslint-disable-next-line max-len + log_message: 'Unhandled Promise Rejection: AggregateError Stacktrace: AggregateError [ECONNREFUSED]: at internalConnectMultiple(node:net:1118:18) at afterConnectMultiple (node:net:1685:7)', + log_time: '2024-09-11T16:59:12.524-04:00' + }, + { + id: 11, + log_type: 'WARN', + log_message: 'Automatically set identifier of the unit "kg" to "kg"', + log_time: '2024-09-12T17:35:29.953+00:00' + }, + { + id: 12, + log_type: 'WARN', + log_message: 'Automatically set identifier of the unit "liter" to "liter"', + log_time: '2024-09-12T17:35:29.954+00:00' + }, + { + id: 13, + log_type: 'WARN', + log_message: 'Automatically set identifier of the unit "Fahrenheit" to "Fahrenheit"', + log_time: '2024-09-12T17:35:29.954+00:00' + }, + { + id: 14, + log_type: 'INFO', + log_message: 'Schema created', + log_time: '2024-10-01T21:53:13.505+00:00' + }, + { + id: 15, + log_type: 'INFO', + log_message: 'Listening on port 3000', + log_time: '2024-10-01T21:53:35.090+00:00' + }, + { + id: 16, + log_type: 'INFO', + // eslint-disable-next-line max-len + log_message: 'The uploaded file /usr/src/app/src/server/tmp/uploads/csvPipeline/2024-10-01_22:49:55.358-29ac-3yrLeap.csv was created to upload readings csv data', + log_time: '2024-10-01T22:49:55.374+00:00' + } +]; + +const logTypes = ['ERROR', 'INFO', 'WARN', 'SILENT']; + +const LogMsgComponent = () => { + const locale = useAppSelector(selectSelectedLanguage); + const [logs, setLogs] = React.useState(initialLogs); + + // Button state to handle showing the log table + const [showLogTable, setShowLogTable] = React.useState(false); + + const [selectedLogTypes, setSelectedLogTypes] = React.useState(logTypes); + const [dateSortOrder, setDateSortOrder] = React.useState<'asc' | 'desc'>('asc'); + const [logDateRange, setLogDateRange] = React.useState<[Date | null, Date | null]>([null, null]); + // Dropdown open state for log type in the header + const [dropdownOpen, setDropdownOpen] = React.useState(false); + + // fetching logs data from server + React.useEffect(() => { + const fetchLogs = async () => { + try { + const data = await logsApi.getAllLogs(); + console.log('data: ', data); + + } catch (error) { + console.error('Failed to fetch logs:', error); + } + }; + fetchLogs(); + console.log('fetching logs: ', fetchLogs); + }, []); + // Handle checkbox change + const handleCheckboxChange = (logType: string) => { + if (selectedLogTypes.includes(logType)) { + // Remove log type if already selected + setSelectedLogTypes(selectedLogTypes.filter(type => type !== logType)); + } else { + // Add log type if not selected + setSelectedLogTypes([...selectedLogTypes, logType]); + } + }; + + const handleDateSort = () => { + const newDateSortOrder = dateSortOrder === 'asc' ? 'desc' : 'asc'; + const sortedLogs = [...logs].sort((a, b) => { + const dateA = new Date(a.log_time); + const dateB = new Date(b.log_time); + if (newDateSortOrder === 'asc') { + return dateA.getTime() - dateB.getTime(); + } else { + return dateB.getTime() - dateA.getTime(); + } + }); + setDateSortOrder(newDateSortOrder); + setLogs(sortedLogs); + }; + + const handleDateRangeChange = (range: [Date | null, Date | null]) => { + setLogDateRange(range); + }; + + // Toggle dropdown in the header + const onToggleDropdown = () => { + setDropdownOpen(!dropdownOpen); + }; + + // Filter logs based on selected log types and date range + const filteredLogs = logs.filter(log => { + const logDate = new Date(log.log_time); + + // Check if log is within the selected date range + const isWithinDateRange = + (!logDateRange || !logDateRange[0] || logDate >= logDateRange[0]) && + (!logDateRange || !logDateRange[1] || logDate <= logDateRange[1]); + + return selectedLogTypes.includes(log.log_type) && isWithinDateRange; + }); + + const handleShowLogTable = () => { + if (!logDateRange || !logDateRange[0] || !logDateRange[1]) { + // showWarnNotification(translate('shifted.data.crosses.leap.year.to.non.leap.year')); + showWarnNotification('You must select a date range'); + } else { + setShowLogTable(true); + } + }; + + return ( + showLogTable ? + <> +

Log Messages

+ +

Date Range:

+ +
+ + + + + + + + + + {filteredLogs.map(log => ( + + + + + + ))} + + +
+ + + Log Type + + + {logTypes.map(logType => ( + + + + ))} + + + Log MessageLog Time {dateSortOrder === 'asc' ? '↑' : '↓'}
{log.log_type}{log.log_message}{new Date(log.log_time).toLocaleString()}
+ + : + +
+
+ Please choose log types and date range for log data +
+
+ + + Log Type + + + {logTypes.map(logType => ( + + + + ))} + + + +

Date Range:

+ + +
+
+ + + + +
+
+
+ ); +}; + +export default LogMsgComponent; + +const headerStyle: React.CSSProperties = { + textAlign: 'center' +}; +const bodyStyle: React.CSSProperties = { + textAlign: 'left' +}; +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + +const tableStyle: React.CSSProperties = { + width: '90%', + margin: 'auto' +}; + +const logFilterStyle: React.CSSProperties = { + display: 'flex', + marginLeft: '9%', + gap: '3%' +}; + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + marginRight: '-2.5%', + padding: 'none' +}; \ No newline at end of file diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 1ffba4030..e656cb081 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -512,7 +512,8 @@ const LocaleTranslationData = { "week": "Week", "yes": "yes", "yesterday": "Yesterday", - "you.cannot.create.a.cyclic.group": "You cannot create a cyclic group" + "you.cannot.create.a.cyclic.group": "You cannot create a cyclic group", + "log.messages": "Log Messages" }, "fr": { "3D": "3D", @@ -1020,7 +1021,8 @@ const LocaleTranslationData = { "week": "Semaine", "yes": " yes\u{26A1}", "yesterday": "Hier", - "you.cannot.create.a.cyclic.group": "Vous ne pouvez pas créer un groupe cyclique" + "you.cannot.create.a.cyclic.group": "Vous ne pouvez pas créer un groupe cyclique", + "log.messages": "Log Messages\u{26A1}" }, "es": { "3D": "3D", @@ -1529,7 +1531,8 @@ const LocaleTranslationData = { "week": "semana", "yes": "sí", "yesterday": "Ayer", - "you.cannot.create.a.cyclic.group": "No se puede crear un grupo cíclico" + "you.cannot.create.a.cyclic.group": "No se puede crear un grupo cíclico", + "log.messages": "Log Messages\u{26A1}" } } diff --git a/src/client/app/utils/api/LogsApi.ts b/src/client/app/utils/api/LogsApi.ts index d1f2a4211..4f3444eb8 100644 --- a/src/client/app/utils/api/LogsApi.ts +++ b/src/client/app/utils/api/LogsApi.ts @@ -5,7 +5,7 @@ */ import ApiBackend from './ApiBackend'; -import {LogData} from '../../types/redux/logs'; +import { LogData } from '../../types/redux/logs'; export default class LogsApi { private readonly backend: ApiBackend; @@ -25,4 +25,9 @@ export default class LogsApi { public async error(log: LogData): Promise { return await this.backend.doPostRequest('/api/logs/error', log); } + + // fetch all logs + public async getAllLogs(): Promise { + return await this.backend.doGetRequest('/api/logs/logsmsg'); + } } diff --git a/src/server/log.js b/src/server/log.js index 2c1a24196..d4d5818fc 100644 --- a/src/server/log.js +++ b/src/server/log.js @@ -5,6 +5,7 @@ const fs = require('fs'); const logFile = require('./config').logFile; const LogEmail = require('./models/LogEmail'); +const LogMsg = require('./models/LogMsg'); const { getConnection } = require('./db'); const moment = require('moment'); @@ -71,13 +72,35 @@ class Logger { // Always log to the logfile. if (this.logToFile) { - fs.appendFile(logFile, messageToLog, err => { + fs.appendFile(logFile, messageToLog, async err => { if (err) { console.error(`Failed to write to log file: ${err} (${err.stack})`); // tslint:disable-line no-console } }); + + fs.readFile(logFile, 'utf8', async (err, data) => { + if (err) { + console.error(`Failed to read log file: ${err} (${err.stack})`); // tslint:disable-line no-console + return; + } + + const logEntries = data.split('\n').filter(entry => entry.trim() !== ''); + for (const entry of logEntries) { + const logParts = entry.match(/\[(.*?)@(.*?)\] (.*)/); + if (logParts) { + const [, logType, logTime, logMessage] = logParts; + const logMsg = new LogMsg(logType, logMessage, new Date(logTime)); + try { + await logMsg.insert(conn); + } catch (err) { + console.error(`Failed to write log to database: ${err} (${err.stack})`); // tslint:disable-line no-console + } + } + } + }) } + // Only log elsewhere if given a high enough priority level. if (level.priority <= this.level.priority && !skipMail) { if (this.logToConsole) { diff --git a/src/server/routes/logs.js b/src/server/routes/logs.js index 7e74ad863..31a04ee70 100644 --- a/src/server/routes/logs.js +++ b/src/server/routes/logs.js @@ -8,6 +8,7 @@ const express = require('express'); const { log } = require('../log'); const validate = require('jsonschema').validate; const adminAuthenticator = require('./authenticator').adminAuthMiddleware; +const LogMsg = require('../models/LogMsg'); const router = express.Router(); router.use(adminAuthenticator('log API')); @@ -55,4 +56,20 @@ router.post('/error', async (req, res) => { } }); + +router.get('/logsmsg', async (req, res) => { + const conn = getConnection(); + try { + const rows = await LogMsg.getAll(conn); + // console.log('can fetch'); + + res.json(rows); + // console.log(rows); + + } catch (err) { + console.error(`Failed to fetch logs: ${err}`); + res.status(500).send('Failed to fetch logs'); + } +}); + module.exports = router; From acc3e1a166f7a546ffe43b8346e3f9cebb06cff0 Mon Sep 17 00:00:00 2001 From: nmqng Date: Sun, 3 Nov 2024 19:27:08 -0500 Subject: [PATCH 07/22] Pre-review version for log message table now able to call log data from server --- .../app/components/admin/LogMsgComponent.tsx | 178 +++++------------- src/client/app/utils/api/LogsApi.ts | 11 ++ src/server/models/LogMsg.js | 14 +- src/server/routes/logs.js | 63 +++++-- src/server/sql/logmsg/get_logs_from_dates.sql | 3 +- src/server/sql/logmsg/get_logs_from_type.sql | 2 +- 6 files changed, 119 insertions(+), 152 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index b01150426..439cc6bb5 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -7,114 +7,22 @@ import { showWarnNotification } from '../../utils/notifications'; import { logsApi } from '../../utils/api'; // import translate from '../../utils/translate'; -const initialLogs = [ - { - id: 1, - log_type: 'ERROR', - // eslint-disable-next-line max-len - log_message: 'Unhandled Promise Rejection: AggregateError Stacktrace: AggregateError [ECONNREFUSED]: at internalConnectMultiple (node:net:1118:18) at afterConnectMultiple (node:net:1685:7)', - log_time: '2024-09-11 16:58:51.303' - }, - { - id: 2, - log_type: 'ERROR', - // eslint-disable-next-line max-len - log_message: 'Unhandled Promise Rejection: AggregateError Stacktrace: AggregateError [ECONNREFUSED]: at internalConnectMultiple(node:net:1118:18) at afterConnectMultiple (node:net:1685:7)', - log_time: '2024-09-11 16:59:12.524' - }, - { - id: 3, - log_type: 'WARN', - log_message: 'Automatically set identifier of the unit "kg" to "kg"', - log_time: '2024-09-12 17:35:29.953' - }, - { - id: 4, - log_type: 'WARN', - log_message: 'Automatically set identifier of the unit "liter" to "liter"', - log_time: '2024-09-12 17:35:29.954' - }, - { - id: 5, - log_type: 'WARN', - log_message: 'Automatically set identifier of the unit "Fahrenheit" to "Fahrenheit"', - log_time: '2024-09-12 17:35:29.954' - }, - { - id: 6, - log_type: 'INFO', - log_message: 'Schema created', - log_time: '2024-10-01 21:53:13.505' - }, - { - id: 7, - log_type: 'INFO', - log_message: 'Listening on port 3000', - log_time: '2024-10-01 21:53:35.09' - }, - { - id: 8, - log_type: 'INFO', - // eslint-disable-next-line max-len - log_message: 'The uploaded file /usr/src/app/src/server/tmp/uploads/csvPipeline/2024-10-01_22:49:55.358-29ac-3yrLeap.csv was created to upload readings csv data', - log_time: '2024-10-01 22:49:55.374' - }, - { - id: 9, - log_type: 'ERROR', - // eslint-disable-next-line max-len - log_message: 'Unhandled Promise Rejection: AggregateError Stacktrace: AggregateError [ECONNREFUSED]: at internalConnectMultiple (node:net:1118:18) at afterConnectMultiple (node:net:1685:7)', - log_time: '2024-09-11T16:58:51.303-04:00' - }, - { - id: 10, - log_type: 'ERROR', - // eslint-disable-next-line max-len - log_message: 'Unhandled Promise Rejection: AggregateError Stacktrace: AggregateError [ECONNREFUSED]: at internalConnectMultiple(node:net:1118:18) at afterConnectMultiple (node:net:1685:7)', - log_time: '2024-09-11T16:59:12.524-04:00' - }, - { - id: 11, - log_type: 'WARN', - log_message: 'Automatically set identifier of the unit "kg" to "kg"', - log_time: '2024-09-12T17:35:29.953+00:00' - }, - { - id: 12, - log_type: 'WARN', - log_message: 'Automatically set identifier of the unit "liter" to "liter"', - log_time: '2024-09-12T17:35:29.954+00:00' - }, - { - id: 13, - log_type: 'WARN', - log_message: 'Automatically set identifier of the unit "Fahrenheit" to "Fahrenheit"', - log_time: '2024-09-12T17:35:29.954+00:00' - }, - { - id: 14, - log_type: 'INFO', - log_message: 'Schema created', - log_time: '2024-10-01T21:53:13.505+00:00' - }, - { - id: 15, - log_type: 'INFO', - log_message: 'Listening on port 3000', - log_time: '2024-10-01T21:53:35.090+00:00' - }, - { - id: 16, - log_type: 'INFO', - // eslint-disable-next-line max-len - log_message: 'The uploaded file /usr/src/app/src/server/tmp/uploads/csvPipeline/2024-10-01_22:49:55.358-29ac-3yrLeap.csv was created to upload readings csv data', - log_time: '2024-10-01T22:49:55.374+00:00' - } -]; +// interface LogMsgData { +// logType: string; +// logMessage: string; +// logTime: string; +// } + +const initialLogs: any[] = []; + const logTypes = ['ERROR', 'INFO', 'WARN', 'SILENT']; -const LogMsgComponent = () => { +/** + * React component that defines the log message page + * @returns LogMsgComponent element + */ +export default function LogMsgComponent() { const locale = useAppSelector(selectSelectedLanguage); const [logs, setLogs] = React.useState(initialLogs); @@ -127,20 +35,8 @@ const LogMsgComponent = () => { // Dropdown open state for log type in the header const [dropdownOpen, setDropdownOpen] = React.useState(false); - // fetching logs data from server - React.useEffect(() => { - const fetchLogs = async () => { - try { - const data = await logsApi.getAllLogs(); - console.log('data: ', data); + // const [selectAllOption, setSelectAllOption] = React.useState(true); - } catch (error) { - console.error('Failed to fetch logs:', error); - } - }; - fetchLogs(); - console.log('fetching logs: ', fetchLogs); - }, []); // Handle checkbox change const handleCheckboxChange = (logType: string) => { if (selectedLogTypes.includes(logType)) { @@ -157,6 +53,8 @@ const LogMsgComponent = () => { const sortedLogs = [...logs].sort((a, b) => { const dateA = new Date(a.log_time); const dateB = new Date(b.log_time); + // const dateA = new Date(a.logTime); + // const dateB = new Date(b.logTime); if (newDateSortOrder === 'asc') { return dateA.getTime() - dateB.getTime(); } else { @@ -178,24 +76,36 @@ const LogMsgComponent = () => { // Filter logs based on selected log types and date range const filteredLogs = logs.filter(log => { - const logDate = new Date(log.log_time); + // const logDate = new Date(log.log_time); + const logDate = new Date(log.logTime); // Check if log is within the selected date range const isWithinDateRange = (!logDateRange || !logDateRange[0] || logDate >= logDateRange[0]) && (!logDateRange || !logDateRange[1] || logDate <= logDateRange[1]); - return selectedLogTypes.includes(log.log_type) && isWithinDateRange; + // return selectedLogTypes.includes(log.log_type) && isWithinDateRange; + return selectedLogTypes.includes(log.logType) && isWithinDateRange; }); - const handleShowLogTable = () => { + /** + * Handle showing the log table + */ + async function handleShowLogTable() { if (!logDateRange || !logDateRange[0] || !logDateRange[1]) { - // showWarnNotification(translate('shifted.data.crosses.leap.year.to.non.leap.year')); showWarnNotification('You must select a date range'); } else { + try { + // get log by date and type + const data = await logsApi.getLogsByDateRangeAndType(logDateRange[0].toISOString(), logDateRange[1].toISOString(), selectedLogTypes); + setLogs(data); + + } catch (error) { + console.error(error); + } setShowLogTable(true); } - }; + } return ( showLogTable ? @@ -240,11 +150,19 @@ const LogMsgComponent = () => { - {filteredLogs.map(log => ( - - {log.log_type} - {log.log_message} - {new Date(log.log_time).toLocaleString()} + {filteredLogs.map((log, index) => ( + + {log.logType} + {log.logMessage} + {new Date(log.logTime).toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 2 + })} ))} @@ -298,9 +216,7 @@ const LogMsgComponent = () => { ); -}; - -export default LogMsgComponent; +} const headerStyle: React.CSSProperties = { textAlign: 'center' diff --git a/src/client/app/utils/api/LogsApi.ts b/src/client/app/utils/api/LogsApi.ts index 4f3444eb8..96ed0e919 100644 --- a/src/client/app/utils/api/LogsApi.ts +++ b/src/client/app/utils/api/LogsApi.ts @@ -30,4 +30,15 @@ export default class LogsApi { public async getAllLogs(): Promise { return await this.backend.doGetRequest('/api/logs/logsmsg'); } + + public async getLogsByDateRangeAndType(startDate: string, endDate: string, types: string[]): Promise { + const requests = types.map( + type => this.backend.doGetRequest('/api/logs/logsmsg/getLogsByDateRangeAndType', + { startDate: startDate, endDate: endDate, logType: type }) + ); + const results = await Promise.all(requests); + + return results.flat() as LogData[]; + } + } diff --git a/src/server/models/LogMsg.js b/src/server/models/LogMsg.js index b0f67007f..b8f23e6d7 100644 --- a/src/server/models/LogMsg.js +++ b/src/server/models/LogMsg.js @@ -30,7 +30,7 @@ class LogMsg { * @returns The new log object. */ static mapRow(row) { - return new LogMsg(row.logType, row.logMessage, row.logTime); + return new LogMsg(row.log_type, row.log_message, row.log_time); } /** @@ -47,9 +47,9 @@ class LogMsg { * @param {*} conn The connection to use. * @returns {Promise.<>} */ - static createLogMsgTypeEnum(conn) { - return conn.none(sqlFile('logmsg/create_log_types_enum.sql')); - } + static createLogMsgTypeEnum(conn) { + return conn.none(sqlFile('logmsg/create_log_types_enum.sql')); + } /** * Returns a promise to insert this log into the database @@ -97,8 +97,8 @@ class LogMsg { * @param conn is the connection to use. * @returns {Promise.>} */ - static async getLogsByType(logType, conn){ - const rows = await conn.any(sqlFile('logmsg/get_logs_from_type.sql'), {logType: logType}); + static async getLogsByType(logType, conn) { + const rows = await conn.any(sqlFile('logmsg/get_logs_from_type.sql'), { logType: logType }); return rows.map(LogMsg.mapRow); } @@ -119,8 +119,8 @@ class LogMsg { endDate: endDate, logType: logType }); - return rows.map(LogMsg.mapRow); + } } module.exports = LogMsg; \ No newline at end of file diff --git a/src/server/routes/logs.js b/src/server/routes/logs.js index 31a04ee70..50c49416d 100644 --- a/src/server/routes/logs.js +++ b/src/server/routes/logs.js @@ -9,6 +9,7 @@ const { log } = require('../log'); const validate = require('jsonschema').validate; const adminAuthenticator = require('./authenticator').adminAuthMiddleware; const LogMsg = require('../models/LogMsg'); +const { getConnection } = require('../db'); const router = express.Router(); router.use(adminAuthenticator('log API')); @@ -23,6 +24,34 @@ const validLog = { } } } + +const validLogMsg = { + type: 'object', + required: ['startDate', 'endDate', 'logType'], + properties: { + startDate: { + type: 'string', + format: 'date-time' + }, + endDate: { + type: 'string', + format: 'date-time' + }, + logType: { + type: 'string', + enum: ['INFO', 'WARN', 'ERROR', 'SILENT'] + }, + + // use this if later database is changed to be able to deal with logType as a list + // logType: { + // type: 'array', + // items: { + // type: 'string', + // enum: ['INFO', 'WARN', 'ERROR', 'SILENT'] + // } + // } + } +} router.post('/info', async (req, res) => { const validationResult = validate(req.body, validLog); if (validationResult.valid) { @@ -57,18 +86,30 @@ router.post('/error', async (req, res) => { }); -router.get('/logsmsg', async (req, res) => { - const conn = getConnection(); - try { - const rows = await LogMsg.getAll(conn); - // console.log('can fetch'); - - res.json(rows); - // console.log(rows); +// router.get('/logsmsg', async (req, res) => { +// const conn = getConnection(); +// try { +// const rows = await LogMsg.getAll(conn); +// res.json(rows); +// } catch (err) { +// console.error(`Failed to fetch in getAll: ${err}`); +// res.sendStatus(500); +// } +// }); - } catch (err) { - console.error(`Failed to fetch logs: ${err}`); - res.status(500).send('Failed to fetch logs'); +router.get('/logsmsg/getLogsByDateRangeAndType', async (req, res) => { + const validationResult = validate(req.query, validLogMsg); + if (!validationResult.valid) { + res.sendStatus(400); + } else { + const conn = getConnection(); + try { + const rows = await LogMsg.getLogsByDateRangeAndType(req.query.startDate, req.query.endDate, req.query.logType, conn); + res.json(rows); + } catch (err) { + console.error(`Failed to fetch logs filter by date range and type: ${err}`); + res.sendStatus(500); + } } }); diff --git a/src/server/sql/logmsg/get_logs_from_dates.sql b/src/server/sql/logmsg/get_logs_from_dates.sql index 7e20b1350..d2383fbcf 100644 --- a/src/server/sql/logmsg/get_logs_from_dates.sql +++ b/src/server/sql/logmsg/get_logs_from_dates.sql @@ -4,8 +4,7 @@ -- Gets logs in table by date range. This is then ordered by time ascending. SELECT - -- Short column names for smaller data. - log_type, log_message as log_msg, log_time + log_type, log_message, log_time FROM logmsg WHERE log_time >= COALESCE(${startDate}, '-infinity'::TIMESTAMP) AND log_time <= COALESCE(${endDate}, 'infinity'::TIMESTAMP) diff --git a/src/server/sql/logmsg/get_logs_from_type.sql b/src/server/sql/logmsg/get_logs_from_type.sql index 0e72fd0d1..174dbde8a 100644 --- a/src/server/sql/logmsg/get_logs_from_type.sql +++ b/src/server/sql/logmsg/get_logs_from_type.sql @@ -4,7 +4,7 @@ -- Gets logs in table by date range. This is then ordered by time ascending. SELECT - log_type, log_message as log_msg, log_time + log_type, log_message , log_time FROM logmsg WHERE log_type = ${logType} ORDER BY log_time ASC; \ No newline at end of file From 02bc0d937fdba3a592d3244425f6792fe1986161 Mon Sep 17 00:00:00 2001 From: nmqng Date: Mon, 4 Nov 2024 14:02:39 -0500 Subject: [PATCH 08/22] fix writing the whole log file to database again to only write new log --- src/server/log.js | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/server/log.js b/src/server/log.js index d4d5818fc..e55495479 100644 --- a/src/server/log.js +++ b/src/server/log.js @@ -78,29 +78,21 @@ class Logger { } }); - fs.readFile(logFile, 'utf8', async (err, data) => { - if (err) { - console.error(`Failed to read log file: ${err} (${err.stack})`); // tslint:disable-line no-console - return; - } - - const logEntries = data.split('\n').filter(entry => entry.trim() !== ''); - for (const entry of logEntries) { - const logParts = entry.match(/\[(.*?)@(.*?)\] (.*)/); - if (logParts) { - const [, logType, logTime, logMessage] = logParts; - const logMsg = new LogMsg(logType, logMessage, new Date(logTime)); - try { - await logMsg.insert(conn); - } catch (err) { - console.error(`Failed to write log to database: ${err} (${err.stack})`); // tslint:disable-line no-console - } + // Write the new log to the database + const logMsgPatern = messageToLog.match(/\[(.*?)@(.*?)\] (.*)/); + if (logMsgPatern) { + const [, logType, logTime, logMessage] = logMsgPatern; + const logMsg = new LogMsg(logType, logMessage, new Date(logTime)); + (async () => { + try { + await logMsg.insert(conn); + } catch (err) { + console.error(`Failed to write log to database: ${err} (${err.stack})`); } - } - }) + })(); + } } - // Only log elsewhere if given a high enough priority level. if (level.priority <= this.level.priority && !skipMail) { if (this.logToConsole) { From b18deaa94dcf61c6b0220502b06202030a9e51de Mon Sep 17 00:00:00 2001 From: nmqng Date: Sat, 9 Nov 2024 01:30:34 -0500 Subject: [PATCH 09/22] improve UI by using pagination --- .../app/components/admin/LogMsgComponent.tsx | 175 ++++++++++++------ 1 file changed, 115 insertions(+), 60 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index 439cc6bb5..a36868dea 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -13,6 +13,8 @@ import { logsApi } from '../../utils/api'; // logTime: string; // } +const PER_PAGE = 25; + const initialLogs: any[] = []; @@ -35,8 +37,12 @@ export default function LogMsgComponent() { // Dropdown open state for log type in the header const [dropdownOpen, setDropdownOpen] = React.useState(false); + // number of log messages to display + const [logLimit, setLogLimit] = React.useState(0); // const [selectAllOption, setSelectAllOption] = React.useState(true); + const [currentPage, setCurrentPage] = React.useState(1); + // Handle checkbox change const handleCheckboxChange = (logType: string) => { if (selectedLogTypes.includes(logType)) { @@ -51,10 +57,9 @@ export default function LogMsgComponent() { const handleDateSort = () => { const newDateSortOrder = dateSortOrder === 'asc' ? 'desc' : 'asc'; const sortedLogs = [...logs].sort((a, b) => { - const dateA = new Date(a.log_time); - const dateB = new Date(b.log_time); - // const dateA = new Date(a.logTime); - // const dateB = new Date(b.logTime); + const dateA = new Date(a.logTime); + const dateB = new Date(b.logTime); + if (newDateSortOrder === 'asc') { return dateA.getTime() - dateB.getTime(); } else { @@ -69,6 +74,10 @@ export default function LogMsgComponent() { setLogDateRange(range); }; + const handlePageChange = (newPage: number) => { + setCurrentPage(newPage); + }; + // Toggle dropdown in the header const onToggleDropdown = () => { setDropdownOpen(!dropdownOpen); @@ -76,7 +85,6 @@ export default function LogMsgComponent() { // Filter logs based on selected log types and date range const filteredLogs = logs.filter(log => { - // const logDate = new Date(log.log_time); const logDate = new Date(log.logTime); // Check if log is within the selected date range @@ -84,26 +92,31 @@ export default function LogMsgComponent() { (!logDateRange || !logDateRange[0] || logDate >= logDateRange[0]) && (!logDateRange || !logDateRange[1] || logDate <= logDateRange[1]); - // return selectedLogTypes.includes(log.log_type) && isWithinDateRange; return selectedLogTypes.includes(log.logType) && isWithinDateRange; }); + const paginatedLogs = filteredLogs.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE); + const totalPages = Math.ceil(filteredLogs.length / PER_PAGE); + /** * Handle showing the log table */ async function handleShowLogTable() { if (!logDateRange || !logDateRange[0] || !logDateRange[1]) { showWarnNotification('You must select a date range'); + } else if (!logLimit || logLimit < 1 || logLimit > 1000) { + showWarnNotification('You must enter a valid number of logs to display'); } else { try { // get log by date and type - const data = await logsApi.getLogsByDateRangeAndType(logDateRange[0].toISOString(), logDateRange[1].toISOString(), selectedLogTypes); + const data = await logsApi.getLogsByDateRangeAndType( + logDateRange[0].toISOString(), logDateRange[1].toISOString(), selectedLogTypes, logLimit.toString()); setLogs(data); - + setShowLogTable(true); + setCurrentPage(1); } catch (error) { console.error(error); } - setShowLogTable(true); } } @@ -112,62 +125,88 @@ export default function LogMsgComponent() { <>

Log Messages

-

Date Range:

- + + Log Type + + {logTypes.map(logType => ( + + + + ))} + + +
+

Date Range:

+ +
+ + + setLogLimit(e.target.valueAsNumber)} + invalid={logLimit < 1 || logLimit > 1000} + /> + +
- +
- + - {filteredLogs.map((log, index) => ( + {paginatedLogs.map((log, index) => ( - + ))} -
- - - Log Type - - - {logTypes.map(logType => ( - - - - ))} - - - Log Type Log Message Log Time {dateSortOrder === 'asc' ? '↑' : '↓'}
{log.logType} {log.logMessage}{new Date(log.logTime).toLocaleString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - fractionalSecondDigits: 2 - })} + {new Date(log.logTime).toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 2 + })} +
+
+ {Array.from({ length: totalPages }, (_, index) => ( + + ))} +
: @@ -175,8 +214,8 @@ export default function LogMsgComponent() {
Please choose log types and date range for log data
-
- +
+ Log Type @@ -194,9 +233,12 @@ export default function LogMsgComponent() { ))} - -

Date Range:

+ + + + + setLogLimit(e.target.valueAsNumber)} + required + invalid={logLimit < 1 || logLimit > 1000} + /> +
- - - - +
@@ -230,13 +283,15 @@ const titleStyle: React.CSSProperties = { const tableStyle: React.CSSProperties = { width: '90%', - margin: 'auto' + margin: '2.5% auto' }; const logFilterStyle: React.CSSProperties = { display: 'flex', marginLeft: '9%', - gap: '3%' + gap: '1%', + flexWrap: 'wrap', + alignItems: 'center' }; const labelStyle: React.CSSProperties = { From f1f951ea3541f862ddd640bee2b1b630346f8612 Mon Sep 17 00:00:00 2001 From: nmqng Date: Sat, 9 Nov 2024 02:00:25 -0500 Subject: [PATCH 10/22] update UI to show only first 80 characters for long log messages --- .../app/components/admin/LogMsgComponent.tsx | 311 ++++++++++-------- 1 file changed, 166 insertions(+), 145 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index a36868dea..f47b67cb6 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; -import { Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, FormGroup, Input, Label, Table } from 'reactstrap'; +import { + Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, + FormGroup, Input, Label, Modal, ModalBody, ModalHeader, Table +} from 'reactstrap'; import DateRangePicker from '@wojtekmaj/react-daterange-picker'; import { useAppSelector } from '../../redux/reduxHooks'; import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; @@ -14,10 +17,7 @@ import { logsApi } from '../../utils/api'; // } const PER_PAGE = 25; - const initialLogs: any[] = []; - - const logTypes = ['ERROR', 'INFO', 'WARN', 'SILENT']; /** @@ -43,6 +43,15 @@ export default function LogMsgComponent() { const [currentPage, setCurrentPage] = React.useState(1); + const [modalOpen, setModalOpen] = React.useState(false); + const [modalLogMessage, setModalLogMessage] = React.useState(''); + + // Open modal with the full log message + const handleLogMessageClick = (logMessage: string) => { + setModalLogMessage(logMessage); + setModalOpen(true); + }; + // Handle checkbox change const handleCheckboxChange = (logType: string) => { if (selectedLogTypes.includes(logType)) { @@ -121,153 +130,165 @@ export default function LogMsgComponent() { } return ( - showLogTable ? - <> -

Log Messages

- - - Log Type - - {logTypes.map(logType => ( - - - - ))} - - -
-

Date Range:

- -
- - - setLogLimit(e.target.valueAsNumber)} - invalid={logLimit < 1 || logLimit > 1000} - /> + <> + {showLogTable ? + (<> +

Log Messages

+ + + Log Type + + {logTypes.map(logType => ( + + + + ))} + + +
+

Date Range:

+ +
+ + + setLogLimit(e.target.valueAsNumber)} + invalid={logLimit < 1 || logLimit > 1000} + /> + +
- -
- - - - - - - - - - {paginatedLogs.map((log, index) => ( - - - - +
Log TypeLog MessageLog Time {dateSortOrder === 'asc' ? '↑' : '↓'}
{log.logType}{log.logMessage} - {new Date(log.logTime).toLocaleString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - fractionalSecondDigits: 2 - })} -
+ + + + + + + + {paginatedLogs.map((log, index) => ( + + + + + + ))} + +
Log TypeLog MessageLog Time {dateSortOrder === 'asc' ? '↑' : '↓'}
{log.logType} handleLogMessageClick(log.logMessage)} + >{log.logMessage.length > 80 ? `${log.logMessage.slice(0, 80)} ...` : log.logMessage} + {new Date(log.logTime).toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 2 + })} +
+
+ {Array.from({ length: totalPages }, (_, index) => ( + ))} - - -
- {Array.from({ length: totalPages }, (_, index) => ( - - ))} -
- - : - -
-
- Please choose log types and date range for log data -
-
- - - Log Type - - - {logTypes.map(logType => ( - - - - ))} - - - - - +
+ ) + : + (
+
+ Please choose log types and date range for log data +
+
+ + + Log Type + + + {logTypes.map(logType => ( + + + + ))} + + + + + - - - - setLogLimit(e.target.valueAsNumber)} - required - invalid={logLimit < 1 || logLimit > 1000} - /> - + + + + setLogLimit(e.target.valueAsNumber)} + required + invalid={logLimit < 1 || logLimit > 1000} + /> + +
+
-
-
-
+
) + } + + setModalOpen(!modalOpen)} centered> + setModalOpen(!modalOpen)}>Log Message + + {modalLogMessage} + + + ); } From fcbd0a8a784286ab981a32eb8590b8c3c80e8832 Mon Sep 17 00:00:00 2001 From: nmqng Date: Sat, 9 Nov 2024 12:02:28 -0500 Subject: [PATCH 11/22] add select all log type option and apply translate function for all texts --- .../app/components/admin/LogMsgComponent.tsx | 74 ++++++++++++------- src/client/app/translations/data.ts | 33 ++++++++- 2 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index f47b67cb6..d2fef0c60 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -8,15 +8,9 @@ import { useAppSelector } from '../../redux/reduxHooks'; import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; import { showWarnNotification } from '../../utils/notifications'; import { logsApi } from '../../utils/api'; -// import translate from '../../utils/translate'; +import translate from '../../utils/translate'; -// interface LogMsgData { -// logType: string; -// logMessage: string; -// logTime: string; -// } - -const PER_PAGE = 25; +const PER_PAGE = 20; const initialLogs: any[] = []; const logTypes = ['ERROR', 'INFO', 'WARN', 'SILENT']; @@ -39,7 +33,7 @@ export default function LogMsgComponent() { // number of log messages to display const [logLimit, setLogLimit] = React.useState(0); - // const [selectAllOption, setSelectAllOption] = React.useState(true); + const [selectAll, setSelectAll] = React.useState(true); const [currentPage, setCurrentPage] = React.useState(1); @@ -63,6 +57,16 @@ export default function LogMsgComponent() { } }; + // Handle "Select All" checkbox change + const handleSelectAllChange = () => { + if (selectAll) { + setSelectedLogTypes([]); + } else { + setSelectedLogTypes(logTypes); + } + setSelectAll(!selectAll); + }; + const handleDateSort = () => { const newDateSortOrder = dateSortOrder === 'asc' ? 'desc' : 'asc'; const sortedLogs = [...logs].sort((a, b) => { @@ -133,11 +137,20 @@ export default function LogMsgComponent() { <> {showLogTable ? (<> -

Log Messages

+

{translate('log.messages')}

- Log Type + {translate('log.type')} + + + {logTypes.map(logType => (
-

Date Range:

+

{translate('date.range')}

setLogLimit(e.target.valueAsNumber)} invalid={logLimit < 1 || logLimit > 1000} /> - + - - - + + + @@ -224,14 +237,21 @@ export default function LogMsgComponent() { : (
- Please choose log types and date range for log data + {translate('please.choose.log.limit.date.range')}
- - Log Type - + {translate('log.type')} + + + {logTypes.map(logType => ( setLogLimit(e.target.valueAsNumber)} required @@ -276,14 +296,14 @@ export default function LogMsgComponent() { />
- +
) } setModalOpen(!modalOpen)} centered> - setModalOpen(!modalOpen)}>Log Message + setModalOpen(!modalOpen)}>{translate('log.message')} {modalLogMessage} diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index e656cb081..2fc94cdf3 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -513,7 +513,16 @@ const LocaleTranslationData = { "yes": "yes", "yesterday": "Yesterday", "you.cannot.create.a.cyclic.group": "You cannot create a cyclic group", - "log.messages": "Log Messages" + "log.messages": "Log Messages", + "log.message": "Log Message", + "log.type": "Log Type", + "log.time": "Log Time", + "num.logs.display": "Number of logs to display", + "refresh": "Refresh", + "from.1.to.1000": "from 1 to 1000", + "show.logs": "Show Log Table", + "please.choose.log.limit.date.range": "Please choose log limit and date range", + "select.all": "Select All" }, "fr": { "3D": "3D", @@ -1022,7 +1031,16 @@ const LocaleTranslationData = { "yes": " yes\u{26A1}", "yesterday": "Hier", "you.cannot.create.a.cyclic.group": "Vous ne pouvez pas créer un groupe cyclique", - "log.messages": "Log Messages\u{26A1}" + "log.messages": "Log Messages\u{26A1}", + "log.message": "Log Message\u{26A1}", + "log.type": "Log Type\u{26A1}", + "log.time": "Log Time\u{26A1}", + "num.logs.display": "Number of logs to display\u{26A1}", + "refresh": "Refresh\u{26A1}", + "from.1.to.1000": "from 1 to 1000\u{26A1}", + "show.logs": "Show Log Table\u{26A1}", + "please.choose.log.limit.date.range": "Please choose log limit and date range\u{26A1}", + "select.all": "Select All\u{26A1}" }, "es": { "3D": "3D", @@ -1532,7 +1550,16 @@ const LocaleTranslationData = { "yes": "sí", "yesterday": "Ayer", "you.cannot.create.a.cyclic.group": "No se puede crear un grupo cíclico", - "log.messages": "Log Messages\u{26A1}" + "log.messages": "Log Messages\u{26A1}", + "log.message": "Log Message\u{26A1}", + "log.type": "Log Type\u{26A1}", + "log.time": "Log Time\u{26A1}", + "num.logs.display": "Number of logs to display\u{26A1}", + "refresh": "Refresh\u{26A1}", + "from.1.to.1000": "from 1 to 1000\u{26A1}", + "show.logs": "Show Log Table\u{26A1}", + "please.choose.log.limit.date.range": "Please choose log limit and date range\u{26A1}", + "select.all": "Select All\u{26A1}" } } From 38b5bbb83696226add732c8cd4bb1c7a65a47d7b Mon Sep 17 00:00:00 2001 From: nmqng Date: Tue, 12 Nov 2024 00:06:41 -0500 Subject: [PATCH 12/22] update UI for log msg page including pagination, show all button, new refresh button, new picking log msg UI --- .../app/components/admin/LogMsgComponent.tsx | 308 ++++++++---------- src/client/app/translations/data.ts | 15 +- 2 files changed, 140 insertions(+), 183 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index d2fef0c60..f132c7b52 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, - FormGroup, Input, Label, Modal, ModalBody, ModalHeader, Table + FormGroup, Input, Label, Modal, ModalBody, ModalHeader, Pagination, PaginationItem, PaginationLink, Table } from 'reactstrap'; import DateRangePicker from '@wojtekmaj/react-daterange-picker'; import { useAppSelector } from '../../redux/reduxHooks'; @@ -22,9 +22,6 @@ export default function LogMsgComponent() { const locale = useAppSelector(selectSelectedLanguage); const [logs, setLogs] = React.useState(initialLogs); - // Button state to handle showing the log table - const [showLogTable, setShowLogTable] = React.useState(false); - const [selectedLogTypes, setSelectedLogTypes] = React.useState(logTypes); const [dateSortOrder, setDateSortOrder] = React.useState<'asc' | 'desc'>('asc'); const [logDateRange, setLogDateRange] = React.useState<[Date | null, Date | null]>([null, null]); @@ -40,6 +37,8 @@ export default function LogMsgComponent() { const [modalOpen, setModalOpen] = React.useState(false); const [modalLogMessage, setModalLogMessage] = React.useState(''); + const [showAllLogs, setShowAllLogs] = React.useState(false); + // Open modal with the full log message const handleLogMessageClick = (logMessage: string) => { setModalLogMessage(logMessage); @@ -96,6 +95,10 @@ export default function LogMsgComponent() { setDropdownOpen(!dropdownOpen); }; + const handleShowAllLogs = () => { + setShowAllLogs(!showAllLogs); + }; + // Filter logs based on selected log types and date range const filteredLogs = logs.filter(log => { const logDate = new Date(log.logTime); @@ -108,7 +111,7 @@ export default function LogMsgComponent() { return selectedLogTypes.includes(log.logType) && isWithinDateRange; }); - const paginatedLogs = filteredLogs.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE); + const paginatedLogs = showAllLogs ? filteredLogs : filteredLogs.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE); const totalPages = Math.ceil(filteredLogs.length / PER_PAGE); /** @@ -125,7 +128,7 @@ export default function LogMsgComponent() { const data = await logsApi.getLogsByDateRangeAndType( logDateRange[0].toISOString(), logDateRange[1].toISOString(), selectedLogTypes, logLimit.toString()); setLogs(data); - setShowLogTable(true); + // setShowLogTable(true); setCurrentPage(1); } catch (error) { console.error(error); @@ -135,172 +138,127 @@ export default function LogMsgComponent() { return ( <> - {showLogTable ? - (<> -

{translate('log.messages')}

- - - {translate('log.type')} - - - - - {logTypes.map(logType => ( - - - - ))} - - -
-

{translate('date.range')}

- -
- -
Log TypeLog MessageLog Time {dateSortOrder === 'asc' ? '↑' : '↓'}{translate('log.type')}{translate('log.message')}{translate('log.time')} {dateSortOrder === 'asc' ? '↑' : '↓'}
- - - - - + + {logTypes.map(logType => ( + + + + ))} + + + + + + + + + setLogLimit(e.target.valueAsNumber)} + invalid={logLimit < 1 || logLimit > 1000} + value={logLimit} + /> + + + + + {logs.length > 0 ? +
{translate('log.type')}{translate('log.message')}{translate('log.time')} {dateSortOrder === 'asc' ? '↑' : '↓'}
+ + + + + + + + + {paginatedLogs.map((log, index) => ( + + + + - - - {paginatedLogs.map((log, index) => ( - - - - - - ))} - -
{translate('log.type')}{translate('log.message')}{translate('log.time')} {dateSortOrder === 'asc' ? '↑' : '↓'}
{log.logType} handleLogMessageClick(log.logMessage)} + >{log.logMessage.length > 80 ? `${log.logMessage.slice(0, 80)} ...` : log.logMessage} + {new Date(log.logTime).toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 2 + })} +
{log.logType} handleLogMessageClick(log.logMessage)} - >{log.logMessage.length > 80 ? `${log.logMessage.slice(0, 80)} ...` : log.logMessage} - {new Date(log.logTime).toLocaleString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - fractionalSecondDigits: 2 - })} -
-
- {Array.from({ length: totalPages }, (_, index) => ( - ))} -
- ) + + : - (
-
- {translate('please.choose.log.limit.date.range')} -
-
- - {translate('log.type')} - - - - - {logTypes.map(logType => ( - - - - ))} - - - - - + {translate('no.logs')}} - - - - setLogLimit(e.target.valueAsNumber)} - required - invalid={logLimit < 1 || logLimit > 1000} - /> - -
- -
-
-
) - } + {!showAllLogs && logs.length !== 0 && + <> + + handlePageChange(1)} /> + + handlePageChange(currentPage - 1)} /> + + + {Array.from({ length: totalPages }, (_, index) => ( + + handlePageChange(index + 1)}> + {index + 1} + + + ))} + + + handlePageChange(currentPage + 1)} /> + + handlePageChange(totalPages)} /> + + + } + + {logs.length > 0 && !showAllLogs && + } + {logs.length > 0 && showAllLogs && + } setModalOpen(!modalOpen)} centered> setModalOpen(!modalOpen)}>{translate('log.message')} @@ -324,19 +282,15 @@ const titleStyle: React.CSSProperties = { const tableStyle: React.CSSProperties = { width: '90%', - margin: '2.5% auto' + margin: '1% auto' }; const logFilterStyle: React.CSSProperties = { display: 'flex', - marginLeft: '9%', + justifyContent: 'center', gap: '1%', - flexWrap: 'wrap', - alignItems: 'center' -}; - -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - marginRight: '-2.5%', - padding: 'none' + alignItems: 'center', + margin: 'auto 25%', + padding: '20px', + border: '2px solid lightgrey' }; \ No newline at end of file diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 2fc94cdf3..459ee5494 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -518,11 +518,12 @@ const LocaleTranslationData = { "log.type": "Log Type", "log.time": "Log Time", "num.logs.display": "Number of logs to display", - "refresh": "Refresh", + "log.msg.update": "Update", "from.1.to.1000": "from 1 to 1000", "show.logs": "Show Log Table", "please.choose.log.limit.date.range": "Please choose log limit and date range", - "select.all": "Select All" + "select.all": "Select All", + "no.logs": "No logs to display. Please select another data range or log type" }, "fr": { "3D": "3D", @@ -1036,11 +1037,12 @@ const LocaleTranslationData = { "log.type": "Log Type\u{26A1}", "log.time": "Log Time\u{26A1}", "num.logs.display": "Number of logs to display\u{26A1}", - "refresh": "Refresh\u{26A1}", + "log.msg.update": "Update\u{26A1}", "from.1.to.1000": "from 1 to 1000\u{26A1}", "show.logs": "Show Log Table\u{26A1}", "please.choose.log.limit.date.range": "Please choose log limit and date range\u{26A1}", - "select.all": "Select All\u{26A1}" + "select.all": "Select All\u{26A1}", + "no.logs": "No logs to display. Please select another data range or log type\u{26A1}" }, "es": { "3D": "3D", @@ -1555,11 +1557,12 @@ const LocaleTranslationData = { "log.type": "Log Type\u{26A1}", "log.time": "Log Time\u{26A1}", "num.logs.display": "Number of logs to display\u{26A1}", - "refresh": "Refresh\u{26A1}", + "log.msg.update": "Update\u{26A1}", "from.1.to.1000": "from 1 to 1000\u{26A1}", "show.logs": "Show Log Table\u{26A1}", "please.choose.log.limit.date.range": "Please choose log limit and date range\u{26A1}", - "select.all": "Select All\u{26A1}" + "select.all": "Select All\u{26A1}", + "no.logs": "No logs to display. Please select another data range or log type\u{26A1}" } } From 5a15a0243d821c18218101e93b155ba6d31ce812 Mon Sep 17 00:00:00 2001 From: nmqng Date: Tue, 12 Nov 2024 00:56:18 -0500 Subject: [PATCH 13/22] bring back log type filter for table and update log type downdrop for fetching data --- .../app/components/admin/LogMsgComponent.tsx | 93 ++++++++++++++----- 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index f132c7b52..d31c890c8 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -22,11 +22,13 @@ export default function LogMsgComponent() { const locale = useAppSelector(selectSelectedLanguage); const [logs, setLogs] = React.useState(initialLogs); - const [selectedLogTypes, setSelectedLogTypes] = React.useState(logTypes); + const [selectedTableLogTypes, setSelectedTableLogTypes] = React.useState(logTypes); + const [selectedUpdateLogTypes, setSelectedUpdateLogTypes] = React.useState(logTypes); const [dateSortOrder, setDateSortOrder] = React.useState<'asc' | 'desc'>('asc'); const [logDateRange, setLogDateRange] = React.useState<[Date | null, Date | null]>([null, null]); // Dropdown open state for log type in the header - const [dropdownOpen, setDropdownOpen] = React.useState(false); + const [typeTableDropdown, setTypeTableDropdown] = React.useState(false); + const [updateLogDropdown, setUpdateLogDropdown] = React.useState(false); // number of log messages to display const [logLimit, setLogLimit] = React.useState(0); @@ -46,22 +48,41 @@ export default function LogMsgComponent() { }; // Handle checkbox change - const handleCheckboxChange = (logType: string) => { - if (selectedLogTypes.includes(logType)) { + const handleTableCheckboxChange = (logType: string) => { + if (selectedTableLogTypes.includes(logType)) { // Remove log type if already selected - setSelectedLogTypes(selectedLogTypes.filter(type => type !== logType)); + setSelectedTableLogTypes(selectedTableLogTypes.filter(type => type !== logType)); } else { // Add log type if not selected - setSelectedLogTypes([...selectedLogTypes, logType]); + setSelectedTableLogTypes([...selectedTableLogTypes, logType]); + } + }; + + const handleUpdateCheckboxChange = (logType: string) => { + if (selectedUpdateLogTypes.includes(logType)) { + // Remove log type if already selected + setSelectedUpdateLogTypes(selectedUpdateLogTypes.filter(type => type !== logType)); + } else { + // Add log type if not selected + setSelectedUpdateLogTypes([...selectedUpdateLogTypes, logType]); } }; // Handle "Select All" checkbox change - const handleSelectAllChange = () => { + const handleTableSelectAll = () => { if (selectAll) { - setSelectedLogTypes([]); + setSelectedTableLogTypes([]); } else { - setSelectedLogTypes(logTypes); + setSelectedTableLogTypes(logTypes); + } + setSelectAll(!selectAll); + }; + + const handleUpdateSelectAll = () => { + if (selectAll) { + setSelectedUpdateLogTypes([]); + } else { + setSelectedUpdateLogTypes(logTypes); } setSelectAll(!selectAll); }; @@ -91,8 +112,12 @@ export default function LogMsgComponent() { }; // Toggle dropdown in the header - const onToggleDropdown = () => { - setDropdownOpen(!dropdownOpen); + const toggleTypeTable = () => { + setTypeTableDropdown(!typeTableDropdown); + }; + + const toggleUpdateLog = () => { + setUpdateLogDropdown(!updateLogDropdown); }; const handleShowAllLogs = () => { @@ -108,7 +133,7 @@ export default function LogMsgComponent() { (!logDateRange || !logDateRange[0] || logDate >= logDateRange[0]) && (!logDateRange || !logDateRange[1] || logDate <= logDateRange[1]); - return selectedLogTypes.includes(log.logType) && isWithinDateRange; + return selectedTableLogTypes.includes(log.logType) && isWithinDateRange; }); const paginatedLogs = showAllLogs ? filteredLogs : filteredLogs.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE); @@ -126,9 +151,8 @@ export default function LogMsgComponent() { try { // get log by date and type const data = await logsApi.getLogsByDateRangeAndType( - logDateRange[0].toISOString(), logDateRange[1].toISOString(), selectedLogTypes, logLimit.toString()); + logDateRange[0].toISOString(), logDateRange[1].toISOString(), selectedUpdateLogTypes, logLimit.toString()); setLogs(data); - // setShowLogTable(true); setCurrentPage(1); } catch (error) { console.error(error); @@ -140,7 +164,7 @@ export default function LogMsgComponent() { <>

{translate('log.messages')}

- + {translate('log.type')} @@ -148,7 +172,7 @@ export default function LogMsgComponent() { {translate('select.all')} @@ -157,8 +181,8 @@ export default function LogMsgComponent() { @@ -200,7 +224,33 @@ export default function LogMsgComponent() { - + @@ -229,7 +279,8 @@ export default function LogMsgComponent() {
{translate('log.type')} + + {translate('log.type')} + + + + + {logTypes.map(logType => ( + + + + ))} + + + {translate('log.message')} {translate('log.time')} {dateSortOrder === 'asc' ? '↑' : '↓'}
: - {translate('no.logs')}} + {translate('no.logs')} + } {!showAllLogs && logs.length !== 0 && <> @@ -288,7 +339,7 @@ const tableStyle: React.CSSProperties = { const logFilterStyle: React.CSSProperties = { display: 'flex', justifyContent: 'center', - gap: '1%', + gap: '1.5%', alignItems: 'center', margin: 'auto 25%', padding: '20px', From 9929eda495d3713847add30314b338d30bb1514a Mon Sep 17 00:00:00 2001 From: nmqng Date: Fri, 15 Nov 2024 00:27:50 -0500 Subject: [PATCH 14/22] update so that instead of making 4 requests for 4 types, logTypes is now being sent as array for fetching, add logLimit for fetching, validation --- src/client/app/utils/api/LogsApi.ts | 17 ++----- src/server/log.js | 51 ++++++++++++++----- src/server/models/LogMsg.js | 10 ++-- src/server/routes/logs.js | 34 ++++--------- .../logmsg/get_logs_from_dates_and_type.sql | 5 +- 5 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/client/app/utils/api/LogsApi.ts b/src/client/app/utils/api/LogsApi.ts index 96ed0e919..48e64168e 100644 --- a/src/client/app/utils/api/LogsApi.ts +++ b/src/client/app/utils/api/LogsApi.ts @@ -26,19 +26,10 @@ export default class LogsApi { return await this.backend.doPostRequest('/api/logs/error', log); } - // fetch all logs - public async getAllLogs(): Promise { - return await this.backend.doGetRequest('/api/logs/logsmsg'); - } - - public async getLogsByDateRangeAndType(startDate: string, endDate: string, types: string[]): Promise { - const requests = types.map( - type => this.backend.doGetRequest('/api/logs/logsmsg/getLogsByDateRangeAndType', - { startDate: startDate, endDate: endDate, logType: type }) - ); - const results = await Promise.all(requests); - - return results.flat() as LogData[]; + public async getLogsByDateRangeAndType(startDate: string, endDate: string, logTypes: string[], logLimit: string): Promise { + const request = await this.backend.doGetRequest('/api/logs/logsmsg/getLogsByDateRangeAndType', + { startDate: startDate, endDate: endDate, logTypes: logTypes.join('-'), logLimit: logLimit }); + return request as LogData[]; } } diff --git a/src/server/log.js b/src/server/log.js index e55495479..9f41f1cb3 100644 --- a/src/server/log.js +++ b/src/server/log.js @@ -51,7 +51,8 @@ class Logger { * @param {boolean?} skipMail Don't e-mail this message even if we would normally emit an e-mail for this level. */ log(level, message, error = null, skipMail = false) { - let messageToLog = `[${level.name}@${moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ')}] ${message}\n`; + let logTime = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + let messageToLog = `[${level.name}@${logTime}] ${message}\n`; const conn = getConnection(); @@ -78,19 +79,43 @@ class Logger { } }); + // NOTE: for running the first time, uncomment the following code block to write all current logs to the database + // after that you can comment out it to write only new logs to the database + // TODO: This should be fix later to check whehter it should write new logs or all logs to the database when running + // fs.readFile(logFile, 'utf8', async (err, data) => { + // if (err) { + // console.error(`Failed to read log file: ${err} (${err.stack})`); + // return; + // } + + // const logEntries = data.split('\n').filter(entry => entry.trim() !== ''); + // for (const entry of logEntries) { + // const logParts = entry.match(/\[(.*?)@(.*?)\] (.*)/); + // if (logParts) { + // const [, logType, logTime, logMessage] = logParts; + // const logMsg = new LogMsg(logType, logMessage, new Date(logTime)); + // try { + // await logMsg.insert(conn); + // } catch (err) { + // console.error(`Failed to write log to database: ${err} (${err.stack})`); + // } + // } + // } + // }) + + + // Comment out the following code block when running the first time to write all logs to the database + // then uncomment it to write only new logs to the database later // Write the new log to the database - const logMsgPatern = messageToLog.match(/\[(.*?)@(.*?)\] (.*)/); - if (logMsgPatern) { - const [, logType, logTime, logMessage] = logMsgPatern; - const logMsg = new LogMsg(logType, logMessage, new Date(logTime)); - (async () => { - try { - await logMsg.insert(conn); - } catch (err) { - console.error(`Failed to write log to database: ${err} (${err.stack})`); - } - })(); - } + const logMsg = new LogMsg(level.name, message, new Date(logTime)); + (async () => { + try { + await logMsg.insert(conn); + } catch (err) { + console.error(`Failed to write log to database: ${err} (${err.stack})`); + } + })(); + } // Only log elsewhere if given a high enough priority level. diff --git a/src/server/models/LogMsg.js b/src/server/models/LogMsg.js index b8f23e6d7..9de056a51 100644 --- a/src/server/models/LogMsg.js +++ b/src/server/models/LogMsg.js @@ -109,18 +109,20 @@ class LogMsg { * If no endDate is specified, all logs after the startDate are returned. * @param {Date} startDate * @param {Date} endDate - * @param logType + * @param {Array} logTypes + * @param {Number} logLimit * @param conn is the connection to use. * @returns {Promise.>} */ - static async getLogsByDateRangeAndType(startDate, endDate, logType, conn) { + static async getLogsByDateRangeAndType(startDate, endDate, logTypes, logLimit = 100, conn) { const rows = await conn.any(sqlFile('logmsg/get_logs_from_dates_and_type.sql'), { startDate: startDate, endDate: endDate, - logType: logType + logTypes: logTypes, + logLimit: logLimit }); - return rows.map(LogMsg.mapRow); + return rows.map(LogMsg.mapRow); } } module.exports = LogMsg; \ No newline at end of file diff --git a/src/server/routes/logs.js b/src/server/routes/logs.js index 50c49416d..28e6a4a85 100644 --- a/src/server/routes/logs.js +++ b/src/server/routes/logs.js @@ -27,7 +27,7 @@ const validLog = { const validLogMsg = { type: 'object', - required: ['startDate', 'endDate', 'logType'], + required: ['startDate', 'endDate', 'logTypes', 'logLimit'], properties: { startDate: { type: 'string', @@ -37,19 +37,15 @@ const validLogMsg = { type: 'string', format: 'date-time' }, - logType: { + logTypes: { type: 'string', - enum: ['INFO', 'WARN', 'ERROR', 'SILENT'] + pattern: '^(INFO|WARN|ERROR|SILENT)(-(INFO|WARN|ERROR|SILENT))*$' + }, + logLimit: { + type: 'string', + // as logLimit is being sent as string, using pattern to validate it represents a number from 1 to 1000 + pattern: '^(?:[1-9][0-9]{0,2}|1000)$' }, - - // use this if later database is changed to be able to deal with logType as a list - // logType: { - // type: 'array', - // items: { - // type: 'string', - // enum: ['INFO', 'WARN', 'ERROR', 'SILENT'] - // } - // } } } router.post('/info', async (req, res) => { @@ -86,17 +82,6 @@ router.post('/error', async (req, res) => { }); -// router.get('/logsmsg', async (req, res) => { -// const conn = getConnection(); -// try { -// const rows = await LogMsg.getAll(conn); -// res.json(rows); -// } catch (err) { -// console.error(`Failed to fetch in getAll: ${err}`); -// res.sendStatus(500); -// } -// }); - router.get('/logsmsg/getLogsByDateRangeAndType', async (req, res) => { const validationResult = validate(req.query, validLogMsg); if (!validationResult.valid) { @@ -104,7 +89,8 @@ router.get('/logsmsg/getLogsByDateRangeAndType', async (req, res) => { } else { const conn = getConnection(); try { - const rows = await LogMsg.getLogsByDateRangeAndType(req.query.startDate, req.query.endDate, req.query.logType, conn); + const logLimit = parseInt(req.query.logLimit); + const rows = await LogMsg.getLogsByDateRangeAndType(req.query.startDate, req.query.endDate, req.query.logTypes.split('-'), logLimit, conn); res.json(rows); } catch (err) { console.error(`Failed to fetch logs filter by date range and type: ${err}`); diff --git a/src/server/sql/logmsg/get_logs_from_dates_and_type.sql b/src/server/sql/logmsg/get_logs_from_dates_and_type.sql index 48450804a..42f6806d3 100644 --- a/src/server/sql/logmsg/get_logs_from_dates_and_type.sql +++ b/src/server/sql/logmsg/get_logs_from_dates_and_type.sql @@ -6,7 +6,8 @@ SELECT log_type, log_message, log_time FROM logmsg -WHERE log_type = ${logType} +WHERE log_type = ANY (${logTypes}::log_msg_type[]) AND log_time >= COALESCE(${startDate}, '-infinity'::TIMESTAMP) AND log_time <= COALESCE(${endDate}, 'infinity'::TIMESTAMP) -ORDER BY log_time ASC; \ No newline at end of file +ORDER BY log_time ASC +LIMIT ${logLimit}; \ No newline at end of file From 43b51fbe2a086382452ad54a8d1d367540d3e86f Mon Sep 17 00:00:00 2001 From: nmqng Date: Fri, 15 Nov 2024 09:58:33 -0500 Subject: [PATCH 15/22] fix some comments from origin draft pull request --- src/server/models/LogMsg.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/server/models/LogMsg.js b/src/server/models/LogMsg.js index 9de056a51..67c1efca9 100644 --- a/src/server/models/LogMsg.js +++ b/src/server/models/LogMsg.js @@ -3,20 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const database = require('./database'); -const { mapToObject, threeDHoleAlgorithm } = require('../util'); -const determineMaxPoints = require('../util/determineMaxPoints'); -const log = require('../log'); -const { isReadonlyKeywordOrPlusOrMinusToken } = require('typescript'); -const LogEmail = require('./LogEmail'); - const sqlFile = database.sqlFile; class LogMsg { /** * Creates a new log - * @param logType - * @param logMessage - * @param {Moment} logTime + * @param logType log type. Have to be INFO, WARN, ERROR, or SILENT + * @param logMessage log information + * @param {Moment} logTime the date and time of the log */ constructor(logType, logMessage, logTime) { this.logType = logType; @@ -105,12 +99,10 @@ class LogMsg { /** * Returns a promise to get all of the logs in between two dates. - * If no startDate is specified, all logs before the endDate are returned. - * If no endDate is specified, all logs after the startDate are returned. - * @param {Date} startDate - * @param {Date} endDate - * @param {Array} logTypes - * @param {Number} logLimit + * @param {Date} startDate start date of the range to get logs + * @param {Date} endDate end date of the range to get logs + * @param {Array} logTypes array of log types to get logs + * @param {Number} logLimit the maximum number of logs to return * @param conn is the connection to use. * @returns {Promise.>} */ From a47e0bf03bb8ee8507989efbaa05253e172455fe Mon Sep 17 00:00:00 2001 From: nmqng Date: Fri, 15 Nov 2024 10:03:13 -0500 Subject: [PATCH 16/22] add comments for LogMsgComponent --- .../app/components/admin/LogMsgComponent.tsx | 98 ++++++++++--------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index d31c890c8..a77bc825b 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -1,3 +1,8 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ import * as React from 'react'; import { Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, @@ -10,8 +15,11 @@ import { showWarnNotification } from '../../utils/notifications'; import { logsApi } from '../../utils/api'; import translate from '../../utils/translate'; +// number of log messages to display per page const PER_PAGE = 20; +// initialize log message array to hold log messages const initialLogs: any[] = []; +// log types for filtering const logTypes = ['ERROR', 'INFO', 'WARN', 'SILENT']; /** @@ -20,25 +28,32 @@ const logTypes = ['ERROR', 'INFO', 'WARN', 'SILENT']; */ export default function LogMsgComponent() { const locale = useAppSelector(selectSelectedLanguage); - const [logs, setLogs] = React.useState(initialLogs); - const [selectedTableLogTypes, setSelectedTableLogTypes] = React.useState(logTypes); + // Log messages state + const [logs, setLogs] = React.useState(initialLogs); + // Selected log types for filtering in the update log const [selectedUpdateLogTypes, setSelectedUpdateLogTypes] = React.useState(logTypes); - const [dateSortOrder, setDateSortOrder] = React.useState<'asc' | 'desc'>('asc'); - const [logDateRange, setLogDateRange] = React.useState<[Date | null, Date | null]>([null, null]); - // Dropdown open state for log type in the header + // Dropdown open state for log type in the header for filtering const [typeTableDropdown, setTypeTableDropdown] = React.useState(false); + // Selected log types for filtering in the table + const [selectedTableLogTypes, setSelectedTableLogTypes] = React.useState(logTypes); + // Dropdown open state for log type in the table for filtering const [updateLogDropdown, setUpdateLogDropdown] = React.useState(false); - - // number of log messages to display + // Sort order for date column in the table + const [dateSortOrder, setDateSortOrder] = React.useState<'asc' | 'desc'>('asc'); + // Log messages date range state + const [logDateRange, setLogDateRange] = React.useState<[Date | null, Date | null]>([null, null]); + // Number of log messages to display const [logLimit, setLogLimit] = React.useState(0); + // "Select All Logs" button state const [selectAll, setSelectAll] = React.useState(true); - + // Current page state for pagination const [currentPage, setCurrentPage] = React.useState(1); - + // Modal state for displaying full log message const [modalOpen, setModalOpen] = React.useState(false); + // Full log message to display in the modal const [modalLogMessage, setModalLogMessage] = React.useState(''); - + // Showing all logs instead of paginated const [showAllLogs, setShowAllLogs] = React.useState(false); // Open modal with the full log message @@ -47,46 +62,34 @@ export default function LogMsgComponent() { setModalOpen(true); }; - // Handle checkbox change + // Handle checkbox change for log type in the table const handleTableCheckboxChange = (logType: string) => { - if (selectedTableLogTypes.includes(logType)) { - // Remove log type if already selected + if (selectedTableLogTypes.includes(logType)) { // Remove log type if already selected setSelectedTableLogTypes(selectedTableLogTypes.filter(type => type !== logType)); - } else { - // Add log type if not selected + } else { // Add log type if not selected setSelectedTableLogTypes([...selectedTableLogTypes, logType]); } }; - + // Handle checkbox change for log type in the update log const handleUpdateCheckboxChange = (logType: string) => { - if (selectedUpdateLogTypes.includes(logType)) { - // Remove log type if already selected + if (selectedUpdateLogTypes.includes(logType)) { // Remove log type if already selected setSelectedUpdateLogTypes(selectedUpdateLogTypes.filter(type => type !== logType)); - } else { - // Add log type if not selected + } else { // Add log type if not selected setSelectedUpdateLogTypes([...selectedUpdateLogTypes, logType]); } }; - // Handle "Select All" checkbox change + // Handle "Select All" checkbox change in the table const handleTableSelectAll = () => { - if (selectAll) { - setSelectedTableLogTypes([]); - } else { - setSelectedTableLogTypes(logTypes); - } + selectAll ? setSelectedTableLogTypes([]) : setSelectedTableLogTypes(logTypes); setSelectAll(!selectAll); }; - + // Handle "Select All" checkbox change in the update log const handleUpdateSelectAll = () => { - if (selectAll) { - setSelectedUpdateLogTypes([]); - } else { - setSelectedUpdateLogTypes(logTypes); - } + selectAll ? setSelectedUpdateLogTypes([]) : setSelectedUpdateLogTypes(logTypes); setSelectAll(!selectAll); }; - + // Handle sorting of logs by date const handleDateSort = () => { const newDateSortOrder = dateSortOrder === 'asc' ? 'desc' : 'asc'; const sortedLogs = [...logs].sort((a, b) => { @@ -102,24 +105,23 @@ export default function LogMsgComponent() { setDateSortOrder(newDateSortOrder); setLogs(sortedLogs); }; - + // Handle date range change const handleDateRangeChange = (range: [Date | null, Date | null]) => { setLogDateRange(range); }; - + // Handle page change for pagination const handlePageChange = (newPage: number) => { setCurrentPage(newPage); }; - - // Toggle dropdown in the header + // Toggle dropdown for type in the table const toggleTypeTable = () => { setTypeTableDropdown(!typeTableDropdown); }; - + // Toggle dropdown for type in the update log const toggleUpdateLog = () => { setUpdateLogDropdown(!updateLogDropdown); }; - + // Handle showing all logs instead of paginated const handleShowAllLogs = () => { setShowAllLogs(!showAllLogs); }; @@ -127,32 +129,34 @@ export default function LogMsgComponent() { // Filter logs based on selected log types and date range const filteredLogs = logs.filter(log => { const logDate = new Date(log.logTime); - // Check if log is within the selected date range const isWithinDateRange = (!logDateRange || !logDateRange[0] || logDate >= logDateRange[0]) && (!logDateRange || !logDateRange[1] || logDate <= logDateRange[1]); - return selectedTableLogTypes.includes(log.logType) && isWithinDateRange; }); + // Paginate logs if not showing all logs const paginatedLogs = showAllLogs ? filteredLogs : filteredLogs.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE); const totalPages = Math.ceil(filteredLogs.length / PER_PAGE); /** - * Handle showing the log table + * Handle showing the log table by fetching from the server */ async function handleShowLogTable() { + // date range must be selected + // TODO: accept not to choose a date range -> show all logs if (!logDateRange || !logDateRange[0] || !logDateRange[1]) { showWarnNotification('You must select a date range'); - } else if (!logLimit || logLimit < 1 || logLimit > 1000) { - showWarnNotification('You must enter a valid number of logs to display'); + } else if (!logLimit || logLimit < 1 || logLimit > 1000) { // number of logs being fetched must be between 1 and 1000 + showWarnNotification(translate('log.limit.required')); } else { try { // get log by date and type const data = await logsApi.getLogsByDateRangeAndType( logDateRange[0].toISOString(), logDateRange[1].toISOString(), selectedUpdateLogTypes, logLimit.toString()); setLogs(data); + // reset pagination to first page after fetching new logs setCurrentPage(1); } catch (error) { console.error(error); @@ -163,6 +167,8 @@ export default function LogMsgComponent() { return ( <>

{translate('log.messages')}

+ + {/* Filter log messages by type, date range, and number of logs for fetching */}
{translate('log.type')} @@ -220,6 +226,7 @@ export default function LogMsgComponent() {
+ {/* Display log messages table */} {logs.length > 0 ? @@ -282,6 +289,7 @@ export default function LogMsgComponent() { {translate('no.logs')} } + {/* pagination */} {!showAllLogs && logs.length !== 0 && <> @@ -306,11 +314,13 @@ export default function LogMsgComponent() { } + {/* Show all logs or in pages button */} {logs.length > 0 && !showAllLogs && } {logs.length > 0 && showAllLogs && } + {/* Modal for displaying full log message */} setModalOpen(!modalOpen)} centered> setModalOpen(!modalOpen)}>{translate('log.message')} From 2819142a9ed6e8366da0031bc175458783475a0b Mon Sep 17 00:00:00 2001 From: nmqng Date: Wed, 20 Nov 2024 00:24:27 -0500 Subject: [PATCH 17/22] update using time interval instead of date for log date range --- .../app/components/admin/LogMsgComponent.tsx | 29 ++++++++++--------- src/client/app/translations/data.ts | 12 ++++---- src/client/app/utils/api/LogsApi.ts | 5 ++-- src/server/models/LogMsg.js | 6 ++-- src/server/routes/logs.js | 16 +++++----- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index a77bc825b..886056b9b 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -4,6 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; +import * as moment from 'moment'; import { Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, FormGroup, Input, Label, Modal, ModalBody, ModalHeader, Pagination, PaginationItem, PaginationLink, Table @@ -14,6 +15,10 @@ import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; import { showWarnNotification } from '../../utils/notifications'; import { logsApi } from '../../utils/api'; import translate from '../../utils/translate'; +import { TimeInterval } from '../../../../common/TimeInterval'; +import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../../utils/dateRangeCompatibility'; +import { Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; + // number of log messages to display per page const PER_PAGE = 20; @@ -42,7 +47,7 @@ export default function LogMsgComponent() { // Sort order for date column in the table const [dateSortOrder, setDateSortOrder] = React.useState<'asc' | 'desc'>('asc'); // Log messages date range state - const [logDateRange, setLogDateRange] = React.useState<[Date | null, Date | null]>([null, null]); + const [logDateRange, setLogDateRange] = React.useState(TimeInterval.unbounded()); // Number of log messages to display const [logLimit, setLogLimit] = React.useState(0); // "Select All Logs" button state @@ -106,8 +111,8 @@ export default function LogMsgComponent() { setLogs(sortedLogs); }; // Handle date range change - const handleDateRangeChange = (range: [Date | null, Date | null]) => { - setLogDateRange(range); + const handleDateRangeChange = (range: Value) => { + setLogDateRange(dateRangeToTimeInterval(range)); }; // Handle page change for pagination const handlePageChange = (newPage: number) => { @@ -128,11 +133,11 @@ export default function LogMsgComponent() { // Filter logs based on selected log types and date range const filteredLogs = logs.filter(log => { - const logDate = new Date(log.logTime); + const logDate = moment(log.logTime); // Check if log is within the selected date range const isWithinDateRange = - (!logDateRange || !logDateRange[0] || logDate >= logDateRange[0]) && - (!logDateRange || !logDateRange[1] || logDate <= logDateRange[1]); + (!logDateRange || !logDateRange.getIsBounded() || logDate >= logDateRange.getStartTimestamp()) && + (!logDateRange || !logDateRange.getIsBounded() || logDate <= logDateRange.getEndTimestamp()); return selectedTableLogTypes.includes(log.logType) && isWithinDateRange; }); @@ -144,17 +149,13 @@ export default function LogMsgComponent() { * Handle showing the log table by fetching from the server */ async function handleShowLogTable() { - // date range must be selected - // TODO: accept not to choose a date range -> show all logs - if (!logDateRange || !logDateRange[0] || !logDateRange[1]) { - showWarnNotification('You must select a date range'); - } else if (!logLimit || logLimit < 1 || logLimit > 1000) { // number of logs being fetched must be between 1 and 1000 + // Number of logs being fetched must be between 1 and 1000 + if (!logLimit || logLimit < 1 || logLimit > 1000) { showWarnNotification(translate('log.limit.required')); } else { try { // get log by date and type - const data = await logsApi.getLogsByDateRangeAndType( - logDateRange[0].toISOString(), logDateRange[1].toISOString(), selectedUpdateLogTypes, logLimit.toString()); + const data = await logsApi.getLogsByDateRangeAndType(logDateRange, selectedUpdateLogTypes, logLimit.toString()); setLogs(data); // reset pagination to first page after fetching new logs setCurrentPage(1); @@ -201,7 +202,7 @@ export default function LogMsgComponent() { { + public async getLogsByDateRangeAndType(timeInterval: TimeInterval, logTypes: string[], logLimit: string): Promise { const request = await this.backend.doGetRequest('/api/logs/logsmsg/getLogsByDateRangeAndType', - { startDate: startDate, endDate: endDate, logTypes: logTypes.join('-'), logLimit: logLimit }); + { timeInterval: timeInterval.toString(), logTypes: logTypes.join('-'), logLimit: logLimit }); return request as LogData[]; } diff --git a/src/server/models/LogMsg.js b/src/server/models/LogMsg.js index 67c1efca9..1735f7c58 100644 --- a/src/server/models/LogMsg.js +++ b/src/server/models/LogMsg.js @@ -106,10 +106,10 @@ class LogMsg { * @param conn is the connection to use. * @returns {Promise.>} */ - static async getLogsByDateRangeAndType(startDate, endDate, logTypes, logLimit = 100, conn) { + static async getLogsByDateRangeAndType(startDate = null, endDate = null, logTypes, logLimit = 100, conn) { const rows = await conn.any(sqlFile('logmsg/get_logs_from_dates_and_type.sql'), { - startDate: startDate, - endDate: endDate, + startDate: startDate || '-infinity', + endDate: endDate || 'infinity', logTypes: logTypes, logLimit: logLimit }); diff --git a/src/server/routes/logs.js b/src/server/routes/logs.js index 28e6a4a85..eef40ae18 100644 --- a/src/server/routes/logs.js +++ b/src/server/routes/logs.js @@ -10,6 +10,7 @@ const validate = require('jsonschema').validate; const adminAuthenticator = require('./authenticator').adminAuthMiddleware; const LogMsg = require('../models/LogMsg'); const { getConnection } = require('../db'); +const { TimeInterval } = require('../../common/TimeInterval'); const router = express.Router(); router.use(adminAuthenticator('log API')); @@ -27,15 +28,12 @@ const validLog = { const validLogMsg = { type: 'object', - required: ['startDate', 'endDate', 'logTypes', 'logLimit'], + required: ['timeInterval', 'logTypes', 'logLimit'], properties: { - startDate: { + timeInterval: { + // it should check for format: 'date-time' but this won't work for case where time is not provided + // when time is not provided, timeInterval value will be 'all' so just check type is string for now type: 'string', - format: 'date-time' - }, - endDate: { - type: 'string', - format: 'date-time' }, logTypes: { type: 'string', @@ -90,7 +88,9 @@ router.get('/logsmsg/getLogsByDateRangeAndType', async (req, res) => { const conn = getConnection(); try { const logLimit = parseInt(req.query.logLimit); - const rows = await LogMsg.getLogsByDateRangeAndType(req.query.startDate, req.query.endDate, req.query.logTypes.split('-'), logLimit, conn); + const timeInterval = TimeInterval.fromString(req.query.timeInterval); + const rows = await LogMsg.getLogsByDateRangeAndType( + timeInterval.startTimestamp, timeInterval.endTimestamp, req.query.logTypes.split('-'), logLimit, conn); res.json(rows); } catch (err) { console.error(`Failed to fetch logs filter by date range and type: ${err}`); From 5c3865e6f8fe5d7fba8648582ea1696f28ff7c15 Mon Sep 17 00:00:00 2001 From: nmqng Date: Wed, 20 Nov 2024 14:24:25 -0500 Subject: [PATCH 18/22] update 'Select All' button action --- .../app/components/admin/LogMsgComponent.tsx | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index 886056b9b..58760d95c 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -50,8 +50,10 @@ export default function LogMsgComponent() { const [logDateRange, setLogDateRange] = React.useState(TimeInterval.unbounded()); // Number of log messages to display const [logLimit, setLogLimit] = React.useState(0); - // "Select All Logs" button state - const [selectAll, setSelectAll] = React.useState(true); + // "Select All Logs" button state for update log + const [selectAllUpdate, setSelectAllUpdate] = React.useState(true); + // "Select All Logs" button state for table log + const [selectAllTable, setSelectAllTable] = React.useState(true); // Current page state for pagination const [currentPage, setCurrentPage] = React.useState(1); // Modal state for displaying full log message @@ -60,6 +62,13 @@ export default function LogMsgComponent() { const [modalLogMessage, setModalLogMessage] = React.useState(''); // Showing all logs instead of paginated const [showAllLogs, setShowAllLogs] = React.useState(false); + // Update button state + const [buttonAvailable, setButtonAvailable] = React.useState(false); + + // Update the availability of the update button each time the selected log types, log limit, or date range changes + React.useEffect(() => { + setButtonAvailable(false); + }, [selectedUpdateLogTypes, logLimit, logDateRange]); // Open modal with the full log message const handleLogMessageClick = (logMessage: string) => { @@ -75,6 +84,7 @@ export default function LogMsgComponent() { setSelectedTableLogTypes([...selectedTableLogTypes, logType]); } }; + // Handle checkbox change for log type in the update log const handleUpdateCheckboxChange = (logType: string) => { if (selectedUpdateLogTypes.includes(logType)) { // Remove log type if already selected @@ -84,15 +94,21 @@ export default function LogMsgComponent() { } }; + // React effect to keep track of the "Select All" checkbox state + React.useEffect(() => { + selectedUpdateLogTypes.length === logTypes.length ? setSelectAllUpdate(true) : setSelectAllUpdate(false); + selectedTableLogTypes.length === logTypes.length ? setSelectAllTable(true) : setSelectAllTable(false); + }, [selectedUpdateLogTypes, selectedTableLogTypes]); + // Handle "Select All" checkbox change in the table const handleTableSelectAll = () => { - selectAll ? setSelectedTableLogTypes([]) : setSelectedTableLogTypes(logTypes); - setSelectAll(!selectAll); + selectAllTable ? setSelectedTableLogTypes([]) : setSelectedTableLogTypes(logTypes); + setSelectAllTable(!selectAllTable); }; // Handle "Select All" checkbox change in the update log const handleUpdateSelectAll = () => { - selectAll ? setSelectedUpdateLogTypes([]) : setSelectedUpdateLogTypes(logTypes); - setSelectAll(!selectAll); + selectAllUpdate ? setSelectedUpdateLogTypes([]) : setSelectedUpdateLogTypes(logTypes); + setSelectAllUpdate(!selectAllUpdate); }; // Handle sorting of logs by date const handleDateSort = () => { @@ -159,6 +175,7 @@ export default function LogMsgComponent() { setLogs(data); // reset pagination to first page after fetching new logs setCurrentPage(1); + setButtonAvailable(true); } catch (error) { console.error(error); } @@ -178,7 +195,7 @@ export default function LogMsgComponent() { @@ -224,7 +241,7 @@ export default function LogMsgComponent() { value={logLimit} /> - + {/* Display log messages table */} @@ -240,7 +257,7 @@ export default function LogMsgComponent() { From ede099ad579a8387c5bd0460630b103248bd7c64 Mon Sep 17 00:00:00 2001 From: nmqng Date: Mon, 2 Dec 2024 23:58:09 -0500 Subject: [PATCH 19/22] resolved PR comments --- .../app/components/HeaderButtonsComponent.tsx | 4 +- src/client/app/components/RouteComponent.tsx | 1 - .../app/components/admin/LogMsgComponent.tsx | 124 +++-- src/client/app/translations/data.ts | 522 +++++++++--------- src/server/log.js | 8 +- src/server/models/LogMsg.js | 42 -- src/server/routes/logs.js | 2 +- src/server/sql/logmsg/get_all_logs.sql | 6 - src/server/sql/logmsg/get_logs_from_dates.sql | 11 - src/server/sql/logmsg/get_logs_from_type.sql | 10 - 10 files changed, 337 insertions(+), 393 deletions(-) delete mode 100644 src/server/sql/logmsg/get_all_logs.sql delete mode 100644 src/server/sql/logmsg/get_logs_from_dates.sql delete mode 100644 src/server/sql/logmsg/get_logs_from_type.sql diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index f0881b58c..244fce906 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -102,7 +102,7 @@ export default function HeaderButtonsComponent() { shouldCSVReadingsButtonDisabled: pathname === '/csvReadings', shouldUnitsButtonDisabled: pathname === '/units', shouldConversionsButtonDisabled: pathname === '/conversions', - shouldLogMsgButtonDisabled: pathname === '/logmsg' + shouldLogMsgButtonDisabled: pathname === '/logmsg', shouldVisualUnitMapButtonDisabled: pathname === '/visual-unit' })); }, [pathname]); @@ -315,7 +315,7 @@ export default function HeaderButtonsComponent() { {translate('log.in')} - + diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index 72dd3019e..15d46daf4 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -60,7 +60,6 @@ const router = createBrowserRouter([ { path: 'csvMeters', element: }, { path: 'maps', element: }, { path: 'units', element: }, - { path: 'conversions', element: }, { path: 'users', element: }, { path: 'logmsg', element: }, { path: 'users', element: }, diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index 58760d95c..9e8350cd0 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -3,29 +3,38 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + import * as React from 'react'; import * as moment from 'moment'; import { Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, + FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalHeader, Pagination, PaginationItem, PaginationLink, Table } from 'reactstrap'; import DateRangePicker from '@wojtekmaj/react-daterange-picker'; import { useAppSelector } from '../../redux/reduxHooks'; import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; -import { showWarnNotification } from '../../utils/notifications'; import { logsApi } from '../../utils/api'; import translate from '../../utils/translate'; import { TimeInterval } from '../../../../common/TimeInterval'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../../utils/dateRangeCompatibility'; import { Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; - // number of log messages to display per page const PER_PAGE = 20; + +enum LogTypes { + ERROR = 'ERROR', + INFO = 'INFO', + WARN = 'WARN', + DEBUG = 'DEBUG', + SILENT = 'SILENT' +} +// log types for filtering +const logTypes = Object.values(LogTypes); + // initialize log message array to hold log messages const initialLogs: any[] = []; -// log types for filtering -const logTypes = ['ERROR', 'INFO', 'WARN', 'SILENT']; /** * React component that defines the log message page @@ -33,37 +42,38 @@ const logTypes = ['ERROR', 'INFO', 'WARN', 'SILENT']; */ export default function LogMsgComponent() { const locale = useAppSelector(selectSelectedLanguage); - // Log messages state const [logs, setLogs] = React.useState(initialLogs); - // Selected log types for filtering in the update log - const [selectedUpdateLogTypes, setSelectedUpdateLogTypes] = React.useState(logTypes); - // Dropdown open state for log type in the header for filtering - const [typeTableDropdown, setTypeTableDropdown] = React.useState(false); - // Selected log types for filtering in the table - const [selectedTableLogTypes, setSelectedTableLogTypes] = React.useState(logTypes); - // Dropdown open state for log type in the table for filtering - const [updateLogDropdown, setUpdateLogDropdown] = React.useState(false); - // Sort order for date column in the table - const [dateSortOrder, setDateSortOrder] = React.useState<'asc' | 'desc'>('asc'); // Log messages date range state const [logDateRange, setLogDateRange] = React.useState(TimeInterval.unbounded()); + // Sort order for date column in the table + const [dateSortOrder, setDateSortOrder] = React.useState<'asc' | 'desc'>('asc'); // Number of log messages to display - const [logLimit, setLogLimit] = React.useState(0); - // "Select All Logs" button state for update log - const [selectAllUpdate, setSelectAllUpdate] = React.useState(true); - // "Select All Logs" button state for table log - const [selectAllTable, setSelectAllTable] = React.useState(true); + const [logLimit, setLogLimit] = React.useState(PER_PAGE); // Current page state for pagination const [currentPage, setCurrentPage] = React.useState(1); - // Modal state for displaying full log message - const [modalOpen, setModalOpen] = React.useState(false); - // Full log message to display in the modal - const [modalLogMessage, setModalLogMessage] = React.useState(''); // Showing all logs instead of paginated const [showAllLogs, setShowAllLogs] = React.useState(false); // Update button state const [buttonAvailable, setButtonAvailable] = React.useState(false); + // Modal state for displaying full log message + const [modalOpen, setModalOpen] = React.useState(false); + // Log type and time to display in the modal header + const [modelHeader, setModelHeader] = React.useState(''); + // Full log message to display in the modal + const [modalLogMessage, setModalLogMessage] = React.useState(''); + // Selected log types for filtering in the update log + const [selectedUpdateLogTypes, setSelectedUpdateLogTypes] = React.useState(logTypes); + // "Select All Logs" button state for update log + const [selectAllUpdate, setSelectAllUpdate] = React.useState(true); + // Dropdown open state for log type in the header for filter + const [typeTableDropdown, setTypeTableDropdown] = React.useState(false); + // Selected log types for filtering in the table + const [selectedTableLogTypes, setSelectedTableLogTypes] = React.useState(logTypes); + // Dropdown open state for log type in the table for filtering + const [updateLogDropdown, setUpdateLogDropdown] = React.useState(false); + // "Select All Logs" button state for table log + const [selectAllTable, setSelectAllTable] = React.useState(true); // Update the availability of the update button each time the selected log types, log limit, or date range changes React.useEffect(() => { @@ -71,25 +81,30 @@ export default function LogMsgComponent() { }, [selectedUpdateLogTypes, logLimit, logDateRange]); // Open modal with the full log message - const handleLogMessageClick = (logMessage: string) => { + const handleLogMessageModal = (logType: string, logTime: string, logMessage: string) => { + setModelHeader(`[${logType}] ${moment(logTime).toLocaleString()}`); setModalLogMessage(logMessage); setModalOpen(true); }; // Handle checkbox change for log type in the table const handleTableCheckboxChange = (logType: string) => { - if (selectedTableLogTypes.includes(logType)) { // Remove log type if already selected + if (selectedTableLogTypes.includes(logType)) { + // Remove log type if already selected setSelectedTableLogTypes(selectedTableLogTypes.filter(type => type !== logType)); - } else { // Add log type if not selected + } else { + // Add log type if not selected setSelectedTableLogTypes([...selectedTableLogTypes, logType]); } }; // Handle checkbox change for log type in the update log const handleUpdateCheckboxChange = (logType: string) => { - if (selectedUpdateLogTypes.includes(logType)) { // Remove log type if already selected + if (selectedUpdateLogTypes.includes(logType)) { + // Remove log type if already selected setSelectedUpdateLogTypes(selectedUpdateLogTypes.filter(type => type !== logType)); - } else { // Add log type if not selected + } else { + // Add log type if not selected setSelectedUpdateLogTypes([...selectedUpdateLogTypes, logType]); } }; @@ -114,13 +129,13 @@ export default function LogMsgComponent() { const handleDateSort = () => { const newDateSortOrder = dateSortOrder === 'asc' ? 'desc' : 'asc'; const sortedLogs = [...logs].sort((a, b) => { - const dateA = new Date(a.logTime); - const dateB = new Date(b.logTime); + const dateA = moment(a.logTime); + const dateB = moment(b.logTime); if (newDateSortOrder === 'asc') { - return dateA.getTime() - dateB.getTime(); + return dateA.valueOf() - dateB.valueOf(); } else { - return dateB.getTime() - dateA.getTime(); + return dateB.valueOf() - dateA.valueOf(); } }); setDateSortOrder(newDateSortOrder); @@ -165,20 +180,15 @@ export default function LogMsgComponent() { * Handle showing the log table by fetching from the server */ async function handleShowLogTable() { - // Number of logs being fetched must be between 1 and 1000 - if (!logLimit || logLimit < 1 || logLimit > 1000) { - showWarnNotification(translate('log.limit.required')); - } else { - try { - // get log by date and type - const data = await logsApi.getLogsByDateRangeAndType(logDateRange, selectedUpdateLogTypes, logLimit.toString()); - setLogs(data); - // reset pagination to first page after fetching new logs - setCurrentPage(1); - setButtonAvailable(true); - } catch (error) { - console.error(error); - } + try { + // get log by date and type + const data = await logsApi.getLogsByDateRangeAndType(logDateRange, selectedUpdateLogTypes, logLimit.toString()); + setLogs(data); + // reset pagination to first page after fetching new logs + setCurrentPage(1); + setButtonAvailable(true); + } catch (error) { + console.error(error); } } @@ -223,7 +233,8 @@ export default function LogMsgComponent() { onChange={handleDateRangeChange} minDate={new Date(1970, 0, 1)} maxDate={new Date()} - locale={locale} // Formats Dates, and Calendar months base on locale + // Formats Dates, and Calendar months base on locale + locale={locale} calendarIcon={null} calendarProps={{ defaultView: 'year' }} /> @@ -237,11 +248,20 @@ export default function LogMsgComponent() { placeholder={translate('from.1.to.1000')} type="number" onChange={e => setLogLimit(e.target.valueAsNumber)} - invalid={logLimit < 1 || logLimit > 1000} + invalid={!logLimit || logLimit < 1 || logLimit > 1000} value={logLimit} /> + + {translate('log.limit.required')} + - + {/* Display log messages table */} @@ -286,7 +306,7 @@ export default function LogMsgComponent() { + ))} @@ -353,10 +315,11 @@ export default function LogMsgComponent() { } {/* Show all logs or in pages button */} - {logs.length > 0 && !showAllLogs && - } - {logs.length > 0 && showAllLogs && - } + {logs.length > 0 && + } + {/* Modal for displaying full log message */} setModalOpen(!modalOpen)} centered> From cfc010d7e46146d9c72a01942990c7485bf175d9 Mon Sep 17 00:00:00 2001 From: nmqng Date: Thu, 5 Dec 2024 00:05:48 -0500 Subject: [PATCH 21/22] resolved PR comments --- package.json | 5 ++- .../app/components/admin/LogMsgComponent.tsx | 15 +++++--- src/client/app/utils/api/LogsApi.ts | 2 +- src/server/log.js | 28 -------------- src/server/models/LogMsg.js | 4 +- src/server/routes/logs.js | 12 ++++-- src/server/services/addLogMsg.js | 37 +++++++++++++++++++ ...ql => get_logmsgs_from_dates_and_type.sql} | 0 ...sert_new_log.sql => insert_new_logmsg.sql} | 0 9 files changed, 60 insertions(+), 43 deletions(-) create mode 100644 src/server/services/addLogMsg.js rename src/server/sql/logmsg/{get_logs_from_dates_and_type.sql => get_logmsgs_from_dates_and_type.sql} (100%) rename src/server/sql/logmsg/{insert_new_log.sql => insert_new_logmsg.sql} (100%) diff --git a/package.json b/package.json index fa5f6d8de..d4911d3d3 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "generateCosineSquaredTestingData": "node -e 'require(\"./src/server/data/automatedTestingData\").generateCosineSquaredTestingData(2.5)'", "generateTestingData": "node -e 'require(\"./src/server/data/automatedTestingData\").generateTestingData()'", "testData": "node -e 'require(\"./src/server/data/automatedTestingData.js\").insertSpecialUnitsConversionsMetersGroups()'", - "webData": "node -e 'require(\"./src/server/data/websiteData.js\").insertWebsiteData()'" + "webData": "node -e 'require(\"./src/server/data/websiteData.js\").insertWebsiteData()'", + "addLogMsg": "node -e 'require(\"./src/server/services/addLogMsg.js\").addLogMsgToDB()'" }, "nodemonConfig": { "watch": [ @@ -135,4 +136,4 @@ "webpack": "~5.76.0", "webpack-cli": "~5.1.4" } -} +} \ No newline at end of file diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index f636aec4d..e3cb26ef1 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -66,12 +66,12 @@ export default function LogMsgComponent() { const [selectedUpdateLogTypes, setSelectedUpdateLogTypes] = React.useState(logTypes); // "Select All Logs" button state for update log const [selectAllUpdate, setSelectAllUpdate] = React.useState(true); + // Dropdown open state for log type in the update log for filtering + const [updateLogDropdown, setUpdateLogDropdown] = React.useState(false); // Dropdown open state for log type in the header for filter const [typeTableDropdown, setTypeTableDropdown] = React.useState(false); // Selected log types for filtering in the table const [selectedTableLogTypes, setSelectedTableLogTypes] = React.useState(logTypes); - // Dropdown open state for log type in the table for filtering - const [updateLogDropdown, setUpdateLogDropdown] = React.useState(false); // "Select All Logs" button state for table log const [selectAllTable, setSelectAllTable] = React.useState(true); @@ -229,7 +229,7 @@ export default function LogMsgComponent() { - + @@ -279,7 +281,9 @@ export default function LogMsgComponent() { + > + {log.logMessage.length > 80 ? `${log.logMessage.slice(0, 80)} ...` : log.logMessage} + ))} @@ -320,7 +324,6 @@ export default function LogMsgComponent() { {!showAllLogs ? `Show All Logs (${logs.length})` : 'Show in pages'} } - {/* Modal for displaying full log message */} setModalOpen(!modalOpen)} centered> setModalOpen(!modalOpen)}>{modelHeader} diff --git a/src/client/app/utils/api/LogsApi.ts b/src/client/app/utils/api/LogsApi.ts index c53611e61..f7c30f633 100644 --- a/src/client/app/utils/api/LogsApi.ts +++ b/src/client/app/utils/api/LogsApi.ts @@ -29,7 +29,7 @@ export default class LogsApi { public async getLogsByDateRangeAndType(timeInterval: TimeInterval, logTypes: string[], logLimit: string): Promise { const request = await this.backend.doGetRequest('/api/logs/logsmsg/getLogsByDateRangeAndType', - { timeInterval: timeInterval.toString(), logTypes: logTypes.join('-'), logLimit: logLimit }); + { timeInterval: timeInterval.toString(), logTypes: logTypes, logLimit: logLimit }); return request as LogData[]; } diff --git a/src/server/log.js b/src/server/log.js index efc4dfc07..838e32f4d 100644 --- a/src/server/log.js +++ b/src/server/log.js @@ -79,33 +79,6 @@ class Logger { } }); - // NOTE: for running the first time, uncomment the following code block to write all current logs to the database - // after that you can comment out it to write only new logs to the database - // TODO: This should be fix later to check whether it should write new logs or all logs to the database when running - // fs.readFile(logFile, 'utf8', async (err, data) => { - // if (err) { - // console.error(`Failed to read log file: ${err} (${err.stack})`); - // return; - // } - - // const logEntries = data.split('\n').filter(entry => entry.trim() !== ''); - // for (const entry of logEntries) { - // const logParts = entry.match(/\[(.*?)@(.*?)\] (.*)/); - // if (logParts) { - // const [, logType, logTime, logMessage] = logParts; - // const logMsg = new LogMsg(logType, logMessage, new Date(logTime)); - // try { - // await logMsg.insert(conn); - // } catch (err) { - // console.error(`Failed to write log to database: ${err} (${err.stack})`); - // } - // } - // } - // }) - - - // Comment out the following code block when running the first time to write all logs to the database - // then uncomment it to write only new logs to the database later // Write the new log to the database const logMsg = new LogMsg(level.name, message, logTime); (async () => { @@ -115,7 +88,6 @@ class Logger { console.error(`Failed to write log to database: ${err} (${err.stack})`); } })(); - } // Only log elsewhere if given a high enough priority level. diff --git a/src/server/models/LogMsg.js b/src/server/models/LogMsg.js index 53be00d44..98a9ecc9c 100644 --- a/src/server/models/LogMsg.js +++ b/src/server/models/LogMsg.js @@ -52,7 +52,7 @@ class LogMsg { */ async insert(conn) { const logMsg = this; - await conn.none(sqlFile('logmsg/insert_new_log.sql'), logMsg); + await conn.none(sqlFile('logmsg/insert_new_logmsg.sql'), logMsg); } /** @@ -65,7 +65,7 @@ class LogMsg { * @returns {Promise.>} */ static async getLogsByDateRangeAndType(startDate = null, endDate = null, logTypes, logLimit = 100, conn) { - const rows = await conn.any(sqlFile('logmsg/get_logs_from_dates_and_type.sql'), { + const rows = await conn.any(sqlFile('logmsg/get_logmsgs_from_dates_and_type.sql'), { startDate: startDate || '-infinity', endDate: endDate || 'infinity', logTypes: logTypes, diff --git a/src/server/routes/logs.js b/src/server/routes/logs.js index 9482d21f1..f941d9b36 100644 --- a/src/server/routes/logs.js +++ b/src/server/routes/logs.js @@ -29,6 +29,7 @@ const validLog = { const validLogMsg = { type: 'object', required: ['timeInterval', 'logTypes', 'logLimit'], + maxProperties: 3, properties: { timeInterval: { // it should check for format: 'date-time' but this won't work for case where time is not provided @@ -36,8 +37,11 @@ const validLogMsg = { type: 'string', }, logTypes: { - type: 'string', - pattern: '^(INFO|WARN|ERROR|SILENT|DEBUG)(-(INFO|WARN|ERROR|SILENT|DEBUG))*$' + type: 'array', + items: { + type: 'string', + enum: ['INFO', 'WARN', 'ERROR', 'SILENT', 'DEBUG'] + } }, logLimit: { type: 'string', @@ -79,7 +83,6 @@ router.post('/error', async (req, res) => { } }); - router.get('/logsmsg/getLogsByDateRangeAndType', async (req, res) => { const validationResult = validate(req.query, validLogMsg); if (!validationResult.valid) { @@ -89,8 +92,9 @@ router.get('/logsmsg/getLogsByDateRangeAndType', async (req, res) => { try { const logLimit = parseInt(req.query.logLimit); const timeInterval = TimeInterval.fromString(req.query.timeInterval); + const logTypes = req.query.logTypes; const rows = await LogMsg.getLogsByDateRangeAndType( - timeInterval.startTimestamp, timeInterval.endTimestamp, req.query.logTypes.split('-'), logLimit, conn); + timeInterval.startTimestamp, timeInterval.endTimestamp, logTypes, logLimit, conn); res.json(rows); } catch (err) { console.error(`Failed to fetch logs filter by date range and type: ${err}`); diff --git a/src/server/services/addLogMsg.js b/src/server/services/addLogMsg.js new file mode 100644 index 000000000..f90a59a39 --- /dev/null +++ b/src/server/services/addLogMsg.js @@ -0,0 +1,37 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +const fs = require('fs'); +const logFile = require('../config').logFile; +const LogMsg = require('../models/LogMsg'); +const { getConnection } = require('../db'); +const moment = require('moment'); + +const addLogMsgToDB = async () => { + try { + const data = await fs.promises.readFile(logFile, 'utf8'); + const logEntries = data.split('\n').filter(entry => entry.trim() !== ''); + const conn = getConnection(); + + for (const entry of logEntries) { + const logParts = entry.match(/\[(.*?)@(.*?)\] (.*)/); + if (logParts) { + const [, logType, logTime, logMessage] = logParts; + const logMsg = new LogMsg(logType, logMessage, moment(logTime)); + try { + await logMsg.insert(conn); + } catch (err) { + console.error(`Failed to write log to database: ${err} (${err.stack})`); + } + } + } + console.log('Log migration completed successfully.'); + } catch (err) { + console.error(`Failed to migrate logs to database: ${err} (${err.stack})`); + } +}; + +module.exports = addLogMsgToDB; \ No newline at end of file diff --git a/src/server/sql/logmsg/get_logs_from_dates_and_type.sql b/src/server/sql/logmsg/get_logmsgs_from_dates_and_type.sql similarity index 100% rename from src/server/sql/logmsg/get_logs_from_dates_and_type.sql rename to src/server/sql/logmsg/get_logmsgs_from_dates_and_type.sql diff --git a/src/server/sql/logmsg/insert_new_log.sql b/src/server/sql/logmsg/insert_new_logmsg.sql similarity index 100% rename from src/server/sql/logmsg/insert_new_log.sql rename to src/server/sql/logmsg/insert_new_logmsg.sql From 0c534f96114dc5141e790cea8795c7aa1e96bcb5 Mon Sep 17 00:00:00 2001 From: nmqng Date: Mon, 9 Dec 2024 16:32:29 -0500 Subject: [PATCH 22/22] resolved PR comments and refactor code --- .../app/components/admin/LogMsgComponent.tsx | 29 +++++++++---------- src/client/app/translations/data.ts | 6 ++++ src/client/app/utils/api/LogsApi.ts | 2 +- .../sql/logmsg/create_log_types_enum.sql | 9 ++++++ .../sql/logmsg/create_logmsg_table.sql | 13 +++++++++ src/server/models/LogMsg.js | 6 +++- src/server/routes/logs.js | 12 ++++---- src/server/services/addLogMsg.js | 10 +++---- 8 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 src/server/migrations/1.0.0-2.0.0/sql/logmsg/create_log_types_enum.sql create mode 100644 src/server/migrations/1.0.0-2.0.0/sql/logmsg/create_logmsg_table.sql diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index e3cb26ef1..19bdfdf42 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -5,12 +5,12 @@ */ import * as React from 'react'; -import * as moment from 'moment'; +import * as moment from 'moment-timezone'; import { orderBy } from 'lodash'; import { Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, - FormFeedback, - FormGroup, Input, Label, Modal, ModalBody, ModalHeader, Pagination, PaginationItem, PaginationLink, Table + FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalHeader, + Pagination, PaginationItem, PaginationLink, Table } from 'reactstrap'; import DateRangePicker from '@wojtekmaj/react-daterange-picker'; import { useAppSelector } from '../../redux/reduxHooks'; @@ -82,7 +82,7 @@ export default function LogMsgComponent() { // Open modal with the full log message const handleLogMessageModal = (logType: string, logTime: string, logMessage: string) => { - setModelHeader(`[${logType}] ${moment(logTime).toLocaleString()}`); + setModelHeader(`[${logType}] ${moment.parseZone(logTime).format('LL LTS [(and ]SSS[ms)]')}`); setModalLogMessage(logMessage); setModalOpen(true); }; @@ -136,10 +136,6 @@ export default function LogMsgComponent() { setDateSortOrder(newDateSortOrder); setLogs(sortedLogs); }; - // Handle page change for pagination - const handlePageChange = (newPage: number) => { - setCurrentPage(newPage); - }; // Filter logs based on selected log types and date range const paginatedLogs = showAllLogs @@ -154,7 +150,8 @@ export default function LogMsgComponent() { async function handleShowLogTable() { try { // get log by date and type - const data = await logsApi.getLogsByDateRangeAndType(logDateRange, selectedUpdateLogTypes, logLimit.toString()); + const data = await logsApi.getLogsByDateRangeAndType( + logDateRange, selectedUpdateLogTypes.toString(), logLimit.toString()); setLogs(data); // reset pagination to first page after fetching new logs setCurrentPage(1); @@ -284,7 +281,7 @@ export default function LogMsgComponent() { > {log.logMessage.length > 80 ? `${log.logMessage.slice(0, 80)} ...` : log.logMessage} - + ))} @@ -297,23 +294,23 @@ export default function LogMsgComponent() { {!showAllLogs && logs.length !== 0 && <> - handlePageChange(1)} /> + setCurrentPage(1)} /> - handlePageChange(currentPage - 1)} /> + setCurrentPage(currentPage - 1)} /> {Array.from({ length: totalPages }, (_, index) => ( - handlePageChange(index + 1)}> + setCurrentPage(index + 1)}> {index + 1} ))} - handlePageChange(currentPage + 1)} /> + setCurrentPage(currentPage + 1)} /> - handlePageChange(totalPages)} /> + setCurrentPage(totalPages)} /> } @@ -321,7 +318,7 @@ export default function LogMsgComponent() { {/* Show all logs or in pages button */} {logs.length > 0 && } {/* Modal for displaying full log message */} diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 95395495f..393ffaf5c 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -435,8 +435,10 @@ const LocaleTranslationData = { "select.meters": "Select Meters", "select.unit": "Select Unit", "show": "Show", + "show.all.logs": "Show All Logs ", "show.grid": "Show grid", "show.options": "Show options", + "show.in.pages": "Show in Pages", "site.settings": "Site Settings", "site.title": "Site Title", "sort": "Sort Order", @@ -966,8 +968,10 @@ const LocaleTranslationData = { "select.meters": "Sélectionnez des Mètres", "select.unit": "Select Unit\u{26A1}", "show": "Montrer", + "show.all.logs": "Show All Logs\u{26A1} ", "show.grid": "Show grid\u{26A1}", "show.options": "Options de désancrage", + "show.in.pages": "Show in Pages\u{26A1}", "site.settings": "Site Settings\u{26A1}", "site.title": "Site Title\u{26A1}", "sort": "Sort Order\u{26A1}", @@ -1498,8 +1502,10 @@ const LocaleTranslationData = { "select.meters": "Seleccionar medidores", "select.unit": "Seleccionar unidad", "show": "Mostrar", + "show.all.logs": "Show All Logs\u{26A1} ", "show.grid": "Mostrar rejilla", "show.options": "Mostrar opciones", + "show.in.pages": "Show in Pages\u{26A1}", "site.settings": "Site Settings\u{26A1}", "site.title": "Site Title\u{26A1}", "sort": "Sort Order\u{26A1}", diff --git a/src/client/app/utils/api/LogsApi.ts b/src/client/app/utils/api/LogsApi.ts index f7c30f633..11d58e8db 100644 --- a/src/client/app/utils/api/LogsApi.ts +++ b/src/client/app/utils/api/LogsApi.ts @@ -27,7 +27,7 @@ export default class LogsApi { return await this.backend.doPostRequest('/api/logs/error', log); } - public async getLogsByDateRangeAndType(timeInterval: TimeInterval, logTypes: string[], logLimit: string): Promise { + public async getLogsByDateRangeAndType(timeInterval: TimeInterval, logTypes: string, logLimit: string): Promise { const request = await this.backend.doGetRequest('/api/logs/logsmsg/getLogsByDateRangeAndType', { timeInterval: timeInterval.toString(), logTypes: logTypes, logLimit: logLimit }); return request as LogData[]; diff --git a/src/server/migrations/1.0.0-2.0.0/sql/logmsg/create_log_types_enum.sql b/src/server/migrations/1.0.0-2.0.0/sql/logmsg/create_log_types_enum.sql new file mode 100644 index 000000000..64df5b2ab --- /dev/null +++ b/src/server/migrations/1.0.0-2.0.0/sql/logmsg/create_log_types_enum.sql @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +DO $$ BEGIN + CREATE TYPE log_msg_type AS ENUM('INFO', 'WARN', 'ERROR', 'DEBUG', 'SILENT'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; \ No newline at end of file diff --git a/src/server/migrations/1.0.0-2.0.0/sql/logmsg/create_logmsg_table.sql b/src/server/migrations/1.0.0-2.0.0/sql/logmsg/create_logmsg_table.sql new file mode 100644 index 000000000..1e6cfc5d8 --- /dev/null +++ b/src/server/migrations/1.0.0-2.0.0/sql/logmsg/create_logmsg_table.sql @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +--create logmsg table +CREATE TABLE IF NOT EXISTS logmsg ( + id SERIAL PRIMARY KEY, + log_type log_msg_type NOT NULL, + log_message TEXT NOT NULL, + log_time TIMESTAMP NOT NULL +); + +-- TODO Consider index optimization for queries \ No newline at end of file diff --git a/src/server/models/LogMsg.js b/src/server/models/LogMsg.js index 98a9ecc9c..119f0e345 100644 --- a/src/server/models/LogMsg.js +++ b/src/server/models/LogMsg.js @@ -52,7 +52,11 @@ class LogMsg { */ async insert(conn) { const logMsg = this; - await conn.none(sqlFile('logmsg/insert_new_logmsg.sql'), logMsg); + await conn.none(sqlFile('logmsg/insert_new_logmsg.sql'), { + logType: logMsg.logType, + logMessage: logMsg.logMessage, + logTime: logMsg.logTime.format('YYYY-MM-DDTHH:mm:ss.SSS') + }); } /** diff --git a/src/server/routes/logs.js b/src/server/routes/logs.js index f941d9b36..1111d318f 100644 --- a/src/server/routes/logs.js +++ b/src/server/routes/logs.js @@ -37,11 +37,8 @@ const validLogMsg = { type: 'string', }, logTypes: { - type: 'array', - items: { - type: 'string', - enum: ['INFO', 'WARN', 'ERROR', 'SILENT', 'DEBUG'] - } + type: 'string', + pattern: '^(INFO|WARN|ERROR|SILENT|DEBUG)(,(INFO|WARN|ERROR|SILENT|DEBUG))*$' }, logLimit: { type: 'string', @@ -86,18 +83,19 @@ router.post('/error', async (req, res) => { router.get('/logsmsg/getLogsByDateRangeAndType', async (req, res) => { const validationResult = validate(req.query, validLogMsg); if (!validationResult.valid) { + log.error('invalid request to getLogsByDateRangeAndType'); res.sendStatus(400); } else { const conn = getConnection(); try { const logLimit = parseInt(req.query.logLimit); const timeInterval = TimeInterval.fromString(req.query.timeInterval); - const logTypes = req.query.logTypes; + const logTypes = req.query.logTypes.split(','); const rows = await LogMsg.getLogsByDateRangeAndType( timeInterval.startTimestamp, timeInterval.endTimestamp, logTypes, logLimit, conn); res.json(rows); } catch (err) { - console.error(`Failed to fetch logs filter by date range and type: ${err}`); + log.error(`Failed to fetch logs filter by date range and type: ${err}`); res.sendStatus(500); } } diff --git a/src/server/services/addLogMsg.js b/src/server/services/addLogMsg.js index f90a59a39..303329061 100644 --- a/src/server/services/addLogMsg.js +++ b/src/server/services/addLogMsg.js @@ -10,14 +10,14 @@ const LogMsg = require('../models/LogMsg'); const { getConnection } = require('../db'); const moment = require('moment'); -const addLogMsgToDB = async () => { +async function addLogMsgToDB() { try { const data = await fs.promises.readFile(logFile, 'utf8'); - const logEntries = data.split('\n').filter(entry => entry.trim() !== ''); + const logEntries = data.split('['); const conn = getConnection(); for (const entry of logEntries) { - const logParts = entry.match(/\[(.*?)@(.*?)\] (.*)/); + const logParts = entry.match(/(.*?)@(.*?)\] ([^\[]*)(?=\[|$)/s); if (logParts) { const [, logType, logTime, logMessage] = logParts; const logMsg = new LogMsg(logType, logMessage, moment(logTime)); @@ -32,6 +32,6 @@ const addLogMsgToDB = async () => { } catch (err) { console.error(`Failed to migrate logs to database: ${err} (${err.stack})`); } -}; +} -module.exports = addLogMsgToDB; \ No newline at end of file +module.exports = { addLogMsgToDB }; \ No newline at end of file
{log.logType} handleLogMessageClick(log.logMessage)} + onClick={() => handleLogMessageModal(log.logType, log.logTime, log.logMessage)} >{log.logMessage.length > 80 ? `${log.logMessage.slice(0, 80)} ...` : log.logMessage} {new Date(log.logTime).toLocaleString('en-US', { @@ -340,7 +360,7 @@ export default function LogMsgComponent() { {/* Modal for displaying full log message */} setModalOpen(!modalOpen)} centered> - setModalOpen(!modalOpen)}>{translate('log.message')} + setModalOpen(!modalOpen)}>{modelHeader} {modalLogMessage} diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index e789bb182..95395495f 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -8,9 +8,9 @@ const LocaleTranslationData = { "en": { "3D": "3D", + "4.weeks": "4 Weeks", "400": "400 Bad Request", "404": "404 Not Found", - "4.weeks": "4 Weeks", "action": "Action", "add.new.meters": "Add new meters", "admin.only": "Admin Only", @@ -18,16 +18,16 @@ const LocaleTranslationData = { "alphabetically": "Alphabetically", "area": "Area:", "area.but.no.unit": "You have entered a nonzero area but no area unit.", + "area.calculate.auto": "Calculate Group Area", "area.error": "Please enter a number for area", "area.normalize": "Normalize by Area", - "area.calculate.auto": "Calculate Group Area", "area.unit": "Area Unit:", "AreaUnitType.feet": "sq. feet", "AreaUnitType.meters": "sq. meters", "AreaUnitType.none": "no unit", - "ascending": "Ascending", - "as.meter.unit": "as meter unit", "as.meter.defaultgraphicunit": "as meter default graphic unit", + "as.meter.unit": "as meter unit", + "ascending": "Ascending", "bar": "Bar", "bar.interval": "Bar Interval", "bar.raw": "Cannot create bar graph on raw units such as temperature", @@ -53,8 +53,6 @@ const LocaleTranslationData = { "confirm.action": "Confirm Action", "contact.us": "Contact us", "conversion": "Conversion", - "conversions": "Conversions", - "ConversionType.conversion": "conversion", "conversion.bidirectional": "Bidirectional:", "conversion.create.destination.meter": "The destination cannot be a meter", "conversion.create.exists": "This conversion already exists", @@ -80,16 +78,14 @@ const LocaleTranslationData = { "conversion.successfully.create.conversion": "Successfully created a conversion.", "conversion.successfully.delete.conversion": "Successfully deleted conversion.", "conversion.successfully.edited.conversion": "Successfully edited conversion.", + "conversions": "Conversions", + "ConversionType.conversion": "conversion", "create.conversion": "Create a Conversion", "create.group": "Create a Group", "create.map": "Create a Map", - "create.user": "Create a User", "create.unit": "Create a Unit", + "create.user": "Create a User", "csv": "CSV", - "csvMeters": "CSV Meters", - "csvReadings": "CSV Readings", - "csv.file": "CSV File:", - "csv.file.error": "File must be in CSV format or GZIP format (.csv or .gz). ", "csv.clear.button": "Clear Form", "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Header Row", @@ -97,6 +93,8 @@ const LocaleTranslationData = { "csv.download.size.limit": "Sorry you don't have permissions to download due to large number of points.", "csv.download.size.warning.size": "Total size of all files will be about (usually within 10% for large exports).", "csv.download.size.warning.verify": "Are you sure you want to download", + "csv.file": "CSV File:", + "csv.file.error": "File must be in CSV format or GZIP format (.csv or .gz). ", "csv.readings.param.create.meter": "Create Meter", "csv.readings.param.honor.dst": "Honor Daylight Savings Time", "csv.readings.param.meter.identifier": "Meter Identifier:", @@ -111,6 +109,8 @@ const LocaleTranslationData = { "csv.tab.readings": "Readings", "csv.upload.meters": "Upload Meters", "csv.upload.readings": "Upload Readings", + "csvMeters": "CSV Meters", + "csvReadings": "CSV Readings", "custom.value": "Custom value", "date.range": 'Date Range', "day": "Day", @@ -120,52 +120,52 @@ const LocaleTranslationData = { "default.area.normalize": "Normalize readings by area by default", "default.area.unit": "Default Area Unit", "default.bar.stacking": "Stack bars by default", - "default.graph.type": "Default Graph Type", - "default.graph.settings": "Default Graph Settings", - "defaultGraphicUnit": "Default Graphic Unit:", - "default.language": "Default Language", - "default.meter.reading.frequency": "Default meter reading frequency", - "default.warning.file.size": "Default Warning File Size", "default.file.size.limit": "Default File Size Limit", + "default.graph.settings": "Default Graph Settings", + "default.graph.type": "Default Graph Type", "default.help.url": "Documentation URL", - "default.time.zone": "Default Time Zone", - "default.meter.minimum.value": "Default meter minimum reading value check", + "default.language": "Default Language", + "default.meter.disable.checks": "Default meter disable checks", + "default.meter.maximum.date": "Default meter maximum reading date check", + "default.meter.maximum.errors": "Default maximum number of errors in meter reading", "default.meter.maximum.value": "Default meter maximum reading value check", "default.meter.minimum.date": "Default meter minimum reading date check", - "default.meter.maximum.date": "Default meter maximum reading date check", + "default.meter.minimum.value": "Default meter minimum reading value check", + "default.meter.reading.frequency": "Default meter reading frequency", "default.meter.reading.gap": "Default meter reading gap", - "default.meter.maximum.errors": "Default maximum number of errors in meter reading", - "default.meter.disable.checks": "Default meter disable checks", + "default.time.zone": "Default Time Zone", + "default.warning.file.size": "Default Warning File Size", + "defaultGraphicUnit": "Default Graphic Unit:", "delete.group": "Delete Group", "delete.map": "Delete Map", "delete.self": "Cannot delete your own Account.", "delete.user": "Delete User", "descending": "Descending", - "discard.changes": "Discard Changes", "disable": "Disable", + "discard.changes": "Discard Changes", "displayable": "Displayable:", - "DisplayableType.none": "none", - "DisplayableType.all": "all", "DisplayableType.admin": "admin", - "error.bounds": "Must be between {min} and {max}.", - "error.displayable": "Displayable will be set to false because no unit is selected.", - "error.displayable.meter": "Meter units will set displayable to none.", - "error.displayable.suffix.input": "Suffix input will set displayable to none.", - "error.greater": "Must be greater than {min}.", - "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.", - "error.negative": "Cannot be negative.", - "error.required": "Required field.", - "error.unknown": "Oops! An error has occurred.", + "DisplayableType.all": "all", + "DisplayableType.none": "none", "edit": "Edit", - "edited": "edited", "edit.a.group": "Edit a Group", "edit.a.meter": "Edit a Meter", "edit.group": "Edit Group", "edit.meter": "Details/Edit Meter", "edit.unit": "Edit Unit", "edit.user": "Edit User", + "edited": "edited", "enable": "Enable", "error.bar": "Show error bars", + "error.bounds": "Must be between {min} and {max}.", + "error.displayable": "Displayable will be set to false because no unit is selected.", + "error.displayable.meter": "Meter units will set displayable to none.", + "error.displayable.suffix.input": "Suffix input will set displayable to none.", + "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.", + "error.greater": "Must be greater than {min}.", + "error.negative": "Cannot be negative.", + "error.required": "Required field.", + "error.unknown": "Oops! An error has occurred.", "export.graph.data": "Export graph data", "export.raw.graph.data": "Export graph meter data", "failed.to.create.map": "Failed to create map", @@ -175,6 +175,7 @@ const LocaleTranslationData = { "failed.to.link.graph": "Failed to link graph", "failed.to.submit.changes": "Failed to submit changes", "false": "False", + "from.1.to.1000": "from 1 to 1000", "gps": "GPS: latitude, longitude", "graph": "Graph", "graph.settings": "Graph Settings", @@ -182,12 +183,12 @@ const LocaleTranslationData = { "group": "Group", "group.all.meters": "All Meters", "group.area.calculate": "Calculate Group Area", - "group.area.calculate.header": "Group Area will be set to ", + "group.area.calculate.error.group.unit": "No group area unit", "group.area.calculate.error.header": "The following meters were excluded from the sum because:", - "group.area.calculate.error.zero": ": area is unset or zero", - "group.area.calculate.error.unit": ": nonzero area but no area unit", "group.area.calculate.error.no.meters": "No meters in group", - "group.area.calculate.error.group.unit": "No group area unit", + "group.area.calculate.error.unit": ": nonzero area but no area unit", + "group.area.calculate.error.zero": ": area is unset or zero", + "group.area.calculate.header": "Group Area will be set to ", "group.create.nounit": "The default graphic unit was changed to no unit from ", "group.delete.group": "Delete Group", "group.delete.issue": "is contained in the following groups and cannot be deleted", @@ -205,14 +206,14 @@ const LocaleTranslationData = { "group.hidden": "At least one group is not visible to you", "group.input.error": "Input invalid so group not created or edited.", "group.name.error": "Please enter a valid name: (must have at least one character that is not a space)", - "groups": "Groups", "group.successfully.create.group": "Successfully created a group.", "group.successfully.edited.group": "Successfully edited group.", + "groups": "Groups", "groups.select": "Select Groups", "has.no.data": "has no current data", "has.used": "has used", - "header.pages": "Pages", "header.options": "Options", + "header.pages": "Pages", "help": "Help", "help.admin.conversioncreate": "This page allows admins to create conversions. Please visit {link} for further details and information.", "help.admin.conversionedit": "This page allows admins to edit conversions. Please visit {link} for further details and information.", @@ -232,14 +233,13 @@ const LocaleTranslationData = { "help.admin.users": "This page allows admins to view and edit users. Please visit {link} for further details and information.", "help.csv.meters": "This page allows admins to upload meters via a CSV file. Please visit {link} for further details and information.", "help.csv.readings": "This page allows certain users to upload readings via a CSV file. Please visit {link} for further details and information.", + "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.", "help.groups.groupdetails": "This page shows detailed information on a group. Please visit {link} for further details and information.", "help.groups.groupview": "This page shows information on groups. Please visit {link} for further details and information.", - "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.", "help.home.area.normalize": "Toggles normalization by area. Meters/Groups without area will be hidden. Please visit {link} for further details and information.", "help.home.bar.days.tip": "Allows user to select the desired number of days for each bar. Please see {link} for further details and information.", "help.home.bar.interval.tip": "Selects the time interval (Day, Week or 4 Weeks) for each bar. Please see {link} for further details and information.", "help.home.bar.stacking.tip": "Bars stack on top of each other. Please see {link} for further details and information.", - "help.home.map.interval.tip": "Selects the time interval (the last Day, Week or 4 Weeks) for map corresponding to bar's time interval. Please see {link} for further details and information.", "help.home.chart.plotly.controls": "These controls are provided by Plotly, the graphics package used by OED. You generally do not need them but they are provided in case you want that level of control. Note that some of these options may not interact nicely with OED features. See Plotly documentation at {link}.", "help.home.chart.redraw.restore": "OED automatically averages data when necessary so the graphs have a reasonable number of points. If you use the controls under the graph to scroll and/or zoom, you may find the resolution at this averaged level is not what you desire. Clicking the \"Redraw\" button will have OED recalculate the averaging and bring in higher resolution for the number of points it displays. If you want to restore the graph to the full range of dates, then click the \"Restore\" button. Please visit {link} for further details and information.", "help.home.chart.select": "Any graph type can be used with any combination of groups and meters. Line graphs show the usage (e.g., kW) vs. time. You can zoom and scroll with the controls right below the graph. Bar shows the total usage (e.g., kWh) for the time frame of each bar where you can control the time frame. Compare allows you to see the current usage vs. the usage in the last previous period for a day, week and four weeks. Map graphs show a spatial image of each meter where the circle size is related to four weeks of usage. 3D graphs show usage vs. day vs. hours in the day. Clicking on one of the choices renders that graphic. Please visit {link} for further details and information.", @@ -248,6 +248,7 @@ const LocaleTranslationData = { "help.home.error.bar": "Toggle error bars with min and max value. Please visit {link} for further details and information.", "help.home.export.graph.data": "With the \"Export graph data\" button, one can export the data for the graph when viewing either a line or bar graphic. The zoom and scroll feature on the line graph allows you to control the time frame of the data exported. The \"Export graph data\" button gives the data points for the graph and not the original meter data. The \"Export graph meter data\" gives the underlying meter data (line graphs only). Please visit {link} for further details and information on when meter data export is allowed.", "help.home.history": "Allows the user to navigate through the recent history of graphs. Please visit {link} for further details and information.", + "help.home.map.interval.tip": "Selects the time interval (the last Day, Week or 4 Weeks) for map corresponding to bar's time interval. Please see {link} for further details and information.", "help.home.navigation": "The \"Graph\" button goes to the graphic page, the \"Pages\" dropdown allows navigation to information pages, the \"Options\" dropdown allows selection of language, hide options and login/out and the \"Help\" button goes to the help pages. See help on the dropdown menus or the linked pages for further information.", "help.home.readings.per.day": "The number of readings shown for each day in a 3D graphic. Please visit {link} for further details and information.", "help.home.select.dateRange": "Select date range used in graphic display. For 3D graphic must be one year or less. Please visit {link} for further details and information.", @@ -290,18 +291,22 @@ const LocaleTranslationData = { "less.energy": "less energy", "line": "Line", "log.in": "Log in", + "log.limit.required": "Number of logs to display must be within 1 to 1000", + "log.message": "Log Message", + "log.messages": "Log Messages", "log.out": "Log out", + "log.time": "Log Time", + "log.type": "Log Type", "login.failed": "Failed logging in", "login.success": "Login Successful", "logo": "Logo", "manage": "Manage", "map": "Map", - "maps": "Maps", + "map.bad.digita": "Greater than 360, please change angle to a number between 0 and 360", + "map.bad.digitb": "Less than 0, please change angle to a number between 0 and 360", "map.bad.load": "Map image file needed", "map.bad.name": "Map name needed", "map.bad.number": "Not a number, please change angle to a number between 0 and 360", - "map.bad.digita": "Greater than 360, please change angle to a number between 0 and 360", - "map.bad.digitb": "Less than 0, please change angle to a number between 0 and 360", "map.calibrate": "Calibrate", "map.calibration": "Calibration status", "map.circle.size": "Map Circle Size", @@ -325,39 +330,35 @@ const LocaleTranslationData = { "map.notify.calibration.needed": "Calibration needed before display", "map.unavailable": "There's not an available map", "map.upload.new.file": "Redo", + "maps": "Maps", "max": "max", "menu": "Menu", "meter": "Meter", - "meters": "Meters", "meter.create": "Create a Meter", "meter.cumulative": "Cumulative:", "meter.cumulativeReset": "Cumulative Reset:", "meter.cumulativeResetEnd": "Cumulative Reset End:", "meter.cumulativeResetStart": "Cumulative Reset Start:", - "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:", + "meter.disableChecks": "Disable Checks", "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?", + "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:", "meter.enabled": "Updates:", "meter.endOnlyTime": "Only End Times:", "meter.endTimeStamp": "End Time Stamp:", - "meter.minVal": "Minimum Reading Value Check", - "meter.maxVal": "Maximum Reading Value Check", - "meter.minDate": "Minimum Reading Date Check", - "meter.maxDate": "Maximum Reading Date Check", - "meter.maxError": "Maximum Number of Errors Check", - "meter.disableChecks": "Disable Checks", "meter.failed.to.create.meter": "Failed to create a meter with message: ", "meter.failed.to.edit.meter": "Failed to edit meter with message: ", "meter.hidden": "At least one meter is not visible to you", "meter.id": "ID", "meter.input.error": "Input invalid so meter not created or edited.", - "meter.unit.change.requires": "needs to be changed before changing this unit's type", - "meter.unitName": "Unit:", - "meter.url": "URL:", "meter.is.displayable": "Display Enabled", "meter.is.enabled": "Updates Enabled", "meter.is.not.displayable": "Display Disabled", "meter.is.not.enabled": "Updates Disabled", - "meter.unit.is.not.editable": "This meter's unit cannot be changed and was put back to the original value because: ", + "meter.maxDate": "Maximum Reading Date Check", + "meter.maxError": "Maximum Number of Errors Check", + "meter.maxVal": "Maximum Reading Value Check", + "meter.minDate": "Minimum Reading Date Check", + "meter.minVal": "Minimum Reading Value Check", "meter.previousEnd": "Previous End Time Stamp:", "meter.reading": "Reading:", "meter.readingDuplication": "Reading Duplication:", @@ -368,19 +369,26 @@ const LocaleTranslationData = { "meter.startTimeStamp": "Start Time Stamp:", "meter.successfully.create.meter": "Successfully created a meter.", "meter.successfully.edited.meter": "Successfully edited meter.", - "meter.timeSort": "Time Sort:", "meter.time.zone": "Time Zone:", + "meter.timeSort": "Time Sort:", "meter.type": "Type:", - "minute": "Minute", + "meter.unit.change.requires": "needs to be changed before changing this unit's type", + "meter.unit.is.not.editable": "This meter's unit cannot be changed and was put back to the original value because: ", + "meter.unitName": "Unit:", + "meter.url": "URL:", + "meters": "Meters", "min": "min", + "minute": "Minute", "more.energy": "more energy", "more.options": "More Options", "name": "Name:", "navigation": "Navigation", "need.more.points": "Need more points", "no": "no", - "note": "Note: ", "no.data.in.range": "No Data In Date Range", + "no.logs": "No logs to display. Please select another log type or data range", + "note": "Note: ", + "num.logs.display": "Number of logs to display", "oed": "Open Energy Dashboard", "oed.description": "Open Energy Dashboard is an independent open source project. ", "oed.version": "OED version ", @@ -403,8 +411,8 @@ const LocaleTranslationData = { "rate.limit.error.first": "You have been rate limited by your OED site", "rate.limit.error.second": "We suggest you try these in this order:", "reading": "Reading:", - "redo.cik.and.refresh.db.views": "Processing changes. This may take a while.", "readings.per.day": "Readings per Day", + "redo.cik.and.refresh.db.views": "Processing changes. This may take a while.", "redraw": "Redraw", "refresh.page.first": "Click the Refresh this page' button below to try again", "refresh.page.second": "If you keep returning to this page wait longer and click 'Refresh this page' button", @@ -418,11 +426,12 @@ const LocaleTranslationData = { "save.meter.edits": "Save meter edits", "save.role.changes": "Save role changes", "second": "Second", + "select.all": "Select All", "select.groups": "Select Groups", "select.map": "Select Map", "select.meter": "Select Meter", - "select.meter.type": "Select Meter Type", "select.meter.group": "Select meter or group to graph", + "select.meter.type": "Select Meter Type", "select.meters": "Select Meters", "select.unit": "Select Unit", "show": "Show", @@ -432,12 +441,11 @@ const LocaleTranslationData = { "site.title": "Site Title", "sort": "Sort Order", "submit": "Submit", - "submitting": "submitting", "submit.changes": "Submit changes", "submit.new.user": "Submit new user", + "submitting": "submitting", "the.unit.of.meter": "The unit of meter", "this.four.weeks": "These four weeks", - "timezone.no": "No timezone", "this.week": "This week", "threeD.area.incompatible": "
is incompatible
with area normalization", "threeD.date": "Date", @@ -449,28 +457,23 @@ const LocaleTranslationData = { "threeD.y.axis.label": "Days of Calendar Year", "TimeSortTypes.decreasing": "decreasing", "TimeSortTypes.increasing": "increasing", + "timezone.no": "No timezone", "today": "Today", "toggle.link": "Toggle chart link", - "toggle.options" : "Toggle options", + "toggle.options": "Toggle options", "total": "total", "true": "True", "TrueFalseType.false": "no", "TrueFalseType.true": "yes", "undefined": "undefined", "unit": "Unit", - "UnitRepresentType.quantity": "quantity", - "UnitRepresentType.flow": "flow", - "UnitRepresentType.raw": "raw", - "UnitType.unit": "unit", - "UnitType.meter": "meter", - "UnitType.suffix": "suffix", "unit.delete.failure": "Failed to deleted unit with error: ", "unit.delete.success": "Successfully deleted unit", "unit.delete.unit": "Delete Unit", "unit.destination.error": "as the destination unit", - "unit.dropdown.displayable.option.none": "None", - "unit.dropdown.displayable.option.all": "All", "unit.dropdown.displayable.option.admin": "admin", + "unit.dropdown.displayable.option.all": "All", + "unit.dropdown.displayable.option.none": "None", "unit.failed.to.create.unit": "Failed to create a unit.", "unit.failed.to.delete.unit": "Delete cannot be done because this unit is used by the following", "unit.failed.to.edit.unit": "Failed to edit unit.", @@ -486,8 +489,14 @@ const LocaleTranslationData = { "unit.suffix": "Suffix:", "unit.type.of.unit": "Type of Unit:", "unit.type.of.unit.suffix": "Added suffix will set type of unit to suffix", + "UnitRepresentType.flow": "flow", + "UnitRepresentType.quantity": "quantity", + "UnitRepresentType.raw": "raw", "units": "Units", "units.conversion.page.title": "Units and Conversions Visual Graphics", + "UnitType.meter": "meter", + "UnitType.suffix": "suffix", + "UnitType.unit": "unit", "unsaved.failure": "Changes failed to save", "unsaved.success": "Changes saved", "unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?", @@ -502,13 +511,13 @@ const LocaleTranslationData = { "upload.readings.csv": "Upload readings CSV file", "used.so.far": "used so far", "used.this.time": "used this time", - "username": "Username:", "user.delete.confirm": "Delete the user: ", "user.password.edit": "Only enter password to update password", "user.password.length": "Password must be a minimum of 8 characters", "user.password.mismatch": "Passwords do not match", "user.role": "Role: ", "user.role.select": "Select Role", + "username": "Username:", "users": "Users", "users.failed.to.create.user": "Failed to create the user: ", "users.failed.to.delete.user": "Failed to delete the user: ", @@ -519,31 +528,20 @@ const LocaleTranslationData = { "uses": "uses", "view.groups": "View Groups", "visit": " or visit our ", - "visual.unit": "Units Visual Graphics", - "visual.input.units.graphic": "Input Units Visual Graphic", "visual.analyzed.units.graphic": "Analyzed Units Visual Graphic", + "visual.input.units.graphic": "Input Units Visual Graphic", + "visual.unit": "Units Visual Graphics", "website": "website", "week": "Week", "yes": "yes", "yesterday": "Yesterday", "you.cannot.create.a.cyclic.group": "You cannot create a cyclic group", - "log.messages": "Log Messages", - "log.message": "Log Message", - "log.type": "Log Type", - "log.time": "Log Time", - "num.logs.display": "Number of logs to display", - "log.msg.update": "Update", - "from.1.to.1000": "from 1 to 1000", - "show.logs": "Show Log Table", - "select.all": "Select All", - "no.logs": "No logs to display. Please select another log type or data range", - "log.limit.required": "You must enter a valid number of logs to display" }, "fr": { "3D": "3D", + "4.weeks": "4 Semaines", "400": "400 Bad Request\u{26A1}", "404": "404 Introuvable", - "4.weeks": "4 Semaines", "action": "Action\u{26A1}", "add.new.meters": "Ajouter de Nouveaux Mètres", "admin.only": "Uniquement pour Les Administrateurs", @@ -551,16 +549,16 @@ const LocaleTranslationData = { "alphabetically": "Alphabétiquement", "area": "Région:", "area.but.no.unit": "You have entered a nonzero area but no area unit.\u{26A1}", + "area.calculate.auto": "Calculate Group Area\u{26A1}", "area.error": "Please enter a number for area\u{26A1}", "area.normalize": "Normalize by Area\u{26A1}", - "area.calculate.auto": "Calculate Group Area\u{26A1}", "area.unit": "Area Unit:\u{26A1}", "AreaUnitType.feet": "pieds carrés", "AreaUnitType.meters": "mètre carré", "AreaUnitType.none": "no unit\u{26A1}", - "ascending": "Ascendant", - "as.meter.unit": "as meter unit\u{26A1}", "as.meter.defaultgraphicunit": "as meter default graphic unit\u{26A1}", + "as.meter.unit": "as meter unit\u{26A1}", + "ascending": "Ascendant", "bar": "Bande", "bar.interval": "Intervalle du Diagramme à Bandes", "bar.raw": "Cannot create bar graph on raw units such as temperature\u{26A1}", @@ -586,8 +584,6 @@ const LocaleTranslationData = { "confirm.action": "Confirm Action\u{26A1}", "contact.us": "Contactez nous", "conversion": "Conversion\u{26A1}", - "conversions": "Conversions\u{26A1}", - "ConversionType.conversion": "conversion\u{26A1}", "conversion.bidirectional": "Bidirectional:\u{26A1}", "conversion.create.destination.meter": "The destination cannot be a meter\u{26A1}", "conversion.create.exists": "This conversion already exists\u{26A1}", @@ -613,16 +609,14 @@ const LocaleTranslationData = { "conversion.successfully.create.conversion": "Successfully created a conversion.\u{26A1}", "conversion.successfully.delete.conversion": "Successfully deleted conversion.\u{26A1}", "conversion.successfully.edited.conversion": "Successfully edited conversion.\u{26A1}", + "conversions": "Conversions\u{26A1}", + "ConversionType.conversion": "conversion\u{26A1}", "create.conversion": "Create a Conversion\u{26A1}", "create.group": "Créer un Groupe", "create.map": "Créer une carte", "create.unit": "Create a Unit\u{26A1}", "create.user": "Créer un utilisateur", "csv": "CSV", - "csvMeters": "CSV Meters\u{26A1}", - "csvReadings": "CSV Readings\u{26A1}", - "csv.file": "Fichier CSV:", - "csv.file.error": "Le fichier doit être au format CSV ou GZIP (.csv ou .gz). ", "csv.clear.button": "Forme claire", "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Ligne d'en-tête", @@ -630,6 +624,8 @@ const LocaleTranslationData = { "csv.download.size.limit": "Sorry you don't have permissions to download due to large number of points.\u{26A1}", "csv.download.size.warning.size": "Total size of all files will be about (usually within 10% for large exports).\u{26A1}", "csv.download.size.warning.verify": "Are you sure you want to download\u{26A1}", + "csv.file": "Fichier CSV:", + "csv.file.error": "Le fichier doit être au format CSV ou GZIP (.csv ou .gz). ", "csv.readings.param.create.meter": "Créer un compteur", "csv.readings.param.honor.dst": "Honor Daylight Savings Time\u{26A1}", "csv.readings.param.meter.identifier": "Identifiant du compteur:", @@ -644,6 +640,8 @@ const LocaleTranslationData = { "csv.tab.readings": "Lectures", "csv.upload.meters": "Téléverser Mètres", "csv.upload.readings": "Téléverser Lectures", + "csvMeters": "CSV Meters\u{26A1}", + "csvReadings": "CSV Readings\u{26A1}", "custom.value": "Custom value\u{26A1}", "date.range": 'Plage de dates', "day": "Journée", @@ -653,52 +651,52 @@ const LocaleTranslationData = { "default.area.normalize": "Normalize readings by area by default\u{26A1}", "default.area.unit": "Default Area Unit\u{26A1}", "default.bar.stacking": "Stack bars by default\u{26A1}", - "default.graph.type": "Type du Diagramme par Défaut", - "default.graph.settings": "Default Graph Settings\u{26A1}", - "defaultGraphicUnit": "Default Graphic Unit:\u{26A1}", - "default.language": "Langue par Défaut", - "default.meter.reading.frequency": "Default meter reading frequency\u{26A1}", - "default.warning.file.size": "Taille du fichier d'avertissement par défaut", "default.file.size.limit": "Limite de taille de fichier par défaut", + "default.graph.settings": "Default Graph Settings\u{26A1}", + "default.graph.type": "Type du Diagramme par Défaut", "default.help.url": "Documentation URL\u{26A1}", - "default.time.zone": "Zona Horaria Predeterminada", - "default.meter.minimum.value": "Default meter minimum reading value check\u{26A1}", + "default.language": "Langue par Défaut", + "default.meter.disable.checks": "Default meter disable checks\u{26A1}", + "default.meter.maximum.date": "Default meter maximum reading date check\u{26A1}", + "default.meter.maximum.errors": "Default maximum number of errors in meter reading\u{26A1}", "default.meter.maximum.value": "Default meter maximum reading value check\u{26A1}", "default.meter.minimum.date": "Default meter minimum reading date check\u{26A1}", - "default.meter.maximum.date": "Default meter maximum reading date check\u{26A1}", + "default.meter.minimum.value": "Default meter minimum reading value check\u{26A1}", + "default.meter.reading.frequency": "Default meter reading frequency\u{26A1}", "default.meter.reading.gap": "Default meter reading gap\u{26A1}", - "default.meter.maximum.errors": "Default maximum number of errors in meter reading\u{26A1}", - "default.meter.disable.checks": "Default meter disable checks\u{26A1}", + "default.time.zone": "Zona Horaria Predeterminada", + "default.warning.file.size": "Taille du fichier d'avertissement par défaut", + "defaultGraphicUnit": "Default Graphic Unit:\u{26A1}", "delete.group": "Supprimer le Groupe", "delete.map": "Supprimer la carte", "delete.self": "Impossible de supprimer votre propre compte.", "delete.user": "Supprimer l'utilisateur", "descending": "Descendant", - "discard.changes": "Annuler les Modifications", "disable": "Désactiver", + "discard.changes": "Annuler les Modifications", "displayable": "Affichable:", - "DisplayableType.none": "none\u{26A1}", - "DisplayableType.all": "all\u{26A1}", "DisplayableType.admin": "admin\u{26A1}", - "error.bounds": "Must be between {min} and {max}.\u{26A1}", - "error.displayable": "Displayable will be set to false because no unit is selected.\u{26A1}", - "error.displayable.meter": "Meter units will set displayable to none.\u{26A1}", - "error.displayable.suffix.input": "Suffix input will set displayable to none.\u{26A1}", - "error.greater": "Must be greater than {min}.\u{26A1}", - "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.\u{26A1}", - "error.negative": "Cannot be negative.\u{26A1}", - "error.required": "Required field.\u{26A1}", - "error.unknown": "Oops! An error has occurred.\u{26A1}", + "DisplayableType.all": "all\u{26A1}", + "DisplayableType.none": "none\u{26A1}", "edit": "Modifier", - "edited": "édité", "edit.a.group": "Modifier le Groupe", "edit.a.meter": "Modifier le Métre", "edit.group": "Modifier Groupe", "edit.meter": "Details/Modifier Métre\u{26A1}", "edit.unit": "Edit Unit\u{26A1}", "edit.user": "Modifier l'utilisateur", + "edited": "édité", "enable": "Activer", "error.bar": "Show error bars\u{26A1}", + "error.bounds": "Must be between {min} and {max}.\u{26A1}", + "error.displayable": "Displayable will be set to false because no unit is selected.\u{26A1}", + "error.displayable.meter": "Meter units will set displayable to none.\u{26A1}", + "error.displayable.suffix.input": "Suffix input will set displayable to none.\u{26A1}", + "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.\u{26A1}", + "error.greater": "Must be greater than {min}.\u{26A1}", + "error.negative": "Cannot be negative.\u{26A1}", + "error.required": "Required field.\u{26A1}", + "error.unknown": "Oops! An error has occurred.\u{26A1}", "export.graph.data": "Exporter les données du diagramme", "export.raw.graph.data": "Export graph meter data\u{26A1}", "failed.to.create.map": "Échec de la création d'une carte", @@ -708,6 +706,7 @@ const LocaleTranslationData = { "failed.to.link.graph": "Échec de lier le graphique", "failed.to.submit.changes": "Échec de l'envoi des modifications", "false": "Faux", + "from.1.to.1000": "from 1 to 1000\u{26A1}", "gps": "GPS: latitude, longitude\u{26A1}", "graph": "Graphique", "graph.settings": "Graph Settings\u{26A1}", @@ -715,12 +714,12 @@ const LocaleTranslationData = { "group": "Groupe", "group.all.meters": "Tous les compteurs", "group.area.calculate": "Calculate Group Area\u{26A1}", - "group.area.calculate.header": "Group Area will be set to \u{26A1}", + "group.area.calculate.error.group.unit": "No group area unit\u{26A1}", "group.area.calculate.error.header": "The following meters were excluded from the sum because:\u{26A1}", - "group.area.calculate.error.zero": ": area is unset or zero\u{26A1}", - "group.area.calculate.error.unit": ": nonzero area but no area unit\u{26A1}", "group.area.calculate.error.no.meters": "No meters in group\u{26A1}", - "group.area.calculate.error.group.unit": "No group area unit\u{26A1}", + "group.area.calculate.error.unit": ": nonzero area but no area unit\u{26A1}", + "group.area.calculate.error.zero": ": area is unset or zero\u{26A1}", + "group.area.calculate.header": "Group Area will be set to \u{26A1}", "group.create.nounit": "The default graphic unit was changed to no unit from \u{26A1}", "group.delete.group": "Delete Group\u{26A1}", "group.delete.issue": "is contained in the following groups and cannot be deleted\u{26A1}", @@ -738,14 +737,14 @@ const LocaleTranslationData = { "group.hidden": "At least one group is not visible to you\u{26A1}", "group.input.error": "Input invalid so group not created or edited.\u{26A1}", "group.name.error": "Please enter a valid name: (must have at least one character that is not a space)\u{26A1}", - "groups": "Groupes", "group.successfully.create.group": "Successfully created a group.\u{26A1}", "group.successfully.edited.group": "Successfully edited group.\u{26A1}", + "groups": "Groupes", "groups.select": "Sélectionnez des Groupes", "has.no.data": "has no current data\u{26A1}", "has.used": "a utilisé", - "header.pages": "Pages\u{26A1}", "header.options": "Options\u{26A1}", + "header.pages": "Pages\u{26A1}", "help": "Help\u{26A1}", "help.admin.conversioncreate": "This page allows admins to create conversions. Please visit {link} for further details and information.\u{26A1}", "help.admin.conversionedit": "This page allows admins to edit conversions. Please visit {link} for further details and information.\u{26A1}", @@ -765,14 +764,13 @@ const LocaleTranslationData = { "help.admin.users": "This page allows admins to view and edit users. Please visit {link} for further details and information.\u{26A1}", "help.csv.meters": "Cette page permet aux administrateurs de téléverser des mètres via un fichier CSV. Veuillez visiter {link} pour plus de détails et d'informations.\u{26A1}", "help.csv.readings": "Cette page permet à certains utilisateurs de télécharger des lectures via un fichier CSV. Veuillez visiter {link} pour plus de détails et d'informations.\u{26A1}", + "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.\u{26A1}", "help.groups.groupdetails": "This page shows detailed information on a group. Please visit {link} for further details and information.\u{26A1}", "help.groups.groupview": "This page shows information on groups. Please visit {link} for further details and information.\u{26A1}", - "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.\u{26A1}", "help.home.area.normalize": "Toggles normalization by area. Meters/Groups without area will be hidden. Please visit {link} for further details and information.\u{26A1}", "help.home.bar.days.tip": "Allows user to select the desired number of days for each bar. Please see {link} for further details and information.\u{26A1}", "help.home.bar.interval.tip": "Selects the time interval (Day, Week or 4 Weeks) for each bar. Please see {link} for further details and information.\u{26A1}", "help.home.bar.stacking.tip": "Bars stack on top of each other. Please see {link} for further details and information.\u{26A1}", - "help.home.map.interval.tip": "for map corresponding to bar's time interval. Please see {link} for further details and information.\u{26A1}", "help.home.chart.plotly.controls": "These controls are provided by Plotly, the graphics package used by OED. You generally do not need them but they are provided in case you want that level of control. Note that some of these options may not interact nicely with OED features. See Plotly documentation at {link}.\u{26A1}", "help.home.chart.redraw.restore": "OED automatically averages data when necessary so the graphs have a reasonable number of points. If you use the controls under the graph to scroll and/or zoom, you may find the resolution at this averaged level is not what you desire. Clicking the \"Redraw\" button will have OED recalculate the averaging and bring in higher resolution for the number of points it displays. If you want to restore the graph to the full range of dates, then click the \"Restore\" button. Please visit {link} for further details and information.\u{26A1}", "help.home.chart.select": "for the time frame of each bar where you can control the time frame. Compare allows you to see the current usage vs. the usage in the last previous period for a day, week and four weeks. Map graphs show a spatial image of each meter where the circle size is related to four weeks of usage. 3D graphs show usage vs. day vs. hours in the day. Clicking on one of the choices renders that graphic. Please visit {link} for further details and information.\u{26A1}", @@ -781,6 +779,7 @@ const LocaleTranslationData = { "help.home.error.bar": "Toggle error bars with min and max value. Please visit {link} for further details and information.\u{26A1}", "help.home.export.graph.data": "With the \"Export graph data\" button, one can export the data for the graph when viewing either a line or bar graphic. The zoom and scroll feature on the line graph allows you to control the time frame of the data exported. The \"Export graph data\" button gives the data points for the graph and not the original meter data. The \"Export graph meter data\" gives the underlying meter data (line graphs only). Please visit {link} for further details and information on when meter data export is allowed.\u{26A1}", "help.home.history": "Permet à l'utilisateur de naviguer dans l'historique récent des graphiques. Veuillez visiter {link} pour plus de détails et d'informations.", + "help.home.map.interval.tip": "for map corresponding to bar's time interval. Please see {link} for further details and information.\u{26A1}", "help.home.navigation": "The \"Graph\" button goes to the graphic page, the \"Pages\" dropdown allows navigation to information pages, the \"Options\" dropdown allows selection of language, hide options and login/out and the \"Help\" button goes to the help pages. See help on the dropdown menus or the linked pages for further information.\u{26A1}", "help.home.readings.per.day": "The number of readings shown for each day in a 3D graphic. Please visit {link} for further details and information.\u{26A1}", "help.home.select.dateRange": "Select date range used in graphic display. For 3D graphic must be one year or less. Please visit {link} for further details and information.\u{26A1}", @@ -823,18 +822,22 @@ const LocaleTranslationData = { "less.energy": "moins d'énergie", "line": "Ligne", "log.in": "Se Connecter", + "log.limit.required": "Number of logs to display must be within 1 to 1000\u{26A1}", + "log.message": "Log Message\u{26A1}", + "log.messages": "Log Messages\u{26A1}", "log.out": "Se Déconnecter", + "log.time": "Log Time\u{26A1}", + "log.type": "Log Type\u{26A1}", "login.failed": "Echec de la connexion", "login.success": "Login Successful\u{26A1}", "logo": "Logo", "manage": "Manage\u{26A1}", "map": "Carte", - "maps": "Plans", + "map.bad.digita": "Supérieur à 360, veuillez changer l'angle en un nombre compris entre 0 et 360", + "map.bad.digitb": "Moins de 0, veuillez changer l'angle en un nombre compris entre 0 et 360", "map.bad.load": "Fichier image de la carte requis", "map.bad.name": "Nom de la carte requis", "map.bad.number": "Pas un nombre, veuillez changer l'angle en un nombre entre 0 et 360", - "map.bad.digita": "Supérieur à 360, veuillez changer l'angle en un nombre compris entre 0 et 360", - "map.bad.digitb": "Moins de 0, veuillez changer l'angle en un nombre compris entre 0 et 360", "map.calibrate": "Étalonner", "map.calibration": "Statut d'étalonnage", "map.circle.size": "Taille du cercle de la carte", @@ -858,39 +861,35 @@ const LocaleTranslationData = { "map.notify.calibration.needed": "Étalonnage nécessaire pour l'affichage", "map.unavailable": "There's not an available map\u{26A1}", "map.upload.new.file": "Refaire", + "maps": "Plans", "max": "max\u{26A1}", "menu": "Menu", "meter": "Mèter", - "meters": "Mèters", "meter.create": "Create a Meter\u{26A1}", "meter.cumulative": "Cumulative:\u{26A1}", "meter.cumulativeReset": "Cumulative Reset:\u{26A1}", "meter.cumulativeResetEnd": "Cumulative Reset End:\u{26A1}", "meter.cumulativeResetStart": "Cumulative Reset Start:\u{26A1}", - "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:\u{26A1}", + "meter.disableChecks": "Disable Checks\u{26A1}", "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?\u{26A1}", + "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:\u{26A1}", "meter.enabled": "Mises à Jour du Mèters", "meter.endOnlyTime": "End Only Time:\u{26A1}", "meter.endTimeStamp": "End Time Stamp:\u{26A1}", - "meter.minVal": "Minimum Reading Value Check\u{26A1}", - "meter.maxVal": "Maximum Reading Value Check\u{26A1}", - "meter.minDate": "Minimum Reading Date Check\u{26A1}", - "meter.maxDate": "Maximum Reading Date Check\u{26A1}", - "meter.maxError": "Maximum Number of Errors Check\u{26A1}", - "meter.disableChecks": "Disable Checks\u{26A1}", "meter.failed.to.create.meter": "Failed to create a meter with message: \u{26A1}", "meter.failed.to.edit.meter": "Failed to edit meter with message: \u{26A1}", "meter.hidden": "At least one meter is not visible to you\u{26A1}", "meter.id": "Identifiant du Mèters", "meter.input.error": "Input invalid so meter not created or edited.\u{26A1}", - "meter.unit.change.requires": "needs to be changed before changing this unit's type\u{26A1}", - "meter.unitName": "Unit:\u{26A1}", - "meter.url": "URL", "meter.is.displayable": "Affichage Activées", "meter.is.enabled": "Mises à Jour Activées", "meter.is.not.displayable": "Affichage Désactivé", "meter.is.not.enabled": "Mises à Jour Désactivées", - "meter.unit.is.not.editable": "This meter's unit cannot be changed and was put back to the original value because: \u{26A1}", + "meter.maxDate": "Maximum Reading Date Check\u{26A1}", + "meter.maxError": "Maximum Number of Errors Check\u{26A1}", + "meter.maxVal": "Maximum Reading Value Check\u{26A1}", + "meter.minDate": "Minimum Reading Date Check\u{26A1}", + "meter.minVal": "Minimum Reading Value Check\u{26A1}", "meter.previousEnd": "Previous End Time Stamp:\u{26A1}", "meter.reading": "Reading:\u{26A1}", "meter.readingDuplication": "Reading Duplication:\u{26A1}", @@ -901,19 +900,26 @@ const LocaleTranslationData = { "meter.startTimeStamp": "Start Time Stamp:\u{26A1}", "meter.successfully.create.meter": "Successfully created a meter.\u{26A1}", "meter.successfully.edited.meter": "Successfully edited meter.\u{26A1}", - "meter.timeSort": "Time Sort:\u{26A1}", "meter.time.zone": "fuseau horaire du mètre", + "meter.timeSort": "Time Sort:\u{26A1}", "meter.type": "Type de Mèters", - "minute": "Minute\u{26A1}", + "meter.unit.change.requires": "needs to be changed before changing this unit's type\u{26A1}", + "meter.unit.is.not.editable": "This meter's unit cannot be changed and was put back to the original value because: \u{26A1}", + "meter.unitName": "Unit:\u{26A1}", + "meter.url": "URL", + "meters": "Mèters", "min": "min\u{26A1}", + "minute": "Minute\u{26A1}", "more.energy": "plus d'énergie", "more.options": "More Options\u{26A1}", "name": "Nom:", "navigation": "Navigation", "need.more.points": "Need more points\u{26A1}", "no": "no\u{26A1}", - "note": "Noter: ", "no.data.in.range": "No Data In Date Range\u{26A1}", + "no.logs": "No logs to display. Please select another log type or date range\u{26A1}", + "note": "Noter: ", + "num.logs.display": "Number of logs to display\u{26A1}", "oed": "Tableau de Bord Ouvert d'énergie", "oed.description": "Le Tableau de Bord Ouvert d'énergie est un projet open source indépendant. ", "oed.version": "OED version \u{26A1}", @@ -936,8 +942,8 @@ const LocaleTranslationData = { "rate.limit.error.first": "You have been rate limited by your OED site\u{26A1}", "rate.limit.error.second": "We suggest you try these in this order:\u{26A1}", "reading": "Reading:\u{26A1}", - "redo.cik.and.refresh.db.views": "Processing changes. This may take a while\u{26A1}", "readings.per.day": "Readings per Day\u{26A1}", + "redo.cik.and.refresh.db.views": "Processing changes. This may take a while\u{26A1}", "redraw": "Redessiner", "refresh.page.first": "Click the Refresh this page' button below to try again\u{26A1}", "refresh.page.second": "If you keep returning to this page wait longer and click 'Refresh this page' button\u{26A1}", @@ -951,11 +957,12 @@ const LocaleTranslationData = { "save.meter.edits": "Enregistrer les modifications de compteur", "save.role.changes": "Save role changes\u{26A1}", "second": "Second\u{26A1}", + "select.all": "Select All\u{26A1}", "select.groups": "Sélectionnez des Groupes", "select.map": "Select Map\u{26A1}", - "select.meter.type": "Select Meter Type\u{26A1}", "select.meter": "Sélectionnez de Mètres", "select.meter.group": "Select meter or group to graph\u{26A1}", + "select.meter.type": "Select Meter Type\u{26A1}", "select.meters": "Sélectionnez des Mètres", "select.unit": "Select Unit\u{26A1}", "show": "Montrer", @@ -965,9 +972,9 @@ const LocaleTranslationData = { "site.title": "Site Title\u{26A1}", "sort": "Sort Order\u{26A1}", "submit": "Soumettre", - "submitting": "submitting\u{26A1}", "submit.changes": "Soumettre les changements", "submit.new.user": "Submit new user\u{26A1}", + "submitting": "submitting\u{26A1}", "the.unit.of.meter": "The unit of meter\u{26A1}", "this.four.weeks": "Cette quatre semaines", "this.week": "Cette semaine", @@ -975,35 +982,26 @@ const LocaleTranslationData = { "threeD.date": "Date", "threeD.date.range.too.long": "Date Range Must be a year or less\u{26A1}", "threeD.incompatible": "Not Compatible with 3D\u{26A1}", - 'threeD.rendering': "Rendering\u{26A1}", "threeD.time": "Temps", - 'threeD.x.axis.label': 'Heures de la journée', - 'threeD.y.axis.label': 'Jours de l\'année calendaire', - "timezone.no": "Pas de fuseau horaire", "TimeSortTypes.decreasing": "décroissant", "TimeSortTypes.increasing": "en augmentant", + "timezone.no": "Pas de fuseau horaire", "today": "Aujourd'hui", "toggle.link": "Bascule du lien du diagramme", - "toggle.options" : "Basculer les options", + "toggle.options": "Basculer les options", "total": "total", "true": "Vrai", "TrueFalseType.false": "no\u{26A1}", "TrueFalseType.true": "yes\u{26A1}", "undefined": "undefined\u{26A1}", "unit": "Unit\u{26A1}", - "UnitRepresentType.quantity": "quantity\u{26A1}", - "UnitRepresentType.flow": "flow\u{26A1}", - "UnitRepresentType.raw": "raw\u{26A1}", - "UnitType.unit": "unit\u{26A1}", - "UnitType.meter": "meter\u{26A1}", - "UnitType.suffix": "suffix\u{26A1}", "unit.delete.failure": "Failed to deleted unit with error: \u{26A1}", "unit.delete.success": "Successfully deleted unit\u{26A1}", "unit.delete.unit": "Delete Unit\u{26A1}", "unit.destination.error": "as the destination unit\u{26A1}", - "unit.dropdown.displayable.option.none": "None\u{26A1}", - "unit.dropdown.displayable.option.all": "All\u{26A1}", "unit.dropdown.displayable.option.admin": "admin\u{26A1}", + "unit.dropdown.displayable.option.all": "All\u{26A1}", + "unit.dropdown.displayable.option.none": "None\u{26A1}", "unit.failed.to.create.unit": "Failed to create a unit.\u{26A1}", "unit.failed.to.delete.unit": "Delete cannot be done because this unit is used by the following\u{26A1}", "unit.failed.to.edit.unit": "Failed to edit unit.\u{26A1}", @@ -1019,8 +1017,14 @@ const LocaleTranslationData = { "unit.suffix": "Suffix:\u{26A1}", "unit.type.of.unit": "Type of Unit:\u{26A1}", "unit.type.of.unit.suffix": "Added suffix will set type of unit to suffix\u{26A1}", + "UnitRepresentType.flow": "flow\u{26A1}", + "UnitRepresentType.quantity": "quantity\u{26A1}", + "UnitRepresentType.raw": "raw\u{26A1}", "units": "Units\u{26A1}", "units.conversion.page.title": "Units and Conversions Visual Graphics\u{26A1}", + "UnitType.meter": "meter\u{26A1}", + "UnitType.suffix": "suffix\u{26A1}", + "UnitType.unit": "unit\u{26A1}", "unsaved.failure": "Changes failed to save\u{26A1}", "unsaved.success": "Changes saved\u{26A1}", "unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?\u{26A1}", @@ -1052,31 +1056,23 @@ const LocaleTranslationData = { "uses": "uses\u{26A1}", "view.groups": "Visionner les groupes", "visit": " ou visitez notre ", - "visual.unit": "Units Visual Graphics\u{26A1}", - "visual.input.units.graphic": "Graphique Visuel des Unités D'Entrée", "visual.analyzed.units.graphic": "Graphique Visuel des Unités Analysées", + "visual.input.units.graphic": "Graphique Visuel des Unités D'Entrée", + "visual.unit": "Units Visual Graphics\u{26A1}", "website": "site web", "week": "Semaine", "yes": " yes\u{26A1}", "yesterday": "Hier", "you.cannot.create.a.cyclic.group": "Vous ne pouvez pas créer un groupe cyclique", - "log.messages": "Log Messages\u{26A1}", - "log.message": "Log Message\u{26A1}", - "log.type": "Log Type\u{26A1}", - "log.time": "Log Time\u{26A1}", - "num.logs.display": "Number of logs to display\u{26A1}", - "log.msg.update": "Update\u{26A1}", - "from.1.to.1000": "from 1 to 1000\u{26A1}", - "show.logs": "Show Log Table\u{26A1}", - "select.all": "Select All\u{26A1}", - "no.logs": "No logs to display. Please select another log type or date range\u{26A1}", - "log.limit.required": "You must enter a valid number of logs to display\u{26A1}" + 'threeD.rendering': "Rendering\u{26A1}", + 'threeD.x.axis.label': 'Heures de la journée', + 'threeD.y.axis.label': 'Jours de l\'année calendaire', }, "es": { "3D": "3D", + "4.weeks": "4 Semanas", "400": "400 Solicitud incorrecta", "404": "404 Página no encontrada", - "4.weeks": "4 Semanas", "action": "Acción", "add.new.meters": "Agregar nuevos medidores", "admin.only": "Solo administrador", @@ -1084,16 +1080,16 @@ const LocaleTranslationData = { "alphabetically": "Alfabéticamente", "area": "Área:", "area.but.no.unit": "Ha ingresado un área distinta a cero sin unidad de área.", + "area.calculate.auto": "Calcular el área del grupo", "area.error": "Por favor indique un número para el área", "area.normalize": "Normalizar según el área", - "area.calculate.auto": "Calcular el área del grupo", "area.unit": "Unidad de área:", "AreaUnitType.feet": "pies cuadrados", "AreaUnitType.meters": "metros cuadrados", "AreaUnitType.none": "sin unidad", - "ascending": "Ascendiente", - "as.meter.unit": "como unidad de medidor", "as.meter.defaultgraphicunit": "como unidad gráfica predeterminada del medidor", + "as.meter.unit": "como unidad de medidor", + "ascending": "Ascendiente", "bar": "Barra", "bar.interval": "Intervalo de barra", "bar.raw": "No se puede crear un gráfico de barras con unidades crudas como la temperatura", @@ -1119,8 +1115,6 @@ const LocaleTranslationData = { "confirm.action": "Confirmar acción", "contact.us": "Contáctenos", "conversion": "Conversión", - "conversions": "Conversiones", - "ConversionType.conversion": "conversión", "conversion.bidirectional": "Bidireccional:", "conversion.create.destination.meter": "La destinación no puede ser un medidor", "conversion.create.exists": "Esta conversión ya existe", @@ -1146,16 +1140,14 @@ const LocaleTranslationData = { "conversion.successfully.create.conversion": "Se creó una conversión con éxito.", "conversion.successfully.delete.conversion": "Se eliminó una conversión con éxito.", "conversion.successfully.edited.conversion": "Se editó una conversión con éxito", + "conversions": "Conversiones", + "ConversionType.conversion": "conversión", "create.conversion": "Crear una conversión", "create.group": "Crear un grupo", "create.map": "Crear un mapa", "create.unit": "Crear una unidad", "create.user": "Crear un usuario", "csv": "CSV", - "csvMeters": "CSV Meters\u{26A1}", - "csvReadings": "CSV Readings\u{26A1}", - "csv.file": "Archivo CSV:", - "csv.file.error": "El archivo debe estar en formato CSV o GZIP (.csv o .gz). ", "csv.clear.button": "Forma clara", "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Fila de cabecera", @@ -1163,6 +1155,8 @@ const LocaleTranslationData = { "csv.download.size.limit": "Perdón, no tienes permiso para descargar por el número de puntos grande.", "csv.download.size.warning.size": "El tamaño todos los archivos juntos será de unos (usualmente dentro de 10% para exportaciones largas).", "csv.download.size.warning.verify": "Estás seguro que quieres descargar", + "csv.file": "Archivo CSV:", + "csv.file.error": "El archivo debe estar en formato CSV o GZIP (.csv o .gz). ", "csv.readings.param.create.meter": "Crear medidor", "csv.readings.param.honor.dst": "Seguir el horario de verano", "csv.readings.param.meter.identifier": "Identificador del medidor:", @@ -1177,6 +1171,8 @@ const LocaleTranslationData = { "csv.tab.readings": "Lecturas", "csv.upload.meters": "Subir medidores CSV", "csv.upload.readings": "Subir lecturas CSV", + "csvMeters": "CSV Meters\u{26A1}", + "csvReadings": "CSV Readings\u{26A1}", "custom.value": "Valor personalizado", "date.range": 'Rango de fechas', "day": "Día", @@ -1186,53 +1182,53 @@ const LocaleTranslationData = { "default.area.normalize": "Normalizar lecturas según el área por defecto", "default.area.unit": "Unidad de área predeterminada", "default.bar.stacking": "Apilar barras por defecto", - "default.graph.type": "Tipo de gráfico por defecto", - "default.graph.settings": "Configuraciones predeterminadas del gráfico", - "defaultGraphicUnit": "Unidad del gráfico predeterminada:", - "default.language": "Idioma predeterminado", - "default.meter.reading.frequency": "Frecuencia de lectura del medidor predeterminada", - "default.site.title": "Título predeterminado de la página ", - "default.warning.file.size": "Advertencia predeterminada de tamaño del archivo", "default.file.size.limit": "Límite predeterminado de tamaño del archivo", + "default.graph.settings": "Configuraciones predeterminadas del gráfico", + "default.graph.type": "Tipo de gráfico por defecto", "default.help.url": "URL Documentación", - "default.time.zone": "Zona de horario predeterminada", - "default.meter.minimum.value": "Revisión del valor de lectura mínima del medidor predeterminado", + "default.language": "Idioma predeterminado", + "default.meter.disable.checks": "Desactivar revisiones de medidor predeterminado", + "default.meter.maximum.date": "Revisión de la fecha de lectura máxima del medidor predeterminado", + "default.meter.maximum.errors": "Número máximo de errores en la lectura del medidor", "default.meter.maximum.value": "Revisión del valor de lectura máxima del medidor predeterminado", "default.meter.minimum.date": "Revisión de la fecha de lectura mínima del medidor predeterminado", - "default.meter.maximum.date": "Revisión de la fecha de lectura máxima del medidor predeterminado", + "default.meter.minimum.value": "Revisión del valor de lectura mínima del medidor predeterminado", + "default.meter.reading.frequency": "Frecuencia de lectura del medidor predeterminada", "default.meter.reading.gap": "Deistancia predeterminada entre lecturas del medidor", - "default.meter.maximum.errors": "Número máximo de errores en la lectura del medidor", - "default.meter.disable.checks": "Desactivar revisiones de medidor predeterminado", + "default.site.title": "Título predeterminado de la página ", + "default.time.zone": "Zona de horario predeterminada", + "default.warning.file.size": "Advertencia predeterminada de tamaño del archivo", + "defaultGraphicUnit": "Unidad del gráfico predeterminada:", "delete.group": "Borrar grupo", "delete.map": "Borrar mapa", "delete.self": "No puedes eliminar tu propia cuenta.", "delete.user": "Borrar usario", "descending": "Descendente", - "discard.changes": "Descartar los cambios", "disable": "Desactivar", + "discard.changes": "Descartar los cambios", "displayable": "Visualizable:", - "DisplayableType.none": "ninguno", - "DisplayableType.all": "todo", "DisplayableType.admin": "administrador", - "error.bounds": "Debe ser entre {min} y {max}.", - "error.displayable": "El elemento visual determinado como falso porque no hay unidad seleccionada.", - "error.displayable.meter": "Las unidades de medición determinarán al elemento visual como ninguno.", - "error.displayable.suffix.input": "Suffix input will set displayable to none.\u{26A1}", - "error.greater": "Debe ser más que {min}.", - "error.gps": "Latitud deber ser entre -90 y 90, y longitud de entre -180 y 180.", - "error.negative": "No puede ser negativo.", - "error.required": "Campo requerido", - "error.unknown": "¡Ups! Ha ocurrido un error.", + "DisplayableType.all": "todo", + "DisplayableType.none": "ninguno", "edit": "Editar", - "edited": "editado", "edit.a.group": "Editar un grupo", "edit.a.meter": "Editar un medidor", "edit.group": "Editar grupo", "edit.meter": "Details/Editar medidor\u{26A1}", "edit.unit": "Editar unidad", "edit.user": "Editar Usuario", + "edited": "editado", "enable": "Activar", "error.bar": "Mostrar las barras de errores", + "error.bounds": "Debe ser entre {min} y {max}.", + "error.displayable": "El elemento visual determinado como falso porque no hay unidad seleccionada.", + "error.displayable.meter": "Las unidades de medición determinarán al elemento visual como ninguno.", + "error.displayable.suffix.input": "Suffix input will set displayable to none.\u{26A1}", + "error.gps": "Latitud deber ser entre -90 y 90, y longitud de entre -180 y 180.", + "error.greater": "Debe ser más que {min}.", + "error.negative": "No puede ser negativo.", + "error.required": "Campo requerido", + "error.unknown": "¡Ups! Ha ocurrido un error.", "export.graph.data": "Exportar los datos del gráfico", "export.raw.graph.data": "Exportar los datos del medidor del gráfico", "failed.to.create.map": "No se pudo crear el mapa", @@ -1242,6 +1238,7 @@ const LocaleTranslationData = { "failed.to.link.graph": "No se pudo vincular el gráfico", "failed.to.submit.changes": "No se pudo entregar los cambios", "false": "Falso", + "from.1.to.1000": "from 1 to 1000\u{26A1}", "gps": "GPS: latitud, longitud", "graph": "Gráfico", "graph.settings": "Graph Settings\u{26A1}", @@ -1249,12 +1246,12 @@ const LocaleTranslationData = { "group": "Grupo", "group.all.meters": "Todos los medidores", "group.area.calculate": "Calcular el área del grupo", - "group.area.calculate.header": "Área del grupo se establecerá en ", + "group.area.calculate.error.group.unit": "No hay unidad de área del grupo", "group.area.calculate.error.header": "Los siguientes medidores fueron excluidos de la suma porque:", - "group.area.calculate.error.zero": ": el área no está determinada o es cero", - "group.area.calculate.error.unit": ": el área es distinta a cero pero no tiene unidad", "group.area.calculate.error.no.meters": "No hay medidores en el grupo", - "group.area.calculate.error.group.unit": "No hay unidad de área del grupo", + "group.area.calculate.error.unit": ": el área es distinta a cero pero no tiene unidad", + "group.area.calculate.error.zero": ": el área no está determinada o es cero", + "group.area.calculate.header": "Área del grupo se establecerá en ", "group.create.nounit": "La unidad predeterminada del gráfico fue cambiado a sin unidad de ", "group.delete.group": "Borrar grupo", "group.delete.issue": "está en los siguientes grupos y no se puede borrar", @@ -1272,14 +1269,14 @@ const LocaleTranslationData = { "group.hidden": "Hay por lo menos un grupo que no es visible para tí", "group.input.error": "Entrada no válida, por tanto no se creó o editó el grupo", "group.name.error": "Por favor indique un nombre válido: (debe tener por lo menos un carácter que no sea un espacio blanco)", - "groups": "Grupos", "group.successfully.create.group": "Grupo creado con éxito.", "group.successfully.edited.group": "Grupo editado con éxito.", + "groups": "Grupos", "groups.select": "Seleccionar grupos", "has.no.data": "No hay datos actuales", "has.used": "ha utilizado", - "header.pages": "Páginas", "header.options": "Opciones", + "header.pages": "Páginas", "help": "Ayuda", "help.admin.conversioncreate": "Esta página permite a los administradores crear conversiones. Por favor visite {link} para más detalles e información.", "help.admin.conversionedit": "Esta página permite a los administradores editar conversiones. Por favor visite {link} para más detalles e información", @@ -1299,14 +1296,13 @@ const LocaleTranslationData = { "help.admin.users": "Esta página permite a los administradores ver y editar usarios. Por favor, visite {link} para más detalles e información.", "help.csv.meters": "Esta página permite a los administradores cargar medidores a través de un archivo CSV. Visite {enlace} para obtener más detalles e información.", "help.csv.readings": "Esta página permite a ciertos usuarios cargar lecturas a través de un archivo CSV. Visite {link} para obtener más detalles e información.", + "help.groups.area.calculate": "Esto sumará el área de todos los medidores en este grupo que tengan un área distinta a cero y una unidad de área. Ignorará cualquier medidor que no tiene área o unidad de área. Si este grupo no tiene unidad de área, no hará nada.", "help.groups.groupdetails": "Esta página muestra información detallada de un grupo. Por favor visite {link} para obtener más detalles e información.", "help.groups.groupview": "Esta página muestra información sobre grupos. Por favor, visite {link} para más detalles e información.", - "help.groups.area.calculate": "Esto sumará el área de todos los medidores en este grupo que tengan un área distinta a cero y una unidad de área. Ignorará cualquier medidor que no tiene área o unidad de área. Si este grupo no tiene unidad de área, no hará nada.", "help.home.area.normalize": "Alterna la normalización por área. Medidores/Grupos sin área quedarán escondidos. Por favor visite {link} para obtener más detalles e información.", "help.home.bar.days.tip": "Permite al usuario seleccionar el número de días deseado para cada barra. Por favor, visite {link} para más detalles e información.", "help.home.bar.interval.tip": "Selecciona el intervalo de tiempo (día, semana o 4 semanas) para cada barra. Por favor, visite {link} para más detalles e información.", "help.home.bar.stacking.tip": "Apila las barras una encima de la otra. Por favor, visite {link} para obtener más detalles e información.", - "help.home.map.interval.tip": "Seleciona el intervalo de tiempo (el último día, semana o 4 semanas) para el mapa correspondiente al intervalo de tiempo de la barra. Por favor visite {link} para mas detalles e información", "help.home.chart.plotly.controls": "Estos controles son proporcionados por Plotly, el paquete de gráficos utilizado por OED. Por lo general no se necesitan pero se proporcionan por si se desea ese nivel de control. Tenga en cuenta que es posible que algunas de estas opciones no interactúen bien con las funciones de OED. Consulte la documentación de Plotly en {link}.", "help.home.chart.redraw.restore": "OED automáticamente toma el promedio de los datos cuando es necesario para que los gráficos tengan un número razonable de puntos. Si usa los controles debajo del gráfico para desplazarse y / o acercarse, puede encontrar que la resolución en este nivel de promedio no es la que desea. Al hacer clic en el botón \"Redraw\" OED volverá a calcular el promedio y obtendrá una resolución más alta para el número de puntos que muestra. Si desea restaurar el gráfico al rango completo de fechas, haga clic en el botón \"Restore\" button. Por favor visite {link} para obtener más detalles e información.", "help.home.chart.select": "Se puede usar cualquier tipo de gráfico con cualquier combinación de grupos y medidores. Los gráficos de líneas muestran el uso (por ejemplo, kW) con el tiempo. Puede hacer zoom y desplazarse con los controles justo debajo del gráfico. La barra muestra el uso total (por ejemplo, kWh) para el período de tiempo de cada barra donde se puede controlar el período de tiempo. Comparar le permite ver el uso actual comparado con el uso del período anterior durante un día, una semana y cuatro semanas. Los gráficos del mapa muestran una imagen espacial de cada medidor donde el tamaño del círculo está relacionado con cuatro semanas de uso. Las gráficas 3D muestran el uso por día y el uso por hora del día. Hacer clic en uno de estas opciones las registra en ese gráfico. Por favor visite {link} para obtener más detalles e información.", @@ -1315,6 +1311,7 @@ const LocaleTranslationData = { "help.home.error.bar": "Alternar barras de error con el valor mínimo y máximo. Por favor, vea {link} para más detalles e información.", "help.home.export.graph.data": "Con el botón \"Exportar datos del gráfico\", uno puede exportar los datos del gráfico al ver una línea o barra el gráfico. La función de zoom y desplazamiento en el gráfico de líneas le permite controlar el período de tiempo de los datos exportados. El botón \"Exportar data de gráfico\" da los puntos de datos para el gráfico y no los datos originales del medidor. \"Exportar el dato gráfhico de medidor\" proporciona los datos subyacentes del medidor (solo gráficos de líneas). Por favor visite {link} para obtener más detalles e información.", "help.home.history": "Permite al usuario navegar por el historial reciente de gráficos. Por favor visite {link} para obtener más detalles e información.", + "help.home.map.interval.tip": "Seleciona el intervalo de tiempo (el último día, semana o 4 semanas) para el mapa correspondiente al intervalo de tiempo de la barra. Por favor visite {link} para mas detalles e información", "help.home.navigation": "El botón \"Gráfico\" va a la página de gráficos, el desplegable \"Páginas\" permite la navigación a las páginas de información, el desplegable \"Opciones\" permite la selección del idioma, opciones de ocultar y de iniciar/cerrar sesión y el botón \"Ayuda\" va a las páginas de ayuda. Busca ayuda en los menús desplegables o las páginas vínculadas para más información.", "help.home.readings.per.day": "El número de lecturas mostrado para cada día en un gráfico 3D. Por favor visite {link} para obtener más detalles e información.", "help.home.select.dateRange": "Seleccione el rango de datos usado en el gráfico mostrado. Para gráficos 3D debe ser un año o menos. Por favor visite {link} para obtener más detalles e información.", @@ -1357,18 +1354,22 @@ const LocaleTranslationData = { "less.energy": "menos energía", "line": "Línea", "log.in": "Iniciar sesión", + "log.limit.required": "Number of logs to display must be within 1 to 1000\u{26A1}", + "log.message": "Log Message\u{26A1}", + "log.messages": "Log Messages\u{26A1}", "log.out": "Cerrar sesión", + "log.time": "Log Time\u{26A1}", + "log.type": "Log Type\u{26A1}", "login.failed": "Error al iniciar sesión", "login.success": "Éxito al iniciar sesión", "logo": "Logo", "manage": "Gestionar", "map": "Mapa", - "maps": "Mapas", + "map.bad.digita": "Mayor a 360, por favor cambiar el angúlo a un número entre 0 a 360", + "map.bad.digitb": "Menor a 0, por favor cambiar el angúlo a un número entre 0 a 360", "map.bad.load": "Se necesita el archivo de la imagen del mapa", "map.bad.name": "Se necesita un nombre para el mapa", "map.bad.number": "No es número, por favor cambiar el angúlo a un número entre 0 a 360", - "map.bad.digita": "Mayor a 360, por favor cambiar el angúlo a un número entre 0 a 360", - "map.bad.digitb": "Menor a 0, por favor cambiar el angúlo a un número entre 0 a 360", "map.calibrate": "Calibrar", "map.calibration": "Estado de calibración", "map.circle.size": "Tamaño del círculo en el mapa", @@ -1392,39 +1393,35 @@ const LocaleTranslationData = { "map.notify.calibration.needed": "Necesita calibración antes de visualizar", "map.unavailable": "There's not an available map\u{26A1}", "map.upload.new.file": "Rehacer", + "maps": "Mapas", "max": "máximo", "menu": "Menú", "meter": "Medidor", - "meters": "Medidores", "meter.create": "Crear un medidor", "meter.cumulative": "Cumulativo", "meter.cumulativeReset": "Reinicio cumulativo:", "meter.cumulativeResetEnd": "Final del reinicio cumulativo:", "meter.cumulativeResetStart": "Comienzo del reinicio cumulativo:", - "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:\u{26A1}", + "meter.disableChecks": "Desactivar revisiones", "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?\u{26A1}", + "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:\u{26A1}", "meter.enabled": "Medidor activado", "meter.endOnlyTime": "Solo tiempos finales.", "meter.endTimeStamp": "Marca de tiempo al final:", - "meter.minVal": "Revisión del valor mínimo de lectura", - "meter.maxVal": "Revisión del valor máximo de lectura", - "meter.minDate": "Revisión de la fecha mínima de lectura", - "meter.maxDate": "Revisión de la fecha máxima de lectura", - "meter.maxError": "Revisión del número máximo de errores", - "meter.disableChecks": "Desactivar revisiones", "meter.failed.to.create.meter": "No se pudo crear un medidor con mensaje: ", "meter.failed.to.edit.meter": "No se pudo editar un medidor con mensaje: ", "meter.hidden": "Al menos un medidor no es visible para tí.", "meter.id": "ID del medidor", "meter.input.error": "Entrada no válida, por tanto no se creó o editó el medidor.", - "meter.unit.change.requires": "necesita cambiarse antes de cambiar el tipo de esta unidad", - "meter.unitName": "Unidad:", - "meter.url": "URL:", "meter.is.displayable": "El medidor es visualizable", "meter.is.enabled": "Actualizaciones activadas", "meter.is.not.displayable": "El medidor no es visualizable", "meter.is.not.enabled": "El medidor no está activado", - "meter.unit.is.not.editable": "La unidad de este medidor no puede cambiarse y se mantuvo el valor original porque: ", + "meter.maxDate": "Revisión de la fecha máxima de lectura", + "meter.maxError": "Revisión del número máximo de errores", + "meter.maxVal": "Revisión del valor máximo de lectura", + "meter.minDate": "Revisión de la fecha mínima de lectura", + "meter.minVal": "Revisión del valor mínimo de lectura", "meter.previousEnd": "Marca de tiempo del final anterior:", "meter.reading": "Lectura:", "meter.readingDuplication": "Duplicación de lectura:", @@ -1435,19 +1432,26 @@ const LocaleTranslationData = { "meter.startTimeStamp": "Marca de tiempo al inicio:", "meter.successfully.create.meter": "Éxito al crear el medidor.", "meter.successfully.edited.meter": "Éxito al editar el medidor.", - "meter.timeSort": "Ordenar por tiempo:", "meter.time.zone": "Zona horaria:", + "meter.timeSort": "Ordenar por tiempo:", "meter.type": "Tipo:", - "minute": "Minuto", + "meter.unit.change.requires": "necesita cambiarse antes de cambiar el tipo de esta unidad", + "meter.unit.is.not.editable": "La unidad de este medidor no puede cambiarse y se mantuvo el valor original porque: ", + "meter.unitName": "Unidad:", + "meter.url": "URL:", + "meters": "Medidores", "min": "mínimo", + "minute": "Minuto", "more.energy": "más energía", "more.options": "More Options\u{26A1}", "name": "Nombre:", "navigation": "Navegación", "need.more.points": "Nececita más puntos", "no": "no", - "note": "Nota: ", "no.data.in.range": "No hay datos para el rango de fechas", + "no.logs": "No logs to display. Please select another log type or data range\u{26A1}", + "note": "Nota: ", + "num.logs.display": "Number of logs to display\u{26A1}", "oed": "Panel de Energía Abierto", "oed.description": "Open Energy Dashboard es un proyecto independiente. ", "oed.version": "Versión OED", @@ -1470,8 +1474,8 @@ const LocaleTranslationData = { "rate.limit.error.first": "You have been rate limited by your OED site\u{26A1}", "rate.limit.error.second": "We suggest you try these in this order:\u{26A1}", "reading": "Lectura:", - "redo.cik.and.refresh.db.views": "Procesando los cambios. Esto tardará un momento.", "readings.per.day": "Lecturas por día", + "redo.cik.and.refresh.db.views": "Procesando los cambios. Esto tardará un momento.", "redraw": "Redibujar", "refresh.page.first": "Click the Refresh this page' button below to try again\u{26A1}", "refresh.page.second": "If you keep returning to this page wait longer and click 'Refresh this page' button\u{26A1}", @@ -1485,11 +1489,12 @@ const LocaleTranslationData = { "save.meter.edits": "Guardar las ediciones al medidor", "save.role.changes": "Guardar los cambios de rol", "second": "Segundo", + "select.all": "Select All\u{26A1}", "select.groups": "Seleccionar grupos", "select.map": "Seleccionar mapa", - "select.meter.type": "Seleccionar tipo de medidor", "select.meter": "Seleccionar medidor", "select.meter.group": "Seleccionar medidor o grupo para hacer gráfico", + "select.meter.type": "Seleccionar tipo de medidor", "select.meters": "Seleccionar medidores", "select.unit": "Seleccionar unidad", "show": "Mostrar", @@ -1499,12 +1504,11 @@ const LocaleTranslationData = { "site.title": "Site Title\u{26A1}", "sort": "Sort Order\u{26A1}", "submit": "Enviar", - "submitting": "Enviando", "submit.changes": "Ingresar los cambios", "submit.new.user": "Ingresar un nuevo usario", + "submitting": "Enviando", "the.unit.of.meter": "La unidad del medidor", "this.four.weeks": "Estas cuatro semanas", - "timezone.no": "sin zona horaria", "this.week": "Esta semana", "threeD.area.incompatible": "
es incompatible
con normalización del área", "threeD.date": "Fecha", @@ -1516,28 +1520,23 @@ const LocaleTranslationData = { "threeD.y.axis.label": "Días del año calendario", "TimeSortTypes.decreasing": "decreciente", "TimeSortTypes.increasing": "creciente", + "timezone.no": "sin zona horaria", "today": "Hoy", "toggle.link": "Alternar enlace de gráfico", - "toggle.options" : "Alternar opciones", + "toggle.options": "Alternar opciones", "total": "total", "true": "Verdad", "TrueFalseType.false": "no", "TrueFalseType.true": "sí", "undefined": "indefinido", "unit": "Unidad", - "UnitRepresentType.quantity": "cantidad", - "UnitRepresentType.flow": "flujo", - "UnitRepresentType.raw": "crudo", - "UnitType.unit": "unidad", - "UnitType.meter": "medidor", - "UnitType.suffix": "sufijo", "unit.delete.failure": "No se pudo borrar la unidad con error: ", "unit.delete.success": "Éxito al borrar la unidad", "unit.delete.unit": "Borrar la unidad", "unit.destination.error": "como la unidad de la destinación", - "unit.dropdown.displayable.option.none": "Ninguna", - "unit.dropdown.displayable.option.all": "Todo", "unit.dropdown.displayable.option.admin": "administrador", + "unit.dropdown.displayable.option.all": "Todo", + "unit.dropdown.displayable.option.none": "Ninguna", "unit.failed.to.create.unit": "No se pudo crear la unidad", "unit.failed.to.delete.unit": "No se puede borrar por que esta unidad está siendo usada por lo siguiente", "unit.failed.to.edit.unit": "No se pudo editar la unidad", @@ -1553,8 +1552,14 @@ const LocaleTranslationData = { "unit.suffix": "Sufijo:", "unit.type.of.unit": "Tipo de unidad:", "unit.type.of.unit.suffix": "El sufijo agregado determina que el tipo de la unidad es sufijo", + "UnitRepresentType.flow": "flujo", + "UnitRepresentType.quantity": "cantidad", + "UnitRepresentType.raw": "crudo", "units": "Unidades", "units.conversion.page.title": "Gráficos Visuales de Unidades y Conversiones", + "UnitType.meter": "medidor", + "UnitType.suffix": "sufijo", + "UnitType.unit": "unidad", "unsaved.failure": "No se pudieron guardar los cambios", "unsaved.success": "Se guardaron los cambios", "unsaved.warning": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir?", @@ -1586,25 +1591,14 @@ const LocaleTranslationData = { "uses": "uses\u{26A1}", "view.groups": "Ver grupos", "visit": " o visite nuestro ", - "visual.unit": "Units Visual Graphics\u{26A1}", - "visual.input.units.graphic": "Gráfico Visual de Unidades de Entrada", "visual.analyzed.units.graphic": "Gráfico Visual de Unidades Analizadas", + "visual.input.units.graphic": "Gráfico Visual de Unidades de Entrada", + "visual.unit": "Units Visual Graphics\u{26A1}", "website": "sitio web", "week": "semana", "yes": "sí", "yesterday": "Ayer", "you.cannot.create.a.cyclic.group": "No se puede crear un grupo cíclico", - "log.messages": "Log Messages\u{26A1}", - "log.message": "Log Message\u{26A1}", - "log.type": "Log Type\u{26A1}", - "log.time": "Log Time\u{26A1}", - "num.logs.display": "Number of logs to display\u{26A1}", - "log.msg.update": "Update\u{26A1}", - "from.1.to.1000": "from 1 to 1000\u{26A1}", - "show.logs": "Show Log Table\u{26A1}", - "select.all": "Select All\u{26A1}", - "no.logs": "No logs to display. Please select another log type or data range\u{26A1}", - "log.limit.required": "You must enter a valid number of logs to display\u{26A1}" } } diff --git a/src/server/log.js b/src/server/log.js index 9f41f1cb3..efc4dfc07 100644 --- a/src/server/log.js +++ b/src/server/log.js @@ -51,8 +51,8 @@ class Logger { * @param {boolean?} skipMail Don't e-mail this message even if we would normally emit an e-mail for this level. */ log(level, message, error = null, skipMail = false) { - let logTime = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); - let messageToLog = `[${level.name}@${logTime}] ${message}\n`; + let logTime = moment(); + let messageToLog = `[${level.name}@${logTime.format('YYYY-MM-DDTHH:mm:ss.SSSZ')}] ${message}\n`; const conn = getConnection(); @@ -81,7 +81,7 @@ class Logger { // NOTE: for running the first time, uncomment the following code block to write all current logs to the database // after that you can comment out it to write only new logs to the database - // TODO: This should be fix later to check whehter it should write new logs or all logs to the database when running + // TODO: This should be fix later to check whether it should write new logs or all logs to the database when running // fs.readFile(logFile, 'utf8', async (err, data) => { // if (err) { // console.error(`Failed to read log file: ${err} (${err.stack})`); @@ -107,7 +107,7 @@ class Logger { // Comment out the following code block when running the first time to write all logs to the database // then uncomment it to write only new logs to the database later // Write the new log to the database - const logMsg = new LogMsg(level.name, message, new Date(logTime)); + const logMsg = new LogMsg(level.name, message, logTime); (async () => { try { await logMsg.insert(conn); diff --git a/src/server/models/LogMsg.js b/src/server/models/LogMsg.js index 1735f7c58..53be00d44 100644 --- a/src/server/models/LogMsg.js +++ b/src/server/models/LogMsg.js @@ -55,48 +55,6 @@ class LogMsg { await conn.none(sqlFile('logmsg/insert_new_log.sql'), logMsg); } - /** - * Returns a promise to get all of the logs from the database - * @param conn the connection to be used. - * @returns {Promise.>} - */ - static async getAll(conn) { - const rows = await conn.any(sqlFile('logmsg/get_all_logs.sql')); - if (rows.length > 0) { - return rows.map(LogMsg.mapRow); - } - } - - /** - * Returns a promise to get all of the logs in between two dates. - * If no startDate is specified, all logs before the endDate are returned. - * If no endDate is specified, all logs after the startDate are returned. - * @param {Date} startDate - * @param {Date} endDate - * @param conn is the connection to use. - * @returns {Promise.>} - */ - static async getLogsByDateRange(startDate, endDate, conn) { - const rows = await conn.any(sqlFile('logmsg/get_logs_from_dates.sql'), { - startDate: startDate, - endDate: endDate - }); - - return rows.map(LogMsg.mapRow); - } - - /** - * Returns a promise to get all of the logs of a certain type - * @param logType - * @param conn is the connection to use. - * @returns {Promise.>} - */ - static async getLogsByType(logType, conn) { - const rows = await conn.any(sqlFile('logmsg/get_logs_from_type.sql'), { logType: logType }); - - return rows.map(LogMsg.mapRow); - } - /** * Returns a promise to get all of the logs in between two dates. * @param {Date} startDate start date of the range to get logs diff --git a/src/server/routes/logs.js b/src/server/routes/logs.js index eef40ae18..9482d21f1 100644 --- a/src/server/routes/logs.js +++ b/src/server/routes/logs.js @@ -37,7 +37,7 @@ const validLogMsg = { }, logTypes: { type: 'string', - pattern: '^(INFO|WARN|ERROR|SILENT)(-(INFO|WARN|ERROR|SILENT))*$' + pattern: '^(INFO|WARN|ERROR|SILENT|DEBUG)(-(INFO|WARN|ERROR|SILENT|DEBUG))*$' }, logLimit: { type: 'string', diff --git a/src/server/sql/logmsg/get_all_logs.sql b/src/server/sql/logmsg/get_all_logs.sql deleted file mode 100644 index b44910172..000000000 --- a/src/server/sql/logmsg/get_all_logs.sql +++ /dev/null @@ -1,6 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - --- gets all logs in the database -SELECT * FROM logmsg; \ No newline at end of file diff --git a/src/server/sql/logmsg/get_logs_from_dates.sql b/src/server/sql/logmsg/get_logs_from_dates.sql deleted file mode 100644 index d2383fbcf..000000000 --- a/src/server/sql/logmsg/get_logs_from_dates.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - --- Gets logs in table by date range. This is then ordered by time ascending. -SELECT - log_type, log_message, log_time -FROM logmsg -WHERE log_time >= COALESCE(${startDate}, '-infinity'::TIMESTAMP) - AND log_time <= COALESCE(${endDate}, 'infinity'::TIMESTAMP) -ORDER BY log_time ASC; \ No newline at end of file diff --git a/src/server/sql/logmsg/get_logs_from_type.sql b/src/server/sql/logmsg/get_logs_from_type.sql deleted file mode 100644 index 174dbde8a..000000000 --- a/src/server/sql/logmsg/get_logs_from_type.sql +++ /dev/null @@ -1,10 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - --- Gets logs in table by date range. This is then ordered by time ascending. -SELECT - log_type, log_message , log_time -FROM logmsg -WHERE log_type = ${logType} -ORDER BY log_time ASC; \ No newline at end of file From c5da842941bdcd847a07a21ef4ef3c8f32c65526 Mon Sep 17 00:00:00 2001 From: nmqng Date: Wed, 4 Dec 2024 16:43:12 -0500 Subject: [PATCH 20/22] refactor LogMsgComponent.tsx and resolved some PR comments --- .../app/components/admin/LogMsgComponent.tsx | 81 +++++-------------- 1 file changed, 22 insertions(+), 59 deletions(-) diff --git a/src/client/app/components/admin/LogMsgComponent.tsx b/src/client/app/components/admin/LogMsgComponent.tsx index 9e8350cd0..f636aec4d 100644 --- a/src/client/app/components/admin/LogMsgComponent.tsx +++ b/src/client/app/components/admin/LogMsgComponent.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import * as moment from 'moment'; +import { orderBy } from 'lodash'; import { Alert, Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, FormFeedback, @@ -18,7 +19,6 @@ import { logsApi } from '../../utils/api'; import translate from '../../utils/translate'; import { TimeInterval } from '../../../../common/TimeInterval'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../../utils/dateRangeCompatibility'; -import { Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; // number of log messages to display per page const PER_PAGE = 20; @@ -109,11 +109,15 @@ export default function LogMsgComponent() { } }; - // React effect to keep track of the "Select All" checkbox state + // React effect to keep track of the "Select All" checkbox state for the update log React.useEffect(() => { selectedUpdateLogTypes.length === logTypes.length ? setSelectAllUpdate(true) : setSelectAllUpdate(false); + }, [selectedUpdateLogTypes]); + + // React effect to keep track of the "Select All" checkbox state for the table + React.useEffect(() => { selectedTableLogTypes.length === logTypes.length ? setSelectAllTable(true) : setSelectAllTable(false); - }, [selectedUpdateLogTypes, selectedTableLogTypes]); + }, [selectedTableLogTypes]); // Handle "Select All" checkbox change in the table const handleTableSelectAll = () => { @@ -128,53 +132,21 @@ export default function LogMsgComponent() { // Handle sorting of logs by date const handleDateSort = () => { const newDateSortOrder = dateSortOrder === 'asc' ? 'desc' : 'asc'; - const sortedLogs = [...logs].sort((a, b) => { - const dateA = moment(a.logTime); - const dateB = moment(b.logTime); - - if (newDateSortOrder === 'asc') { - return dateA.valueOf() - dateB.valueOf(); - } else { - return dateB.valueOf() - dateA.valueOf(); - } - }); + const sortedLogs = orderBy(logs, ['logTime'], [newDateSortOrder]); setDateSortOrder(newDateSortOrder); setLogs(sortedLogs); }; - // Handle date range change - const handleDateRangeChange = (range: Value) => { - setLogDateRange(dateRangeToTimeInterval(range)); - }; // Handle page change for pagination const handlePageChange = (newPage: number) => { setCurrentPage(newPage); }; - // Toggle dropdown for type in the table - const toggleTypeTable = () => { - setTypeTableDropdown(!typeTableDropdown); - }; - // Toggle dropdown for type in the update log - const toggleUpdateLog = () => { - setUpdateLogDropdown(!updateLogDropdown); - }; - // Handle showing all logs instead of paginated - const handleShowAllLogs = () => { - setShowAllLogs(!showAllLogs); - }; // Filter logs based on selected log types and date range - const filteredLogs = logs.filter(log => { - const logDate = moment(log.logTime); - // Check if log is within the selected date range - const isWithinDateRange = - (!logDateRange || !logDateRange.getIsBounded() || logDate >= logDateRange.getStartTimestamp()) && - (!logDateRange || !logDateRange.getIsBounded() || logDate <= logDateRange.getEndTimestamp()); - return selectedTableLogTypes.includes(log.logType) && isWithinDateRange; - }); - - // Paginate logs if not showing all logs - const paginatedLogs = showAllLogs ? filteredLogs : filteredLogs.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE); - const totalPages = Math.ceil(filteredLogs.length / PER_PAGE); + const paginatedLogs = showAllLogs + ? logs.filter(log => selectedTableLogTypes.includes(log.logType)) + : logs.filter(log => selectedTableLogTypes.includes(log.logType)) + .slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE); + const totalPages = Math.ceil(logs.length / PER_PAGE); /** * Handle showing the log table by fetching from the server @@ -198,7 +170,7 @@ export default function LogMsgComponent() { {/* Filter log messages by type, date range, and number of logs for fetching */}
- + setUpdateLogDropdown(!updateLogDropdown)}> {translate('log.type')} @@ -230,7 +202,7 @@ export default function LogMsgComponent() { setLogDateRange(dateRangeToTimeInterval(e))} minDate={new Date(1970, 0, 1)} maxDate={new Date()} // Formats Dates, and Calendar months base on locale @@ -270,7 +242,7 @@ export default function LogMsgComponent() {
- + setTypeTableDropdown(!typeTableDropdown)}> {translate('log.type')} @@ -308,17 +280,7 @@ export default function LogMsgComponent() { style={{ cursor: 'pointer' }} onClick={() => handleLogMessageModal(log.logType, log.logTime, log.logMessage)} >{log.logMessage.length > 80 ? `${log.logMessage.slice(0, 80)} ...` : log.logMessage} - - {new Date(log.logTime).toLocaleString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - fractionalSecondDigits: 2 - })} - {moment(log.logTime).format('MM/DD/YYYY, hh:mm:ss.SS A')}
{translate('log.message')}{translate('log.time')} {dateSortOrder === 'asc' ? '↑' : '↓'} + {translate('log.time')} {dateSortOrder === 'asc' ? '↑' : '↓'} +
handleLogMessageModal(log.logType, log.logTime, log.logMessage)} - >{log.logMessage.length > 80 ? `${log.logMessage.slice(0, 80)} ...` : log.logMessage} {moment(log.logTime).format('MM/DD/YYYY, hh:mm:ss.SS A')}
{moment(log.logTime).format('MM/DD/YYYY, hh:mm:ss.SS A')}{moment.parseZone(log.logTime).format('LL LTS')}