diff --git a/tools/bulk/index.html b/tools/bulk/index.html index f04f846a..98b3f3ac 100644 --- a/tools/bulk/index.html +++ b/tools/bulk/index.html @@ -12,7 +12,7 @@ - +
diff --git a/tools/bulk/scripts.js b/tools/bulk/scripts.js index 0f889fd3..bcaf752c 100644 --- a/tools/bulk/scripts.js +++ b/tools/bulk/scripts.js @@ -1,8 +1,14 @@ const log = document.getElementById('logger'); const adminVersion = new URLSearchParams(window.location.search).get('hlx-admin-version'); const adminVersionSuffix = adminVersion ? `?hlx-admin-version=${adminVersion}` : ''; +const simpleJobStatus = { data: { resources: [] }, links: { job: '' } }; +let startTime = null; +let total = 0; +let operation = ''; +let slow = false; +let forceUpdate = false; -const append = (string, status = 'unknown') => { +const logAppend = (string, status = 'unknown') => { const p = document.createElement('p'); p.textContent = string; if (status !== 'unknown') { @@ -13,6 +19,60 @@ const append = (string, status = 'unknown') => { return p; }; +function updateBulkStatus(element, jobStatus) { + const getProgressSummary = () => { + const progress = { + processed: 0, + success: 0, + warnings: 0, + errors: 0, + }; + if (jobStatus.data && jobStatus.data.resources) { + jobStatus.data.resources.forEach((res) => { + if (res.status) progress.processed += 1; + if (res.status === 200 || res.status === 204) { + progress.success += 1; + } else if (res.status === 404) { + progress.warnings += 1; + } else if (res.status >= 500) { + progress.errors += 1; + } + }); + } + return progress; + }; + + const { + processed, + success, + warnings, + errors, + } = getProgressSummary(jobStatus); + const VERB = { + preview: 'previewed', + live: 'published', + unpreview: 'unpreviewed', + unpublish: 'unpublished', + index: 'indexed', + cache: 'purged', + }; + const [, , owner, repo] = new URL(jobStatus.links.job).pathname.split('/'); + element.innerHTML = `${success} ${warnings} ${errors} ${processed}/${total} URL(s) bulk ${VERB[operation]} [${((new Date().valueOf() - startTime.valueOf()) / 1000).toFixed(2)}s] + `; + element.querySelector('.status-success').addEventListener('click', () => { + element.querySelector('.status-details').setAttribute('aria-expanded', 'true'); + element.querySelector('.status-details textarea').textContent = jobStatus.data.resources.filter((r) => r.status === 200 || r.status === 204).map((r) => `https://main--${repo}--${owner}.aem.page${r.path}`).join('\n'); + }); + element.querySelector('.status-warning').addEventListener('click', () => { + element.querySelector('.status-details').setAttribute('aria-expanded', 'true'); + element.querySelector('.status-details textarea').textContent = jobStatus.data.resources.filter((r) => r.status === 404).map((r) => `https://main--${repo}--${owner}.aem.page${r.path}`).join('\n'); + }); + element.querySelector('.status-error').addEventListener('click', () => { + element.querySelector('.status-details').setAttribute('aria-expanded', 'true'); + element.querySelector('.status-details textarea').textContent = jobStatus.data.resources.filter((r) => r.status >= 500).map((r) => `https://main--${repo}--${owner}.aem.page${r.path}`).join('\n'); + }); +} + function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); @@ -20,16 +80,17 @@ function sleep(ms) { } document.getElementById('urls-form').addEventListener('submit', async (e) => { + document.getElementById('urls-form').disabled = true; + startTime = new Date(); e.preventDefault(); - let counter = 0; const urls = document.getElementById('urls').value .split('\n') .map((u) => u.trim()) .filter((u) => u.length > 0); - const total = urls.length; - const operation = document.getElementById('operation').dataset.value; - const slow = document.getElementById('slow').checked; - const forceUpdate = document.getElementById('force').checked; + total = urls.length; + operation = document.getElementById('operation').dataset.value; + slow = document.getElementById('slow').checked; + forceUpdate = document.getElementById('force').checked; const executeOperation = async (url) => { const { hostname, pathname } = new URL(url); @@ -48,34 +109,33 @@ document.getElementById('urls-form').addEventListener('submit', async (e) => { const resp = await fetch(adminURL, { method, }); - resp.text().then(() => { - counter += 1; - append(`${counter}/${total}: ${adminURL}`, resp.status); - document.getElementById('total').textContent = `${counter}/${total}`; + simpleJobStatus.data.resources.push({ + status: resp.status, + path: pathname, }); }; - const dequeue = async () => { + const dequeue = async (bulkLog) => { while (urls.length) { const url = urls.shift(); // eslint-disable-next-line no-await-in-loop await executeOperation(url, total); + updateBulkStatus(bulkLog, simpleJobStatus); // eslint-disable-next-line no-await-in-loop if (slow) await sleep(1500); } + updateBulkStatus(bulkLog, simpleJobStatus); + document.getElementById('urls-form').disabled = false; }; const doBulkOperation = async () => { if (total > 0) { - const VERB = { - preview: 'preview', - live: 'publish', - }; const { hostname } = new URL(urls[0]); // use first URL to determine project details const [branch, repo, owner] = hostname.split('.')[0].split('--'); - const bulkText = `$1/${total} URL(s) bulk ${VERB[operation]}ed on ${owner}/${repo} ${forceUpdate ? '(force update)' : ''}`; - const bulkLog = append(bulkText.replace('$1', 0)); + const bulkLog = logAppend(''); + updateBulkStatus(bulkLog, { data: { resources: [] }, links: { job: `https://admin.hlx.page/job/${owner}/${repo}/main/simplejob` } }); const paths = urls.map((url) => new URL(url).pathname); + const bulkResp = await fetch(`https://admin.hlx.page/${operation}/${owner}/${repo}/${branch}/*${adminVersionSuffix}`, { method: 'POST', body: JSON.stringify({ @@ -87,41 +147,24 @@ document.getElementById('urls-form').addEventListener('submit', async (e) => { }, }); if (!bulkResp.ok) { - append(`Failed to bulk ${VERB[operation]} ${paths.length} URLs on ${origin}: ${await bulkResp.text()}`); + logAppend(`Failed on ${origin}: ${await bulkResp.text()}`); } else { const { job } = await bulkResp.json(); const { name } = job; const jobStatusPoll = window.setInterval(async () => { try { - const jobResp = await fetch(`https://admin.hlx.page/job/${owner}/${repo}/${branch}/${VERB[operation]}/${name}/details`); + const verb = operation === 'live' ? 'publish' : operation; + const jobResp = await fetch(`https://admin.hlx.page/job/${owner}/${repo}/${branch}/${verb}/${name}/details`); const jobStatus = await jobResp.json(); - const { - state, - progress: { - processed = 0, - } = {}, - startTime, - stopTime, - data: { - resources = [], - } = {}, - } = jobStatus; - if (state === 'stopped') { + if (jobStatus.state === 'stopped') { // job done, stop polling window.clearInterval(jobStatusPoll); - // show job summary - resources.forEach((res) => append(`${res.path} (${res.status})`, res.status)); - bulkLog.textContent = bulkText.replace('$1', processed); - const duration = (new Date(stopTime).valueOf() - - new Date(startTime).valueOf()) / 1000; - append(`bulk ${operation} completed in ${duration}s`); - } else { - // show job progress - bulkLog.textContent = bulkText.replace('$1', processed); } + updateBulkStatus(bulkLog, jobStatus); } catch (error) { // eslint-disable-next-line no-console console.error(`failed to get status for job ${name}: ${error}`); + document.getElementById('urls-form').disabled = false; window.clearInterval(jobStatusPoll); } }, 1000); @@ -133,13 +176,16 @@ document.getElementById('urls-form').addEventListener('submit', async (e) => { // use bulk preview/publish API doBulkOperation(urls); } else { - append(`URLs: ${urls.length}`); let concurrency = ['live', 'unpublish', 'unpreview'].includes(operation) ? 40 : 3; if (slow) { concurrency = 1; } + const bulkLog = logAppend(''); + const [, repo, owner] = new URL(urls[0]).hostname.split('--'); + simpleJobStatus.data = { resources: [], links: { job: `https://admin.hlx.page/job/${owner}/${repo}/main/simplejob` } }; + updateBulkStatus(bulkLog, simpleJobStatus); for (let i = 0; i < concurrency; i += 1) { - dequeue(urls); + dequeue(bulkLog); } } }); diff --git a/tools/bulk/styles.css b/tools/bulk/styles.css index 30a5a179..58276881 100644 --- a/tools/bulk/styles.css +++ b/tools/bulk/styles.css @@ -40,4 +40,42 @@ .bulk-ops .status-light.http3::before { color: var(--yellow-900); -} \ No newline at end of file +} + +.status-pill { + display: inline-block; + padding: 0.5em 1em; + border-radius: 1.5em; + min-width: 5em; + color: #fff; + text-align: center; +} + +.status-success { + background-color: var(--green-900); +} + +.status-warning { + background-color: var(--yellow-900); +} + +.status-error { + background-color: var(--red-900); +} + +.status-details[aria-expanded="false"] { + display: none; +} + +.status-details[aria-expanded="true"] { + margin-top: 1em; + display: block; +} + +.status-details textarea { + width: 100%; + height: 400px; + font-family: var(--code-font-family); + font-size: var(--body-size-s); +} +