/* 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 {
actionCreators as ac,
actionTypes as at,
} from "common/Actions.sys.mjs";
import { ASRouterUtils } from "../../asrouter/asrouter-utils";
import { connect } from "react-redux";
import React from "react";
import { SimpleHashRouter } from "./SimpleHashRouter";
import { CopyButton } from "./CopyButton";
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();
}
const LAYOUT_VARIANTS = {
basic: "Basic default layout (on by default in nightly)",
staging_spocs: "A layout with all spocs shown",
"dev-test-all":
"A little bit of everything. Good layout for testing all components",
"dev-test-feeds": "Stress testing for slow feeds",
};
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 collapse/open ;
}
}
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 Personalization extends React.PureComponent {
constructor(props) {
super(props);
this.togglePersonalization = this.togglePersonalization.bind(this);
}
togglePersonalization() {
this.props.dispatch(
ac.OnlyToMain({
type: at.DISCOVERY_STREAM_PERSONALIZATION_TOGGLE,
})
);
}
render() {
const { lastUpdated, initialized } = this.props.state.Personalization;
return (
Personalization Last Updated
{relativeTime(lastUpdated) || "(no data)"}
Personalization Initialized
{initialized ? "true" : "false"}
);
}
}
export class DiscoveryStreamAdmin extends React.PureComponent {
constructor(props) {
super(props);
this.restorePrefDefaults = this.restorePrefDefaults.bind(this);
this.setConfigValue = this.setConfigValue.bind(this);
this.expireCache = this.expireCache.bind(this);
this.refreshCache = this.refreshCache.bind(this);
this.idleDaily = this.idleDaily.bind(this);
this.systemTick = this.systemTick.bind(this);
this.syncRemoteSettings = this.syncRemoteSettings.bind(this);
this.changeEndpointVariant = this.changeEndpointVariant.bind(this);
this.onStoryToggle = this.onStoryToggle.bind(this);
this.state = {
toggledStories: {},
};
}
setConfigValue(name, value) {
this.props.dispatch(
ac.OnlyToMain({
type: at.DISCOVERY_STREAM_CONFIG_SET_VALUE,
data: { name, value },
})
);
}
restorePrefDefaults(event) {
this.props.dispatch(
ac.OnlyToMain({
type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS,
})
);
}
refreshCache() {
const { config } = this.props.state.DiscoveryStream;
this.props.dispatch(
ac.OnlyToMain({
type: at.DISCOVERY_STREAM_CONFIG_CHANGE,
data: config,
})
);
}
dispatchSimpleAction(type) {
this.props.dispatch(
ac.OnlyToMain({
type,
})
);
}
systemTick() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYSTEM_TICK);
}
expireCache() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_EXPIRE_CACHE);
}
idleDaily() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_IDLE_DAILY);
}
syncRemoteSettings() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYNC_RS);
}
changeEndpointVariant(event) {
const endpoint = this.props.state.DiscoveryStream.config.layout_endpoint;
if (endpoint) {
this.setConfigValue(
"layout_endpoint",
endpoint.replace(
/layout_variant=.+/,
`layout_variant=${event.target.value}`
)
);
}
}
renderComponent(width, component) {
return (
Type
{component.type}
Width
{width}
{component.feed && this.renderFeed(component.feed)}
);
}
isCurrentVariant(id) {
const endpoint = this.props.state.DiscoveryStream.config.layout_endpoint;
const isMatch = endpoint && !!endpoint.match(`layout_variant=${id}`);
return isMatch;
}
renderFeedData(url) {
const { feeds } = this.props.state.DiscoveryStream;
const feed = feeds.data[url].data;
return (
Feed url: {url}
{feed.recommendations?.map(story => this.renderStoryData(story))}
);
}
renderFeedsData() {
const { feeds } = this.props.state.DiscoveryStream;
return (
{Object.keys(feeds.data).map(url => this.renderFeedData(url))}
);
}
renderSpocs() {
const { spocs } = this.props.state.DiscoveryStream;
let spocsData = [];
if (spocs.data && spocs.data.spocs && spocs.data.spocs.items) {
spocsData = spocs.data.spocs.items || [];
}
return (
spocs_endpoint
{spocs.spocs_endpoint}
Data last fetched
{relativeTime(spocs.lastUpdated)}
Spoc data
{spocsData.map(spoc => this.renderStoryData(spoc))}
Spoc frequency caps
{spocs.frequency_caps.map(spoc => this.renderStoryData(spoc))}
);
}
onStoryToggle(story) {
const { toggledStories } = this.state;
this.setState({
toggledStories: {
...toggledStories,
[story.id]: !toggledStories[story.id],
},
});
}
renderStoryData(story) {
let storyData = "";
if (this.state.toggledStories[story.id]) {
storyData = JSON.stringify(story, null, 2);
}
return (
{story.id}
{storyData}
);
}
renderFeed(feed) {
const { feeds } = this.props.state.DiscoveryStream;
if (!feed.url) {
return null;
}
return (
Feed url
{feed.url}
Data last fetched
{relativeTime(
feeds.data[feed.url] ? feeds.data[feed.url].lastUpdated : null
) || "(no data)"}
);
}
render() {
const prefToggles = "enabled hardcoded_layout show_spocs collapsible".split(
" "
);
const { config, lastUpdated, layout } = this.props.state.DiscoveryStream;
const personalized =
this.props.otherPrefs["discoverystream.personalization.enabled"];
return (
Restore Pref Defaults
{" "}
Refresh Cache
Expire Cache
{" "}
Trigger System Tick
{" "}
Trigger Idle Daily
Sync Remote Settings
{prefToggles.map(pref => (
))}
Endpoint variant
You can also change this manually by changing this pref:{" "}
browser.newtabpage.activity-stream.discoverystream.config
Caching info
Data last fetched
{relativeTime(lastUpdated) || "(no data)"}
Layout
{layout.map((row, rowIndex) => (
{row.components.map((component, componentIndex) => (
{this.renderComponent(row.width, component)}
))}
))}
Personalization
Spocs
{this.renderSpocs()}
Feeds Data
{this.renderFeedsData()}
);
}
}
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);
}
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);
this.props.notifyContent({
message: state.message,
});
});
}
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}
{isBlocked ? "Unblock" : "Block"}
{
// eslint-disable-next-line no-nested-ternary
isBlocked ? null : isModified ? (
this.resetJSON(msg)}
>
Reset
) : (
Show
)
}
{isBlocked ? null : (
this.modifyJson(msg)}
>
Modify
)}
{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);
this.props.notifyContent({
message: state.message,
});
});
}
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}
/>
{isBlocked ? "Unblock" : "Block"}
this.resetPBJSON(msg)}
>
Reset JSON
);
}
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 (
this.toggleAllMessages(messagesToShow)}
>
Collapse/Expand All
{" "}
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 (
Unblock All Snippets
Show messages from{" "}
all providers
{this.state.providers.map(provider => (
{provider.id}
))}
{this.state.messageFilter !== "all" &&
!this.state.messageFilter.includes("_local_testing") ? (
Reset All
) : null}
);
}
renderMessageGroupsFilter() {
if (!this.state.groups) {
return null;
}
return (
Show messages from {/* eslint-disable-next-line jsx-a11y/no-onchange */}
all groups
{this.state.groups.map(group => (
{group.id}
))}
);
}
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 (
);
}
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 (
);
}
onChangeAttributionParameters(event) {
const { name, value } = event.target;
this.setState(({ attributionParameters }) => {
const updatedParameters = { ...attributionParameters };
updatedParameters[name] = value;
return { attributionParameters: updatedParameters };
});
}
setAttribution(e) {
ASRouterUtils.sendMessage({
type: "FORCE_ATTRIBUTION",
data: this.state.attributionParameters,
}).then(this.setStateFromParent);
}
_getGroupImpressionsCount(id, frequency) {
if (frequency) {
return this.state.groupImpressions[id]
? this.state.groupImpressions[id].length
: 0;
}
return "n/a";
}
renderDiscoveryStream() {
const { config } = this.props.DiscoveryStream;
return (
Enabled
{config.enabled ? "yes" : "no"}
Endpoint
{config.endpoint || "(empty)"}
);
}
renderAttributionParamers() {
return (
Attribution Parameters
{" "}
This forces the browser to set some attribution parameters, useful for
testing the Return To AMO feature. Clicking on 'Force Attribution',
with the default values in each field, will demo the Return To AMO
flow with the addon called 'uBlock Origin'. If you wish to try
different attribution parameters, enter them in the text boxes. If you
wish to try a different addon with the Return To AMO flow, make sure
the 'content' text box has a string that is 'rta:base64(addonID)', the
base64 string of the addonID prefixed with 'rta:'. The addon must
currently be a recommended addon on AMO. Then click 'Force
Attribution'. Clicking on 'Force Attribution' with blank text boxes
reset attribution data.
);
}
renderErrorMessage({ id, errors }) {
const providerId = {id} ;
// .reverse() so that the last error (most recent) is first
return errors
.map(({ error, timestamp }, cellKey) => (
{cellKey === errors.length - 1 ? providerId : null}
{error.message}
{relativeTime(timestamp)}
))
.reverse();
}
renderErrors() {
const providersWithErrors =
this.state.providers &&
this.state.providers.filter(p => p.errors && p.errors.length);
if (providersWithErrors && providersWithErrors.length) {
return (
Provider ID
Message
Timestamp
{providersWithErrors.map(this.renderErrorMessage)}
);
}
return No errors
;
}
renderPBTab() {
if (!this.state.messages) {
return null;
}
let messagesToShow = this.state.messages.filter(
message => message.template === "pb_newtab"
);
return (
);
}
getSection() {
const [section] = this.props.location.routes;
switch (section) {
case "private":
return (
Private Browsing Messages
{this.renderPBTab()}
);
case "targeting":
return (
Targeting Utilities
Expire Cache
{" "}
(This expires the cache in ASR Targeting for bookmarks and top
sites)
{this.renderTargetingParameters()}
{this.renderAttributionParamers()}
);
case "groups":
return (
Message Groups
Reset group impressions
Enabled
Impressions count
Custom frequency
User preferences
{this.state.groups &&
this.state.groups.map(
(
{ id, enabled, frequency, userPreferences = [] },
index
) => (
{this._getGroupImpressionsCount(id, frequency)}
{JSON.stringify(frequency, null, 2)}
{userPreferences.join(", ")}
)
)}
{this.renderMessageGroupsFilter()}
{this.renderMessagesByGroup()}
);
case "ds":
return (
Discovery Stream
);
case "errors":
return (
ASRouter Errors
{this.renderErrors()}
);
default:
return (
Message Providers{" "}
Restore default prefs
{this.state.providers ? this.renderProviders() : null}
Messages
{this.renderMessageFilter()}
{this.renderMessages()}
);
}
}
render() {
return (
AS Router Admin
{" "}
Need help using these tools? Check out our{" "}
documentation
{this.getSection()}
);
}
}
export class CollapseToggle extends React.PureComponent {
constructor(props) {
super(props);
this.onCollapseToggle = this.onCollapseToggle.bind(this);
this.state = { collapsed: false };
}
get renderAdmin() {
const { props } = this;
return (
props.location.hash &&
(props.location.hash.startsWith("#asrouter") ||
props.location.hash.startsWith("#devtools"))
);
}
onCollapseToggle(e) {
e.preventDefault();
this.setState(state => ({ collapsed: !state.collapsed }));
}
setBodyClass() {
if (this.renderAdmin && !this.state.collapsed) {
global.document.body.classList.add("no-scroll");
} else {
global.document.body.classList.remove("no-scroll");
}
}
componentDidMount() {
this.setBodyClass();
}
componentDidUpdate() {
this.setBodyClass();
}
componentWillUnmount() {
global.document.body.classList.remove("no-scroll");
ASRouterUtils.removeListener(this.onMessageFromParent);
}
render() {
const { props } = this;
const { renderAdmin } = this;
const isCollapsed = this.state.collapsed || !renderAdmin;
const label = `${isCollapsed ? "Expand" : "Collapse"} devtools`;
return (
{renderAdmin ? (
) : null}
);
}
}
const _ASRouterAdmin = props => (
);
export const ASRouterAdmin = connect(state => ({
Sections: state.Sections,
DiscoveryStream: state.DiscoveryStream,
Personalization: state.Personalization,
Prefs: state.Prefs,
}))(_ASRouterAdmin);