Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-146930 - Rollout tool plugin #3463

Merged
merged 6 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions libs/blocks/rollout/rollout.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/* Rollout Modal styling */
.modal {
padding: 20px;
max-width: 500px;
margin: 40px auto;
text-align: center;
font-family: Arial, sans-serif;
}

.modal.warning {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
background-color: #fff3e0;
border: 1px solid #ffb74d;
border-radius: 8px;
padding: 15px 20px;
text-align: left;
}

.warning-icon {
width: 24px;
height: 24px;
background-color: #f57c00;
border-radius: 50%;
position: relative;
}

.warning-icon:before {
content: "!";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-weight: bold;
font-size: 16px;
}

.warning-text {
color: #e65100;
font-size: 20px;
font-weight: 800;
}

.warning-text-sub {
font-size: 14px;
font-weight: 500;
}

.env-label {
font-weight: 600;
margin-bottom: 10px;
color: #333;
font-size: 16px;
}

.radio-group {
display: flex;
flex-direction: column;
gap: 10px;
margin: 10px 0;
}

.radio-group label {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #fff;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
}

.radio-group label:hover {
border-color: #0078d4;
background-color: #f0f8ff;
box-shadow: 0 3px 6px rgb(0 0 0 / 10%);
}

.radio-group input[type="radio"] {
margin-right: 8px;
accent-color: #0078d4;
width: 16px;
height: 16px;
}

.button-group {
display: flex;
justify-content: center;
margin-top: 15px;
}

.rollout-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
font-size: 14px;
font-weight: bold;
color: #fff;
background: linear-gradient(135deg, #0078d4, #005a9e);
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 3px 6px rgb(0 0 0 / 10%);
}

