Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
82179b2
[OGUI-1193] Add empty field filter support to SQL query builder
isaachilly Jun 1, 2026
aa87f9c
[OGUI-1193] Add matchEmpty/excludeEmpty criteria to LogFilter model
isaachilly Jun 1, 2026
ce86c0c
[OGUI-1193] Add empty-filter toggles and UI logic
isaachilly Jun 1, 2026
a6c5621
[OGUI-1193] Add tests for empty-field filter SQL logic
isaachilly Jun 1, 2026
448b170
[OGUI-1193] ESLint Fixes
isaachilly Jun 2, 2026
2d70870
[OGUI-1193] Reset $-prefixed opposite empty flag too
isaachilly Jun 2, 2026
e98872d
[OGUI-1193] Update context-menu reset tests to also check empty opera…
isaachilly Jun 2, 2026
a588e18
[OGUI-1193] Add tests for empty field toggle
isaachilly Jun 2, 2026
49bb108
[OGUI-1193] Add tests for empty Toggle in LiveMode
isaachilly Jun 2, 2026
019362d
[OGUI-1193] Fix live-mode due to failing test notifying of issue
isaachilly Jun 2, 2026
e8da942
[OGUI-1193] - Testing CI failing test fix
isaachilly Jun 2, 2026
a81f5a7
[OGUI-1193] - Adjust now passing test back a bit to be useful
isaachilly Jun 2, 2026
19d3c46
Merge branch 'dev' into feature/ILG/OGUI-1193/Filters-should-allow-fo…
isaachilly Jun 2, 2026
7afc799
[OGUI-1193] Improve maintainability
isaachilly Jun 2, 2026
e1270e5
[OGUI-1193] Wrap OR criteria to preserve precedence
isaachilly Jun 2, 2026
eff5c2f
[OGUI-1193] Fix ESLint errors/warnings
isaachilly Jun 3, 2026
dfaf451
[OGUI-1193] Separate timestamp and text field handling in context menu
isaachilly Jun 4, 2026
e4a6134
[OGUI-1193] Remove redundant check
isaachilly Jun 4, 2026
e2195eb
[OGUI-1193] Update a comment to be more informative
isaachilly Jun 4, 2026
619cdff
[OGUI-1193] Validate operator in setCriteria and update tests
isaachilly Jun 4, 2026
c76f282
[OGUI-1193] Adjust live-mode test timeouts
isaachilly Jun 4, 2026
26fb7a1
[OGUI-1193] Fix CI test with incorrect timeout
isaachilly Jun 4, 2026
eca180f
Add matchEmpty/excludeEmpty and refactor filters
isaachilly Jun 4, 2026
affb6f2
[OGUI-1193] Add matchEmpty/excludeEmpty test constants
isaachilly Jun 4, 2026
42cf557
[OGUI-1193] Fix textarea incorrect height and with issues
isaachilly Jun 4, 2026
3db3c6a
[OGUI-1193] Keep order consistent between $ and non-$ operators
isaachilly Jun 4, 2026
3de3f92
[OGUI-1193] Remove includes when checking severity
isaachilly Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion InfoLogger/lib/controller/ConfigController.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ConfigController {
* Handler for providing configuration for the InfoLogger optional services
* @param {ExpressJS.Request} _ - object for the HTTP request.
* @param {ExpressJS.Response} res - response with the configuration object
* @returns {*} response returned.
* @returns {Promise<void>} response returned.
*/
async getConfigurationHandler(_, res) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
/**
* Check whether provided service was configured and if so whether is available or not
* @param {object} service - Service object to check
* @returns {Function} Express middleware function
* @returns {(req: Request, res: Response, next: NextFunction) => void} Express middleware function
*/
const serviceAvailabilityCheck = (service) =>

/**
* Express middleware function
* @param {Request} req - HTTP request object
* @param {Response} res - HTTP response object
* @param {Function} next - Next middleware function
* @param {NextFunction} next - Next middleware function
* @returns {void} - calls next or res depending on service availability
*/
(req, res, next) => {
Expand Down
90 changes: 48 additions & 42 deletions InfoLogger/lib/services/QueryService.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,20 +211,22 @@ class QueryService {
if (!filters[field]) {
continue;
}
const separator = field === 'message' ? '\n' : ' ';
for (const operator in filters[field]) {
if (filters[field][operator] === null || !operator.includes('$')) {
if (filters[field][operator] === null || filters[field][operator] === false || !operator.includes('$')) {
continue;
}

if (operator === '$since' || operator === '$until') {
if (operator === '$matchEmpty' || operator === '$excludeEmpty') {
// no parameterized value needed for $matchEmpty or $excludeEmpty, the SQL is static
} else if (operator === '$since' || operator === '$until') {
// read date, both input and output are GMT, no timezone to consider here
values.push(new Date(filters[field][operator]).getTime() / 1000);
} else {
const separator = field === 'message' ? '\n' : ' ';
if ((operator === '$match' || operator === '$exclude') && filters[field][operator].split(separator).length > 1
) {
const subValues = filters[field][operator].split(separator);
subValues.forEach((value) => values.push(value));
values.push(...subValues);
} else {
values.push(filters[field][operator]);
}
Expand All @@ -240,54 +242,58 @@ class QueryService {
criteria.push(`\`${field}\`<=?`);
break;
case '$match': {
const separator = field === 'message' ? '\n' : ' ';
const criteriaArray = filters[field].match.split(separator);
if (criteriaArray.length <= 1) {
if (criteriaArray.toString().includes('%')) {
criteria.push(`\`${field}\` LIKE (?)`);
} else {
criteria.push(`\`${field}\` = ?`);
}

// Either create a LIKE match or an exact match
const toMatchCondition = (crit) =>
crit.includes('%')
? `\`${field}\` LIKE (?)`
: `\`${field}\` = ?`;

const matchStr = criteriaArray.map(toMatchCondition).join(' OR ');

const matchEmpty = filters[field].$matchEmpty;
if (matchEmpty) {
criteria.push(`(${matchStr} OR \`${field}\` = '' OR \`${field}\` IS NULL)`);
} else if (criteriaArray.length > 1) {
// Wrap so the OR doesn't bind looser than the AND between criteria in the WHERE clause
criteria.push(`(${matchStr})`);
} else {
let criteriaString = '(';
criteriaArray.forEach((crit) => {
if (crit.includes('%')) {
criteriaString += `\`${field}\` LIKE (?) OR `;
} else {
criteriaString += `\`${field}\` = ? OR `;
}
});
criteriaString = criteriaString.substr(0, criteriaString.length - 4);
criteriaString += ')';
criteria.push(criteriaString);
criteria.push(matchStr);
}
break;
}
case '$exclude': {
const separator = field === 'message' ? '\n' : ' ';
const criteriaArray = filters[field].exclude.split(separator);
if (criteriaArray.length <= 1) {
if (criteriaArray.toString().includes('%')) {
criteria.push(`NOT(\`${field}\` LIKE (?) AND \`${field}\` IS NOT NULL)`);
} else {
criteria.push(`NOT(\`${field}\` = ? AND \`${field}\` IS NOT NULL)`);
}
} else {
let criteriaString = 'NOT(';
criteriaArray.forEach((crit) => {
if (crit.includes('%')) {
criteriaString += `\`${field}\` LIKE (?) AND \`${field}\` IS NOT NULL OR `;
} else {
criteriaString += `\`${field}\` = ? AND \`${field}\` IS NOT NULL OR `;
}
});
criteriaString = criteriaString.substr(0, criteriaString.length - 4);
criteriaString += ')';
criteria.push(criteriaString);
}

const toExcludeCondition = (crit) =>
crit.includes('%')
? `\`${field}\` LIKE (?) AND \`${field}\` IS NOT NULL`
: `\`${field}\` = ? AND \`${field}\` IS NOT NULL`;

const excludeStr = criteriaArray.length > 1
? criteriaArray.map((c) => `(${toExcludeCondition(c)})`).join(' OR ')
: toExcludeCondition(criteriaArray[0]);

criteria.push(`NOT(${excludeStr})`);

const excludeEmpty = filters[field].$excludeEmpty;
if (excludeEmpty) {
criteria.push(`(\`${field}\` != '' AND \`${field}\` IS NOT NULL)`);
}
break;
}
case '$matchEmpty':
// If no match value but $matchEmpty is true, we want to match only empty values
if (!filters[field].$match) {
criteria.push(`(\`${field}\` = '' OR \`${field}\` IS NULL)`);
}
break;
case '$excludeEmpty':
if (!filters[field].$exclude) {
criteria.push(`(\`${field}\` != '' AND \`${field}\` IS NOT NULL)`);
}
break;
case '$in':
criteria.push(`\`${field}\` IN (?)`);
break;
Expand Down
2 changes: 1 addition & 1 deletion InfoLogger/lib/utils/fromSqlToNativeError.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const { NotFoundError, TimeoutError, UnauthorizedAccessError } = require('@alice
* The purpose is to translate MySQL errors to native JS errors
* Source: https://github.com/mariadb-corporation/mariadb-connector-nodejs/blob/c3a9e333243a1d92b22f4ca1e5a574ab0de77cea/lib/const/error-code.js#L1040
* @param {SqlError} error - the error from a catch or callback
* @throws throws a native JS error
* @throws {Error} throws a native JS error
*/
const fromSqlToNativeError = (error) => {
const { code, errno, sqlMessage } = error;
Expand Down
46 changes: 45 additions & 1 deletion InfoLogger/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,51 @@ footer { border-top: 1px solid var(--color-gray); }
.btn:hover { background-color: #a0a0a0; color: var(--color-white); }
.btn:active, .btn.active, .dropdown-open > .btn { background-color: #c0c0c0; }

.text-area-for-message:focus { width: 50%; height: 10rem !important; right: 0; position: absolute; }
.filter-input-group { display: flex;
position: relative;
}
.filter-input-group .form-control {
height: 2em;
border-radius: .25rem;
z-index: 1;
}
.filter-input-group:has(.empty-toggle.active) .form-control,
.filter-input-group:hover .form-control {
border-radius: .25rem 0 0 .25rem;
}

.empty-toggle {
display: none;
padding: 0 0.15rem;
font-size: 0.7rem;
font-weight: bold;
border-radius: 0 .25rem .25rem 0;
opacity: 0.6;
}
.filter-input-group:hover .empty-toggle:not(.active) {
display: block;
}
.empty-toggle:hover {
opacity: 1;
}
.empty-toggle.active {
opacity: 1;
display: block;
}

.text-area-for-message {
resize: none;
}
.text-area-for-message:focus {
width: calc(400% + 12px);
height: 10rem !important;
inset: 0 0 auto auto;
position: absolute;
z-index: 10;
border-radius: .25rem;
}
.filter-input-group:has(.text-area-for-message:focus) .empty-toggle { display: none; }

a.disabled { pointer-events: none; cursor: default; }

.cell-context-menu-overlay {
Expand Down
6 changes: 3 additions & 3 deletions InfoLogger/public/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
* Limit the number of calls to `fn` to 1 per `time` maximum.
* First call is immediate if `time` have been waited already.
* All other calls before end of `time` window will lead to 1 exececution at the end of window.
* @param {string} fn - function to be called
* @param {string} time - ms
* @returns {Function} lambda function to be called to call `fn`
* @param {(...args: unknown[]) => void} fn - function to be called
* @param {number} time - ms
* @returns {(...args: unknown[]) => void} lambda function to be called to call `fn`
* @example
* let f = callRateLimiter((arg) => console.log('called', arg), 1000);
* 00:00:00 f(1);f(2);f(3);f(4);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@
/**
* Operators used with the text filters.
*/
export const TEXT_FILTER_OPERATORS = Object.freeze(['since', 'until', 'match', 'exclude']);
export const TEXT_FILTER_OPERATORS = Object.freeze([
'since',
'until',
'match',
'exclude',
'matchEmpty',
'excludeEmpty',
]);
40 changes: 31 additions & 9 deletions InfoLogger/public/log/cellContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,44 @@ export const cellContextMenu = (model) => {
}, model.log.filter.criterias.level.max === 1),
];
}
if (isTimestamp) {
const { since, until } = model.log.filter.criterias.timestamp;
return [
createMenuItem(iconCheck(), 'success', 'From', () => {
model.log.setCriteria('timestamp', 'since', value);
hideMenu();
}),
createMenuItem(iconBan(), 'danger', 'To', () => {
model.log.setCriteria('timestamp', 'until', value);
hideMenu();
}),
createMenuItem(iconTrash(), 'danger', 'Clear Filter', () => {
model.log.setCriteria('timestamp', 'since', '');
model.log.setCriteria('timestamp', 'until', '');
hideMenu();
}, !since && !until),
];
}

const { match, exclude, matchEmpty, excludeEmpty } = model.log.filter.criterias[field];
const isClear = !match && !exclude && !matchEmpty && !excludeEmpty;

return [
createMenuItem(iconCheck(), 'success', isTimestamp ? 'From' : 'Match', () => {
model.log.setCriteria(field, isTimestamp ? 'since' : 'match', isTimestamp ? value : appendFilter('match'));
createMenuItem(iconCheck(), 'success', 'Match', () => {
model.log.setCriteria(field, 'match', appendFilter('match'));
hideMenu();
}),
createMenuItem(iconBan(), 'danger', isTimestamp ? 'To' : 'Exclude', () => {
model.log.setCriteria(field, isTimestamp ? 'until' : 'exclude', isTimestamp ? value : appendFilter('exclude'));
createMenuItem(iconBan(), 'danger', 'Exclude', () => {
model.log.setCriteria(field, 'exclude', appendFilter('exclude'));
hideMenu();
}),
createMenuItem(iconTrash(), 'danger', 'Clear Filter', () => {
model.log.setCriteria(field, isTimestamp ? 'until' : 'exclude', '');
model.log.setCriteria(field, isTimestamp ? 'since' : 'match', '');
model.log.setCriteria(field, 'match', '');
model.log.setCriteria(field, 'exclude', '');
model.log.setCriteria(field, 'matchEmpty', false);
model.log.setCriteria(field, 'excludeEmpty', false);
hideMenu();
}, isTimestamp
? !model.log.filter.criterias.timestamp.since && !model.log.filter.criterias.timestamp.until
: !model.log.filter.criterias[field].match && !model.log.filter.criterias[field].exclude),
}, isClear),
];
};

Expand Down
Loading