/* 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 collapse/open ;
}
}
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 (
Type
{component.type}
Width
{width}
{component.feed && this.renderFeed(component.feed)}
);
}
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 (
Restore Pref Defaults
{" "}
Refresh Cache
Expire Cache
{" "}
Trigger System Tick
{" "}
Trigger Idle Daily
Sync Remote Settings
{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);