tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

DiscoveryStreamAdmin.jsx (27643B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
      6 import { connect } from "react-redux";
      7 import React, { useEffect } from "react";
      8 
      9 // Pref Constants
     10 const PREF_AD_SIZE_MEDIUM_RECTANGLE = "newtabAdSize.mediumRectangle";
     11 const PREF_AD_SIZE_BILLBOARD = "newtabAdSize.billboard";
     12 const PREF_AD_SIZE_LEADERBOARD = "newtabAdSize.leaderboard";
     13 const PREF_SECTIONS_ENABLED = "discoverystream.sections.enabled";
     14 const PREF_SPOC_PLACEMENTS = "discoverystream.placements.spocs";
     15 const PREF_SPOC_COUNTS = "discoverystream.placements.spocs.counts";
     16 const PREF_CONTEXTUAL_ADS_ENABLED =
     17  "discoverystream.sections.contextualAds.enabled";
     18 const PREF_CONTEXTUAL_BANNER_PLACEMENTS =
     19  "discoverystream.placements.contextualBanners";
     20 const PREF_CONTEXTUAL_BANNER_COUNTS =
     21  "discoverystream.placements.contextualBanners.counts";
     22 const PREF_UNIFIED_ADS_ENABLED = "unifiedAds.spocs.enabled";
     23 const PREF_UNIFIED_ADS_ENDPOINT = "unifiedAds.endpoint";
     24 const PREF_ALLOWED_ENDPOINTS = "discoverystream.endpoints";
     25 const PREF_OHTTP_CONFIG = "discoverystream.ohttp.configURL";
     26 const PREF_OHTTP_RELAY = "discoverystream.ohttp.relayURL";
     27 
     28 const Row = props => (
     29  <tr className="message-item" {...props}>
     30    {props.children}
     31  </tr>
     32 );
     33 
     34 function relativeTime(timestamp) {
     35  if (!timestamp) {
     36    return "";
     37  }
     38  const seconds = Math.floor((Date.now() - timestamp) / 1000);
     39  const minutes = Math.floor((Date.now() - timestamp) / 60000);
     40  if (seconds < 2) {
     41    return "just now";
     42  } else if (seconds < 60) {
     43    return `${seconds} seconds ago`;
     44  } else if (minutes === 1) {
     45    return "1 minute ago";
     46  } else if (minutes < 600) {
     47    return `${minutes} minutes ago`;
     48  }
     49  return new Date(timestamp).toLocaleString();
     50 }
     51 
     52 export class ToggleStoryButton extends React.PureComponent {
     53  constructor(props) {
     54    super(props);
     55    this.handleClick = this.handleClick.bind(this);
     56  }
     57 
     58  handleClick() {
     59    this.props.onClick(this.props.story);
     60  }
     61 
     62  render() {
     63    return <button onClick={this.handleClick}>collapse/open</button>;
     64  }
     65 }
     66 
     67 export class TogglePrefCheckbox extends React.PureComponent {
     68  constructor(props) {
     69    super(props);
     70    this.onChange = this.onChange.bind(this);
     71  }
     72 
     73  onChange(event) {
     74    this.props.onChange(this.props.pref, event.target.checked);
     75  }
     76 
     77  render() {
     78    return (
     79      <>
     80        <input
     81          type="checkbox"
     82          checked={this.props.checked}
     83          onChange={this.onChange}
     84          disabled={this.props.disabled}
     85        />{" "}
     86        {this.props.pref}{" "}
     87      </>
     88    );
     89  }
     90 }
     91 
     92 export class Personalization extends React.PureComponent {
     93  constructor(props) {
     94    super(props);
     95    this.togglePersonalization = this.togglePersonalization.bind(this);
     96  }
     97 
     98  togglePersonalization() {
     99    this.props.dispatch(
    100      ac.OnlyToMain({
    101        type: at.DISCOVERY_STREAM_PERSONALIZATION_TOGGLE,
    102      })
    103    );
    104  }
    105 
    106  render() {
    107    const { lastUpdated, initialized } = this.props.state.Personalization;
    108    return (
    109      <React.Fragment>
    110        <table>
    111          <tbody>
    112            <Row>
    113              <td colSpan="2">
    114                <TogglePrefCheckbox
    115                  checked={this.props.personalized}
    116                  pref="personalized"
    117                  onChange={this.togglePersonalization}
    118                />
    119              </td>
    120            </Row>
    121            <Row>
    122              <td className="min">Personalization Last Updated</td>
    123              <td>{relativeTime(lastUpdated) || "(no data)"}</td>
    124            </Row>
    125            <Row>
    126              <td className="min">Personalization Initialized</td>
    127              <td>{initialized ? "true" : "false"}</td>
    128            </Row>
    129          </tbody>
    130        </table>
    131      </React.Fragment>
    132    );
    133  }
    134 }
    135 
    136 export class DiscoveryStreamAdminUI extends React.PureComponent {
    137  constructor(props) {
    138    super(props);
    139    this.restorePrefDefaults = this.restorePrefDefaults.bind(this);
    140    this.setConfigValue = this.setConfigValue.bind(this);
    141    this.expireCache = this.expireCache.bind(this);
    142    this.refreshCache = this.refreshCache.bind(this);
    143    this.showPlaceholder = this.showPlaceholder.bind(this);
    144    this.idleDaily = this.idleDaily.bind(this);
    145    this.systemTick = this.systemTick.bind(this);
    146    this.syncRemoteSettings = this.syncRemoteSettings.bind(this);
    147    this.onStoryToggle = this.onStoryToggle.bind(this);
    148    this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this);
    149    this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this);
    150    this.resetBlocks = this.resetBlocks.bind(this);
    151    this.refreshInferredPersonalization =
    152      this.refreshInferredPersonalization.bind(this);
    153    this.refreshTopicSelectionCache =
    154      this.refreshTopicSelectionCache.bind(this);
    155    this.handleSectionsToggle = this.handleSectionsToggle.bind(this);
    156    this.toggleIABBanners = this.toggleIABBanners.bind(this);
    157    this.handleAllizomToggle = this.handleAllizomToggle.bind(this);
    158    this.sendConversionEvent = this.sendConversionEvent.bind(this);
    159    this.state = {
    160      toggledStories: {},
    161      weatherQuery: "",
    162    };
    163  }
    164 
    165  setConfigValue(configName, configValue) {
    166    this.props.dispatch(
    167      ac.OnlyToMain({
    168        type: at.DISCOVERY_STREAM_CONFIG_SET_VALUE,
    169        data: { name: configName, value: configValue },
    170      })
    171    );
    172  }
    173 
    174  restorePrefDefaults() {
    175    this.props.dispatch(
    176      ac.OnlyToMain({
    177        type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS,
    178      })
    179    );
    180  }
    181 
    182  refreshCache() {
    183    const { config } = this.props.state.DiscoveryStream;
    184    this.props.dispatch(
    185      ac.OnlyToMain({
    186        type: at.DISCOVERY_STREAM_CONFIG_CHANGE,
    187        data: config,
    188      })
    189    );
    190  }
    191 
    192  refreshInferredPersonalization() {
    193    this.props.dispatch(
    194      ac.OnlyToMain({
    195        type: at.INFERRED_PERSONALIZATION_REFRESH,
    196      })
    197    );
    198  }
    199 
    200  refreshTopicSelectionCache() {
    201    this.props.dispatch(
    202      ac.SetPref("discoverystream.topicSelection.onboarding.displayCount", 0)
    203    );
    204    this.props.dispatch(
    205      ac.SetPref("discoverystream.topicSelection.onboarding.maybeDisplay", true)
    206    );
    207  }
    208 
    209  dispatchSimpleAction(type) {
    210    this.props.dispatch(
    211      ac.OnlyToMain({
    212        type,
    213      })
    214    );
    215  }
    216 
    217  resetBlocks() {
    218    this.props.dispatch(
    219      ac.OnlyToMain({
    220        type: at.DISCOVERY_STREAM_DEV_BLOCKS_RESET,
    221      })
    222    );
    223  }
    224 
    225  systemTick() {
    226    this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYSTEM_TICK);
    227  }
    228 
    229  expireCache() {
    230    this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_EXPIRE_CACHE);
    231  }
    232 
    233  showPlaceholder() {
    234    this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SHOW_PLACEHOLDER);
    235  }
    236 
    237  idleDaily() {
    238    this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_IDLE_DAILY);
    239  }
    240 
    241  syncRemoteSettings() {
    242    this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYNC_RS);
    243  }
    244 
    245  handleWeatherUpdate(e) {
    246    this.setState({ weatherQuery: e.target.value || "" });
    247  }
    248 
    249  handleWeatherSubmit(e) {
    250    e.preventDefault();
    251    const { weatherQuery } = this.state;
    252    this.props.dispatch(ac.SetPref("weather.query", weatherQuery));
    253  }
    254 
    255  toggleIABBanners(e) {
    256    const { pressed, id } = e.target;
    257 
    258    // Set the active pref to true/false
    259    switch (id) {
    260      case "newtab_billboard":
    261        // Update boolean pref for billboard ad size
    262        this.props.dispatch(ac.SetPref(PREF_AD_SIZE_BILLBOARD, pressed));
    263 
    264        break;
    265      case "newtab_leaderboard":
    266        // Update boolean pref for billboard ad size
    267        this.props.dispatch(ac.SetPref(PREF_AD_SIZE_LEADERBOARD, pressed));
    268 
    269        break;
    270      case "newtab_rectangle":
    271        // Update boolean pref for mediumRectangle (MREC) ad size
    272        this.props.dispatch(ac.SetPref(PREF_AD_SIZE_MEDIUM_RECTANGLE, pressed));
    273 
    274        break;
    275    }
    276 
    277    // Note: The counts array is passively updated whenever the placements array is updated.
    278    // The default pref values for each are:
    279    // PREF_SPOC_PLACEMENTS: "newtab_spocs"
    280    // PREF_SPOC_COUNTS: "6"
    281    const generateSpocPrefValues = () => {
    282      const placements =
    283        this.props.otherPrefs[PREF_SPOC_PLACEMENTS]?.split(",")
    284          .map(item => item.trim())
    285          .filter(item => item) || [];
    286 
    287      const counts =
    288        this.props.otherPrefs[PREF_SPOC_COUNTS]?.split(",")
    289          .map(item => item.trim())
    290          .filter(item => item) || [];
    291 
    292      // Confirm that the IAB type will have a count value of "1"
    293      const supportIABAdTypes = [
    294        "newtab_leaderboard",
    295        "newtab_rectangle",
    296        "newtab_billboard",
    297      ];
    298      let countValue;
    299      if (supportIABAdTypes.includes(id)) {
    300        countValue = "1"; // Default count value for all IAB ad types
    301      } else {
    302        throw new Error("IAB ad type not supported");
    303      }
    304 
    305      if (pressed) {
    306        // If pressed is true, add the id to the placements array
    307        if (!placements.includes(id)) {
    308          placements.push(id);
    309          counts.push(countValue);
    310        }
    311      } else {
    312        // If pressed is false, remove the id from the placements array
    313        const index = placements.indexOf(id);
    314        if (index !== -1) {
    315          placements.splice(index, 1);
    316          counts.splice(index, 1);
    317        }
    318      }
    319 
    320      return {
    321        placements: placements.join(", "),
    322        counts: counts.join(", "),
    323      };
    324    };
    325 
    326    const { placements, counts } = generateSpocPrefValues();
    327 
    328    // Update prefs with new values
    329    this.props.dispatch(ac.SetPref(PREF_SPOC_PLACEMENTS, placements));
    330    this.props.dispatch(ac.SetPref(PREF_SPOC_COUNTS, counts));
    331 
    332    // If contextual ads, sections, and one of the banners are enabled
    333    // update the contextualBanner prefs to include the banner value and count
    334    // Else, clear the prefs
    335    if (PREF_CONTEXTUAL_ADS_ENABLED && PREF_SECTIONS_ENABLED) {
    336      if (PREF_AD_SIZE_BILLBOARD && placements.includes("newtab_billboard")) {
    337        this.props.dispatch(
    338          ac.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, "newtab_billboard")
    339        );
    340        this.props.dispatch(ac.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, "1"));
    341      } else if (
    342        PREF_AD_SIZE_LEADERBOARD &&
    343        placements.includes("newtab_leaderboard")
    344      ) {
    345        this.props.dispatch(
    346          ac.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, "newtab_leaderboard")
    347        );
    348        this.props.dispatch(ac.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, "1"));
    349      } else {
    350        this.props.dispatch(ac.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, ""));
    351        this.props.dispatch(ac.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, ""));
    352      }
    353    }
    354  }
    355 
    356  handleSectionsToggle(e) {
    357    const { pressed } = e.target;
    358    this.props.dispatch(ac.SetPref(PREF_SECTIONS_ENABLED, pressed));
    359    this.props.dispatch(
    360      ac.SetPref("discoverystream.sections.cards.enabled", pressed)
    361    );
    362  }
    363 
    364  sendConversionEvent() {
    365    const detail = {
    366      partnerId: "295BEEF7-1E3B-4128-B8F8-858E12AA660B",
    367      lookbackDays: 7,
    368      impressionType: "default",
    369    };
    370    const event = new CustomEvent("FirefoxConversionNotification", {
    371      detail,
    372      bubbles: true,
    373      composed: true,
    374    });
    375    window?.dispatchEvent(event);
    376  }
    377 
    378  renderComponent(width, component) {
    379    return (
    380      <table>
    381        <tbody>
    382          <Row>
    383            <td className="min">Type</td>
    384            <td>{component.type}</td>
    385          </Row>
    386          <Row>
    387            <td className="min">Width</td>
    388            <td>{width}</td>
    389          </Row>
    390          {component.feed && this.renderFeed(component.feed)}
    391        </tbody>
    392      </table>
    393    );
    394  }
    395 
    396  renderWeatherData() {
    397    const { suggestions } = this.props.state.Weather;
    398    let weatherTable;
    399    if (suggestions) {
    400      weatherTable = (
    401        <div className="weather-section">
    402          <form onSubmit={this.handleWeatherSubmit}>
    403            <label htmlFor="weather-query">Weather query</label>
    404            <input
    405              type="text"
    406              min="3"
    407              max="10"
    408              id="weather-query"
    409              onChange={this.handleWeatherUpdate}
    410              value={this.weatherQuery}
    411            />
    412            <button type="submit">Submit</button>
    413          </form>
    414          <table>
    415            <tbody>
    416              {suggestions.map(suggestion => (
    417                <tr className="message-item" key={suggestion.city_name}>
    418                  <td className="message-id">
    419                    <span>
    420                      {suggestion.city_name} <br />
    421                    </span>
    422                  </td>
    423                  <td className="message-summary">
    424                    <pre>{JSON.stringify(suggestion, null, 2)}</pre>
    425                  </td>
    426                </tr>
    427              ))}
    428            </tbody>
    429          </table>
    430        </div>
    431      );
    432    }
    433    return weatherTable;
    434  }
    435 
    436  renderPersonalizationData() {
    437    const {
    438      inferredInterests,
    439      coarseInferredInterests,
    440      coarsePrivateInferredInterests,
    441    } = this.props.state.InferredPersonalization;
    442    return (
    443      <div>
    444        {" "}
    445        Inferred Interests:
    446        <pre>{JSON.stringify(inferredInterests, null, 2)}</pre> Coarse Inferred
    447        Interests:
    448        <pre>{JSON.stringify(coarseInferredInterests, null, 2)}</pre> Coarse
    449        Inferred Interests With Differential Privacy:
    450        <pre>{JSON.stringify(coarsePrivateInferredInterests, null, 2)}</pre>
    451      </div>
    452    );
    453  }
    454 
    455  renderFeedData(url) {
    456    const { feeds } = this.props.state.DiscoveryStream;
    457    const feed = feeds.data[url].data;
    458    return (
    459      <React.Fragment>
    460        <h4>Feed url: {url}</h4>
    461        <table>
    462          <tbody>
    463            {feed.recommendations?.map(story => this.renderStoryData(story))}
    464          </tbody>
    465        </table>
    466      </React.Fragment>
    467    );
    468  }
    469 
    470  renderFeedsData() {
    471    const { feeds } = this.props.state.DiscoveryStream;
    472    return (
    473      <React.Fragment>
    474        {Object.keys(feeds.data).map(url => this.renderFeedData(url))}
    475      </React.Fragment>
    476    );
    477  }
    478 
    479  renderImpressionsData() {
    480    const { impressions } = this.props.state.DiscoveryStream;
    481    return (
    482      <>
    483        <h4>Feed Impressions</h4>
    484        <table>
    485          <tbody>
    486            {Object.keys(impressions.feed).map(key => {
    487              return (
    488                <Row key={key}>
    489                  <td className="min">{key}</td>
    490                  <td>{relativeTime(impressions.feed[key]) || "(no data)"}</td>
    491                </Row>
    492              );
    493            })}
    494          </tbody>
    495        </table>
    496      </>
    497    );
    498  }
    499 
    500  renderBlocksData() {
    501    const { blocks } = this.props.state.DiscoveryStream;
    502    return (
    503      <>
    504        <h4>Blocks</h4>
    505        <button className="button" onClick={this.resetBlocks}>
    506          Reset Blocks
    507        </button>{" "}
    508        <table>
    509          <tbody>
    510            {Object.keys(blocks).map(key => {
    511              return (
    512                <Row key={key}>
    513                  <td className="min">{key}</td>
    514                </Row>
    515              );
    516            })}
    517          </tbody>
    518        </table>
    519      </>
    520    );
    521  }
    522 
    523  handleAllizomToggle(e) {
    524    const prefs = this.props.otherPrefs;
    525    const unifiedAdsSpocsEnabled = prefs[PREF_UNIFIED_ADS_ENABLED];
    526    if (!unifiedAdsSpocsEnabled) {
    527      return;
    528    }
    529    const { pressed } = e.target;
    530    const { dispatch } = this.props;
    531    const allowedEndpoints = prefs[PREF_ALLOWED_ENDPOINTS];
    532    const setPref = (pref = "", value = "") => {
    533      dispatch(ac.SetPref(pref, value));
    534    };
    535    const clearPref = (pref = "") => {
    536      dispatch(
    537        ac.OnlyToMain({
    538          type: at.CLEAR_PREF,
    539          data: {
    540            name: pref,
    541          },
    542        })
    543      );
    544    };
    545    if (pressed) {
    546      setPref(PREF_UNIFIED_ADS_ENDPOINT, "https://ads.allizom.org/");
    547      setPref(
    548        PREF_ALLOWED_ENDPOINTS,
    549        `${allowedEndpoints},https://ads.allizom.org/`
    550      );
    551      setPref(
    552        PREF_OHTTP_CONFIG,
    553        "https://stage.ohttp-gateway.nonprod.webservices.mozgcp.net/ohttp-configs"
    554      );
    555      setPref(
    556        PREF_OHTTP_RELAY,
    557        "https://mozilla-ohttp-relay-test.edgecompute.app/"
    558      );
    559    } else {
    560      clearPref(PREF_UNIFIED_ADS_ENDPOINT);
    561      clearPref(PREF_ALLOWED_ENDPOINTS);
    562      clearPref(PREF_OHTTP_CONFIG);
    563      clearPref(PREF_OHTTP_RELAY);
    564    }
    565  }
    566 
    567  renderSpocs() {
    568    const { spocs } = this.props.state.DiscoveryStream;
    569 
    570    const unifiedAdsSpocsEnabled =
    571      this.props.otherPrefs[PREF_UNIFIED_ADS_ENABLED];
    572 
    573    // Determine which mechanism is querying the UAPI ads server
    574    const PREF_UNIFIED_ADS_ADSFEED_ENABLED = "unifiedAds.adsFeed.enabled";
    575    const adsFeedEnabled =
    576      this.props.otherPrefs[PREF_UNIFIED_ADS_ADSFEED_ENABLED];
    577 
    578    const unifiedAdsEndpoint = this.props.otherPrefs[PREF_UNIFIED_ADS_ENDPOINT];
    579    const spocsEndpoint = unifiedAdsSpocsEnabled
    580      ? unifiedAdsEndpoint
    581      : spocs.spocs_endpoint;
    582 
    583    let spocsData = [];
    584    let allizomEnabled = spocsEndpoint?.includes("allizom");
    585 
    586    if (
    587      spocs.data &&
    588      spocs.data.newtab_spocs &&
    589      spocs.data.newtab_spocs.items
    590    ) {
    591      spocsData = spocs.data.newtab_spocs.items || [];
    592    }
    593 
    594    return (
    595      <React.Fragment>
    596        <table>
    597          <tbody>
    598            <Row>
    599              <td colSpan="2">
    600                <moz-toggle
    601                  id="sections-toggle"
    602                  disabled={!unifiedAdsSpocsEnabled || null}
    603                  pressed={allizomEnabled || null}
    604                  onToggle={this.handleAllizomToggle}
    605                  label="Toggle allizom"
    606                />
    607              </td>
    608            </Row>
    609            <Row>
    610              <td className="min">adsfeed enabled</td>
    611              <td>{adsFeedEnabled ? "true" : "false"}</td>
    612            </Row>
    613            <Row>
    614              <td className="min">spocs endpoint</td>
    615              <td>{spocsEndpoint}</td>
    616            </Row>
    617            <Row>
    618              <td className="min">Data last fetched</td>
    619              <td>{relativeTime(spocs.lastUpdated)}</td>
    620            </Row>
    621          </tbody>
    622        </table>
    623        <h4>Spoc data</h4>
    624        <table>
    625          <tbody>{spocsData.map(spoc => this.renderStoryData(spoc))}</tbody>
    626        </table>
    627        <h4>Spoc frequency caps</h4>
    628        <table>
    629          <tbody>
    630            {spocs.frequency_caps.map(spoc => this.renderStoryData(spoc))}
    631          </tbody>
    632        </table>
    633      </React.Fragment>
    634    );
    635  }
    636 
    637  onStoryToggle(story) {
    638    const { toggledStories } = this.state;
    639    this.setState({
    640      toggledStories: {
    641        ...toggledStories,
    642        [story.id]: !toggledStories[story.id],
    643      },
    644    });
    645  }
    646 
    647  renderStoryData(story) {
    648    let storyData = "";
    649    if (this.state.toggledStories[story.id]) {
    650      storyData = JSON.stringify(story, null, 2);
    651    }
    652    return (
    653      <tr className="message-item" key={story.id}>
    654        <td className="message-id">
    655          <span>
    656            {story.id} <br />
    657          </span>
    658          <ToggleStoryButton story={story} onClick={this.onStoryToggle} />
    659        </td>
    660        <td className="message-summary">
    661          <pre>{storyData}</pre>
    662        </td>
    663      </tr>
    664    );
    665  }
    666 
    667  renderFeed(feed) {
    668    const { feeds } = this.props.state.DiscoveryStream;
    669    if (!feed.url) {
    670      return null;
    671    }
    672    return (
    673      <React.Fragment>
    674        <Row>
    675          <td className="min">Feed url</td>
    676          <td>{feed.url}</td>
    677        </Row>
    678        <Row>
    679          <td className="min">Data last fetched</td>
    680          <td>
    681            {relativeTime(
    682              feeds.data[feed.url] ? feeds.data[feed.url].lastUpdated : null
    683            ) || "(no data)"}
    684          </td>
    685        </Row>
    686      </React.Fragment>
    687    );
    688  }
    689 
    690  render() {
    691    const prefToggles = "enabled collapsible".split(" ");
    692    const { config, layout } = this.props.state.DiscoveryStream;
    693    const personalized =
    694      this.props.otherPrefs["discoverystream.personalization.enabled"];
    695    const sectionsEnabled = this.props.otherPrefs[PREF_SECTIONS_ENABLED];
    696 
    697    // Prefs for IAB Banners
    698    const mediumRectangleEnabled =
    699      this.props.otherPrefs[PREF_AD_SIZE_MEDIUM_RECTANGLE];
    700    const billboardsEnabled = this.props.otherPrefs[PREF_AD_SIZE_BILLBOARD];
    701    const leaderboardEnabled = this.props.otherPrefs[PREF_AD_SIZE_LEADERBOARD];
    702    const spocPlacements = this.props.otherPrefs[PREF_SPOC_PLACEMENTS];
    703    const mediumRectangleEnabledPressed =
    704      mediumRectangleEnabled && spocPlacements.includes("newtab_rectangle");
    705    const billboardPressed =
    706      billboardsEnabled && spocPlacements.includes("newtab_billboard");
    707    const leaderboardPressed =
    708      leaderboardEnabled && spocPlacements.includes("newtab_leaderboard");
    709 
    710    return (
    711      <div>
    712        <button className="button" onClick={this.restorePrefDefaults}>
    713          Restore Pref Defaults
    714        </button>{" "}
    715        <button className="button" onClick={this.refreshCache}>
    716          Refresh Cache
    717        </button>
    718        <br />
    719        <button className="button" onClick={this.expireCache}>
    720          Expire Cache
    721        </button>{" "}
    722        <button className="button" onClick={this.systemTick}>
    723          Trigger System Tick
    724        </button>{" "}
    725        <button className="button" onClick={this.idleDaily}>
    726          Trigger Idle Daily
    727        </button>
    728        <br />
    729        <button
    730          className="button"
    731          onClick={this.refreshInferredPersonalization}
    732        >
    733          Refresh Inferred Personalization
    734        </button>
    735        <br />
    736        <button className="button" onClick={this.syncRemoteSettings}>
    737          Sync Remote Settings
    738        </button>{" "}
    739        <button className="button" onClick={this.refreshTopicSelectionCache}>
    740          Refresh Topic selection count
    741        </button>
    742        <br />
    743        <button className="button" onClick={this.showPlaceholder}>
    744          Show Placeholder Cards
    745        </button>{" "}
    746        <div className="toggle-wrapper">
    747          <moz-toggle
    748            id="sections-toggle"
    749            pressed={sectionsEnabled || null}
    750            onToggle={this.handleSectionsToggle}
    751            label="Toggle DS Sections"
    752          />
    753        </div>
    754        {/* Collapsible Sections for experiments for easy on/off */}
    755        <details className="details-section">
    756          <summary>IAB Banner Ad Sizes</summary>
    757          <div className="toggle-wrapper">
    758            <moz-toggle
    759              id="newtab_leaderboard"
    760              pressed={leaderboardPressed || null}
    761              onToggle={this.toggleIABBanners}
    762              label="Enable IAB Leaderboard"
    763            />
    764          </div>
    765          <div className="toggle-wrapper">
    766            <moz-toggle
    767              id="newtab_billboard"
    768              pressed={billboardPressed || null}
    769              onToggle={this.toggleIABBanners}
    770              label="Enable IAB Billboard"
    771            />
    772          </div>
    773          <div className="toggle-wrapper">
    774            <moz-toggle
    775              id="newtab_rectangle"
    776              pressed={mediumRectangleEnabledPressed || null}
    777              onToggle={this.toggleIABBanners}
    778              label="Enable IAB Medium Rectangle (MREC)"
    779            />
    780          </div>
    781        </details>
    782        <button className="button" onClick={this.sendConversionEvent}>
    783          Send conversion event
    784        </button>
    785        <table>
    786          <tbody>
    787            {prefToggles.map(pref => (
    788              <Row key={pref}>
    789                <td>
    790                  <TogglePrefCheckbox
    791                    checked={config[pref]}
    792                    pref={pref}
    793                    onChange={this.setConfigValue}
    794                  />
    795                </td>
    796              </Row>
    797            ))}
    798          </tbody>
    799        </table>
    800        <h3>Layout</h3>
    801        {layout.map((row, rowIndex) => (
    802          <div key={`row-${rowIndex}`}>
    803            {row.components.map((component, componentIndex) => (
    804              <div key={`component-${componentIndex}`} className="ds-component">
    805                {this.renderComponent(row.width, component)}
    806              </div>
    807            ))}
    808          </div>
    809        ))}
    810        <h3>Personalization</h3>
    811        <Personalization
    812          personalized={personalized}
    813          dispatch={this.props.dispatch}
    814          state={{
    815            Personalization: this.props.state.Personalization,
    816          }}
    817        />
    818        <h3>Spocs</h3>
    819        {this.renderSpocs()}
    820        <h3>Feeds Data</h3>
    821        <div className="large-data-container">{this.renderFeedsData()}</div>
    822        <h3>Impressions Data</h3>
    823        <div className="large-data-container">
    824          {this.renderImpressionsData()}
    825        </div>
    826        <h3>Blocked Data</h3>
    827        <div className="large-data-container">{this.renderBlocksData()}</div>
    828        <h3>Weather Data</h3>
    829        {this.renderWeatherData()}
    830        <h3>Personalization Data</h3>
    831        {this.renderPersonalizationData()}
    832      </div>
    833    );
    834  }
    835 }
    836 
    837 export class DiscoveryStreamAdminInner extends React.PureComponent {
    838  constructor(props) {
    839    super(props);
    840    this.setState = this.setState.bind(this);
    841  }
    842 
    843  render() {
    844    return (
    845      <div
    846        className={`discoverystream-admin ${
    847          this.props.collapsed ? "collapsed" : "expanded"
    848        }`}
    849      >
    850        <main className="main-panel">
    851          <h1>Discovery Stream Admin</h1>
    852 
    853          <p className="helpLink">
    854            <span className="icon icon-small-spacer icon-info" />{" "}
    855            <span>
    856              Need to access the ASRouter Admin dev tools?{" "}
    857              <a target="blank" href="about:asrouter">
    858                Click here
    859              </a>
    860            </span>
    861          </p>
    862 
    863          <React.Fragment>
    864            <DiscoveryStreamAdminUI
    865              state={{
    866                DiscoveryStream: this.props.DiscoveryStream,
    867                Personalization: this.props.Personalization,
    868                Weather: this.props.Weather,
    869                InferredPersonalization: this.props.InferredPersonalization,
    870              }}
    871              otherPrefs={this.props.Prefs.values}
    872              dispatch={this.props.dispatch}
    873            />
    874          </React.Fragment>
    875        </main>
    876      </div>
    877    );
    878  }
    879 }
    880 
    881 export function CollapseToggle(props) {
    882  const { devtoolsCollapsed } = props;
    883  const label = `${devtoolsCollapsed ? "Expand" : "Collapse"} devtools`;
    884 
    885  useEffect(() => {
    886    // Set or remove body class depending on devtoolsCollapsed state
    887    if (devtoolsCollapsed) {
    888      globalThis.document.body.classList.remove("no-scroll");
    889    } else {
    890      globalThis.document.body.classList.add("no-scroll");
    891    }
    892 
    893    // Cleanup on unmount
    894    return () => {
    895      globalThis.document.body.classList.remove("no-scroll");
    896    };
    897  }, [devtoolsCollapsed]);
    898 
    899  return (
    900    <>
    901      <a
    902        href={devtoolsCollapsed ? "#devtools" : "#"}
    903        title={label}
    904        aria-label={label}
    905        className={`discoverystream-admin-toggle ${
    906          devtoolsCollapsed ? "expanded" : "collapsed"
    907        }`}
    908      >
    909        <span className="icon icon-devtools" />
    910      </a>
    911      {!devtoolsCollapsed ? (
    912        <DiscoveryStreamAdminInner {...props} collapsed={devtoolsCollapsed} />
    913      ) : null}
    914    </>
    915  );
    916 }
    917 
    918 const _DiscoveryStreamAdmin = props => <CollapseToggle {...props} />;
    919 
    920 export const DiscoveryStreamAdmin = connect(state => ({
    921  Sections: state.Sections,
    922  DiscoveryStream: state.DiscoveryStream,
    923  Personalization: state.Personalization,
    924  InferredPersonalization: state.InferredPersonalization,
    925  Prefs: state.Prefs,
    926  Weather: state.Weather,
    927 }))(_DiscoveryStreamAdmin);