/* 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 { connect } from "react-redux"; import React from "react"; import { SimpleHashRouter } from "./SimpleHashRouter"; 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 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 DiscoveryStreamAdminUI 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.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); } renderComponent(width, component) { return ( {component.feed && this.renderFeed(component.feed)}
Type {component.type} Width {width}
); } 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 collapsible".split(" "); const { config, layout } = this.props.state.DiscoveryStream; const personalized = this.props.otherPrefs["discoverystream.personalization.enabled"]; return (
{" "}
{" "} {" "}
{prefToggles.map(pref => ( ))}

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 DiscoveryStreamAdminInner extends React.PureComponent { constructor(props) { super(props); this.setState = this.setState.bind(this); } render() { return (

Discovery Stream Admin

{" "} Need to access the ASRouter Admin dev tools?{" "} Click here

); } } 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("#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"); } render() { const { props } = this; const { renderAdmin } = this; const isCollapsed = this.state.collapsed || !renderAdmin; const label = `${isCollapsed ? "Expand" : "Collapse"} devtools`; return ( {renderAdmin ? ( ) : null} ); } } const _DiscoveryStreamAdmin = props => ( ); export const DiscoveryStreamAdmin = connect(state => ({ Sections: state.Sections, DiscoveryStream: state.DiscoveryStream, Personalization: state.Personalization, Prefs: state.Prefs, }))(_DiscoveryStreamAdmin);