.rollout-btn:hover {
background: linear-gradient(135deg, #005a9e, #003e6b);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgb(0 0 0 / 15%);
}

.rollout-btn:active {
background: linear-gradient(135deg, #004080, #002a57);
transform: translateY(1px);
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
}

.rollout-btn:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
box-shadow: none;
}

.rollout-btn-text {
font-size: 14px;
font-weight: normal;
color: inherit;
}
201 changes: 201 additions & 0 deletions libs/blocks/rollout/rollout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { createTag } from '../../utils/utils.js';

const urlData = {
urlBranch: '',
urlRepo: '',
urlOwner: '',
urlPathRemainder: '',
currentPageLang: '',
referrer: '',
host: '',
project: '',
};

/**
* Extracts language code from URL path
* @param {string} url - URL to parse
* @returns {string|null} Language code or null if invalid
*/
const getLanguageCode = (url) => {
if (!url) return null;

try {
const pathSegments = new URL(url).pathname.split('/').filter(Boolean);
if (pathSegments[0] !== 'langstore') return null;

const langPattern = /^[a-z]{2,3}(-[A-Za-z]{4})?(-[A-Za-z0-9]{2,3})?$/;
return langPattern.test(pathSegments[1]) ? pathSegments[1] : null;
} catch (err) {
return null;
}

Check warning on line 30 in libs/blocks/rollout/rollout.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/rollout/rollout.js#L29-L30

Added lines #L29 - L30 were not covered by tests
};

/**
* Parses URL and sets URL data
* @param {string} url - URL to parse
* @param {boolean} allowEmptyPaths - Whether to allow empty paths
* @returns {Object|null} URL data object or null if invalid
*/
const setUrlData = (url, allowEmptyPaths = false) => {
if (!url) return null;

try {
const urlParts = url.split('--');
if (urlParts.length !== 3) {
return null;
}

Check warning on line 46 in libs/blocks/rollout/rollout.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/rollout/rollout.js#L45-L46

Added lines #L45 - L46 were not covered by tests

const hlxPageIndex = urlParts[2].indexOf('.hlx.page');
const aemPageIndex = urlParts[2].indexOf('.aem.page');
const pageIndex = hlxPageIndex >= 0 ? hlxPageIndex : aemPageIndex;
const pageType = hlxPageIndex >= 0 ? '.hlx.page' : '.aem.page';

const pathLengthCheck = allowEmptyPaths ? pageType.length - 1 : pageType.length;
if (pageIndex < 0 || pageIndex + pathLengthCheck >= urlParts[2].length) {
return null;
}

Check warning on line 56 in libs/blocks/rollout/rollout.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/rollout/rollout.js#L55-L56

Added lines #L55 - L56 were not covered by tests

Object.assign(urlData, {
urlBranch: urlParts[0].slice(8), // remove "https://"
urlRepo: urlParts[1],
urlOwner: urlParts[2].slice(0, pageIndex),
urlPathRemainder: urlParts[2].slice(pageIndex + pageType.length),
currentPageLang: getLanguageCode(url),
});

return urlData;
} catch (err) {
return null;
}

Check warning on line 69 in libs/blocks/rollout/rollout.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/rollout/rollout.js#L68-L69

Added lines #L68 - L69 were not covered by tests
};

/**
* Creates and configures radio button element
* @param {string} value - Radio button value
* @param {boolean} checked - Whether radio is checked
* @returns {HTMLElement} Label containing radio button
*/
const createRadioButton = (value, checked = false) => {
const label = createTag('label');
const radio = createTag('input', {
type: 'radio',
name: 'deployTarget',
value,
required: true,
checked,
});
label.appendChild(radio);
label.appendChild(document.createTextNode(value.charAt(0).toUpperCase() + value.slice(1)));
return label;
};

/**
* Builds UI elements
* @param {HTMLElement} el - Container element
* @param {string} previewUrl - Preview URL
*/
const buildUi = async (el, previewUrl) => {
try {
const modal = createTag('div', { class: 'modal' });
const radioGroup = createTag('div', { class: 'radio-group' });
const envLabel = createTag('div', { class: 'env-label' }, 'Environment');
radioGroup.appendChild(envLabel);

radioGroup.appendChild(createRadioButton('stage', true));
radioGroup.appendChild(createRadioButton('prod'));

const rolloutBtn = createTag('button', { class: 'rollout-btn' });
rolloutBtn.append(
createTag('span', { class: 'rollout-btn-text' }, 'Rollout'),
);

rolloutBtn.addEventListener('click', () => {
const selectedEnv = document.querySelector('input[name="deployTarget"]:checked')?.value;
if (!selectedEnv) return;

const locV3ConfigUrl = new URL(
'tools/locui-create',
`https://${urlData.urlBranch}--${urlData.urlRepo}--${urlData.urlOwner}.hlx.page`,
);

const params = {
milolibs: 'milostudio-stage',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Sabya,
Sorry for bringing this a little late.
You may need to set this value dynamically, as milolibs value change depending on env. We wanted to keep prod and stage code separate.

Prod:
milolibs = milostudio

stage and lower env:
milolibs= milostudio-stage

CC: @nkthakur48 , @raga-adbe-gh

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @sabyamon Could you please have a look at the comment provided by @saurabhsircar11

ref: urlData.urlBranch,
repo: urlData.urlRepo,
owner: urlData.urlOwner,
host: urlData.host,
project: urlData.project,
env: selectedEnv,
type: 'rollout',
encodedUrls: previewUrl,
language: urlData.currentPageLang,
};

Object.entries(params).forEach(([key, value]) => {
if (value) locV3ConfigUrl.searchParams.append(key, value);
});

window.open(locV3ConfigUrl, '_blank');
});

const buttonGroup = createTag('div', { class: 'button-group' });
modal.appendChild(radioGroup);
buttonGroup.appendChild(rolloutBtn);
modal.appendChild(buttonGroup);

el.appendChild(modal);
} catch (err) {
el.innerHTML = '<div class="modal">Error building interface</div>';
}

Check warning on line 149 in libs/blocks/rollout/rollout.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/rollout/rollout.js#L148-L149

Added lines #L148 - L149 were not covered by tests
};

/**
* Sets up the rollout interface
* @param {HTMLElement} el - Container element
* @param {string} previewUrl - Preview URL
*/
const setup = async (el, previewUrl) => {
if (!el || !previewUrl) return;

const data = setUrlData(previewUrl, true);
if (!data) {
el.innerHTML = '<div class="modal">Invalid URL format</div>';
return;
}

Check warning on line 164 in libs/blocks/rollout/rollout.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/rollout/rollout.js#L162-L164

Added lines #L162 - L164 were not covered by tests
el.innerHTML = '';
await buildUi(el, previewUrl);
};

/**
* Initializes the rollout tool
* @param {HTMLElement} el - Container element
* @param {string} search - Search params string
* @returns {Promise<boolean>} Success status
*/
export default async function init(el, search = window.location.search) {
if (!el) return false;

try {
const params = new URLSearchParams(search);
const referrer = params?.get('referrer')?.trim();
const host = params?.get('host')?.trim();
const project = params?.get('project')?.trim();

if (!referrer || !host || !project) {
el.innerHTML = '<div class="modal">Missing required parameters</div>';
return false;
}

if (!referrer.includes('/langstore/')) {
el.innerHTML = '<div class="modal warning"><div class="warning-icon"></div><div class="warning-text">This page is not eligible for rollout<br><span class="warning-text-sub">Only pages under /langstore/ are eligible for rollout</span></div></div>';
return false;
}

Object.assign(urlData, { referrer, host, project });
await setup(el, referrer);
return true;
} catch (err) {
el.innerHTML = '<div class="modal">Initialization failed</div>';
return false;
}

Check warning on line 200 in libs/blocks/rollout/rollout.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/rollout/rollout.js#L198-L200

Added lines #L198 - L200 were not covered by tests
}
1 change: 1 addition & 0 deletions test/blocks/rollout/mocks/body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="rollout" data-block-status="loaded" daa-lh="b1|rollout"></div>
Loading
Loading