diff --git a/services/harmony/app/frontends/labels.ts b/services/harmony/app/frontends/labels.ts index 21e81eaf2..aa23a8fc5 100644 --- a/services/harmony/app/frontends/labels.ts +++ b/services/harmony/app/frontends/labels.ts @@ -14,7 +14,7 @@ import { keysToLowerCase } from '../util/object'; * @param req - The request sent by the client * @param res - The response to send to the client * @param next - The next function in the call chain - * @returns Resolves when the request is complete + * @returns Resolves when the request is complete, returning labels on success */ export async function addJobLabels( req: HarmonyRequest, res: Response, next: NextFunction, @@ -28,7 +28,7 @@ export async function addJobLabels( }); res.status(201); - res.send('OK'); + res.send({ labels: req.body.label }); } catch (e) { req.context.logger.error(e); next(e); diff --git a/services/harmony/app/frontends/workflow-ui.ts b/services/harmony/app/frontends/workflow-ui.ts index 0204c7d0e..01880fa10 100644 --- a/services/harmony/app/frontends/workflow-ui.ts +++ b/services/harmony/app/frontends/workflow-ui.ts @@ -102,21 +102,27 @@ function parseQuery( /* eslint-disable @typescript-eslint/no-explicit-any */ tableQuery.allowUsers = !(requestQuery.disallowuser === 'on'); tableQuery.allowProviders = !(requestQuery.disallowprovider === 'on'); const selectedOptions: { field: string, dbValue: string, value: string }[] = JSON.parse(requestQuery.tablefilter); + const validStatusSelections = selectedOptions .filter(option => option.field === 'status' && Object.values(statusEnum).includes(option.dbValue)); const statusValues = validStatusSelections.map(option => option.dbValue); + const validServiceSelections = selectedOptions .filter(option => option.field === 'service' && serviceNames.includes(option.dbValue)); const serviceValues = validServiceSelections.map(option => option.dbValue); + const validUserSelections = selectedOptions .filter(option => isAdminAccess && /^user: [A-Za-z0-9\.\_]{4,30}$/.test(option.value)); const userValues = validUserSelections.map(option => option.value.split('user: ')[1]); + const validLabelSelections = selectedOptions .filter(option => /^label: .{1,100}$/.test(option.value)); - const labelValues = validLabelSelections.map(option => option.value.split('label: ')[1].toLowerCase()); + const labelValues = validLabelSelections.map(option => option.dbValue || option.value.split('label: ')[1].toLowerCase()); + const validProviderSelections = selectedOptions .filter(option => /^provider: [A-Za-z0-9_]{1,100}$/.test(option.value)); const providerValues = validProviderSelections.map(option => option.value.split('provider: ')[1].toLowerCase()); + if ((statusValues.length + serviceValues.length + userValues.length + providerValues.length) > maxFilters) { throw new RequestValidationError(`Maximum amount of filters (${maxFilters}) was exceeded.`); } @@ -209,6 +215,9 @@ function jobRenderingFunctions(logger: Logger, requestQuery: Record return this.request; } }, + jobLabels(): string { + return JSON.stringify(this.labels || []); + }, jobLabelsDisplay(): string { return this.labels.map((label) => { const labelText = truncateString(label, 30); diff --git a/services/harmony/app/middleware/label.ts b/services/harmony/app/middleware/label.ts index 77d516f4a..a786a18b4 100644 --- a/services/harmony/app/middleware/label.ts +++ b/services/harmony/app/middleware/label.ts @@ -23,13 +23,6 @@ export default async function handleLabelParameter( // If 'label' exists, convert it to an array (if not already) and assign it to 'label' in the body if (label) { const labels = parseMultiValueParameter(label); - for (const lbl of labels) { - if (lbl.indexOf(',') > -1) { - res.status(400); - res.send('Labels cannot contain commas'); - return; - } - } const normalizedLabels = labels.map(normalizeLabel); for (const lbl of normalizedLabels) { if (lbl === '') { diff --git a/services/harmony/app/models/job.ts b/services/harmony/app/models/job.ts index b864f32b8..ff892ea71 100644 --- a/services/harmony/app/models/job.ts +++ b/services/harmony/app/models/job.ts @@ -499,11 +499,11 @@ export class Job extends DBRecord implements JobRecord { includeLabels = false, ): Promise<{ data: Job[]; pagination: ILengthAwarePagination }> { let query; - + const labelDelimiter = '*&%$#'; // can't use comma because some labels contain commas if (includeLabels) { if (constraints.labels) { // Requesting to limit the jobs based on the provided labels query = tx(Job.table) - .select(`${Job.table}.*`, tx.raw(`STRING_AGG(${LABELS_TABLE}.value, ',' order by value) AS label_values`)) + .select(`${Job.table}.*`, tx.raw(`STRING_AGG(${LABELS_TABLE}.value, '${labelDelimiter}' order by value) AS label_values`)) .leftOuterJoin(`${JOBS_LABELS_TABLE}`, `${Job.table}.jobID`, '=', `${JOBS_LABELS_TABLE}.job_id`) .leftOuterJoin(`${LABELS_TABLE}`, `${JOBS_LABELS_TABLE}.label_id`, '=', `${LABELS_TABLE}.id`) // Subquery that filters to get the list of jobIDs that match any of the provided labels @@ -524,7 +524,7 @@ export class Job extends DBRecord implements JobRecord { .modify((queryBuilder) => modifyQuery(queryBuilder, constraints)); } else { query = tx(Job.table) - .select(`${Job.table}.*`, tx.raw(`STRING_AGG(${LABELS_TABLE}.value, ',' order by value) AS label_values`)) + .select(`${Job.table}.*`, tx.raw(`STRING_AGG(${LABELS_TABLE}.value, '${labelDelimiter}' order by value) AS label_values`)) .leftOuterJoin(`${JOBS_LABELS_TABLE}`, `${Job.table}.jobID`, '=', `${JOBS_LABELS_TABLE}.job_id`) .leftOuterJoin(`${LABELS_TABLE}`, `${JOBS_LABELS_TABLE}.label_id`, '=', `${LABELS_TABLE}.id`) .where(setTableNameForWhereClauses(Job.table, constraints.where)) @@ -550,7 +550,7 @@ export class Job extends DBRecord implements JobRecord { const jobs: Job[] = items.data.map((j: JobWithLabels) => { const job = new Job(j); if (includeLabels && j.label_values) { - job.labels = j.label_values.split(','); + job.labels = j.label_values.split(labelDelimiter); } else { job.labels = []; } diff --git a/services/harmony/app/models/label.ts b/services/harmony/app/models/label.ts index 7b7337d99..71741a72e 100644 --- a/services/harmony/app/models/label.ts +++ b/services/harmony/app/models/label.ts @@ -31,10 +31,10 @@ export function checkLabel(label: string): string { * Trim the whitespace from the beginning/end of a label and convert it to lowercase * * @param label - the label to normalize - * @returns - label converted to lowercase with leading/trailing whitespace trimmed and commas removed + * @returns - label converted to lowercase with leading/trailing whitespace trimmed */ export function normalizeLabel(label: string): string { - return label.trim().toLowerCase().replaceAll(',', ''); + return label.trim().toLowerCase(); } /** diff --git a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html index c0dae40bb..6e0341781 100644 --- a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html @@ -41,15 +41,16 @@ {{^isAdminRoute}} {{#jobs.length}} - {{#labels.length}}