blob: 486c8972f2790daa27a751850c2930719c5e10a2 [file] [log] [blame]
import {Operator, getUrlParameter} from './utils';
import {LitElement, html, css, render} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {StorageUtil} from './storage';
function nextTick(ts: number) {
return new Promise(resolve => {
setTimeout(resolve, ts);
});
}
// Since content-script can not access window.Gerrit,
// here checks the readiness based on #mainHeader element
// Wait at most 5s before considering it as loaded.
const MAX_WAIT_TIME = 5000;
const getHeaderEl = () => {
if (document.querySelector('#app')) {
return document
.querySelector('#app')
.shadowRoot.querySelector('#app-element')
.shadowRoot.querySelector('#mainHeader');
} else {
return document
.querySelector('gr-app')
.shadowRoot.querySelector('gr-app-element')
.shadowRoot.querySelector('gr-main-header');
}
};
const onGerritReady = async () => {
let header;
try {
header = getHeaderEl();
} catch (e) {}
let waitTime = 0;
while (!header) {
if (waitTime > MAX_WAIT_TIME) break;
waitTime += 1000;
await nextTick(1000);
try {
header = getHeaderEl();
} catch (e) {}
}
return true;
};
let numOfSnackBars = 0;
@customElement('gdh-snackbar')
export class HelperSnackbar extends LitElement {
@property({type: String}) message = '';
@property({type: Number}) position = 0;
render() {
this.style.top = `${this.position * 40}px`;
return html`<div>${this.message}</div>`;
}
static get styles() {
return css`
:host {
position: absolute;
top: 0px;
right: 0px;
background-color: black;
color: white;
padding: 10px;
z-index: 100;
}
`;
}
}
@customElement('gdh-tip')
export class HelperTip extends LitElement {
@state() numErrors = 0;
constructor() {
super();
this.interceptErrors();
}
interceptErrors() {
let original = console.error;
console.error = (...args) => {
original.call(console, ...args);
this.numErrors++;
};
}
render() {
const errors = this.numErrors > 0 ? ` (${this.numErrors} js errors)` : '';
return html`<div>Gerrit dev helper is enabled ${errors}</div>`;
}
static get styles() {
return css`
:host {
z-index: 10000;
display: block;
position: fixed;
bottom: 0;
right: 0;
background-color: red;
}
div {
color: white;
font-weight: bold;
padding: 10px;
}
`;
}
}
// Apply injection rules to Gerrit sites if enabled
chrome.runtime.sendMessage({type: 'isEnabled'}, async isEnabled => {
if (!isEnabled) return;
console.log('Gerrit FE Dev Helper is enabled.');
render(html`<gdh-tip></gdh-tip>`, document.body);
const storage = new StorageUtil();
const rules = await storage.getRules();
for (const rule of rules) {
if (rule.disabled) return;
if (rule.operator === Operator.INJECT_HTML_CODE) {
const el = document.createElement('div');
el.innerHTML = rule.destination;
document.body.appendChild(el);
} else if (rule.operator === Operator.INJECT_JS_PLUGIN) {
onGerritReady().then(() => {
const link = document.createElement('script');
link.setAttribute('src', rule.destination);
link.setAttribute('crossorigin', 'anonymous');
document.head.appendChild(link);
});
} else if (rule.operator === Operator.INJECT_JS_MODULE_PLUGIN) {
onGerritReady().then(() => {
const link = document.createElement('script');
link.setAttribute('type', 'module');
link.setAttribute('src', rule.destination);
link.setAttribute('crossorigin', 'anonymous');
document.head.appendChild(link);
});
} else if (rule.operator === Operator.INJECT_EXP) {
const exps = getUrlParameter('experiment');
const hasSearchString = !!window.location.search;
const injectedExpNotInExps = new Set(
rule.destination
.trim()
.split(',')
.filter(exp => !exps.includes(exp.trim()))
);
if (injectedExpNotInExps.size) {
const addedParams = [...injectedExpNotInExps].reduce(
(str, exp) => (str += `experiment=${exp}&`),
''
);
window.location.href += hasSearchString
? `&${addedParams}`
: `?${addedParams}`;
}
}
}
// Test redirect rules. Show a warning, if they are obviously not working as intended.
for (const rule of rules) {
if (
rule.operator === Operator.REDIRECT &&
!rule.disabled &&
// only test for js/html
/\.(js|html)+$/.test(rule.destination)
) {
fetch(rule.destination)
.then(res => {
if (res.status < 200 || res.status >= 300)
throw new Error('Resource not found');
})
.catch(e => {
const message = `You may have an invalid redirect rule from ${rule.target} to ${rule.destination}`;
const id = `snack-${numOfSnackBars}`;
render(
html`
<gdh-snackbar
id=${id}
.message=${message}
.position=${numOfSnackBars}
></gdh-snackbar>
`,
document.body
);
numOfSnackBars++;
// in case body is unresolved
document.body.style.display = 'block';
document.body.style.opacity = '1';
setTimeout(() => {
document.getElementById(id)?.remove();
numOfSnackBars--;
}, 10 * 1000);
});
}
}
});