/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ import { ASRouterUtils } from "../../asrouter-utils"; import React from "react"; import ReactDOM from "react-dom"; import { SimpleHashRouter } from "./SimpleHashRouter"; import { CopyButton } from "./CopyButton"; import { ImpressionsSection } from "./ImpressionsSection"; const Row = props => ( {props.children} ); function relativeTime(timestamp) { if (!timestamp) { return ""; } const seconds = Math.floor((Date.now() - timestamp) / 1000); const minutes = Math.floor((Date.now() - timestamp) / 60000); if (seconds < 2) { return "just now"; } else if (seconds < 60) { return `${seconds} seconds ago`; } else if (minutes === 1) { return "1 minute ago"; } else if (minutes < 600) { return `${minutes} minutes ago`; } return new Date(timestamp).toLocaleString(); } export class ToggleStoryButton extends React.PureComponent { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { this.props.onClick(this.props.story); } render() { return ; } } export class ToggleMessageJSON extends React.PureComponent { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { this.props.toggleJSON(this.props.msgId); } render() { let iconName = this.props.isCollapsed ? "icon icon-arrowhead-forward-small" : "icon icon-arrowhead-down-small"; return ( ); } } export class TogglePrefCheckbox extends React.PureComponent { constructor(props) { super(props); this.onChange = this.onChange.bind(this); } onChange(event) { this.props.onChange(this.props.pref, event.target.checked); } render() { return ( <> {" "} {this.props.pref}{" "} ); } } export class ASRouterAdminInner extends React.PureComponent { constructor(props) { super(props); this.handleEnabledToggle = this.handleEnabledToggle.bind(this); this.handleUserPrefToggle = this.handleUserPrefToggle.bind(this); this.onChangeMessageFilter = this.onChangeMessageFilter.bind(this); this.onChangeMessageGroupsFilter = this.onChangeMessageGroupsFilter.bind(this); this.unblockAll = this.unblockAll.bind(this); this.handleClearAllImpressionsByProvider = this.handleClearAllImpressionsByProvider.bind(this); this.handleExpressionEval = this.handleExpressionEval.bind(this); this.onChangeTargetingParameters = this.onChangeTargetingParameters.bind(this); this.onChangeAttributionParameters = this.onChangeAttributionParameters.bind(this); this.setAttribution = this.setAttribution.bind(this); this.onCopyTargetingParams = this.onCopyTargetingParams.bind(this); this.onNewTargetingParams = this.onNewTargetingParams.bind(this); this.handleOpenPB = this.handleOpenPB.bind(this); this.selectPBMessage = this.selectPBMessage.bind(this); this.resetPBJSON = this.resetPBJSON.bind(this); this.resetPBMessageState = this.resetPBMessageState.bind(this); this.toggleJSON = this.toggleJSON.bind(this); this.toggleAllMessages = this.toggleAllMessages.bind(this); this.resetGroups = this.resetGroups.bind(this); this.onMessageFromParent = this.onMessageFromParent.bind(this); this.setStateFromParent = this.setStateFromParent.bind(this); this.setState = this.setState.bind(this); this.state = { messageFilter: "all", messageGroupsFilter: "all", collapsedMessages: [], modifiedMessages: [], selectedPBMessage: "", evaluationStatus: {}, stringTargetingParameters: null, newStringTargetingParameters: null, copiedToClipboard: false, attributionParameters: { source: "addons.mozilla.org", medium: "referral", campaign: "non-fx-button", content: `rta:${btoa("uBlock0@raymondhill.net")}`, experiment: "ua-onboarding", variation: "chrome", ua: "Google Chrome 123", dltoken: "00000000-0000-0000-0000-000000000000", }, }; } onMessageFromParent({ type, data }) { // These only exists due to onPrefChange events in ASRouter switch (type) { case "UpdateAdminState": { this.setStateFromParent(data); break; } } } setStateFromParent(data) { this.setState(data); if (!this.state.stringTargetingParameters) { const stringTargetingParameters = {}; for (const param of Object.keys(data.targetingParameters)) { stringTargetingParameters[param] = JSON.stringify( data.targetingParameters[param], null, 2 ); } this.setState({ stringTargetingParameters }); } } componentWillMount() { ASRouterUtils.addListener(this.onMessageFromParent); const endpoint = ASRouterUtils.getPreviewEndpoint(); ASRouterUtils.sendMessage({ type: "ADMIN_CONNECT_STATE", data: { endpoint }, }).then(this.setStateFromParent); } componentWillUnmount() { ASRouterUtils.removeListener(this.onMessageFromParent); } handleBlock(msg) { return () => ASRouterUtils.blockById(msg.id); } handleUnblock(msg) { return () => ASRouterUtils.unblockById(msg.id); } resetJSON(msg) { // reset the displayed JSON for the given message document.getElementById(`${msg.id}-textarea`).value = JSON.stringify( msg, null, 2 ); // remove the message from the list of modified IDs let index = this.state.modifiedMessages.indexOf(msg.id); this.setState(prevState => ({ modifiedMessages: [ ...prevState.modifiedMessages.slice(0, index), ...prevState.modifiedMessages.slice(index + 1), ], })); } handleOverride(id) { return () => ASRouterUtils.overrideMessage(id).then(state => { this.setStateFromParent(state); }); } resetPBMessageState() { // Iterate over Private Browsing messages and block/unblock each one to clear impressions const PBMessages = this.state.messages.filter( message => message.template === "pb_newtab" ); // messages from state go here PBMessages.forEach(message => { if (message?.id) { ASRouterUtils.blockById(message.id); ASRouterUtils.unblockById(message.id); } }); // Clear the selected messages & radio buttons document.getElementById("clear radio").checked = true; this.selectPBMessage("clear"); } resetPBJSON(msg) { // reset the displayed JSON for the given message document.getElementById(`${msg.id}-textarea`).value = JSON.stringify( msg, null, 2 ); } handleOpenPB() { ASRouterUtils.sendMessage({ type: "FORCE_PRIVATE_BROWSING_WINDOW", data: { message: { content: this.state.selectedPBMessage } }, }); } expireCache() { ASRouterUtils.sendMessage({ type: "EXPIRE_QUERY_CACHE" }); } resetPref() { ASRouterUtils.sendMessage({ type: "RESET_PROVIDER_PREF" }); } resetGroups(id, value) { ASRouterUtils.sendMessage({ type: "RESET_GROUPS_STATE", }).then(this.setStateFromParent); } handleExpressionEval() { const context = {}; for (const param of Object.keys(this.state.stringTargetingParameters)) { const value = this.state.stringTargetingParameters[param]; context[param] = value ? JSON.parse(value) : null; } ASRouterUtils.sendMessage({ type: "EVALUATE_JEXL_EXPRESSION", data: { expression: this.refs.expressionInput.value, context, }, }).then(this.setStateFromParent); } onChangeTargetingParameters(event) { const { name } = event.target; const { value } = event.target; this.setState(({ stringTargetingParameters }) => { let targetingParametersError = null; const updatedParameters = { ...stringTargetingParameters }; updatedParameters[name] = value; try { JSON.parse(value); } catch (e) { console.error(`Error parsing value of parameter ${name}`); targetingParametersError = { id: name }; } return { copiedToClipboard: false, evaluationStatus: {}, stringTargetingParameters: updatedParameters, targetingParametersError, }; }); } unblockAll() { return ASRouterUtils.sendMessage({ type: "UNBLOCK_ALL", }).then(this.setStateFromParent); } handleClearAllImpressionsByProvider() { const providerId = this.state.messageFilter; if (!providerId) { return; } const userPrefInfo = this.state.userPrefs; const isUserEnabled = providerId in userPrefInfo ? userPrefInfo[providerId] : true; ASRouterUtils.sendMessage({ type: "DISABLE_PROVIDER", data: providerId, }); if (!isUserEnabled) { ASRouterUtils.sendMessage({ type: "SET_PROVIDER_USER_PREF", data: { id: providerId, value: true }, }); } ASRouterUtils.sendMessage({ type: "ENABLE_PROVIDER", data: providerId, }); } handleEnabledToggle(event) { const provider = this.state.providerPrefs.find( p => p.id === event.target.dataset.provider ); const userPrefInfo = this.state.userPrefs; const isUserEnabled = provider.id in userPrefInfo ? userPrefInfo[provider.id] : true; const isSystemEnabled = provider.enabled; const isEnabling = event.target.checked; if (isEnabling) { if (!isUserEnabled) { ASRouterUtils.sendMessage({ type: "SET_PROVIDER_USER_PREF", data: { id: provider.id, value: true }, }); } if (!isSystemEnabled) { ASRouterUtils.sendMessage({ type: "ENABLE_PROVIDER", data: provider.id, }); } } else { ASRouterUtils.sendMessage({ type: "DISABLE_PROVIDER", data: provider.id, }); } this.setState({ messageFilter: "all" }); } handleUserPrefToggle(event) { const action = { type: "SET_PROVIDER_USER_PREF", data: { id: event.target.dataset.provider, value: event.target.checked }, }; ASRouterUtils.sendMessage(action); this.setState({ messageFilter: "all" }); } onChangeMessageFilter(event) { this.setState({ messageFilter: event.target.value }); } onChangeMessageGroupsFilter(event) { this.setState({ messageGroupsFilter: event.target.value }); } // Simulate a copy event that sets to clipboard all targeting paramters and values onCopyTargetingParams(event) { const stringTargetingParameters = { ...this.state.stringTargetingParameters, }; for (const key of Object.keys(stringTargetingParameters)) { // If the value is not set the parameter will be lost when we stringify if (stringTargetingParameters[key] === undefined) { stringTargetingParameters[key] = null; } } const setClipboardData = e => { e.preventDefault(); e.clipboardData.setData( "text", JSON.stringify(stringTargetingParameters, null, 2) ); document.removeEventListener("copy", setClipboardData); this.setState({ copiedToClipboard: true }); }; document.addEventListener("copy", setClipboardData); document.execCommand("copy"); } onNewTargetingParams(event) { this.setState({ newStringTargetingParameters: event.target.value }); event.target.classList.remove("errorState"); this.refs.targetingParamsEval.innerText = ""; try { const stringTargetingParameters = JSON.parse(event.target.value); this.setState({ stringTargetingParameters }); } catch (e) { event.target.classList.add("errorState"); this.refs.targetingParamsEval.innerText = e.message; } } toggleJSON(msgId) { if (this.state.collapsedMessages.includes(msgId)) { let index = this.state.collapsedMessages.indexOf(msgId); this.setState(prevState => ({ collapsedMessages: [ ...prevState.collapsedMessages.slice(0, index), ...prevState.collapsedMessages.slice(index + 1), ], })); } else { this.setState(prevState => ({ collapsedMessages: prevState.collapsedMessages.concat(msgId), })); } } handleChange(msgId) { if (!this.state.modifiedMessages.includes(msgId)) { this.setState(prevState => ({ modifiedMessages: prevState.modifiedMessages.concat(msgId), })); } } renderMessageItem(msg) { const isBlockedByGroup = this.state.groups .filter(group => msg.groups.includes(group.id)) .some(group => !group.enabled); const msgProvider = this.state.providers.find(provider => provider.id === msg.provider) || {}; const isProviderExcluded = msgProvider.exclude && msgProvider.exclude.includes(msg.id); const isMessageBlocked = this.state.messageBlockList.includes(msg.id) || this.state.messageBlockList.includes(msg.campaign); const isBlocked = isMessageBlocked || isBlockedByGroup || isProviderExcluded; const impressions = this.state.messageImpressions[msg.id] ? this.state.messageImpressions[msg.id].length : 0; const isCollapsed = this.state.collapsedMessages.includes(msg.id); const isModified = this.state.modifiedMessages.includes(msg.id); const aboutMessagePreviewSupported = [ "infobar", "spotlight", "cfr_doorhanger", ].includes(msg.template); let itemClassName = "message-item"; if (isBlocked) { itemClassName += " blocked"; } return ( {msg.id}
{ // eslint-disable-next-line no-nested-ternary isBlocked ? null : isModified ? ( ) : ( ) } {isBlocked ? null : ( )} {aboutMessagePreviewSupported ? ( `about:messagepreview?json=${encodeURIComponent(btoa(text))}` } label="Share" copiedLabel="Copied!" inputSelector={`#${msg.id}-textarea`} className={"button share"} /> ) : null}
({impressions} impressions) {isBlocked && ( Block reason: {isBlockedByGroup && " Blocked by group"} {isProviderExcluded && " Excluded by provider"} {isMessageBlocked && " Message blocked"} )}
              
            
); } selectPBMessage(msgId) { if (msgId === "clear") { this.setState({ selectedPBMessage: "", }); } else { let selected = document.getElementById(`${msgId} radio`); let msg = JSON.parse(document.getElementById(`${msgId}-textarea`).value); if (selected.checked) { this.setState({ selectedPBMessage: msg?.content, }); } else { this.setState({ selectedPBMessage: "", }); } } } modifyJson(content) { const message = JSON.parse( document.getElementById(`${content.id}-textarea`).value ); return ASRouterUtils.modifyMessageJson(message).then(state => { this.setStateFromParent(state); }); } renderPBMessageItem(msg) { const isBlocked = this.state.messageBlockList.includes(msg.id) || this.state.messageBlockList.includes(msg.campaign); const impressions = this.state.messageImpressions[msg.id] ? this.state.messageImpressions[msg.id].length : 0; const isCollapsed = this.state.collapsedMessages.includes(msg.id); let itemClassName = "message-item"; if (isBlocked) { itemClassName += " blocked"; } return ( {msg.id}

({impressions} impressions)
this.selectPBMessage(msg.id)} disabled={isBlocked} />
            
          
); } toggleAllMessages(messagesToShow) { if (this.state.collapsedMessages.length) { this.setState({ collapsedMessages: [], }); } else { Array.prototype.forEach.call(messagesToShow, msg => { this.setState(prevState => ({ collapsedMessages: prevState.collapsedMessages.concat(msg.id), })); }); } } renderMessages() { if (!this.state.messages) { return null; } const messagesToShow = this.state.messageFilter === "all" ? this.state.messages : this.state.messages.filter( message => message.provider === this.state.messageFilter && message.template !== "pb_newtab" ); return (

{" "} To modify a message, change the JSON and click 'Modify' to see your changes. Click 'Reset' to restore the JSON to the original. Click 'Share' to copy a link to the clipboard that can be used to preview the message by opening the link in Nightly/local builds.

{messagesToShow.map(msg => this.renderMessageItem(msg))}
); } renderMessagesByGroup() { if (!this.state.messages) { return null; } const messagesToShow = this.state.messageGroupsFilter === "all" ? this.state.messages.filter(m => m.groups.length) : this.state.messages.filter(message => message.groups.includes(this.state.messageGroupsFilter) ); return ( {messagesToShow.map(msg => this.renderMessageItem(msg))}
); } renderPBMessages() { if (!this.state.messages) { return null; } const messagesToShow = this.state.messages.filter( message => message.template === "pb_newtab" ); return ( {messagesToShow.map(msg => this.renderPBMessageItem(msg))}
); } renderMessageFilter() { if (!this.state.providers) { return null; } return (

Show messages from{" "} {this.state.messageFilter !== "all" && !this.state.messageFilter.includes("_local_testing") ? ( ) : null}

); } renderMessageGroupsFilter() { if (!this.state.groups) { return null; } return (

Show messages from {/* eslint-disable-next-line jsx-a11y/no-onchange */}

); } renderTableHead() { return ( Provider ID Source Cohort Last Updated ); } renderProviders() { const providersConfig = this.state.providerPrefs; const providerInfo = this.state.providers; const userPrefInfo = this.state.userPrefs; return ( {this.renderTableHead()} {providersConfig.map((provider, i) => { const isTestProvider = provider.id.includes("_local_testing"); const info = providerInfo.find(p => p.id === provider.id) || {}; const isUserEnabled = provider.id in userPrefInfo ? userPrefInfo[provider.id] : true; const isSystemEnabled = isTestProvider || provider.enabled; let label = "local"; if (provider.type === "remote") { label = ( endpoint ( {info.url} ) ); } else if (provider.type === "remote-settings") { label = `remote settings (${provider.collection})`; } else if (provider.type === "remote-experiments") { label = ( remote settings ( nimbus-desktop-experiments ) ); } let reasonsDisabled = []; if (!isSystemEnabled) { reasonsDisabled.push("system pref"); } if (!isUserEnabled) { reasonsDisabled.push("user pref"); } if (reasonsDisabled.length) { label = `disabled via ${reasonsDisabled.join(", ")}`; } return ( ); })}
{isTestProvider ? ( ) : ( )} {provider.id} {label} {provider.cohort} {info.lastUpdated ? new Date(info.lastUpdated).toLocaleString() : ""}
); } renderTargetingParameters() { // There was no error and the result is truthy const success = this.state.evaluationStatus.success && !!this.state.evaluationStatus.result; const result = JSON.stringify(this.state.evaluationStatus.result, null, 2) || "(Empty result)"; return (

Evaluate JEXL expression