diff --git a/services/harmony/app/frontends/workflow-ui.ts b/services/harmony/app/frontends/workflow-ui.ts
index 01880fa10..c62c8f7b5 100644
--- a/services/harmony/app/frontends/workflow-ui.ts
+++ b/services/harmony/app/frontends/workflow-ui.ts
@@ -53,6 +53,8 @@ interface TableQuery {
userValues: string[],
providerValues: string[],
labelValues: string[],
+ messageCategoryValues: string[],
+ allowMessageCategoryValues: boolean,
from: Date,
to: Date,
dateKind: 'createdAt' | 'updatedAt',
@@ -85,10 +87,12 @@ function parseQuery( /* eslint-disable @typescript-eslint/no-explicit-any */
userValues: [],
providerValues: [],
labelValues: [],
+ messageCategoryValues: [],
allowStatuses: true,
allowServices: true,
allowUsers: true,
allowProviders: true,
+ allowMessageCategoryValues: true,
// date controls
from: undefined,
to: undefined,
@@ -101,6 +105,7 @@ function parseQuery( /* eslint-disable @typescript-eslint/no-explicit-any */
tableQuery.allowServices = !(requestQuery.disallowservice === 'on');
tableQuery.allowUsers = !(requestQuery.disallowuser === 'on');
tableQuery.allowProviders = !(requestQuery.disallowprovider === 'on');
+ tableQuery.allowMessageCategoryValues = !(requestQuery.disallowmessagecategory === 'on');
const selectedOptions: { field: string, dbValue: string, value: string }[] = JSON.parse(requestQuery.tablefilter);
const validStatusSelections = selectedOptions
@@ -123,6 +128,10 @@ function parseQuery( /* eslint-disable @typescript-eslint/no-explicit-any */
.filter(option => /^provider: [A-Za-z0-9_]{1,100}$/.test(option.value));
const providerValues = validProviderSelections.map(option => option.value.split('provider: ')[1].toLowerCase());
+ const validMessageCategorySelections = selectedOptions
+ .filter(option => /^message category: .{1,100}$/.test(option.value));
+ const messageCategoryValues = validMessageCategorySelections.map(option => option.dbValue || option.value.split('message category: ')[1].toLowerCase());
+
if ((statusValues.length + serviceValues.length + userValues.length + providerValues.length) > maxFilters) {
throw new RequestValidationError(`Maximum amount of filters (${maxFilters}) was exceeded.`);
}
@@ -130,12 +139,14 @@ function parseQuery( /* eslint-disable @typescript-eslint/no-explicit-any */
.concat(validServiceSelections)
.concat(validUserSelections)
.concat(validProviderSelections)
- .concat(validLabelSelections));
+ .concat(validLabelSelections)
+ .concat(validMessageCategorySelections));
tableQuery.statusValues = statusValues;
tableQuery.serviceValues = serviceValues;
tableQuery.userValues = userValues;
tableQuery.providerValues = providerValues;
tableQuery.labelValues = labelValues;
+ tableQuery.messageCategoryValues = messageCategoryValues;
}
// everything in the Workflow UI uses the browser timezone, so we need a timezone offset
const offSetMs = parseInt(requestQuery.tzoffsetminutes || 0) * 60 * 1000;
@@ -426,6 +437,7 @@ export async function getJob(
updatedAtChecked: dateKind == 'updatedAt' ? 'checked' : '',
createdAtChecked: dateKind != 'updatedAt' ? 'checked' : '',
disallowStatusChecked: requestQuery.disallowstatus === 'on' ? 'checked' : '',
+ disallowMessageCategoryChecked: requestQuery.disallowmessagecategory === 'on' ? 'checked' : '',
selectedFilters: originalValues,
version,
isAdminRoute: req.context.isAdminAccess,
@@ -481,8 +493,10 @@ function workItemRenderingFunctions(job: Job, isAdmin: boolean, isLogViewer: boo
badgeClasses[WorkItemStatus.SUCCESSFUL] = 'success';
badgeClasses[WorkItemStatus.RUNNING] = 'info';
badgeClasses[WorkItemStatus.QUEUED] = 'warning';
+ badgeClasses[WorkItemStatus.WARNING] = 'warning';
return {
workflowItemBadge(): string { return badgeClasses[this.status]; },
+ workflowItemStatus(): string { return this.message_category ? `${this.status}: ${this.message_category}` : this.status; },
workflowItemStep(): string { return sanitizeImage(this.serviceID); },
workflowItemCreatedAt(): string { return this.createdAt.getTime(); },
workflowItemUpdatedAt(): string { return this.updatedAt.getTime(); },
@@ -536,6 +550,12 @@ function tableQueryToWorkItemQuery(tableFilter: TableQuery, jobID: string, id?:
in: tableFilter.allowStatuses,
};
}
+ if (tableFilter.messageCategoryValues.length) {
+ itemQuery.whereIn.message_category = {
+ values: tableFilter.messageCategoryValues,
+ in: tableFilter.allowMessageCategoryValues,
+ };
+ }
if (tableFilter.from || tableFilter.to) {
itemQuery.dates = { field: tableFilter.dateKind };
itemQuery.dates.from = tableFilter.from;
diff --git a/services/harmony/app/models/work-item-interface.ts b/services/harmony/app/models/work-item-interface.ts
index 72986ec0b..c3db41fc5 100644
--- a/services/harmony/app/models/work-item-interface.ts
+++ b/services/harmony/app/models/work-item-interface.ts
@@ -91,6 +91,7 @@ export interface WorkItemQuery {
};
whereIn?: {
status?: { in: boolean, values: string[] };
+ message_category?: { in: boolean, values: string[] };
};
dates?: {
from?: Date;
diff --git a/services/harmony/app/models/work-item.ts b/services/harmony/app/models/work-item.ts
index 93eb233be..bf4136b35 100644
--- a/services/harmony/app/models/work-item.ts
+++ b/services/harmony/app/models/work-item.ts
@@ -431,7 +431,7 @@ export async function queryAll(
if (constraint.in) {
void queryBuilder.whereIn(field, constraint.values);
} else {
- void queryBuilder.whereNotIn(field, constraint.values);
+ void queryBuilder.where(builder => builder.whereNotIn(field, constraint.values).orWhereNull(field));
}
}
}
diff --git a/services/harmony/app/views/workflow-ui/job/index.mustache.html b/services/harmony/app/views/workflow-ui/job/index.mustache.html
index 852be7034..288347e6b 100644
--- a/services/harmony/app/views/workflow-ui/job/index.mustache.html
+++ b/services/harmony/app/views/workflow-ui/job/index.mustache.html
@@ -63,6 +63,12 @@
negate statuses
+
+
+
+
page size
diff --git a/services/harmony/app/views/workflow-ui/job/work-item-table-row.mustache.html b/services/harmony/app/views/workflow-ui/job/work-item-table-row.mustache.html
index d25e01e21..7b35c4692 100644
--- a/services/harmony/app/views/workflow-ui/job/work-item-table-row.mustache.html
+++ b/services/harmony/app/views/workflow-ui/job/work-item-table-row.mustache.html
@@ -2,7 +2,7 @@
{{workflowStepIndex}} |
{{workflowItemStep}} |
{{id}} |
-
{{status}} |
+
{{workflowItemStatus}} |
{{#isAdminOrLogViewer}}
{{{workflowItemLogsButton}}} |
{{/isAdminOrLogViewer}}
diff --git a/services/harmony/public/js/workflow-ui/job/index.js b/services/harmony/public/js/workflow-ui/job/index.js
index f22616653..0b1d5c93f 100644
--- a/services/harmony/public/js/workflow-ui/job/index.js
+++ b/services/harmony/public/js/workflow-ui/job/index.js
@@ -14,6 +14,7 @@ async function init() {
});
params.tableFilter = document.getElementsByName('tableFilter')[0].getAttribute('data-value');
params.disallowStatus = document.getElementsByName('disallowStatus')[0].checked ? 'on' : '';
+ params.disallowMessageCategory = document.getElementsByName('disallowMessageCategory')[0].checked ? 'on' : '';
params.dateKind = document.getElementById('dateKindUpdated').checked ? 'updatedAt' : 'createdAt';
// kick off job state change links logic if this user is allowed to change the job state
diff --git a/services/harmony/public/js/workflow-ui/job/work-items-table.js b/services/harmony/public/js/workflow-ui/job/work-items-table.js
index a44d7f8b6..269d642fe 100644
--- a/services/harmony/public/js/workflow-ui/job/work-items-table.js
+++ b/services/harmony/public/js/workflow-ui/job/work-items-table.js
@@ -1,4 +1,4 @@
-import { formatDates, initCopyHandler } from '../table.js';
+import { formatDates, initCopyHandler, trimForDisplay } from '../table.js';
import toasts from '../toasts.js';
import PubSub from '../../pub-sub.js';
@@ -36,7 +36,7 @@ import PubSub from '../../pub-sub.js';
*/
async function load(params, checkJobStatus) {
let tableUrl = `./${params.jobID}/work-items?page=${params.currentPage}&limit=${params.limit}&checkJobStatus=${checkJobStatus}`;
- tableUrl += `&tableFilter=${encodeURIComponent(params.tableFilter)}&disallowStatus=${params.disallowStatus}`;
+ tableUrl += `&tableFilter=${encodeURIComponent(params.tableFilter)}&disallowStatus=${params.disallowStatus}&disallowMessageCategory=${params.disallowMessageCategory}`;
tableUrl += `&fromDateTime=${encodeURIComponent(params.fromDateTime)}&toDateTime=${encodeURIComponent(params.toDateTime)}`;
tableUrl += `&tzOffsetMinutes=${params.tzOffsetMinutes}&dateKind=${params.dateKind}`;
const res = await fetch(tableUrl);
@@ -97,13 +97,17 @@ function initFilter(tableFilter) {
{ value: 'status: running', dbValue: 'running', field: 'status' },
{ value: 'status: failed', dbValue: 'failed', field: 'status' },
{ value: 'status: queued', dbValue: 'queued', field: 'status' },
+ { value: 'status: warning', dbValue: 'warning', field: 'status' },
];
const allowedValues = allowedList.map((t) => t.value);
+ allowedList.push({ value: 'message category: nodata', dbValue: 'nodata', field: 'message_category' });
// eslint-disable-next-line no-new
const tagInput = new Tagify(filterInput, {
whitelist: allowedList,
+ delimiters: null,
validate(tag) {
- if (allowedValues.includes(tag.value)) {
+ if (allowedValues.includes(tag.value)
+ || /^message category: .{1,100}$/.test(tag.value)) {
return true;
}
return false;
@@ -115,6 +119,21 @@ function initFilter(tableFilter) {
enabled: 0,
closeOnSelect: true,
},
+ templates: {
+ tag(tagData) {
+ return `
+
+
+ ${trimForDisplay(tagData.value.split(': ')[1], 20)}
+
+ `;
+ },
+ },
});
const initialTags = JSON.parse(tableFilter);
tagInput.addTags(initialTags);
diff --git a/services/harmony/test/workflow-ui/work-items-table-row.ts b/services/harmony/test/workflow-ui/work-items-table-row.ts
index f461d296c..87e4b5412 100644
--- a/services/harmony/test/workflow-ui/work-items-table-row.ts
+++ b/services/harmony/test/workflow-ui/work-items-table-row.ts
@@ -37,7 +37,7 @@ const step2 = buildWorkflowStep(
// build the items
// give them an id so we know what id to request in the tests
const item1 = buildWorkItem(
- { jobID: targetJob.jobID, workflowStepIndex: 1, serviceID: step1ServiceId, status: WorkItemStatus.RUNNING, id: 1 },
+ { jobID: targetJob.jobID, workflowStepIndex: 1, serviceID: step1ServiceId, status: WorkItemStatus.RUNNING, id: 1, message_category: 'smoothly' },
);
const item2 = buildWorkItem(
{ jobID: targetJob.jobID, workflowStepIndex: 1, serviceID: step1ServiceId, status: WorkItemStatus.SUCCESSFUL, id: 2 },
@@ -190,6 +190,10 @@ describe('Workflow UI work items table row route', function () {
const listing = this.res.text;
expect((listing.match(/retry-button/g) || []).length).to.equal(1);
});
+ it('returns the message_category along with the main status', async function () {
+ const listing = this.res.text;
+ expect(listing).to.contain(`
${WorkItemStatus.RUNNING.valueOf()}: smoothly`);
+ });
});
describe('who requests a queued work item for their job', function () {
@@ -288,7 +292,7 @@ describe('Workflow UI work items table row route', function () {
expect(listing).to.not.contain(`
${WorkItemStatus.SUCCESSFUL.valueOf()}`);
expect(listing).to.not.contain(`
${WorkItemStatus.CANCELED.valueOf()}`);
expect(listing).to.not.contain(`
${WorkItemStatus.READY.valueOf()}`);
- expect(listing).to.contain(`
${WorkItemStatus.RUNNING.valueOf()}`);
+ expect(listing).to.contain(`
${WorkItemStatus.RUNNING.valueOf()}: smoothly`);
});
});
});
diff --git a/services/harmony/test/workflow-ui/work-items-table.ts b/services/harmony/test/workflow-ui/work-items-table.ts
index bbfe11e3d..ee325a550 100644
--- a/services/harmony/test/workflow-ui/work-items-table.ts
+++ b/services/harmony/test/workflow-ui/work-items-table.ts
@@ -124,13 +124,15 @@ describe('Workflow UI work items table route', function () {
await step2.save(this.trx);
await otherJob.save(this.trx);
- const otherItem1 = buildWorkItem({ jobID: otherJob.jobID, status: WorkItemStatus.CANCELED });
+ const otherItem1 = buildWorkItem({ jobID: otherJob.jobID, status: WorkItemStatus.CANCELED, message_category: 'jeepers' });
await otherItem1.save(this.trx);
const otherItem2 = buildWorkItem({ jobID: otherJob.jobID, status: WorkItemStatus.FAILED });
await otherItem2.save(this.trx);
await otherItem3.save(this.trx);
const otherItem4 = buildWorkItem({ jobID: otherJob.jobID, status: WorkItemStatus.READY });
await otherItem4.save(this.trx);
+ const otherItem5 = buildWorkItem({ jobID: otherJob.jobID, status: WorkItemStatus.WARNING, message_category: 'no-data' });
+ await otherItem5.save(this.trx);
const otherStep1 = buildWorkflowStep({ jobID: otherJob.jobID, stepIndex: 1 });
await otherStep1.save(this.trx);
const otherStep2 = buildWorkflowStep({ jobID: otherJob.jobID, stepIndex: 2 });
@@ -572,7 +574,7 @@ describe('Workflow UI work items table route', function () {
hookWorkflowUIWorkItems({ username: 'adam', jobID: otherJob.jobID });
it('returns metrics logs links for each each work item', function () {
const listing = this.res.text;
- expect((listing.match(/logs-metrics/g) || []).length).to.equal(4);
+ expect((listing.match(/logs-metrics/g) || []).length).to.equal(5);
});
});
@@ -585,6 +587,34 @@ describe('Workflow UI work items table route', function () {
});
});
+ describe('when the admin filters otherJob\'s items by status IN [WARNING]', function () {
+ hookWorkflowUIWorkItems({ username: 'adam', jobID: otherJob.jobID,
+ query: { tableFilter: '[{"value":"status: warning","dbValue":"warning","field":"status"}]' } });
+ it('contains the WARNING work item', async function () {
+ expect((this.res.text.match(/work-item-table-row/g) || []).length).to.equal(1);
+ expect(this.res.text).to.contain(`
${WorkItemStatus.WARNING.valueOf()}: no-data`);
+ });
+ });
+
+ describe('when the admin filters otherJob\'s items by message_category IN [no-data]', function () {
+ hookWorkflowUIWorkItems({ username: 'adam', jobID: otherJob.jobID,
+ query: { tableFilter: '[{"value":"message category: no-data","dbValue":"no-data","field":"message_category"}]' } });
+ it('contains the no-data work item', async function () {
+ expect((this.res.text.match(/work-item-table-row/g) || []).length).to.equal(1);
+ expect(this.res.text).to.contain(`
${WorkItemStatus.WARNING.valueOf()}: no-data`);
+ });
+ });
+
+ describe('when the admin filters otherJob\'s items by message_category NOT IN [no-data]', function () {
+ hookWorkflowUIWorkItems({ username: 'adam', jobID: otherJob.jobID,
+ query: { disallowMessageCategory: 'on', tableFilter: '[{"value":"message category: no-data","dbValue":"no-data","field":"message_category"}]' } });
+ it('contains all but the no-data work item', async function () {
+ expect((this.res.text.match(/work-item-table-row/g) || []).length).to.equal(4);
+ expect(this.res.text).to.not.contain(`
${WorkItemStatus.WARNING.valueOf()}: no-data`);
+ expect(this.res.text).to.contain(`
${WorkItemStatus.CANCELED.valueOf()}: jeepers`);
+ });
+ });
+
describe('when the user is not part of the admin group', function () {
hookAdminWorkflowUIWorkItems({ username: 'eve', jobID: targetJob.jobID });
it('returns an error', function () {