From 9716c518296b333b59b2f3afd39274024af7e6d7 Mon Sep 17 00:00:00 2001 From: Markus Haack Date: Wed, 11 Dec 2024 14:51:34 +0100 Subject: [PATCH 1/6] feat: initial version using bulk job APIs to preview --- nx/blocks/bulk/bulk.js | 93 ++++++++++++++++++++++++++--------------- nx/blocks/bulk/index.js | 38 +++++++++++++++++ 2 files changed, 98 insertions(+), 33 deletions(-) diff --git a/nx/blocks/bulk/bulk.js b/nx/blocks/bulk/bulk.js index a902e40b..f6496a05 100644 --- a/nx/blocks/bulk/bulk.js +++ b/nx/blocks/bulk/bulk.js @@ -1,11 +1,10 @@ import { LitElement, html, nothing } from '../../deps/lit/lit-core.min.js'; import { getConfig } from '../../scripts/nexter.js'; -import makeBatches from '../../public/utils/batch.js'; import getStyle from '../../utils/styles.js'; import getSvg from '../../utils/svg.js'; -import { formatUrls, sendAction, throttle } from './index.js'; +import { formatUrls, getJobStatus, triggerJob } from './index.js'; const { nxBase } = getConfig(); const style = await getStyle(import.meta.url); @@ -15,12 +14,13 @@ const ICONS = [ `${nxBase}/img/icons/Smock_ChevronRight_18_N.svg`, ]; -const SUCCESS_CODES = [200, 201, 204]; +const SUCCESS_CODES = [200, 201, 204, 304]; const MOCK_URLS = 'https://main--bacom-sandbox--adobecom.hlx.live/\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/aaa-northeast-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/aaa-northeast-case-study-updatedcaastags\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/abb-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/academy-of-art-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/accent-group-ecommerce-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/aci-worldwide-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/adobe-campaign-orchestration-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/adobe-digital-legal-workflow-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/adobe-digital-onboarding-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/adobe-digital-university-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/adobe-inside-adobe-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/adobe-promo-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/adobe-summit-2023-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/adp-workfront-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/aftia-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/airbus-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/aisg-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/al-ghandi-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/alma-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/alshaya-group-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/altisource-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/americord-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/analogic-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/aon-hewitt-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/apollo-tyres-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/ariel-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/armor-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/asics-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/asus-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/avidxchange-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/avionte-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/bank-of-new-zealand-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/barilla-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/bbva-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/bbva-workfront-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/ben-and-jerrys-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/benefytt-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/best-buy-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/best-western-hotels-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/biomedica-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/blackmores-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/bmw-group-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/bny-mellon-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/boots-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/border-states-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/bose-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/brand-safety-institute-case-study\nhttps://main--bacom-sandbox--adobecom.hlx.live/customer-success-stories/breville-case-study'; class NxBulk extends LitElement { static properties = { + _jobStatus: { state: true }, _baseUrls: { state: true }, _successUrls: { state: true }, _errorUrls: { state: true }, @@ -42,30 +42,54 @@ class NxBulk extends LitElement { getSvg({ parent: this.shadowRoot, paths: ICONS }); } - processBatch(completeUrls) { - completeUrls.forEach((url) => { - const idx = this._baseUrls.findIndex(({ pathname }) => pathname === url.pathname); - const [spliced] = this._baseUrls.splice(idx, 1); - if (SUCCESS_CODES.some((code) => code === spliced.status)) { - this._successUrls.unshift(spliced); - } else { - this._errorUrls.unshift(spliced); - } - }); - } + processJobStatus(jobStatus) { + if (jobStatus.progress) { + this._jobStatus = jobStatus.progress; + this._jobStatus.stopped = jobStatus.state === 'stopped'; + } - async sendBatch(batch, label) { - const finishedBatch = await Promise.all(batch.map(async (url) => sendAction(url, label))); - this.processBatch(finishedBatch); + if (jobStatus.data?.resources) { + this._successUrls = jobStatus.data.resources.filter( + (res) => SUCCESS_CODES.includes(res.status), + ); + this._errorUrls = jobStatus.data.resources.filter( + (res) => !SUCCESS_CODES.includes(res.status), + ); + } this.requestUpdate(); } + async pollJobStatus(job, setProgress) { + let jobStatus; + let stopped = false; + while (!stopped) { + const status = await getJobStatus(`${job.links.self}`); + if (status?.stopTime) { + jobStatus = status; + stopped = true; + } + if (status) setProgress(status); + } + if (stopped) { + jobStatus = await getJobStatus(`${job.links.self}/details`, true); + setProgress(jobStatus); + } + return jobStatus; + } + resetState() { this._cancel = false; this._cancelText = 'Cancel'; this._baseUrls = []; this._successUrls = []; this._errorUrls = []; + this._jobStatus = { + stopped: false, + total: 0, + processed: 0, + failed: 0, + success: 0, + }; } handleDeleteCheck() { @@ -100,17 +124,17 @@ class NxBulk extends LitElement { const { urls, action, delete: hasDelete, label } = Object.fromEntries(data); this._baseUrls = formatUrls(urls, action, hasDelete); - const batches = makeBatches(this._baseUrls, 10); + const jobResult = await triggerJob(this._baseUrls, label); - for (const batch of batches) { - if (this._cancel) { - this._cancelText = 'Cancelled'; - break; - } - const start = Date.now(); - await this.sendBatch(batch, label); - await throttle(start); + if (jobResult.status !== 202) { + this.resetState(); + // TODO error message toast + return; } + + await this.pollJobStatus(jobResult, (status) => { + this.processJobStatus(status); + }); } handleSelect(e) { @@ -118,12 +142,16 @@ class NxBulk extends LitElement { } get _totalCount() { - return this._baseUrls.length + this._successUrls.length + this._errorUrls.length; + return this._jobStatus.total || this._baseUrls.length; + } + + get _remainingCount() { + return this._totalCount - this._jobStatus.processed; } renderBadge(name, length, hasCancel) { const lowerName = name.toLowerCase(); - const hasExpand = length > 0 && lowerName !== 'total'; + const hasExpand = this._jobStatus.stopped && length > 0 && (lowerName !== 'total' || lowerName === 'remaining'); return html`
@@ -149,7 +177,7 @@ class NxBulk extends LitElement {
- ${this.renderBadge('Remaining', this._baseUrls.length, this._baseUrls.length > 1)} - ${this.renderBadge('Errors', this._errorUrls.length)} - ${this.renderBadge('Success', this._successUrls.length)} + ${this.renderBadge('Remaining', this._remainingCount, this._remainingCount > 1)} + ${this.renderBadge('Errors', this._jobStatus.failed)} + ${this.renderBadge('Success', this._jobStatus.success)} ${this.renderBadge('Total', this._totalCount)}
${this.renderList('Errors', this._errorUrls)} ${this.renderList('Success', this._successUrls)} - ${this.renderList('Remaining', this._baseUrls)} `; } } diff --git a/nx/blocks/bulk/index.js b/nx/blocks/bulk/index.js index 627027c5..139fd9ca 100644 --- a/nx/blocks/bulk/index.js +++ b/nx/blocks/bulk/index.js @@ -51,3 +51,41 @@ export async function sendAction(url, label) { } return url; } + +export async function triggerJob(urls) { + try { + const method = urls[0].hasDelete ? 'DELETE' : 'POST'; + const opts = { method }; + const origin = isBulkDa(urls[0]?.action) ? DA_ORIGIN : AEM_ORIGIN; + + opts.body = JSON.stringify({ paths: urls.map((url) => url.pathname) }); + opts.headers = { 'Content-Type': 'application/json; charset=utf-8' }; + + // TODO remove CI version from URL + const aemUrl = `${origin}/${urls[0].action}/${urls[0].org}/${urls[0].repo}/${urls[0].ref}/*?hlx-admin-version=ci`; + const resp = await daFetch(aemUrl, opts); + + if (resp.status !== 202) { + return { status: resp.status, message: 'Job failed to trigger.' }; + } + return await resp.json(); + } catch (error) { + return { status: 500, message: 'Job failed to trigger.' }; + } +} + +export async function getJobStatus(jobUrl, force = false) { + if (!force) { + await new Promise((resolve) => { + setTimeout(() => resolve(), 1000); + }); + } + try { + const opts = { method: 'GET' }; + const status = await daFetch(jobUrl, opts); + const result = await status.json(); + return result; + } catch (error) { + return error; + } +} From e6bf6871643083ed10257c53bf7d4958d534fe73 Mon Sep 17 00:00:00 2001 From: Markus Haack Date: Wed, 11 Dec 2024 17:30:45 +0100 Subject: [PATCH 2/6] fix: cancel jobs --- nx/blocks/bulk/bulk.css | 10 ++++++++++ nx/blocks/bulk/bulk.js | 42 +++++++++++++++++++++++++++++++---------- nx/blocks/bulk/index.js | 17 ++++++++++++++--- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/nx/blocks/bulk/bulk.css b/nx/blocks/bulk/bulk.css index 4e4bbd0b..45d4465e 100644 --- a/nx/blocks/bulk/bulk.css +++ b/nx/blocks/bulk/bulk.css @@ -219,3 +219,13 @@ select { .is-expanded .toggle-list-icon svg { transform: rotate(90deg); } + +.error { + align-content: center; + padding: 10px; + background: var(--s2-red); + border-radius: var(--s2-radius-100); + font-weight: 700; + color: #fff; + font-size: 14px; +} \ No newline at end of file diff --git a/nx/blocks/bulk/bulk.js b/nx/blocks/bulk/bulk.js index f6496a05..87377edb 100644 --- a/nx/blocks/bulk/bulk.js +++ b/nx/blocks/bulk/bulk.js @@ -4,7 +4,7 @@ import { getConfig } from '../../scripts/nexter.js'; import getStyle from '../../utils/styles.js'; import getSvg from '../../utils/svg.js'; -import { formatUrls, getJobStatus, triggerJob } from './index.js'; +import { cancelJob, formatUrls, getJobStatus, triggerJob } from './index.js'; const { nxBase } = getConfig(); const style = await getStyle(import.meta.url); @@ -21,6 +21,7 @@ const MOCK_URLS = 'https://main--bacom-sandbox--adobecom.hlx.live/\nhttps://main class NxBulk extends LitElement { static properties = { _jobStatus: { state: true }, + _jobUrl: { state: true }, _baseUrls: { state: true }, _successUrls: { state: true }, _errorUrls: { state: true }, @@ -28,6 +29,7 @@ class NxBulk extends LitElement { _cancel: { state: true }, _cancelText: { state: true }, _showVersion: { state: true }, + _error: { attribute: false }, }; constructor() { @@ -47,14 +49,16 @@ class NxBulk extends LitElement { this._jobStatus = jobStatus.progress; this._jobStatus.stopped = jobStatus.state === 'stopped'; } - - if (jobStatus.data?.resources) { + if (jobStatus.cancelled) { + this.resetState(); + } else if (jobStatus.data?.resources) { this._successUrls = jobStatus.data.resources.filter( (res) => SUCCESS_CODES.includes(res.status), ); this._errorUrls = jobStatus.data.resources.filter( - (res) => !SUCCESS_CODES.includes(res.status), + (res) => res.status && !SUCCESS_CODES.includes(res.status), ); + this._baseUrls = jobStatus.data.resources.filter((res) => !res.status); } this.requestUpdate(); } @@ -83,22 +87,33 @@ class NxBulk extends LitElement { this._baseUrls = []; this._successUrls = []; this._errorUrls = []; + this._error = ''; this._jobStatus = { stopped: false, total: 0, processed: 0, failed: 0, success: 0, + notmodified: 0, }; + this._jobUrl = ''; + if (this.shadowRoot) { + const cards = this.shadowRoot.querySelectorAll('.detail-card'); + const lists = this.shadowRoot.querySelectorAll('.url-list'); + [...cards, ...lists].forEach((el) => { el.classList.remove('is-expanded'); }); + } } handleDeleteCheck() { this._isDelete = !this._isDelete; } - handleCancel() { + async handleCancel() { this._cancel = true; this._cancelText = 'Canceling'; + if (this._jobUrl && !this._jobStatus.stopped) { + await cancelJob(this._jobUrl); + } } handleToggleList(e) { @@ -126,12 +141,13 @@ class NxBulk extends LitElement { this._baseUrls = formatUrls(urls, action, hasDelete); const jobResult = await triggerJob(this._baseUrls, label); - if (jobResult.status !== 202) { + if (jobResult.error) { this.resetState(); - // TODO error message toast + this._error = jobResult.message || 'Something went wrong.'; return; } + this._jobUrl = jobResult.links.self; await this.pollJobStatus(jobResult, (status) => { this.processJobStatus(status); }); @@ -145,6 +161,10 @@ class NxBulk extends LitElement { return this._jobStatus.total || this._baseUrls.length; } + get _successCount() { + return this._jobStatus.success + this._jobStatus.notmodified; + } + get _remainingCount() { return this._totalCount - this._jobStatus.processed; } @@ -177,7 +197,7 @@ class NxBulk extends LitElement {