Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harmony 1998 - Add message_category to workflow ui #692

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 21 additions & 1 deletion services/harmony/app/frontends/workflow-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ interface TableQuery {
userValues: string[],
providerValues: string[],
labelValues: string[],
messageCategoryValues: string[],
allowMessageCategoryValues: boolean,
from: Date,
to: Date,
dateKind: 'createdAt' | 'updatedAt',
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -123,19 +128,25 @@ 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.`);
}
originalValues = JSON.stringify(validStatusSelections
.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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(); },
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions services/harmony/app/models/work-item-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface WorkItemQuery {
};
whereIn?: {
status?: { in: boolean, values: string[] };
message_category?: { in: boolean, values: string[] };
};
dates?: {
from?: Date;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
negate statuses
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="disallowMessageCategory" {{disallowMessageCategoryChecked}}>
<label class="form-check-label" for="disallowMessageCategory">
negate message categories
</label>
</div>
<div class="input-group mt-2">
<span class="input-group-text">page size</span>
<input name="limit" type="number" class="form-control" value="{{limit}}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<th scope="row">{{workflowStepIndex}}</th>
<td>{{workflowItemStep}}</td>
<td>{{id}}</td>
<td><span class="badge rounded-pill bg-{{workflowItemBadge}}">{{status}}</span></td>
<td><span class="badge rounded-pill bg-{{workflowItemBadge}}">{{workflowItemStatus}}</span></td>
{{#isAdminOrLogViewer}}
<td>{{{workflowItemLogsButton}}}</td>
{{/isAdminOrLogViewer}}
Expand Down
1 change: 1 addition & 0 deletions services/harmony/public/js/workflow-ui/job/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 22 additions & 3 deletions services/harmony/public/js/workflow-ui/job/work-items-table.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -115,6 +119,21 @@ function initFilter(tableFilter) {
enabled: 0,
closeOnSelect: true,
},
templates: {
tag(tagData) {
return `<tag title="${tagData.dbValue}"
contenteditable='false'
spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag}"
${this.getAttributes(tagData)}>
<x title='' class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
<div>
<span class="${this.settings.classNames.tagText}">${trimForDisplay(tagData.value.split(': ')[1], 20)}</span>
</div>
</tag>`;
},
},
});
const initialTags = JSON.parse(tableFilter);
tagInput.addTags(initialTags);
Expand Down
8 changes: 6 additions & 2 deletions services/harmony/test/workflow-ui/work-items-table-row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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(`<span class="badge rounded-pill bg-info">${WorkItemStatus.RUNNING.valueOf()}: smoothly</span>`);
});
});

describe('who requests a queued work item for their job', function () {
Expand Down Expand Up @@ -288,7 +292,7 @@ describe('Workflow UI work items table row route', function () {
expect(listing).to.not.contain(`<span class="badge rounded-pill bg-success">${WorkItemStatus.SUCCESSFUL.valueOf()}</span>`);
expect(listing).to.not.contain(`<span class="badge rounded-pill bg-secondary">${WorkItemStatus.CANCELED.valueOf()}</span>`);
expect(listing).to.not.contain(`<span class="badge rounded-pill bg-primary">${WorkItemStatus.READY.valueOf()}</span>`);
expect(listing).to.contain(`<span class="badge rounded-pill bg-info">${WorkItemStatus.RUNNING.valueOf()}</span>`);
expect(listing).to.contain(`<span class="badge rounded-pill bg-info">${WorkItemStatus.RUNNING.valueOf()}: smoothly</span>`);
});
});
});
Expand Down
22 changes: 21 additions & 1 deletion services/harmony/test/workflow-ui/work-items-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ describe('Workflow UI work items table route', function () {
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 });
Expand Down Expand Up @@ -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);
});
});

Expand All @@ -585,6 +587,24 @@ 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(`<span class="badge rounded-pill bg-warning">${WorkItemStatus.WARNING.valueOf()}: no-data</span>`);
});
});

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(`<span class="badge rounded-pill bg-warning">${WorkItemStatus.WARNING.valueOf()}: no-data</span>`);
});
});

describe('when the user is not part of the admin group', function () {
hookAdminWorkflowUIWorkItems({ username: 'eve', jobID: targetJob.jobID });
it('returns an error', function () {
Expand Down