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 1978 - Add/create label from jobs page of workflow ui #681

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
873ca92
HARMONY-1978: Add ability to create/add new label to selected jobs
vinnyinverso Jan 16, 2025
9d870ee
HARMONY-1978: Close dropdown and add some whitespace to create link
vinnyinverso Jan 16, 2025
adb5942
HARMONY-1978: Update fixture
vinnyinverso Jan 16, 2025
723cb79
HARMONY-1978: Add insertNewLabelAlphabetically
vinnyinverso Jan 21, 2025
da27cb0
HARMONY-1978: Add insertNew param to handleSubmitClick
vinnyinverso Jan 21, 2025
2ea5726
HARMONY-1978: Extract function for post-response handling
vinnyinverso Jan 21, 2025
015140b
HARMONY-1978: Add some tests for label creation logic
vinnyinverso Jan 21, 2025
02425a5
HARMONY-1978: Test textContent of create-label-link
vinnyinverso Jan 21, 2025
74bf305
HARMONY-1978: Test labels-li display
vinnyinverso Jan 21, 2025
aa1335f
HARMONY-1978: Test adding then removing a search value
vinnyinverso Jan 21, 2025
dc97dd9
HARMONY-1978: Move showAllLabels to dropdown hidden action
vinnyinverso Jan 22, 2025
1c60ad6
Merge branch 'main' into harmony-1978
vinnyinverso Jan 22, 2025
c9ba1ea
HARMONY-1978: Remove comma validation
vinnyinverso Jan 22, 2025
7729025
Merge branch 'harmony-1978' of https://github.com/nasa/harmony into h…
vinnyinverso Jan 22, 2025
84df899
HARMONY-1978: Remove comma validation
vinnyinverso Jan 22, 2025
0cc8509
HARMONY-1978: Remove comma validation
vinnyinverso Jan 22, 2025
c2c5b12
HARMONY-1978: WIP - Dynamically insert tag in filter input after crea…
vinnyinverso Jan 23, 2025
a6aa202
HARMONY-1978: WIP - Dynamically insert the new label option
vinnyinverso Jan 23, 2025
eafaa4a
HARMONY-1978: WIP - Dynamically insert the new label option
vinnyinverso Jan 23, 2025
cbb093a
HARMONY-1978:
vinnyinverso Jan 23, 2025
d73e10c
HARMONY-1978 - WIP: TODO - truncated label values should pull value f…
vinnyinverso Jan 23, 2025
f81f6fd
HARMONY-1978: Use labels' db value attribute if present for db query
vinnyinverso Jan 24, 2025
9d257b5
HARMONY-1978: Trim labels that are too long
vinnyinverso Jan 24, 2025
9029b31
HARMONY-1978: Use a new delimiter for job.labels
vinnyinverso Jan 24, 2025
c6c6b49
HARMONY-1978: Prevent toast getting cut off by higher z-index items
vinnyinverso Jan 24, 2025
e1c6193
HARMONY-1978: Ensure that comma does not generate a new tag
vinnyinverso Jan 24, 2025
4bb5261
HARMONY-1978: Remove console.log
vinnyinverso Jan 24, 2025
c177419
HARMONY-1978: Update test mock
vinnyinverso Jan 24, 2025
2e3c530
HARMONY-1978: Unnecessary class
vinnyinverso Jan 24, 2025
9bf20b5
HARMONY-1978: Fix commas bug
vinnyinverso Jan 24, 2025
9b8ab2c
HARMONY-1978: Updated tets to reflect new data attribute format
vinnyinverso Jan 27, 2025
5cbbe6f
HARMONY-1978: Add newlines for readability
vinnyinverso Jan 27, 2025
304350f
HARMONY-1978: Comment
vinnyinverso Jan 27, 2025
371e325
HARMONY-1978: Refactor so that labelsModule can be a const
vinnyinverso Jan 27, 2025
ef7d561
HARMONY-1978: Remove extraneous line
vinnyinverso Jan 27, 2025
1ffc694
HARMONY-1978: Satisfy TS compiler
vinnyinverso Jan 27, 2025
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
4 changes: 2 additions & 2 deletions services/harmony/app/frontends/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion services/harmony/app/frontends/workflow-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(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.`);
}
Expand Down Expand Up @@ -209,6 +215,9 @@ function jobRenderingFunctions(logger: Logger, requestQuery: Record<string, any>
return this.request;
}
},
jobLabels(): string {
return JSON.stringify(this.labels || []);
},
jobLabelsDisplay(): string {
return this.labels.map((label) => {
const labelText = truncateString(label, 30);
Expand Down
7 changes: 0 additions & 7 deletions services/harmony/app/middleware/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 === '') {
Expand Down
8 changes: 4 additions & 4 deletions services/harmony/app/models/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,11 +499,11 @@ export class Job extends DBRecord implements JobRecord {
includeLabels = false,
): Promise<{ data: Job[]; pagination: ILengthAwarePagination }> {
let query;

const labelDelimiter = '*&%$#';
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
Expand All @@ -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))
Expand All @@ -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 = [];
}
Expand Down
4 changes: 2 additions & 2 deletions services/harmony/app/models/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand Down
14 changes: 7 additions & 7 deletions services/harmony/app/views/workflow-ui/jobs/index.mustache.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@
<!-- job state change links will go here -->
{{^isAdminRoute}}
{{#jobs.length}}
{{#labels.length}}
<li id="label-nav-item" class="nav-item dropstart d-none">
<a id="label-dropdown-a" class="nav-link dropdown-toggle py-0 px-2" data-bs-toggle="dropdown" data-bs-auto-close="outside" href="#" role="button" aria-expanded="false">label</a>
<ul class="dropdown-menu mt-2">
<li class="mx-2 mb-2">
<input type="text" class="form-control" id="label-search" placeholder="label name">
<ul id="label-dropdown-menu" class="dropdown-menu mt-2">
<li class="mx-2">
<input type="text" class="form-control" id="label-search" placeholder="label name" maxlength="255">
</li>
<li id="no-match-li" class="fw-light text-center fs-6" style="display: none;"><i>no matches</i></li>
<li>
<li id="no-match-li" class="fw-light text-center fs-6 mx-2 mt-2" style="display: none;">
<a href="#" id="create-label-link">Create Label</a>
</li>
<li id="labels-li" style="display: none;">
<ul id="labels-list">
{{#labels}}
<li class="label-li"><a class="dropdown-item label-item text-truncate" name="{{.}}" data-value="{{.}}" href="#">{{.}}</a></li>
Expand All @@ -58,7 +59,6 @@
</li>
</ul>
</li>
{{/labels.length}}
{{/jobs.length}}
{{/isAdminRoute}}
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
data-truncated="{{jobRequestIsTruncated}}"></i>
<span title="{{jobRequest}}" class="text-muted job-url-text">{{jobRequestDisplay}}</span>
</div>
<div class="ml-1" id="job-labels-display-{{jobID}}" data-labels="{{labels}}">
<div class="ml-1" id="job-labels-display-{{jobID}}" data-labels="{{jobLabels}}">
{{{jobLabelsDisplay}}}
</div>
</div>
12 changes: 12 additions & 0 deletions services/harmony/public/css/workflow-ui/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,16 @@

.label-li {
max-width: 200px;
}

#label-dropdown-menu {
max-width: 40vw;
}

#create-label-link {
word-wrap: break-word;
}

.toast, .toast-container {
z-index: 1021;
}
4 changes: 2 additions & 2 deletions services/harmony/public/js/workflow-ui/jobs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ if (isAdminRoute) {
params.dateKind = document.getElementById('dateKindUpdated').checked ? 'updatedAt' : 'createdAt';
params.sortGranules = document.getElementById('sort-granules').value;

jobsTable.init(params);
const tagInputPromise = jobsTable.init(params);

const jobStatusLinks = new JobsStatusChangeLinks();
jobStatusLinks.init('job-state-links-container', 'job-selected');
Expand All @@ -35,5 +35,5 @@ toasts.init();

const labelDropdown = document.getElementById('label-dropdown-a');
if (labelDropdown) {
labels.init();
labels.init(tagInputPromise);
}
47 changes: 38 additions & 9 deletions services/harmony/public/js/workflow-ui/jobs/jobs-table.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-param-reassign */
import { formatDates, initCopyHandler } from '../table.js';
import { formatDates, initCopyHandler, trimForDisplay } from '../table.js';
import PubSub from '../../pub-sub.js';

// all of the currently selected job IDs
Expand All @@ -9,12 +9,13 @@ let statuses = [];

/**
* Build the jobs filter with filter facets like 'status' and 'user'.
* @param {string} currentUser - the current Harmony user
* @param {string[]} services - service names from services.yml
* @param {string[]} providers - array of provider ids
* @param {string[]} labels - known job labels
* @param {boolean} isAdminRoute - whether the current page is /admin/...
* @param {object[]} tableFilter - initial tags that will populate the input
* @param {string} currentUser - the current Harmony user
* @param {string[]} services - service names from services.yml
* @param {string[]} providers - array of provider ids
* @param {string[]} labels - known job labels
* @param {boolean} isAdminRoute - whether the current page is /admin/...
* @param {object[]} tableFilter - initial tags that will populate the input
* @returns the tag input instance
*/
function initFilter(currentUser, services, providers, labels, isAdminRoute, tableFilter) {
const filterInput = document.querySelector('input[name="tableFilter"]');
Expand All @@ -33,14 +34,15 @@ function initFilter(currentUser, services, providers, labels, isAdminRoute, tabl
allowedList.push(...serviceList);
const providerList = providers.map((provider) => ({ value: `provider: ${provider}`, dbValue: provider, field: 'provider' }));
allowedList.push(...providerList);
const labelList = labels.map((label) => ({ value: `label: ${label}`, dbValue: label, field: 'label' }));
const labelList = labels.map((label) => ({ value: `label: ${trimForDisplay(label, 30)}`, dbValue: label, field: 'label', searchBy: label }));
allowedList.push(...labelList);
if (isAdminRoute) {
allowedList.push({ value: `user: ${currentUser}`, dbValue: currentUser, field: 'user' });
}
const allowedValues = allowedList.map((t) => t.value);
const tagInput = new Tagify(filterInput, {
whitelist: allowedList,
delimiters: null, // prevent characters like "," from triggering input submission
validate(tag) {
if (allowedValues.includes(tag.value)
|| /^provider: [A-Za-z0-9_]{1,100}$/.test(tag.value)
Expand All @@ -60,9 +62,26 @@ function initFilter(currentUser, services, providers, labels, isAdminRoute, tabl
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);

return tagInput;
}

/**
Expand Down Expand Up @@ -231,17 +250,27 @@ const jobsTable = {
* tzOffsetMinutes - offset from UTC
* dateKind - updatedAt or createdAt
* sortGranules - sort the rows ascending ('asc') or descending ('desc')
* @returns the tag input instance
*/
async init(params) {
PubSub.subscribe(
'row-state-change',
async () => loadRows(params),
);
formatDates('.date-td');
initFilter(params.currentUser, params.services, params.providers, params.labels, params.isAdminRoute, params.tableFilter);
const tagInput = initFilter(
params.currentUser,
params.services,
params.providers,
params.labels,
params.isAdminRoute,
params.tableFilter,
);
initCopyHandler('.copy-request');
initSelectHandler('.select-job');
initSelectAllHandler();

return tagInput;
},

/**
Expand Down
Loading
Loading