diff --git a/config/hpa.yaml b/config/hpa.yaml index dd0d9e134..8f4e1e0d5 100644 --- a/config/hpa.yaml +++ b/config/hpa.yaml @@ -1,4 +1,4 @@ -apiVersion: autoscaling/v2beta2 +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: $SERVICE_NAME diff --git a/package-lock.json b/package-lock.json index 1af92cd1d..d765e9924 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3481,7 +3481,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e435cda02..636e93ffe 100644 --- a/package.json +++ b/package.json @@ -79,5 +79,8 @@ "pg": "^8.12.0", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0" + }, + "overrides": { + "cross-spawn": "7.0.5" } } diff --git a/packages/util/package-lock.json b/packages/util/package-lock.json index 330c845a1..12ebc107e 100644 --- a/packages/util/package-lock.json +++ b/packages/util/package-lock.json @@ -1955,20 +1955,6 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2608,6 +2594,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -2870,6 +2871,21 @@ "node": ">=8.0.0" } }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -3713,6 +3729,21 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-processinfo/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -5032,6 +5063,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/rimraf/node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -7850,17 +7896,6 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8111,7 +8146,7 @@ "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "7.0.5", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", @@ -8139,6 +8174,19 @@ "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + } } }, "eslint-config-airbnb-base": { @@ -8553,8 +8601,21 @@ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^3.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + } } }, "fromentries": { @@ -9126,13 +9187,24 @@ "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "7.0.5", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", "uuid": "^8.3.2" }, "dependencies": { + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -10118,13 +10190,24 @@ "glob": "^10.3.7" }, "dependencies": { + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, diff --git a/packages/util/package.json b/packages/util/package.json index 51afabe0f..f9c960029 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -77,6 +77,7 @@ "typescript": "^4.4.4" }, "overrides": { - "braces": "^3.0.3" + "braces": "^3.0.3", + "cross-spawn": "7.0.5" } } \ No newline at end of file diff --git a/services/giovanni-adapter/package-lock.json b/services/giovanni-adapter/package-lock.json index dc4602db3..b534f57a4 100644 --- a/services/giovanni-adapter/package-lock.json +++ b/services/giovanni-adapter/package-lock.json @@ -3830,10 +3830,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -10396,9 +10397,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -10556,7 +10557,7 @@ "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "7.0.5", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", @@ -10872,7 +10873,7 @@ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^3.0.2" } }, @@ -11310,7 +11311,7 @@ "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "7.0.5", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", @@ -12232,7 +12233,7 @@ "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, diff --git a/services/giovanni-adapter/package.json b/services/giovanni-adapter/package.json index 1376c0f4a..3d13129be 100644 --- a/services/giovanni-adapter/package.json +++ b/services/giovanni-adapter/package.json @@ -86,6 +86,7 @@ }, "overrides": { "braces": "^3.0.3", - "fast-xml-parser": "4.4.1" + "fast-xml-parser": "4.4.1", + "cross-spawn": "7.0.5" } } diff --git a/services/harmony/app/frontends/workflow-ui.ts b/services/harmony/app/frontends/workflow-ui.ts index d62fca80c..f8a593a92 100644 --- a/services/harmony/app/frontends/workflow-ui.ts +++ b/services/harmony/app/frontends/workflow-ui.ts @@ -20,8 +20,9 @@ import { serviceNames } from '../models/services'; import { getEdlGroupInformation, isAdminUser } from '../util/edl-api'; import { ILengthAwarePagination } from 'knex-paginate'; import { handleWorkItemUpdateWithJobId } from '../backends/workflow-orchestration/work-item-updates'; -import { getLabelsForUser } from '../models/label'; +import { getLabelsForUser, getRecentLabelsForUser } from '../models/label'; import { logAsyncExecutionTime } from '../util/log-execution'; +import _ from 'lodash'; // Default to retrieving this number of work items per page const defaultWorkItemPageSize = 100; @@ -168,10 +169,11 @@ function getPaginationDisplay(pagination: ILengthAwarePagination): { from: strin * a row of the jobs table. * @param logger - the logger to use * @param requestQuery - the query parameters from the request - * @param checked - whether the job should be selected + * @param isAdminRoute - whether the current endpoint being accessed is /admin/* + * @param jobIDs - list of IDs for selected jobs * @returns an object with rendering functions */ -function jobRenderingFunctions(logger: Logger, requestQuery: Record, jobIDs: string[] = []): object { +function jobRenderingFunctions(logger: Logger, requestQuery: Record, isAdminRoute: boolean, jobIDs: string[] = []): object { return { jobBadge(): string { return statusClass[this.status]; @@ -210,7 +212,7 @@ function jobRenderingFunctions(logger: Logger, requestQuery: Record jobLabelsDisplay(): string { return this.labels.map((label) => { const labelText = truncateString(label, 30); - return `${labelText}`; + return `${_.escape(labelText)}`; }).join(' '); }, jobMessage(): string { @@ -220,7 +222,7 @@ function jobRenderingFunctions(logger: Logger, requestQuery: Record }, jobSelectBox(): string { const checked = jobIDs.indexOf(this.jobID) > -1 ? 'checked' : ''; - if (this.hasTerminalStatus()) { + if (this.hasTerminalStatus() && isAdminRoute) { return ''; } return ``; @@ -325,7 +327,8 @@ export async function getJobs( const isAdminRoute = req.context.isAdminAccess; const providerIds = (await Job.getProviderIdsSnapshot(db, req.context.logger)) .map((providerId) => providerId.toUpperCase()); - const labels = await (await logAsyncExecutionTime(getLabelsForUser, 'HWIUWJI.getLabelsForUser', req.context.logger))(db, req.user, env.labelFilterCompletionCount, isAdminRoute); + const recentLabels = await (await logAsyncExecutionTime(getRecentLabelsForUser, 'HWIUWJI.getRecentLabelsForUser', req.context.logger))(db, req.user, env.labelFilterCompletionCount, isAdminRoute); + const labels = env.uiLabeling ? await getLabelsForUser(db, req.user) : []; const requestQuery = keysToLowerCase(req.query); const fromDateTime = requestQuery.fromdatetime; const toDateTime = requestQuery.todatetime; @@ -342,7 +345,7 @@ export async function getJobs( const previousPage = pageLinks.find((l) => l.rel === 'prev'); const currentPage = pageLinks.find((l) => l.rel === 'self'); const paginationDisplay = getPaginationDisplay(pagination); - const selectAllBox = jobs.some((j) => !j.hasTerminalStatus()) ? + const selectAllBox = jobs.length > 0 && (!isAdminRoute || jobs.some((j) => !j.hasTerminalStatus())) ? '' : ''; res.render('workflow-ui/jobs/index', { version, @@ -352,10 +355,11 @@ export async function getJobs( currentUser: req.user, isAdminRoute, jobs, + labels, selectAllBox, serviceNames: JSON.stringify(serviceNames), providerIds: JSON.stringify(providerIds), - labels: JSON.stringify(labels), + recentLabels: JSON.stringify(recentLabels), sortGranules: requestQuery.sortgranules, disallowStatusChecked: !tableQuery.allowStatuses ? 'checked' : '', disallowServiceChecked: !tableQuery.allowServices ? 'checked' : '', @@ -375,7 +379,7 @@ export async function getJobs( { ...nextPage, linkTitle: 'next' }, { ...lastPage, linkTitle: 'last' }, ], - ...jobRenderingFunctions(req.context.logger, requestQuery), + ...jobRenderingFunctions(req.context.logger, requestQuery, isAdminRoute), }); } catch (e) { req.context.logger.error(e); @@ -545,6 +549,7 @@ export async function getWorkItemsTable( const { jobID } = req.params; const { checkJobStatus } = req.query; try { + const isAdminRoute = req.context.isAdminAccess; const { isAdmin, isLogViewer } = await getEdlGroupInformation( req.user, req.context.logger, ); @@ -590,7 +595,7 @@ export async function getWorkItemsTable( linkHref() { return this.href; }, - ...jobRenderingFunctions(req.context.logger, requestQuery), + ...jobRenderingFunctions(req.context.logger, requestQuery, isAdminRoute), }); } catch (e) { req.context.logger.error(e); @@ -649,24 +654,22 @@ export async function getJobsTable( req: HarmonyRequest, res: Response, next: NextFunction, ): Promise { try { + const isAdminRoute = req.context.isAdminAccess; const { jobIDs } = req.body; - const { isAdmin } = await getEdlGroupInformation( - req.user, req.context.logger, - ); const requestQuery = keysToLowerCase(req.query); - const { tableQuery } = parseQuery(requestQuery, JobStatus, req.context.isAdminAccess); - const jobQuery = tableQueryToJobQuery(tableQuery, isAdmin, req.user); + const { tableQuery } = parseQuery(requestQuery, JobStatus, isAdminRoute); + const jobQuery = tableQueryToJobQuery(tableQuery, isAdminRoute, req.user); const { page, limit } = getPagingParams(req, env.defaultJobListPageSize, 1, true, true); const jobsRes = await Job.queryAll(db, jobQuery, page, limit, true); const jobs = jobsRes.data; const { pagination } = jobsRes; - const selectAllChecked = jobs.every((j) => j.hasTerminalStatus() || (jobIDs.indexOf(j.jobID) > -1)) ? 'checked' : ''; - const selectAllBox = jobs.some((j) => !j.hasTerminalStatus()) ? + const selectAllChecked = jobs.every((j) => (j.hasTerminalStatus() && isAdminRoute) || (jobIDs.indexOf(j.jobID) > -1)) ? 'checked' : ''; + const selectAllBox = jobs.length > 0 && (!isAdminRoute || jobs.some((j) => !j.hasTerminalStatus())) ? `` : ''; const tableContext = { jobs, selectAllBox, - ...jobRenderingFunctions(req.context.logger, requestQuery, jobIDs), + ...jobRenderingFunctions(req.context.logger, requestQuery, isAdminRoute, jobIDs), isAdminRoute: req.context.isAdminAccess, }; const tableHtml = await new Promise((resolve, reject) => req.app.render( diff --git a/services/harmony/app/markdown/apis.md b/services/harmony/app/markdown/apis.md index bde1c7170..4e2372324 100644 --- a/services/harmony/app/markdown/apis.md +++ b/services/harmony/app/markdown/apis.md @@ -66,7 +66,7 @@ As such it accepts parameters in the URL path as well as query parameters. | height | number of rows to return in the output coverage | | forceAsync | if "true", override the default API behavior and always treat the request as asynchronous | | format | the mime-type of the output format to return | -| label | the label(s) to add for the job that runs the request. Multiple labels can be specified as a comma-separated list. A label can contain any characters up to a 255 character limit, but if a label contains commas the request can only be a POST with with the label field in the body. It is best practice to avoid commas in labels. Labels will be rejected if deemed inappropriate. Labels are always converted to lower case. +| label | the label(s) to add for the job that runs the request. Multiple labels can be specified as a comma-separated list. A label can contain any characters except for commas up to a 255 character limit. Labels will be rejected if deemed inappropriate. Labels are always converted to lower case. | maxResults | limits the number of input files processed in the request | | skipPreview | if "true", override the default API behavior and never auto-pause jobs | | ignoreErrors | if "true", continue processing a request to completion even if some items fail. If "false" immediately fail the request. Defaults to true | diff --git a/services/harmony/app/middleware/label.ts b/services/harmony/app/middleware/label.ts index c881db059..77d516f4a 100644 --- a/services/harmony/app/middleware/label.ts +++ b/services/harmony/app/middleware/label.ts @@ -18,20 +18,27 @@ export default async function handleLabelParameter( // Check if 'label' exists in the query parameters (GET), form-encoded body, or JSON body const lowerCaseQuery = keysToLowerCase(req.query); const lowerCaseBody = keysToLowerCase(req.body); - let label = lowerCaseQuery.label || lowerCaseBody.label; + const label = lowerCaseQuery.label || lowerCaseBody.label; // If 'label' exists, convert it to an array (if not already) and assign it to 'label' in the body if (label) { - label = parseMultiValueParameter(label); - label = label.map(normalizeLabel); - for (const lbl of 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 === '') { res.status(400); res.send('Labels must contain at least one non-whitespace character'); return; } } - req.body.label = label; + req.body.label = normalizedLabels; } // Call next to pass control to the next middleware or route handler diff --git a/services/harmony/app/models/label.ts b/services/harmony/app/models/label.ts index a090168f6..7b7337d99 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 + * @returns - label converted to lowercase with leading/trailing whitespace trimmed and commas removed */ export function normalizeLabel(label: string): string { - return label.trim().toLowerCase(); + return label.trim().toLowerCase().replaceAll(',', ''); } /** @@ -135,6 +135,26 @@ export async function getLabelsForJob( return rows.map((row) => row.value); } +/** + * Returns the labels for a given user + * @param trx - the transaction to use for querying + * @param username - the username associated with the labels + * + * @returns A promise that resolves to an array of strings, one for each label + */ +export async function getLabelsForUser( + trx: Transaction, + username: string, +): Promise { + const query = trx(USERS_LABELS_TABLE) + .select(['value']) + .where({ username }) + .orderBy('value'); + + const rows = (await query).map((object) => object.value); + return rows; +} + /** * Set the labels for a given job/user. This is atomic - all the labels are set at once. Any * existing labels are replaced. @@ -238,7 +258,7 @@ export async function deleteLabelsFromJobs( * @returns up to `count` most recently used labels for the user, or for all users if this is * coming from an /admin route */ -export async function getLabelsForUser( +export async function getRecentLabelsForUser( trx: Transaction, username: string, count: number, diff --git a/services/harmony/app/util/env.ts b/services/harmony/app/util/env.ts index d5df1a469..421f92b99 100644 --- a/services/harmony/app/util/env.ts +++ b/services/harmony/app/util/env.ts @@ -1,4 +1,4 @@ -import { IsInt, IsNotEmpty, IsPositive, Matches, Min } from 'class-validator'; +import { IsBoolean, IsInt, IsNotEmpty, IsPositive, Matches, Min } from 'class-validator'; import { HarmonyEnv, memorySizeRegex } from '@harmony/util/env'; import _ from 'lodash'; import * as path from 'path'; @@ -120,6 +120,9 @@ class HarmonyServerEnv extends HarmonyEnv { @IsInt() @Min(1) labelFilterCompletionCount: number; + + @IsBoolean() + uiLabeling: boolean; } const localPath = path.resolve(__dirname, '../../env-defaults'); 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 83e261f71..852be7034 100644 --- a/services/harmony/app/views/workflow-ui/job/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/job/index.mustache.html @@ -14,9 +14,7 @@ - + @@ -29,10 +27,6 @@
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 174a16467..c0dae40bb 100644 --- a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html @@ -14,9 +14,7 @@ - + @@ -29,10 +27,6 @@ @@ -56,7 +76,7 @@ {{> workflow-ui/date-time-picker}}
diff --git a/services/harmony/app/views/workflow-ui/request-preview.mustache.html b/services/harmony/app/views/workflow-ui/request-preview.mustache.html index c30f4b2c3..0b4cbbbfc 100644 --- a/services/harmony/app/views/workflow-ui/request-preview.mustache.html +++ b/services/harmony/app/views/workflow-ui/request-preview.mustache.html @@ -4,7 +4,7 @@ data-truncated="{{jobRequestIsTruncated}}"> {{jobRequestDisplay}}
-
+
{{{jobLabelsDisplay}}}
\ No newline at end of file diff --git a/services/harmony/env-defaults b/services/harmony/env-defaults index 71bc60d72..f156730ad 100644 --- a/services/harmony/env-defaults +++ b/services/harmony/env-defaults @@ -259,6 +259,13 @@ DEFAULT_POD_GRACE_PERIOD_SECS=14400 # script that creates the env var config map for local development LOCALSTACK_K8S_HOST=localstack +########################################################################### +# Feature Toggles # +########################################################################### + +# Toggle whether the labeling dropdown should be enabled in the workflow ui +UI_LABELING=true + ########################################################################### # Prometheus Config # # # diff --git a/services/harmony/package-lock.json b/services/harmony/package-lock.json index 2a293db77..a5a083e66 100644 --- a/services/harmony/package-lock.json +++ b/services/harmony/package-lock.json @@ -114,6 +114,7 @@ "@types/chai": "^4.2.18", "@types/chai-as-promised": "^7.1.4", "@types/geojson": "^7946.0.10", + "@types/jsdom": "^21.1.7", "@types/json2csv": "^5.0.3", "@types/leaflet": "^1.5.15", "@types/lodash": "^4.14.176", @@ -144,6 +145,7 @@ "fast-check": "^2.19.0", "get-pkg-repo": "^5.0.0", "javascript-typescript-langserver": "^2.11.3", + "jsdom": "^25.0.1", "jsonschema": "^1.4.1", "just-permutations": "2.1.1", "lerna": "^8.1.8", @@ -3114,6 +3116,21 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@npmcli/arborist/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/arborist/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -3392,6 +3409,21 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@npmcli/map-workspaces/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/map-workspaces/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -3533,6 +3565,21 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@npmcli/metavuln-calculator/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/metavuln-calculator/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -3735,6 +3782,21 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@npmcli/package-json/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/package-json/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -3972,6 +4034,44 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@npmcli/run-script/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/run-script/node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/run-script/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/run-script/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4918,6 +5018,21 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@sigstore/sign/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sigstore/sign/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -6237,6 +6352,18 @@ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz", "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==" }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -8712,19 +8839,6 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", @@ -8747,6 +8861,19 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csv-parse": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.2.tgz", @@ -8774,6 +8901,57 @@ "node": ">=0.10" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -8857,6 +9035,13 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -9934,6 +10119,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -10108,6 +10308,21 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -10686,6 +10901,21 @@ "node": ">=8.0.0" } }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -11162,6 +11392,41 @@ "inBundle": true, "license": "ISC" }, + "node_modules/gdal-async/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/gdal-async/node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/gdal-async/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/gdal-async/node_modules/debug": { "version": "4.3.4", "inBundle": true, @@ -12551,6 +12816,19 @@ "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -13262,6 +13540,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -13425,7 +13710,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true }, "node_modules/isobject": { "version": "3.0.1", @@ -13503,6 +13789,21 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-processinfo/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/istanbul-lib-processinfo/node_modules/p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -13863,6 +14164,138 @@ "node": ">=12.0.0" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsep": { "version": "1.3.9", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", @@ -16403,6 +16836,21 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/npm-registry-fetch/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/npm-registry-fetch/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -16599,6 +17047,13 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nx": { "version": "19.6.5", "resolved": "https://registry.npmjs.org/nx/-/nx-19.6.5.tgz", @@ -17690,6 +18145,21 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/pacote/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/pacote/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -17905,6 +18375,32 @@ "parse-path": "^7.0.0" } }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parsedbf": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-1.1.1.tgz", @@ -18522,9 +19018,10 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -19255,6 +19752,13 @@ "integrity": "sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==", "dev": true }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -19375,6 +19879,19 @@ "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "dev": true }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/schemes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/schemes/-/schemes-1.4.0.tgz", @@ -20504,6 +21021,13 @@ "node": ">=0.10.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -20729,6 +21253,26 @@ "node": ">=8" } }, + "node_modules/tldts": { + "version": "6.1.54", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.54.tgz", + "integrity": "sha512-rDaL1t59gb/Lg0HPMUGdV1vAKLQcXwU74D26aMaYV4QW7mnMvShd1Vmkg3HYAPWx2JCTUmsrXt/Yl9eJ5UFBQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.54" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.54", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.54.tgz", + "integrity": "sha512-5cc42+0G0EjYRDfIJHKraaT3I5kPm7j6or3Zh1T9sF+Ftj1T+isT4thicUyQQ1bwN7/xjHQIuY2fXCoXP8Haqg==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -21074,6 +21618,21 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/tuf-js/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/tuf-js/node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -21779,6 +22338,19 @@ "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==", "dev": true }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/walk-up-path": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", @@ -21849,6 +22421,42 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -21862,6 +22470,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -22117,9 +22726,10 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -22142,6 +22752,16 @@ "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", "dev": true }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -22198,6 +22818,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xorshift": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/xorshift/-/xorshift-1.2.0.tgz", @@ -24760,13 +25387,24 @@ "unique-filename": "^3.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -24948,13 +25586,24 @@ "balanced-match": "^1.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -25046,13 +25695,24 @@ "unique-filename": "^3.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -25181,13 +25841,24 @@ "balanced-match": "^1.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -25342,13 +26013,41 @@ "unique-filename": "^3.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -25998,13 +26697,24 @@ "unique-filename": "^3.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -27056,6 +27766,17 @@ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz", "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==" }, + "@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -28929,16 +29650,6 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, "crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", @@ -28951,6 +29662,15 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "requires": { + "rrweb-cssom": "^0.7.1" + } + }, "csv-parse": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.2.tgz", @@ -28971,6 +29691,43 @@ "assert-plus": "^1.0.0" } }, + "data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "requires": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "dependencies": { + "tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "requires": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -29023,6 +29780,12 @@ } } }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -29577,7 +30340,7 @@ "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "7.0.5", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", @@ -29619,6 +30382,17 @@ "uri-js": "^4.2.2" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -29951,7 +30725,7 @@ "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", "dev": true, "requires": { - "cross-spawn": "^7.0.3", + "cross-spawn": "7.0.5", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", @@ -29960,6 +30734,19 @@ "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + } } }, "expand-template": { @@ -30410,8 +31197,21 @@ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^3.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + } } }, "forever-agent": { @@ -30748,6 +31548,31 @@ "version": "1.1.0", "bundled": true }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "debug": { "version": "4.3.4", "bundled": true, @@ -30772,7 +31597,7 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" }, "dependencies": { @@ -31713,6 +32538,15 @@ "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==" }, + "html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^3.1.1" + } + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -32214,6 +33048,12 @@ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -32326,7 +33166,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true }, "isobject": { "version": "3.0.1", @@ -32379,13 +33220,24 @@ "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "7.0.5", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", "uuid": "^8.3.2" }, "dependencies": { + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -32663,6 +33515,100 @@ "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, + "jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "requires": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "requires": { + "tldts": "^6.1.32" + } + }, + "tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "requires": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, "jsep": { "version": "1.3.9", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", @@ -34591,13 +35537,24 @@ "unique-filename": "^3.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -34727,6 +35684,12 @@ "set-blocking": "^2.0.0" } }, + "nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true + }, "nx": { "version": "19.6.5", "resolved": "https://registry.npmjs.org/nx/-/nx-19.6.5.tgz", @@ -35569,13 +36532,24 @@ "unique-filename": "^3.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -35723,6 +36697,23 @@ "parse-path": "^7.0.0" } }, + "parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "requires": { + "entities": "^4.5.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + } + } + }, "parsedbf": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-1.1.1.tgz", @@ -36191,9 +37182,9 @@ } }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "pure-rand": { "version": "5.0.5", @@ -36717,6 +37708,12 @@ "integrity": "sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==", "dev": true }, + "rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -36792,6 +37789,15 @@ "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "dev": true }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, "schemes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/schemes/-/schemes-1.4.0.tgz", @@ -37651,6 +38657,12 @@ "integrity": "sha512-Kb3PrPYz4HanVF1LVGuAdW6LoVgIwjUYJGzFe7NDrBLCN4lsV/5J0MFurV+ygS4bRVwrCEt2c7MQ1R2a72oJDw==", "dev": true }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -37840,6 +38852,21 @@ "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==" }, + "tldts": { + "version": "6.1.54", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.54.tgz", + "integrity": "sha512-rDaL1t59gb/Lg0HPMUGdV1vAKLQcXwU74D26aMaYV4QW7mnMvShd1Vmkg3HYAPWx2JCTUmsrXt/Yl9eJ5UFBQw==", + "dev": true, + "requires": { + "tldts-core": "^6.1.54" + } + }, + "tldts-core": { + "version": "6.1.54", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.54.tgz", + "integrity": "sha512-5cc42+0G0EjYRDfIJHKraaT3I5kPm7j6or3Zh1T9sF+Ftj1T+isT4thicUyQQ1bwN7/xjHQIuY2fXCoXP8Haqg==", + "dev": true + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -38088,13 +39115,24 @@ "unique-filename": "^3.0.0" } }, + "cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, @@ -38624,6 +39662,15 @@ "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==", "dev": true }, + "w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "requires": { + "xml-name-validator": "^5.0.0" + } + }, "walk-up-path": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", @@ -38688,6 +39735,32 @@ } } }, + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -38701,6 +39774,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "requires": { "isexe": "^2.0.0" } @@ -38898,9 +39972,9 @@ } }, "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "requires": {} }, "xml": { @@ -38909,6 +39983,12 @@ "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", "dev": true }, + "xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true + }, "xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -38955,6 +40035,12 @@ } } }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "xorshift": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/xorshift/-/xorshift-1.2.0.tgz", diff --git a/services/harmony/package.json b/services/harmony/package.json index 464544556..310601875 100644 --- a/services/harmony/package.json +++ b/services/harmony/package.json @@ -170,6 +170,7 @@ "@types/superagent": "^4.1.11", "@types/supertest": "^2.0.10", "@types/wellknown": "^0.5.8", + "@types/jsdom":"^21.1.7", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "axios-mock-adapter": "^1.18.2", @@ -204,7 +205,8 @@ "strict-npm-engines": "^0.0.1", "superagent": "^8.0.6", "supertest": "^6.1.3", - "ts-node-dev": "^2.0.0" + "ts-node-dev": "^2.0.0", + "jsdom": "^25.0.1" }, "overrides": { "trim-newlines": "4.0.1", @@ -212,6 +214,7 @@ "semver": "^7.6.2", "braces": "^3.0.3", "fast-xml-parser": "4.4.1", - "jsonpath-plus": "^10.0.7" + "jsonpath-plus": "^10.0.7", + "cross-spawn": "7.0.5" } } diff --git a/services/harmony/public/css/workflow-ui/default.css b/services/harmony/public/css/workflow-ui/default.css index bb087380f..62fc1508b 100644 --- a/services/harmony/public/css/workflow-ui/default.css +++ b/services/harmony/public/css/workflow-ui/default.css @@ -54,4 +54,24 @@ .job-alert { margin-bottom: 0; +} + +#label-nav-item > * { + z-index: 1021; +} + +.dropdown-divider { + border-top: 1px solid #6b7c8d; +} + +#labels-list { + overflow: hidden; + overflow-y: auto; + max-height: calc(100vh - 300px); + list-style: none; + padding: 5px 0; +} + +.label-li { + max-width: 200px; } \ No newline at end of file diff --git a/services/harmony/public/js/workflow-ui/jobs/index.js b/services/harmony/public/js/workflow-ui/jobs/index.js index a529da211..f33990500 100644 --- a/services/harmony/public/js/workflow-ui/jobs/index.js +++ b/services/harmony/public/js/workflow-ui/jobs/index.js @@ -1,6 +1,7 @@ import jobsTable from './jobs-table.js'; import JobsStatusChangeLinks from './jobs-status-change-links.js'; import toasts from '../toasts.js'; +import labels from '../labels.js'; const params = {}; @@ -31,3 +32,8 @@ const jobStatusLinks = new JobsStatusChangeLinks(); jobStatusLinks.init('job-state-links-container', 'job-selected'); toasts.init(); + +const labelDropdown = document.getElementById('label-dropdown-a'); +if (labelDropdown) { + labels.init(); +} diff --git a/services/harmony/public/js/workflow-ui/jobs/jobs-status-change-links.js b/services/harmony/public/js/workflow-ui/jobs/jobs-status-change-links.js index e7a6ac87c..541a84bab 100644 --- a/services/harmony/public/js/workflow-ui/jobs/jobs-status-change-links.js +++ b/services/harmony/public/js/workflow-ui/jobs/jobs-status-change-links.js @@ -44,9 +44,10 @@ class JobsStatusChangeLinks extends StatusChangeLinks { event.preventDefault(); const link = event.target; const jobIDs = jobsTable.getJobIds(); - const postfix = jobIDs.length > 1 ? 's' : ''; + const actionableJobIDs = this.getActionableJobIDs(jobIDs, link); + const postfix = actionableJobIDs.length > 1 ? 's' : ''; // eslint-disable-next-line no-alert, no-restricted-globals - if (!confirm(`Are you sure you want to ${(link.textContent || link.innerText).trim()} ${jobIDs.length} job${postfix}?`)) { + if (!confirm(`Are you sure you want to ${(link.textContent || link.innerText).trim()} ${actionableJobIDs.length} job${postfix}?`)) { return; } toasts.showUpper('Changing job state...'); @@ -57,10 +58,10 @@ class JobsStatusChangeLinks extends StatusChangeLinks { Accept: 'application/json', 'Content-Type': 'application/json', }, - body: JSON.stringify({ jobIDs }), + body: JSON.stringify({ jobIDs: actionableJobIDs }), }); const data = await res.json(); - const isAre = jobIDs.length > 1 ? 'are' : 'is'; + const isAre = actionableJobIDs.length > 1 ? 'are' : 'is'; if (res.status === 200) { toasts.showUpper(`The selected job${postfix} ${isAre} now ${data.status}.`); } else if (data.description) { @@ -73,6 +74,25 @@ class JobsStatusChangeLinks extends StatusChangeLinks { ); } + /** + * Filter the job IDs to only those jobs that can be operated on by the action + * represented by the link's href attribute. + * @param {string[]} jobIDs - the job IDs to filter + * @param {EventTarget} link - the link whose href will be used as the filter + * @returns filtered list of job IDs + */ + getActionableJobIDs(jobIDs, link) { + const actionableJobIDs = []; + for (const jobID of jobIDs) { + const links = this.fetchLinksForStatuses([jobsTable.getJobStatus(jobID)]); + const jobHasTargetLink = links.some((linkForStatus) => link.getAttribute('href') === linkForStatus.href); + if (jobHasTargetLink) { + actionableJobIDs.push(jobID); + } + } + return actionableJobIDs; + } + /** * Get job state change links (pause, resume, etc.) depending on jobs' statuses. * @param {string[]} statuses - fetch links relevant to these job statuses @@ -83,25 +103,17 @@ class JobsStatusChangeLinks extends StatusChangeLinks { const hasRunningWithErrors = statuses.indexOf('running_with_errors') > -1; const hasPreviewing = statuses.indexOf('previewing') > -1; const hasPaused = statuses.indexOf('paused') > -1; - const hasCompleteWithErrors = statuses.indexOf('complete_with_errors') > -1; - const hasCanceled = statuses.indexOf('canceled') > -1; - const hasFailed = statuses.indexOf('failed') > -1; - const hasSuccessful = statuses.indexOf('successful') > -1; - const hasTerminalStatus = hasCompleteWithErrors || hasCanceled || hasFailed || hasSuccessful; - if (hasTerminalStatus) { - return []; - } - const hasActionableStatus = hasRunning || hasRunningWithErrors || hasPreviewing || hasPaused; - if (hasActionableStatus) { + const hasActiveStatus = hasRunning || hasRunningWithErrors || hasPreviewing; + if (hasActiveStatus || hasPaused) { links.push(cancelLink); } - if (!hasPaused && hasActionableStatus) { + if (hasActiveStatus) { links.push(pauseLink); } - if (hasPaused && !hasRunning && !hasRunningWithErrors && !hasPreviewing) { + if (hasPaused) { links.push(resumeLink); } - if (hasPreviewing && !hasRunning && !hasRunningWithErrors && !hasPaused) { + if (hasPreviewing) { links.push(skipPreviewLink); } return links; diff --git a/services/harmony/public/js/workflow-ui/jobs/jobs-table.js b/services/harmony/public/js/workflow-ui/jobs/jobs-table.js index c3605307e..ce8a0434b 100644 --- a/services/harmony/public/js/workflow-ui/jobs/jobs-table.js +++ b/services/harmony/public/js/workflow-ui/jobs/jobs-table.js @@ -84,6 +84,25 @@ function refreshSelected() { PubSub.publish('job-selected'); } +/** + * Shows a visual counter for how many jobs have been selected via checkbox. + * @param {number} count - the number to display + */ +function setJobCounterDisplay(count) { + const jobCounterElement = document.getElementById('job-counter'); + jobCounterElement.textContent = count; + const display = ` job${count === 1 ? '' : 's'}`; + const jobCounterMessageElement = document.getElementById('job-counter-message'); + jobCounterMessageElement.textContent = display; + if (count === 0) { + jobCounterElement.classList.add('d-none'); + jobCounterMessageElement.classList.add('d-none'); + } else { + jobCounterElement.classList.remove('d-none'); + jobCounterMessageElement.classList.remove('d-none'); + } +} + /** * Intitialize the select box click handler for all job rows. * @param {string} selector - defines which box(es) to bind the handler to @@ -106,6 +125,7 @@ function initSelectHandler(selector) { const numSelected = jobIDs.length; const areAllJobsSelected = numSelectable === numSelected; document.getElementById('select-jobs').checked = areAllJobsSelected; + setJobCounterDisplay(jobIDs.length); PubSub.publish('job-selected'); }); }); @@ -135,6 +155,7 @@ function initSelectAllHandler() { jobEl.checked = false; } }); + setJobCounterDisplay(jobIDs.length); PubSub.publish('job-selected'); }); } @@ -229,6 +250,15 @@ const jobsTable = { getJobIds() { return jobIDs; }, + + /** + * Gets the status of the specified job. + * @param {string} jobID - the job to retrieve status for + * @returns the job status string + */ + getJobStatus(jobID) { + return document.querySelector(`#select-${jobID}`).getAttribute('data-status'); + }, }; export default jobsTable; diff --git a/services/harmony/public/js/workflow-ui/labels.js b/services/harmony/public/js/workflow-ui/labels.js new file mode 100644 index 000000000..04fed5957 --- /dev/null +++ b/services/harmony/public/js/workflow-ui/labels.js @@ -0,0 +1,235 @@ +/* eslint-disable no-continue */ +import jobsTable from './jobs/jobs-table.js'; +import toasts from './toasts.js'; +import PubSub from '../pub-sub.js'; + +let bsDropdown; +let labelLinks; +let labelDropdown; +let labelNavItem; + +/** + * Responds to a submit link click event by adding or removing + * a label. + * (hits relevant Harmony url, shows user the response). + * @param {Event} event - the click event + * @param {string} method - the HTTP method + */ +async function handleSubmitClick(event, method) { + event.preventDefault(); + const labelName = event.target.getAttribute('data-value'); + const jobIds = jobsTable.getJobIds(); + const postfix = jobIds.length === 1 ? '' : 's'; + let action = method === 'PUT' ? 'Adding' : 'Removing'; + toasts.showUpper(`${action} label...`); + const res = await fetch('/labels', { + method, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ jobId: jobIds, label: [labelName] }), + }); + action = method === 'PUT' ? 'Added' : 'Removed'; + if (res.status === 201 || res.status === 204) { + toasts.showUpper(`${action} "${labelName}" label for ${jobIds.length} job${postfix}.`); + PubSub.publish( + 'row-state-change', + ); + } else { + toasts.showUpper('The update failed.'); + } +} + +/** + * Promotes the specified label items by inserting clones at the + * top of the list and hiding the original label items. + * @param {string[]} labelNames - the list of labels to promote + */ +function promoteLabels(labelNames) { + const labelNamesReversed = labelNames.reverse(); + const labelsListElement = document.getElementById('labels-list'); + for (const name of labelNamesReversed) { + const labelElement = labelsListElement.querySelector(`a[name="${name}"]`).parentNode; + const labelElementClone = labelElement.cloneNode(true); + labelElementClone.setAttribute('title', `remove "${labelElementClone.innerText}" label from all selected jobs`); + labelElementClone.classList.add('label-clone'); + labelElementClone.addEventListener('click', (event) => { + bsDropdown.hide(); + handleSubmitClick(event, 'DELETE'); + }, false); + const labelCloneAnchor = labelElementClone.firstChild; + labelCloneAnchor.innerText = `✔️ ${labelCloneAnchor.innerText}`; + document.getElementById('labels-list').prepend(labelElementClone); + labelElement.style.display = 'none'; + labelElement.classList.add('label-promoted'); + } +} + +/** + * Demotes any labels (back to their normal alphabetical position) + * that were promoted to the top of the list. + * In practice, this means deleting the promoted clone, and unhiding the + * original label. + */ +function demoteLabels() { + const clonedLabels = document.getElementsByClassName('label-clone'); + while (clonedLabels[0]) { + clonedLabels[0].parentNode.removeChild(clonedLabels[0]); + } + Array.from(document.getElementsByClassName('label-promoted')) + .forEach((el) => el.classList.remove('label-promoted')); +} + +/** + * Filters the list of label items based on user input. + */ +function filterLabelsList() { + const searchValue = document.querySelector('#label-search').value.toLowerCase().trim(); + const labelItems = document.querySelectorAll('#labels-list .label-li'); + let visibleCount = 0; + for (const labelItem of labelItems) { + if (labelItem.classList.contains('label-promoted')) { // skip, stays hidden + continue; + } + const labelName = labelItem.firstChild.getAttribute('data-value').toLowerCase().trim(); + const isMatch = labelName.startsWith(searchValue); + labelItem.style.display = isMatch ? '' : 'none'; + if (isMatch) visibleCount += 1; + } + document.getElementById('no-match-li').style.display = visibleCount === 0 ? '' : 'none'; +} + +/** + * Unhides all label items. + */ +function showAllLabels() { + const labelItems = document.querySelectorAll('#labels-list .label-li'); + for (const labelItem of labelItems) { + labelItem.style.display = ''; + } +} + +/** + * Get the intersection set of the labels of selected jobs so that + * we know which labels will be promoted (to the top of the labels list) + * and marked for potential removal from their associated jobs. + */ +function getLabelsIntersectionForSelectedJobs() { + let labelsSet = new Set(); + let firstChecked = true; + document.querySelectorAll('.select-job').forEach((jobEl) => { + if (jobEl.checked) { + const jobID = jobEl.getAttribute('data-id'); + const labelsString = document.querySelector(`#job-labels-display-${jobID}`).getAttribute('data-labels'); + const currentSet = new Set(); + const labels = labelsString === '' ? [] : labelsString.split(','); + labels.forEach((item) => currentSet.add(item)); + if (firstChecked) { // init labelsSet + labels.forEach((item) => labelsSet.add(item)); + firstChecked = false; + } else { + labelsSet = labelsSet.intersection(currentSet); + } + } + }); + return Array.from(labelsSet); +} + +/** + * Disable all label anchor elements if no jobs are selected. + * @param {number} selectedJobsCount - count of selected jobs + * @param {Element[]} labelItemLinks - list of label link elements + */ +function setLabelLinksDisabled(selectedJobsCount, labelItemLinks) { + if (selectedJobsCount === 0) { + for (const labelItemLink of labelItemLinks) { + labelItemLink.classList.add('disabled'); + } + } +} + +/** + * Enable all label anchor elements. + * @param {Element[]} labelItemLinks - list of label link elements + */ +function setLabelLinksEnabled(labelItemLinks) { + for (const labelItemLink of labelItemLinks) { + labelItemLink.classList.remove('disabled'); + } +} + +/** + * Hide/show labels dropdown based on the number of jobs selected. + * @param {number} selectedJobsCount - count of selected jobs + */ +function toggleLabelNavVisibility(selectedJobsCount) { + if (selectedJobsCount === 0) { + labelNavItem.classList.add('d-none'); + } else { + labelNavItem.classList.remove('d-none'); + } +} + +/** + * Bind event handlers to their respective elements. + */ +function bindEventListeners() { + const labelSearchElement = document.getElementById('label-search'); + labelSearchElement.addEventListener('keyup', () => { + filterLabelsList(); + }); + document.querySelectorAll('.label-item').forEach((item) => { + item.addEventListener('click', (event) => { + bsDropdown.hide(); + handleSubmitClick(event, 'PUT'); + }, false); + }); + labelDropdown.addEventListener('hidden.bs.dropdown', () => { + demoteLabels(); + setLabelLinksEnabled(labelLinks); + document.getElementById('label-search').value = ''; + showAllLabels(); + document.getElementById('no-match-li').style.display = 'none'; + }); + labelDropdown.addEventListener('show.bs.dropdown', () => { + promoteLabels(getLabelsIntersectionForSelectedJobs()); + const selectedJobsCount = jobsTable.getJobIds().length; + setLabelLinksDisabled(selectedJobsCount, labelLinks); + }); +} + +/** + * The labeling dropdown object allows users to + * add and remove labels from selected jobs. + */ +export default { + + /** + * Initializes the labeling interactivity associated with + * the labels dropdown link. + */ + init() { + // the anchor elements that correspond to a label + labelLinks = Array.from(document.querySelectorAll('#labels-list .label-li a')); + // the dropdown that contains label list items + labelDropdown = document.getElementById('label-dropdown-a'); + labelNavItem = document.getElementById('label-nav-item'); + if (labelDropdown) { + bsDropdown = new bootstrap.Dropdown(labelDropdown); + } + bindEventListeners(); + PubSub.subscribe( + 'job-selected', + () => this.toggleLabelNavVisibility(jobsTable.getJobIds().length), + ); + }, + promoteLabels, + demoteLabels, + getLabelsIntersectionForSelectedJobs, + setLabelLinksDisabled, + setLabelLinksEnabled, + filterLabelsList, + showAllLabels, + toggleLabelNavVisibility, +}; diff --git a/services/harmony/public/js/workflow-ui/nav-links.js b/services/harmony/public/js/workflow-ui/nav-links.js deleted file mode 100644 index e13288988..000000000 --- a/services/harmony/public/js/workflow-ui/nav-links.js +++ /dev/null @@ -1,116 +0,0 @@ -import toasts from './toasts.js'; -import PubSub from '../pub-sub.js'; - -/** - * Transform link objects to an HTML string representing the links nav. - * @param {Object[]} links - link array (of links with title, href, type, rel) - * @returns HTML as a string - */ -function buildLinksHtml(links) { - const linkToLi = (link) => `
  • - - ${link.href.split('/').pop()} - -
  • `; - return ` - - `; -} - -/** - * Responds to a nav link click event - * (hits relevant Harmony url, shows user the response). - * @param {Event} event - the click event - */ -async function handleClick(event) { - event.preventDefault(); - toasts.showUpper('Changing job state...'); - const link = event.target; - const stateChangeUrl = link.getAttribute('href'); - const res = await fetch(stateChangeUrl); - const data = await res.json(); - if (res.status === 200) { - toasts.showUpper(`The job is now ${data.status}`); - PubSub.publish('table-state-change'); - } else if (data.description) { - toasts.showUpper(data.description); - } else { - toasts.showUpper('The update failed.'); - } -} - -/** - * Transform the links to HTML and insert them in the specified container. - * Also attaches a click event listener to the link. - * @param {Object[]} links - link array (of links with title, href, type, rel) - * @param {string} linksContainerId - id of the container to place the HTML within - */ -function insertLinksHtml(links, linksContainerId) { - const html = buildLinksHtml(links); - document.getElementById(linksContainerId).innerHTML = html; - document.querySelectorAll('.state-change-link').forEach((link) => { - link.addEventListener('click', (event) => { - handleClick(event); - }, false); - }); -} - -/** - * Get job state change links (pause, resume, etc.) from Harmony and insert them in the UI. - * @param {string} linksContainerId - id of the container to place the HTML within - * @param {string} jobId - the job id to fetch links for - */ -async function fetchAndInsertLinks(linksContainerId, jobId) { - const linksUrl = `./${jobId}/links?all=true`; - const res = await fetch(linksUrl); - if (res.status === 200) { - const links = await res.json(); - if (links.length) { - insertLinksHtml(links, linksContainerId); - } - } -} - -/** - * Hide/show links depending on the job state. - * @param {string} jobId the id of the current job - */ -async function enableLinks(jobId) { - const linksUrl = `./${jobId}/links?all=false`; - const res = await fetch(linksUrl); - if (res.status === 200) { - const validLinks = await res.json(); - document.querySelectorAll('.state-change-link').forEach((el) => { - const rel = el.getAttribute('rel'); - if (validLinks.find((l) => l.rel === rel)) { - el.classList.remove('d-none'); - } else { - el.classList.add('d-none'); - } - }); - } -} - -/** - * Builds job state change navigation links and handles - * all relevant user interactions with those links. - */ -export default { - - /** - * Initialize job state change nav links. - * @param {string} linksContainerId - id of the container to place the links within - * @param {string} jobId - the job id to fetch links for - */ - async init(linksContainerId, jobId) { - await fetchAndInsertLinks(linksContainerId, jobId); - // keep the hidden/visible state of the links in sync with - // the work items table - PubSub.subscribe( - 'work-items-table-loaded', - () => enableLinks(jobId), - ); - }, -}; diff --git a/services/harmony/public/js/workflow-ui/status-change-links.js b/services/harmony/public/js/workflow-ui/status-change-links.js index 5833ea3d6..dd42b1f50 100644 --- a/services/harmony/public/js/workflow-ui/status-change-links.js +++ b/services/harmony/public/js/workflow-ui/status-change-links.js @@ -33,11 +33,7 @@ class StatusChangeLinks { ${link.href.split('/').pop()} `; - return ` - - `; + return `${links.map(linkToLi).join('')}`; } /** @@ -57,7 +53,9 @@ class StatusChangeLinks { */ insertLinksHtml(links, linksContainerId) { const html = this.buildLinksHtml(links); - document.getElementById(linksContainerId).innerHTML = html; + const tmp = document.createElement('ul'); + tmp.innerHTML = html; + document.getElementById(linksContainerId).prepend(...tmp.childNodes); document.querySelectorAll('.state-change-link').forEach((link) => { link.addEventListener('click', (event) => { this.handleClick(event); diff --git a/services/harmony/test/labels/label_crud.ts b/services/harmony/test/labels/label_crud.ts index 738096033..bf0e99e4b 100644 --- a/services/harmony/test/labels/label_crud.ts +++ b/services/harmony/test/labels/label_crud.ts @@ -7,7 +7,7 @@ import hookServersStartStop from '../helpers/servers'; import db from '../../app/util/db'; import env from '../../app/util/env'; import { stub } from 'sinon'; -import { getLabelsForUser } from '../../app/models/label'; +import { getRecentLabelsForUser } from '../../app/models/label'; describe('Get Labels', function () { const joeJob = buildJob({ username: 'joe' }); @@ -27,7 +27,7 @@ describe('Get Labels', function () { await addJobsLabels(this.frontend, [joeJob.jobID], ['foo', 'bar'], 'joe'); await addJobsLabels(this.frontend, [jillJob.jobID], ['foo', 'boo'], 'jill'); // get up to ten labels across all users - const labels = await getLabelsForUser( + const labels = await getRecentLabelsForUser( db, 'adam', 10, @@ -44,7 +44,7 @@ describe('Get Labels', function () { await addJobsLabels(this.frontend, [joeJob.jobID], ['two'], 'joe'); await addJobsLabels(this.frontend, [jillJob.jobID], ['three', 'four'], 'jill'); // get up to three labels across all users - const labels = await getLabelsForUser( + const labels = await getRecentLabelsForUser( db, 'adam', 3, diff --git a/services/harmony/test/workflow-ui/jobs-status-change-links.ts b/services/harmony/test/workflow-ui/jobs-status-change-links.ts index d76a6fe93..019c5aaed 100644 --- a/services/harmony/test/workflow-ui/jobs-status-change-links.ts +++ b/services/harmony/test/workflow-ui/jobs-status-change-links.ts @@ -1,10 +1,37 @@ import { expect } from 'chai'; import { describe, it, before } from 'mocha'; import JobsStatusChangeLinks from '../../public/js/workflow-ui/jobs/jobs-status-change-links'; +import { JSDOM } from 'jsdom'; +import path from 'path'; describe('JobsStatusChangeLinks', function () { const jobsStatusChangeLinks = new JobsStatusChangeLinks(); + + describe('getActionableJobIDs()', () => { + const previewingJobID = '580b48e6-845e-4e83-bcb8-60a1a3b0b6b9'; + const runningJobID = '058184f7-498c-4aa5-a3df-96a3a49b7d19'; + const pausedJobID = '38d2b820-0b52-475d-8cb0-0b9f7775f767'; + beforeEach(async () => { + const dom = await JSDOM.fromFile(path.resolve(__dirname, 'labels.html'), { url: 'http://localhost' }); + global.window = dom.window as unknown as Window & typeof globalThis; + global.document = dom.window.document; + }); + describe('when the jobIDs specified cannot be paused, and the target link=pauser', function () { + it('returns no job IDs', () => { + const targetLink = document.querySelector("a.state-change-link[rel='pauser']"); + const actionableJobIDs = jobsStatusChangeLinks.getActionableJobIDs([pausedJobID], targetLink); + expect(actionableJobIDs.length).to.equal(0); + }); + }); + describe('when 2 of the 3 jobIDs specified can be paused, and the target link=pauser', function () { + it('returns 2 job IDs', () => { + const targetLink = document.querySelector("a.state-change-link[rel='pauser']"); + const actionableJobIDs = jobsStatusChangeLinks.getActionableJobIDs([pausedJobID, runningJobID, previewingJobID], targetLink); + expect(actionableJobIDs).to.deep.equal([runningJobID, previewingJobID]); + }); + }); + }); describe('fetchLinks()', function () { let links; before(async function () { @@ -22,8 +49,8 @@ describe('JobsStatusChangeLinks', function () { before(async function () { links = jobsStatusChangeLinks.fetchLinksForStatuses(['paused', 'successful']); }); - it('Returns 0 job status change links', function () { - expect(links.length).to.eq(0); + it('Returns 2 job status change links', function () { + expect(links.length).to.eq(2); }); }); describe('with paused status', function () { @@ -80,11 +107,12 @@ describe('JobsStatusChangeLinks', function () { before(async function () { links = jobsStatusChangeLinks.fetchLinksForStatuses(['running', 'previewing']); }); - it('Returns pause and cancel status change link', function () { - expect(links.length).to.eq(2); + it('Returns pause, cancel and skip preview status change link', function () { + expect(links.length).to.eq(3); const linkRels = links.map((l) => l.rel); expect(linkRels).contains('pauser'); expect(linkRels).contains('canceler'); + expect(linkRels).contains('preview-skipper'); }); }); describe('with running and paused statuses', function () { @@ -92,10 +120,12 @@ describe('JobsStatusChangeLinks', function () { before(async function () { links = jobsStatusChangeLinks.fetchLinksForStatuses(['running', 'paused']); }); - it('Returns cancel status change link', function () { - expect(links.length).to.eq(1); + it('Returns cancel, resume and pause status change link', function () { + expect(links.length).to.eq(3); const linkRels = links.map((l) => l.rel); expect(linkRels).contains('canceler'); + expect(linkRels).contains('resumer'); + expect(linkRels).contains('pauser'); }); }); }); diff --git a/services/harmony/test/workflow-ui/jobs-table.ts b/services/harmony/test/workflow-ui/jobs-table.ts index ad04d00af..aa14c1847 100644 --- a/services/harmony/test/workflow-ui/jobs-table.ts +++ b/services/harmony/test/workflow-ui/jobs-table.ts @@ -14,7 +14,7 @@ import { setLabelsForJob } from '../../app/models/label'; // main objects used in the tests -const boJob1 = buildJob({ status: JobStatus.FAILED, username: 'bo', provider_id: 'provider_a' }); +const boJob1 = buildJob({ status: JobStatus.RUNNING, username: 'bo', provider_id: 'provider_a' }); const boJob2 = buildJob({ status: JobStatus.SUCCESSFUL, username: 'bo', service_name: 'cog-maker', provider_id: 'provider_b' }); const adamJob1 = buildJob({ status: JobStatus.RUNNING, username: 'adam', provider_id: 'provider_a' }); const woodyJob1 = buildJob({ status: JobStatus.RUNNING, username: 'woody' }); @@ -132,6 +132,21 @@ describe('Workflow UI jobs table route', function () { }); }); + describe('a user with all nonterminal jobs selected using the non-admin route', function () { + hookWorkflowUIJobRows({ username: 'bo', jobIDs: [boJob1.jobID], query: { page: 1, limit: 10 } }); + // "select all" box should be unchecked because of the 1 unselected terminal job (which can be selected for tagging) + it('returns the select all jobs checkbox unchecked', async function () { + const response = this.res.text; + expect(response).contains(''); + }); + // the 1 unselected terminal job can be selected for tagging + it('has 1 select job checkbox unchecked and 1 checked', function () { + const response = this.res.text; + expect(response).contains(``); + expect(response).contains(``); + }); + }); + describe('a user who is an admin', function () { describe('using the provider ids filter', function () { hookAdminWorkflowUIJobRows({ username: 'adam', jobIDs: allJobIds, @@ -172,13 +187,14 @@ describe('Workflow UI jobs table route', function () { }); }); - describe('using a user filter with the non-admin route', function () { + describe('an admin using the non-admin route and a user filter', function () { hookWorkflowUIJobRows({ username: 'adam', jobIDs: [woodyJob1.jobID], query: { disallowUser: true, tableFilter: '[{"value":"user: woody","dbValue":"woody","field":"user"}]' } }); - it('ignores the user filter and returns all jobs', function () { + it('ignores the user filter and only returns the jobs for that user', function () { const response = this.res.text; - expect(response).contains(``); - expect((response.match(/job-table-row/g) || []).length).to.eq(totalJobsCount); + // user filters are not available on non-admin route because + // this route only returns jobs for the authenticated user + expect((response.match(/job-table-row/g) || []).length).to.eq(1); }); }); @@ -209,64 +225,37 @@ describe('Workflow UI jobs table route', function () { }); describe('with one page', function () { - hookWorkflowUIJobRows({ username: 'adam', jobIDs: [boJob1.jobID, adamJob1.jobID], query: { page: 1, limit: 10 } }); + hookAdminWorkflowUIJobRows({ username: 'adam', jobIDs: [boJob1.jobID, adamJob1.jobID], query: { page: 1, limit: 10 } }); it('returns all jobs', async function () { const response = this.res.text; expect((response.match(/job-table-row/g) || []).length).to.eq(totalJobsCount); }); }); describe('with two pages', function () { - hookWorkflowUIJobRows({ username: 'adam', jobIDs: [boJob1.jobID, adamJob1.jobID], query: { page: 1, limit: 1 } }); + hookAdminWorkflowUIJobRows({ username: 'adam', jobIDs: [boJob1.jobID, adamJob1.jobID], query: { page: 1, limit: 1 } }); it('returns updated paging links, with a link to the last and next page', function () { const response = this.res.text; - expect(response.replace(/\s/g, '')).contains( - ``.replace(/\s/g, ''), - ); - }); - }); - describe('with all nonterminal jobs selected', function () { - hookWorkflowUIJobRows({ username: 'adam', jobIDs: [woodyJob1.jobID, woodyJob2.jobID, adamJob1.jobID], query: { page: 1, limit: 10 } }); - it('returns the select all jobs checkbox checked', async function () { - const response = this.res.text; - expect(response).contains(''); - }); - it('has all select job checkboxes checked', function () { - const response = this.res.text; - expect(response).contains(``); - expect(response).contains(``); + expect(response).contains('title="first">first'); + expect(response).contains('title="previous">previous'); + expect(response).contains('title="next">next'); + expect(response).contains('title="last">last'); + expect(response).contains(`1-1 of ${totalJobsCount} (page 1 of ${totalJobsCount})`); }); }); - describe('with 1 nonterminal job selected and one nonterminal job not selected', function () { - hookWorkflowUIJobRows({ username: 'adam', jobIDs: [woodyJob1.jobID], query: { page: 1, limit: 10 } }); + describe('with all nonterminal jobs selected using the admin route', function () { + hookAdminWorkflowUIJobRows({ username: 'adam', jobIDs: [woodyJob1.jobID, adamJob1.jobID, boJob1.jobID], query: { page: 1, limit: 10 } }); + // "select all" box should be unchecked because 1 job is still running it('returns the select all jobs checkbox unchecked', async function () { const response = this.res.text; expect(response).contains(''); }); - it('has one job checkbox checked', async function () { + it('has only 4 select job checkboxes, for the nonterminal jobs', function () { const response = this.res.text; expect(response).contains(``); - }); - it('has one job checkbox unchecked', async function () { - const response = this.res.text; - expect(response).contains(``); + expect(response).contains(``); + expect(response).contains(``); + expect(response).contains(``); + expect(response).not.contains(``); }); }); }); diff --git a/services/harmony/test/workflow-ui/jobs.ts b/services/harmony/test/workflow-ui/jobs.ts index d32d12ac1..7b5c2af22 100644 --- a/services/harmony/test/workflow-ui/jobs.ts +++ b/services/harmony/test/workflow-ui/jobs.ts @@ -403,6 +403,10 @@ describe('Workflow UI jobs route', function () { expect(listing).to.not.contain('status: successful'); expect(listing).to.not.contain('status: running'); }); + it('returns the select all jobs checkbox to support actions like tagging', async function () { + const response = this.res.text; + expect(response).contains(''); + }); }); describe('who filters by status IN [failed, successful]', function () { @@ -445,6 +449,14 @@ describe('Workflow UI jobs route', function () { }); }); + describe('who filters by an valid username via the nonadmin route', function () { + hookWorkflowUIJobs({ username: 'adam', tableFilter: '[{"value":"user: adam"}]' }); + it('ignores the username because the user filter is unavailable via the nonadmin route', function () { + const listing = this.res.text; + expect(listing).to.not.contain('user: adam'); + }); + }); + describe('who filters by status NOT IN [failed, successful]', function () { const tableFilter = '[{"value":"status: failed","dbValue":"failed","field":"status"},{"value":"status: successful","dbValue":"successful","field":"status"}]'; hookWorkflowUIJobs({ username: 'woody', disallowStatus: 'on', tableFilter }); @@ -695,6 +707,10 @@ describe('Workflow UI jobs route', function () { const listing = this.res.text; expect(listing).to.contain(mustache.render('{{provider}}', { provider: 'provider: provider_b' })); }); + it('does not return the select all checkbox since there are no nonterminal jobs', async function () { + const response = this.res.text; + expect(response).not.contains(' + + + + Harmony + + + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + + + + +
    + from + +
    +
    + to + +
    +
    + + +
    +
    + + +
    + + + ​ + +
    + + +
    +
    + + +
    +
    + + +
    +
    + page size + +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    jobIDservicestatusmessage +
    +
    granules
      + +
    +
    progresscreatedAtupdatedAt
    580b48e6-845e-4e83-bcb8-60a1a3b0b6b9harmony/service-examplepreviewingThe job is generating a preview before auto-pausing1770%10/29/2024, 2:55:17 PM10/29/2024, 3:00:37 PM
    +
    +
    + + /C1233800302-EEDTEST/ogc-api-coverages/.../all/...?format=image/tiff +
    +
    + +
    +
    058184f7-498c-4aa5-a3df-96a3a49b7d19harmony/service-examplerunningThe job is generating a preview before auto-pausing1770%10/29/2024, 2:55:03 PM10/29/2024, 3:00:40 PM
    +
    +
    + + /C1233800302-EEDTEST/ogc-api-coverages/.../all/...?format=image/tiff&label=blue +
    +
    + blue +
    +
    38d2b820-0b52-475d-8cb0-0b9f7775f767harmony/service-examplepausedThe job is generating a preview before auto-pausing1770%10/29/2024, 2:54:57 PM10/29/2024, 3:00:44 PM
    +
    +
    + + /C1233800302-EEDTEST/ogc-api-coverages/.../all/...?format=image/tiff&label=blue&label=yellow&label=green +
    +
    + blue green yellow +
    +
    +
    +
    + + + + + \ No newline at end of file diff --git a/services/harmony/test/workflow-ui/labels.ts b/services/harmony/test/workflow-ui/labels.ts new file mode 100644 index 000000000..6655dd01c --- /dev/null +++ b/services/harmony/test/workflow-ui/labels.ts @@ -0,0 +1,99 @@ +import { JSDOM } from 'jsdom'; +import { expect } from 'chai'; +import path from 'path'; +import Labels from '../../public/js/workflow-ui/labels'; + +beforeEach(async () => { + const dom = await JSDOM.fromFile(path.resolve(__dirname, 'labels.html'), { url: 'http://localhost' }); + global.window = dom.window as unknown as Window & typeof globalThis; + global.document = dom.window.document; +}); + +describe('labels.js', () => { + describe('promoteLabels', () => { + it('promotes the given list of labels', () => { + const labelsListElement = document.getElementById('labels-list'); + const greenLi = labelsListElement.querySelector('a[name="green"]').closest('li'); + Labels.promoteLabels(['green']); + const isPromoted = greenLi.classList.contains('label-promoted'); + expect(isPromoted); + }); + }); + describe('demoteLabels', () => { + it('demotes all promoted labels and removes clones', () => { + const labelsListElement = document.getElementById('labels-list'); + Labels.promoteLabels(['green']); + Labels.demoteLabels(); + const greenLi = labelsListElement.querySelector('a[name="green"]').closest('li'); + const isPromoted = greenLi.classList.contains('label-promoted'); + expect(!isPromoted); + const clones = Array.from(document.getElementsByClassName('label-clone')); + expect(clones.length).to.equal(0); + }); + }); + describe('getLabelsIntersectionForSelectedJobs', () => { + it('gets the intersection set of labels for selected jobs', () => { + (document.getElementById('select-058184f7-498c-4aa5-a3df-96a3a49b7d19') as HTMLInputElement).checked = true; + (document.getElementById('select-38d2b820-0b52-475d-8cb0-0b9f7775f767') as HTMLInputElement).checked = true; + expect(Labels.getLabelsIntersectionForSelectedJobs()).to.deep.equal(['blue']); + }); + it('returns [] when there are no selected jobs', () => { + expect(Labels.getLabelsIntersectionForSelectedJobs()).to.deep.equal([]); + }); + }); + describe('setLabelLinksDisabled', () => { + it('sets all label links disabled when 0 jobs are selected', () => { + const labelLinks = Array.from(document.querySelectorAll('#labels-list .label-li a')); + Labels.setLabelLinksDisabled(0, labelLinks); + const disabledLabelLinks = Array.from(document.querySelectorAll('#labels-list .label-li a.disabled')); + expect(disabledLabelLinks.length).to.equal(3); + }); + }); + describe('setLabelLinksEnabled', () => { + it('sets all label links enabled', () => { + const labelLinks = Array.from(document.querySelectorAll('#labels-list .label-li a')); + Labels.setLabelLinksDisabled(0, labelLinks); + Labels.setLabelLinksEnabled(labelLinks); + for (const l of labelLinks) { + expect(!l.classList.contains('disabled')); + } + }); + }); + describe('filterLabelsList', () => { + it('hides labels that do not match the search input value', () => { + (document.querySelector('#label-search') as HTMLInputElement).value = 'blu'; + Labels.filterLabelsList(); + const labelsListElement = document.getElementById('labels-list'); + const blueLi = labelsListElement.querySelector('a[name="blue"]').closest('li'); + const greenLi = labelsListElement.querySelector('a[name="green"]').closest('li'); + const yellowLi = labelsListElement.querySelector('a[name="yellow"]').closest('li'); + expect(blueLi.style.display).to.not.equal('none'); + expect(greenLi.style.display).to.equal('none'); + expect(yellowLi.style.display).to.equal('none'); + }); + it('shows a no matches list item when the search input value does not match any labels', () => { + (document.querySelector('#label-search') as HTMLInputElement).value = 'bluez'; + Labels.filterLabelsList(); + const labelsListElement = document.getElementById('labels-list'); + const blueLi = labelsListElement.querySelector('a[name="blue"]').closest('li'); + const greenLi = labelsListElement.querySelector('a[name="green"]').closest('li'); + const yellowLi = labelsListElement.querySelector('a[name="yellow"]').closest('li'); + const noMatchLi = document.getElementById('no-match-li'); + expect(blueLi.style.display).to.equal('none'); + expect(greenLi.style.display).to.equal('none'); + expect(yellowLi.style.display).to.equal('none'); + expect(noMatchLi.style.display).to.not.equal('none'); + }); + }); + describe('showAllLabels', () => { + it('unhides all labels', () => { + (document.querySelector('#label-search') as HTMLInputElement).value = 'bluez'; + Labels.filterLabelsList(); + Labels.showAllLabels(); + const labelItems = document.querySelectorAll('#labels-list .label-li'); + for (const labelItem of labelItems) { + expect((labelItem as HTMLInputElement).style.display).to.equal(''); + } + }); + }); +}); diff --git a/services/query-cmr/package-lock.json b/services/query-cmr/package-lock.json index 30f22c23b..06c8c2198 100644 --- a/services/query-cmr/package-lock.json +++ b/services/query-cmr/package-lock.json @@ -4195,10 +4195,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -11652,9 +11653,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -11844,7 +11845,7 @@ "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "7.0.5", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", @@ -12206,7 +12207,7 @@ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^3.0.2" } }, @@ -12712,7 +12713,7 @@ "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "7.0.5", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", @@ -13817,7 +13818,7 @@ "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^4.0.1" } }, diff --git a/services/query-cmr/package.json b/services/query-cmr/package.json index 2397ce66c..58f9032a2 100644 --- a/services/query-cmr/package.json +++ b/services/query-cmr/package.json @@ -101,6 +101,7 @@ }, "overrides": { "braces": "^3.0.3", - "fast-xml-parser": "4.4.1" + "fast-xml-parser": "4.4.1", + "cross-spawn": "7.0.5" } } diff --git a/services/work-failer/package-lock.json b/services/work-failer/package-lock.json index 588729bd7..095326753 100644 --- a/services/work-failer/package-lock.json +++ b/services/work-failer/package-lock.json @@ -4006,10 +4006,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -11514,9 +11515,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -11714,7 +11715,7 @@ "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "7.0.5", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", @@ -12052,7 +12053,7 @@ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^3.0.2" } }, @@ -12543,7 +12544,7 @@ "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "7.0.5", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", diff --git a/services/work-failer/package.json b/services/work-failer/package.json index 67f83ec6b..7326b67b6 100644 --- a/services/work-failer/package.json +++ b/services/work-failer/package.json @@ -99,6 +99,7 @@ "overrides": { "semver": "^7.6.2", "braces": "^3.0.3", - "fast-xml-parser": "4.4.1" + "fast-xml-parser": "4.4.1", + "cross-spawn": "7.0.5" } } diff --git a/services/work-reaper/package-lock.json b/services/work-reaper/package-lock.json index bd718b95c..23fe1908b 100644 --- a/services/work-reaper/package-lock.json +++ b/services/work-reaper/package-lock.json @@ -3655,10 +3655,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -10637,9 +10638,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -10832,7 +10833,7 @@ "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "7.0.5", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", @@ -11146,7 +11147,7 @@ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^3.0.2" } }, @@ -11590,7 +11591,7 @@ "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "7.0.5", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", diff --git a/services/work-reaper/package.json b/services/work-reaper/package.json index 8ed6364d8..ca370607e 100644 --- a/services/work-reaper/package.json +++ b/services/work-reaper/package.json @@ -84,6 +84,7 @@ }, "overrides": { "braces": "^3.0.3", - "fast-xml-parser": "4.4.1" + "fast-xml-parser": "4.4.1", + "cross-spawn": "7.0.5" } } diff --git a/services/work-updater/package-lock.json b/services/work-updater/package-lock.json index 5bf08e827..7785e3045 100644 --- a/services/work-updater/package-lock.json +++ b/services/work-updater/package-lock.json @@ -4019,10 +4019,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -11504,9 +11505,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -11704,7 +11705,7 @@ "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "7.0.5", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", @@ -12042,7 +12043,7 @@ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "7.0.5", "signal-exit": "^3.0.2" } }, @@ -12533,7 +12534,7 @@ "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "7.0.5", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", diff --git a/services/work-updater/package.json b/services/work-updater/package.json index f479c7165..3e36b54dd 100644 --- a/services/work-updater/package.json +++ b/services/work-updater/package.json @@ -97,6 +97,7 @@ "overrides": { "semver": "^7.6.2", "braces": "^3.0.3", - "fast-xml-parser": "4.4.1" + "fast-xml-parser": "4.4.1", + "cross-spawn": "7.0.5" } }