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);