add routing support for selected instance

This commit is contained in:
Tao Bojlén 2019-07-21 18:05:07 +00:00
parent 17488ff8a0
commit 39d279debd
33 changed files with 524 additions and 367 deletions

View file

@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- It's now shown in the front-end if an instance wasn't crawled because of its robots.txt. - It's now shown in the front-end if an instance wasn't crawled because of its robots.txt.
- You can now link directly to instances at e.g. /instance/mastodon.social.
- Instance details now have a link to the corresponding fediverse.network page.
### Changed ### Changed
### Deprecated ### Deprecated
### Removed ### Removed

View file

@ -68,6 +68,6 @@ config :backend, :crawler,
config :backend, Backend.Scheduler, config :backend, Backend.Scheduler,
jobs: [ jobs: [
# Every 15 minutes # Every 5 minutes
{"*/15 * * * *", {Backend.Scheduler, :prune_crawls, [12, "hour"]}} {"*/5 * * * *", {Backend.Scheduler, :prune_crawls, [12, "month"]}}
] ]

View file

@ -3,6 +3,7 @@
The React frontend for [fediverse.space](https://fediverse.space). Written in Typescript. The React frontend for [fediverse.space](https://fediverse.space). Written in Typescript.
- Set the environment variable `REACT_APP_STAGING=true` when building to use the staging backend. - Set the environment variable `REACT_APP_STAGING=true` when building to use the staging backend.
- React components are organized into atoms, molecules, organisms, and screens according to [Atomic Design](http://bradfrost.com/blog/post/atomic-web-design/).
# Default README # Default README

View file

@ -32,6 +32,7 @@
"@blueprintjs/icons": "^3.9.1", "@blueprintjs/icons": "^3.9.1",
"@blueprintjs/select": "^3.9.0", "@blueprintjs/select": "^3.9.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"connected-react-router": "^6.5.2",
"cross-fetch": "^3.0.4", "cross-fetch": "^3.0.4",
"cytoscape": "^3.8.1", "cytoscape": "^3.8.1",
"cytoscape-popper": "^1.0.4", "cytoscape-popper": "^1.0.4",

View file

@ -3,11 +3,12 @@ import * as React from "react";
import { Button, Classes, Dialog } from "@blueprintjs/core"; import { Button, Classes, Dialog } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons"; import { IconNames } from "@blueprintjs/icons";
import { BrowserRouter, Route } from "react-router-dom"; import { ConnectedRouter } from "connected-react-router";
import { Nav } from "./components/Nav"; import { Route, RouteComponentProps } from "react-router-dom";
import { AboutScreen } from "./components/screens/AboutScreen"; import { Nav } from "./components/organisms/";
import { GraphScreen } from "./components/screens/GraphScreen"; import { AboutScreen, GraphScreen } from "./components/screens/";
import { DESKTOP_WIDTH_THRESHOLD } from "./constants"; import { DESKTOP_WIDTH_THRESHOLD, IInstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants";
import { history } from "./index";
interface IAppLocalState { interface IAppLocalState {
mobileDialogOpen: boolean; mobileDialogOpen: boolean;
@ -20,14 +21,19 @@ export class AppRouter extends React.Component<{}, IAppLocalState> {
public render() { public render() {
return ( return (
<BrowserRouter> <ConnectedRouter history={history}>
<div className={`${Classes.DARK} App`}> <div className={`${Classes.DARK} App`}>
<Nav /> <Nav />
<Route exact={true} path="/" component={GraphScreen} /> {/* We use `children={}` instead of `component={}` such that the graph is never unmounted */}
<Route exact={true} path="/">
<Route path={INSTANCE_DOMAIN_PATH}>
{(routeProps: RouteComponentProps<IInstanceDomainPath>) => <GraphScreen {...routeProps} />}
</Route>
</Route>
<Route path="/about" component={AboutScreen} /> <Route path="/about" component={AboutScreen} />
{this.renderMobileDialog()} {this.renderMobileDialog()}
</div> </div>
</BrowserRouter> </ConnectedRouter>
); );
} }

View file

@ -1,5 +0,0 @@
import { NonIdealState } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import * as React from "react";
export const ErrorState: React.SFC = () => <NonIdealState icon={IconNames.ERROR} title={"Something went wrong."} />;

View file

@ -1,101 +0,0 @@
import { get } from "lodash";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { selectAndLoadInstance } from "../redux/actions";
import { IAppState, IGraph } from "../redux/types";
import Cytoscape from "./Cytoscape";
import { ErrorState } from "./ErrorState";
import { FloatingResetButton } from "./FloatingResetButton";
interface IGraphProps {
graph?: IGraph;
currentInstanceName: string | null;
selectAndLoadInstance: (name: string) => void;
}
class GraphImpl extends React.Component<IGraphProps> {
private cytoscapeComponent: React.RefObject<Cytoscape>;
public constructor(props: IGraphProps) {
super(props);
this.cytoscapeComponent = React.createRef();
}
public render() {
if (!this.props.graph) {
return <ErrorState />;
}
return (
<div>
<Cytoscape
elements={this.props.graph}
onInstanceSelect={this.onInstanceSelect}
onInstanceDeselect={this.onInstanceDeselect}
ref={this.cytoscapeComponent}
/>
<FloatingResetButton onClick={this.resetGraphPosition} />
</div>
);
}
public componentDidUpdate(prevProps: IGraphProps) {
const { currentInstanceName } = this.props;
if (prevProps.currentInstanceName !== currentInstanceName) {
const cy = this.getCytoscape();
cy.$id(prevProps.currentInstanceName).unselect();
if (currentInstanceName) {
// Select instance
cy.$id(`${currentInstanceName}`).select();
// Center it
const selected = cy.$id(currentInstanceName);
cy.center(selected);
}
}
}
private resetGraphPosition = () => {
const cy = this.getCytoscape();
const { currentInstanceName } = this.props;
if (currentInstanceName) {
cy.zoom({
level: 0.2,
position: cy.$id(currentInstanceName).position()
});
} else {
cy.zoom({
level: 0.2,
position: { x: 0, y: 0 }
});
}
};
private onInstanceSelect = (domain: string) => {
this.props.selectAndLoadInstance(domain);
};
private onInstanceDeselect = () => {
this.props.selectAndLoadInstance("");
};
private getCytoscape = () => {
const cy = get(this.cytoscapeComponent, "current.cy");
if (!cy) {
throw new Error("Expected cytoscape component but did not find one.");
}
return cy;
};
}
const mapStateToProps = (state: IAppState) => ({
currentInstanceName: state.currentInstance.currentInstanceName,
graph: state.data.graph
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any)
});
const Graph = connect(
mapStateToProps,
mapDispatchToProps
)(GraphImpl);
export default Graph;

View file

@ -1,41 +0,0 @@
import * as React from "react";
import { Alignment, Button, Navbar } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { InstanceSearch } from "./InstanceSearch";
interface INavState {
aboutIsOpen: boolean;
}
export class Nav extends React.Component<{}, INavState> {
constructor(props: any) {
super(props);
this.state = { aboutIsOpen: false };
}
public render() {
const StyledLink = styled(Link)`
color: white !important;
`;
return (
<Navbar fixedToTop={true}>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>fediverse.space</Navbar.Heading>
<Navbar.Divider />
<StyledLink to="/">
<Button icon={IconNames.GLOBE_NETWORK} text="Home" minimal={true} />
</StyledLink>
<StyledLink to="/about">
<Button icon={IconNames.INFO_SIGN} text="About" minimal={true} />
</StyledLink>
</Navbar.Group>
<Navbar.Group align={Alignment.RIGHT}>
<InstanceSearch />
</Navbar.Group>
</Navbar>
);
}
}

View file

@ -1,7 +0,0 @@
import styled from "styled-components";
export const Page = styled.div`
max-width: 800px;
margin: auto;
padding: 2em;
`;

View file

@ -0,0 +1,26 @@
import * as React from "react";
import styled from "styled-components";
const Backdrop = styled.div`
position: absolute;
top: 50px;
bottom: 0;
left: 0;
right: 0;
background-color: #293742;
z-index: 100;
`;
const Container = styled.div`
max-width: 800px;
margin: auto;
padding: 2em;
`;
const Page: React.FC = ({ children }) => (
<Backdrop>
<Container>{children}</Container>
</Backdrop>
);
export default Page;

View file

@ -0,0 +1,2 @@
export { default as Page } from "./Page";
export { default as FloatingCard } from "./FloatingCard";

View file

@ -1,30 +1,32 @@
import cytoscape from "cytoscape"; import cytoscape from "cytoscape";
import popper from "cytoscape-popper";
import * as React from "react"; import * as React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import styled from "styled-components"; import styled from "styled-components";
import tippy, { Instance } from "tippy.js"; import tippy, { Instance } from "tippy.js";
import { DEFAULT_NODE_COLOR, SELECTED_NODE_COLOR } from "../constants"; import { DEFAULT_NODE_COLOR, SELECTED_NODE_COLOR } from "../../constants";
const EntireWindowDiv = styled.div` const CytoscapeContainer = styled.div`
position: absolute; width: 100%;
top: 50px; height: 100%;
bottom: 0;
right: 0;
left: 0;
`; `;
interface ICytoscapeProps { interface ICytoscapeProps {
currentNodeId: string | null;
elements: cytoscape.ElementsDefinition; elements: cytoscape.ElementsDefinition;
onInstanceSelect: (domain: string) => void; navigateToInstancePath: (domain: string) => void;
onInstanceDeselect: () => void; navigateToRoot: () => void;
} }
class Cytoscape extends React.Component<ICytoscapeProps> { class Cytoscape extends React.Component<ICytoscapeProps> {
public cy?: cytoscape.Core; private cy?: cytoscape.Core;
public shouldComponentUpdate(prevProps: ICytoscapeProps) {
// We only want to update this component if the current instance selection changes.
// We know that the `elements` prop will never change so we skip the expensive computations here.
return prevProps.currentNodeId !== this.props.currentNodeId;
}
public componentDidMount() { public componentDidMount() {
const container = ReactDOM.findDOMNode(this); const container = ReactDOM.findDOMNode(this);
cytoscape.use(popper as any);
this.cy = cytoscape({ this.cy = cytoscape({
autoungrabify: true, autoungrabify: true,
container: container as any, container: container as any,
@ -90,11 +92,11 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
"font-size": 50, "font-size": 50,
"min-zoomed-font-size": 16 "min-zoomed-font-size": 16
}) })
.selector(".hidden") .selector(".hidden") // used to hide nodes not in the neighborhood of the selected
.style({ .style({
display: "none" display: "none"
}) })
.selector(".thickEdge") .selector(".thickEdge") // when a node is selected, make edges thicker so you can actually see them
.style({ .style({
width: 2 width: 2
}) })
@ -102,8 +104,8 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
this.cy.nodes().on("select", e => { this.cy.nodes().on("select", e => {
const instanceId = e.target.data("id"); const instanceId = e.target.data("id");
if (instanceId) { if (instanceId && instanceId !== this.props.currentNodeId) {
this.props.onInstanceSelect(instanceId); this.props.navigateToInstancePath(instanceId);
} }
const neighborhood = this.cy!.$id(instanceId).closedNeighborhood(); const neighborhood = this.cy!.$id(instanceId).closedNeighborhood();
@ -119,7 +121,6 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
}); });
}); });
this.cy.nodes().on("unselect", e => { this.cy.nodes().on("unselect", e => {
this.props.onInstanceDeselect();
this.cy!.batch(() => { this.cy!.batch(() => {
this.cy!.nodes().removeClass("hidden"); this.cy!.nodes().removeClass("hidden");
this.cy!.edges().removeClass("thickEdge"); this.cy!.edges().removeClass("thickEdge");
@ -128,14 +129,17 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
this.cy.on("click", e => { this.cy.on("click", e => {
// Clicking on the background should also deselect // Clicking on the background should also deselect
const target = e.target; const target = e.target;
if (!target) { if (!target || target === this.cy || target.isEdge()) {
this.props.onInstanceDeselect(); // Go to the URL "/"
this.props.navigateToRoot();
} }
this.cy!.batch(() => {
this.cy!.nodes().removeClass("hidden");
this.cy!.edges().removeClass("thickEdge");
});
}); });
this.setNodeSelection();
}
public componentDidUpdate(prevProps: ICytoscapeProps) {
this.setNodeSelection(prevProps.currentNodeId);
} }
public componentWillUnmount() { public componentWillUnmount() {
@ -145,8 +149,47 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
} }
public render() { public render() {
return <EntireWindowDiv />; return <CytoscapeContainer />;
}
public resetGraphPosition() {
if (!this.cy) {
return;
}
const { currentNodeId } = this.props;
if (currentNodeId) {
this.cy.zoom({
level: 0.2,
position: this.cy.$id(currentNodeId).position()
});
} else {
this.cy.zoom({
level: 0.2,
position: { x: 0, y: 0 }
});
} }
} }
/**
* Updates cytoscape's internal state to match our props.
*/
private setNodeSelection = (prevNodeId?: string | null) => {
if (!this.cy) {
return;
}
if (prevNodeId) {
this.cy.$id(prevNodeId).unselect();
}
const { currentNodeId } = this.props;
if (currentNodeId) {
// Select instance
this.cy.$id(currentNodeId).select();
// Center it
const selected = this.cy.$id(currentNodeId);
this.cy.center(selected);
}
};
}
export default Cytoscape; export default Cytoscape;

View file

@ -0,0 +1,7 @@
import { NonIdealState } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import * as React from "react";
const ErrorState: React.SFC = () => <NonIdealState icon={IconNames.ERROR} title={"Something went wrong."} />;
export default ErrorState;

View file

@ -1,12 +1,13 @@
import { Button } from "@blueprintjs/core"; import { Button } from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import FloatingCard from "./FloatingCard"; import { FloatingCard } from "../atoms/";
interface IFloatingResetButtonProps { interface IFloatingResetButtonProps {
onClick?: () => any; onClick?: () => any;
} }
export const FloatingResetButton: React.FC<IFloatingResetButtonProps> = ({ onClick }) => ( const FloatingResetButton: React.FC<IFloatingResetButtonProps> = ({ onClick }) => (
<FloatingCard> <FloatingCard>
<Button icon="compass" onClick={onClick} /> <Button icon="compass" onClick={onClick} />
</FloatingCard> </FloatingCard>
); );
export default FloatingResetButton;

View file

@ -0,0 +1,3 @@
export { default as Cytoscape } from "./Cytoscape";
export { default as ErrorState } from "./ErrorState";
export { default as FloatingResetButton } from "./FloatingResetButton";

View file

@ -0,0 +1,76 @@
import * as React from "react";
import { connect } from "react-redux";
import { push } from "connected-react-router";
import { Dispatch } from "redux";
import styled from "styled-components";
import { IAppState, IGraph } from "../../redux/types";
import { domainMatchSelector } from "../../util";
import { Cytoscape, ErrorState, FloatingResetButton } from "../molecules/";
const GraphDiv = styled.div`
flex: 3;
`;
// TODO: merge this component with Cytoscape.tsx
interface IGraphProps {
graph?: IGraph;
currentInstanceName: string | null;
navigate: (path: string) => void;
}
class GraphImpl extends React.Component<IGraphProps> {
private cytoscapeComponent: React.RefObject<Cytoscape>;
public constructor(props: IGraphProps) {
super(props);
this.cytoscapeComponent = React.createRef();
}
public render() {
if (!this.props.graph) {
return <ErrorState />;
}
return (
<GraphDiv>
<Cytoscape
currentNodeId={this.props.currentInstanceName}
elements={this.props.graph}
navigateToInstancePath={this.navigateToInstancePath}
navigateToRoot={this.navigateToRoot}
ref={this.cytoscapeComponent}
/>
<FloatingResetButton onClick={this.resetGraphPosition} />
</GraphDiv>
);
}
private resetGraphPosition = () => {
if (this.cytoscapeComponent.current) {
this.cytoscapeComponent.current.resetGraphPosition();
}
};
private navigateToInstancePath = (domain: string) => {
this.props.navigate(`/instance/${domain}`);
};
private navigateToRoot = () => {
this.props.navigate("/");
};
}
const mapStateToProps = (state: IAppState) => {
const match = domainMatchSelector(state);
return {
currentInstanceName: match && match.params.domain,
graph: state.data.graph
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
navigate: (path: string) => dispatch(push(path))
});
const Graph = connect(
mapStateToProps,
mapDispatchToProps
)(GraphImpl);
export default Graph;

View file

@ -6,14 +6,15 @@ import { Button, MenuItem } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons"; import { IconNames } from "@blueprintjs/icons";
import { IItemRendererProps, ItemPredicate, Select } from "@blueprintjs/select"; import { IItemRendererProps, ItemPredicate, Select } from "@blueprintjs/select";
import { RouteComponentProps, withRouter } from "react-router"; import { push } from "connected-react-router";
import { selectAndLoadInstance } from "../redux/actions"; import { IAppState, IInstance } from "../../redux/types";
import { IAppState, IInstance } from "../redux/types"; import { domainMatchSelector } from "../../util";
interface IInstanceSearchProps extends RouteComponentProps { interface IInstanceSearchProps {
currentInstanceName: string | null; currentInstanceName: string | null;
pathname: string;
instances?: IInstance[]; instances?: IInstance[];
selectAndLoadInstance: (instanceName: string) => void; selectInstance: (instanceName: string) => void;
} }
const InstanceSelect = Select.ofType<IInstance>(); const InstanceSelect = Select.ofType<IInstance>();
@ -26,7 +27,9 @@ class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
itemRenderer={this.itemRenderer} itemRenderer={this.itemRenderer}
onItemSelect={this.onItemSelect} onItemSelect={this.onItemSelect}
itemPredicate={this.itemPredicate} itemPredicate={this.itemPredicate}
disabled={!this.props.instances || this.props.location.pathname !== "/"} disabled={
!this.props.instances || (!this.props.pathname.startsWith("/instance/") && this.props.pathname !== "/")
}
initialContent={this.renderInitialContent()} initialContent={this.renderInitialContent()}
noResults={this.renderNoResults()} noResults={this.renderNoResults()}
popoverProps={{ popoverClassName: "fediverse-instance-search-popover" }} popoverProps={{ popoverClassName: "fediverse-instance-search-popover" }}
@ -66,20 +69,23 @@ class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
}; };
private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => { private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => {
this.props.selectAndLoadInstance(item.name); this.props.selectInstance(item.name);
}; };
} }
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => {
currentInstanceName: state.currentInstance.currentInstanceName, const match = domainMatchSelector(state);
instances: state.data.instances return {
}); currentInstanceName: match && match.params.domain,
instances: state.data.instances,
pathname: state.router.location.pathname
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any) selectInstance: (domain: string) => dispatch(push(`/instance/${domain}`))
}); });
export const InstanceSearch = withRouter( const InstanceSearch = connect(
connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(InstanceSearchImpl) )(InstanceSearchImpl);
); export default InstanceSearch;

View file

@ -0,0 +1,56 @@
import * as React from "react";
import { Alignment, Navbar } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { Classes } from "@blueprintjs/core";
import { match, NavLink } from "react-router-dom";
import { InstanceSearch } from ".";
import { IInstanceDomainPath } from "../../constants";
interface INavState {
aboutIsOpen: boolean;
}
const linkIsActive = (currMatch: match<IInstanceDomainPath>, location: Location) => {
return location.pathname === "/" || location.pathname.startsWith("/instance/");
};
class Nav extends React.Component<{}, INavState> {
constructor(props: any) {
super(props);
this.state = { aboutIsOpen: false };
}
public render() {
return (
<Navbar fixedToTop={true}>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>fediverse.space</Navbar.Heading>
<Navbar.Divider />
<NavLink
to="/"
className={`${Classes.BUTTON} ${Classes.MINIMAL} bp3-icon-${IconNames.GLOBE_NETWORK}`}
activeClassName={Classes.INTENT_PRIMARY}
isActive={linkIsActive as any}
>
Home
</NavLink>
<NavLink
to="/about"
className={`${Classes.BUTTON} ${Classes.MINIMAL} bp3-icon-${IconNames.INFO_SIGN}`}
activeClassName={Classes.INTENT_PRIMARY}
exact={true}
>
About
</NavLink>
</Navbar.Group>
<Navbar.Group align={Alignment.RIGHT}>
<InstanceSearch />
</Navbar.Group>
</Navbar>
);
}
}
export default Nav;

View file

@ -3,7 +3,6 @@ import moment from "moment";
import * as numeral from "numeral"; import * as numeral from "numeral";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux";
import sanitize from "sanitize-html"; import sanitize from "sanitize-html";
import { import {
@ -27,10 +26,53 @@ import {
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons"; import { IconNames } from "@blueprintjs/icons";
import { selectAndLoadInstance } from "../redux/actions"; import { Link } from "react-router-dom";
import { IAppState, IGraph, IInstanceDetails } from "../redux/types"; import styled from "styled-components";
import FullDiv from "./atoms/FullDiv"; import { IAppState, IGraph, IInstanceDetails } from "../../redux/types";
import { ErrorState } from "./ErrorState"; import { domainMatchSelector } from "../../util";
import { ErrorState } from "../molecules/";
import { FullDiv } from "../styled-components";
interface IClosedProp {
closed?: boolean;
}
const SidebarContainer = styled.div<IClosedProp>`
position: fixed;
top: 50px;
bottom: 0;
right: ${props => (props.closed ? "-400px" : 0)};
min-width: 400px;
width: 25%;
z-index: 20;
overflow: scroll;
overflow-x: hidden;
transition-property: all;
transition-duration: 0.5s;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
@media screen and (min-width: 1600px) {
right: ${props => (props.closed ? "-25%" : 0)};
}
`;
const StyledCard = styled(Card)`
min-height: 100%;
width: 100%;
`;
const StyledButton = styled(Button)`
position: absolute;
top: 0;
left: -40px;
z-index: 20;
transition-property: all;
transition-duration: 0.5s;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
`;
const StyledHTMLTable = styled(HTMLTable)`
width: 100%;
`;
const StyledLinkToFdNetwork = styled.div`
margin-top: 3em;
text-align: center;
`;
interface ISidebarProps { interface ISidebarProps {
graph?: IGraph; graph?: IGraph;
@ -38,7 +80,6 @@ interface ISidebarProps {
instanceLoadError: boolean; instanceLoadError: boolean;
instanceDetails: IInstanceDetails | null; instanceDetails: IInstanceDetails | null;
isLoadingInstanceDetails: boolean; isLoadingInstanceDetails: boolean;
selectAndLoadInstance: (instanceName: string) => void;
} }
interface ISidebarState { interface ISidebarState {
isOpen: boolean; isOpen: boolean;
@ -63,21 +104,12 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
} }
public render() { public render() {
const closedClass = this.state.isOpen ? "" : " closed";
const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT; const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT;
return ( return (
<div> <SidebarContainer closed={!this.state.isOpen}>
<Button <StyledButton onClick={this.handleToggle} large={true} icon={buttonIcon} minimal={true} />
onClick={this.handleToggle} <StyledCard elevation={Elevation.TWO}>{this.renderSidebarContents()}</StyledCard>
large={true} </SidebarContainer>
icon={buttonIcon}
className={"fediverse-sidebar-toggle-button" + closedClass}
minimal={true}
/>
<Card className={"fediverse-sidebar" + closedClass} elevation={Elevation.TWO}>
{this.renderSidebarContents()}
</Card>
</div>
); );
} }
@ -149,6 +181,16 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} /> <Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
<Tab id="peers" title="Known peers" panel={this.renderPeers()} /> <Tab id="peers" title="Known peers" panel={this.renderPeers()} />
</Tabs> </Tabs>
<StyledLinkToFdNetwork>
<a
href={`https://fediverse.network/${this.props.instanceName}`}
target="_blank"
rel="noopener noreferrer"
className={`${Classes.BUTTON} bp3-icon-${IconNames.LINK}`}
>
See more statistics at fediverse.network
</a>
</StyledLinkToFdNetwork>
</div> </div>
); );
}; };
@ -196,7 +238,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
const { version, userCount, statusCount, domainCount, lastUpdated, insularity } = this.props.instanceDetails; const { version, userCount, statusCount, domainCount, lastUpdated, insularity } = this.props.instanceDetails;
return ( return (
<div> <div>
<HTMLTable small={true} striped={true} className="fediverse-sidebar-table"> <StyledHTMLTable small={true} striped={true}>
<tbody> <tbody>
<tr> <tr>
<td>Version</td> <td>Version</td>
@ -238,7 +280,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td> <td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
</tr> </tr>
</tbody> </tbody>
</HTMLTable> </StyledHTMLTable>
</div> </div>
); );
}; };
@ -261,9 +303,13 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
const neighborRows = orderBy(neighbors, ["weight"], ["desc"]).map((neighborDetails: any, idx: number) => ( const neighborRows = orderBy(neighbors, ["weight"], ["desc"]).map((neighborDetails: any, idx: number) => (
<tr key={idx}> <tr key={idx}>
<td> <td>
<AnchorButton minimal={true} onClick={this.selectInstance}> <Link
to={`/instance/${neighborDetails.neighbor}`}
className={`${Classes.BUTTON} ${Classes.MINIMAL}`}
role="button"
>
{neighborDetails.neighbor} {neighborDetails.neighbor}
</AnchorButton> </Link>
</td> </td>
<td>{neighborDetails.weight.toFixed(4)}</td> <td>{neighborDetails.weight.toFixed(4)}</td>
</tr> </tr>
@ -274,7 +320,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
The mention ratio is the average of how many times the two instances mention each other per status. A mention The mention ratio is the average of how many times the two instances mention each other per status. A mention
ratio of 1 would mean that every single status contained a mention of a user on the other instance. ratio of 1 would mean that every single status contained a mention of a user on the other instance.
</p> </p>
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table"> <StyledHTMLTable small={true} striped={true} interactive={false}>
<thead> <thead>
<tr> <tr>
<th>Instance</th> <th>Instance</th>
@ -282,7 +328,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
</tr> </tr>
</thead> </thead>
<tbody>{neighborRows}</tbody> <tbody>{neighborRows}</tbody>
</HTMLTable> </StyledHTMLTable>
</div> </div>
); );
}; };
@ -293,11 +339,11 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
return; return;
} }
const peerRows = peers.map(instance => ( const peerRows = peers.map(instance => (
<tr key={instance.name} onClick={this.selectInstance}> <tr key={instance.name}>
<td> <td>
<AnchorButton minimal={true} onClick={this.selectInstance}> <Link to={`/instance/${instance.name}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
{instance.name} {instance.name}
</AnchorButton> </Link>
</td> </td>
</tr> </tr>
)); ));
@ -306,9 +352,9 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<p className={Classes.TEXT_MUTED}> <p className={Classes.TEXT_MUTED}>
All the instances, past and present, that {this.props.instanceName} knows about. All the instances, past and present, that {this.props.instanceName} knows about.
</p> </p>
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table"> <StyledHTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
<tbody>{peerRows}</tbody> <tbody>{peerRows}</tbody>
</HTMLTable> </StyledHTMLTable>
</div> </div>
); );
}; };
@ -399,23 +445,17 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
private openInstanceLink = () => { private openInstanceLink = () => {
window.open("https://" + this.props.instanceName, "_blank"); window.open("https://" + this.props.instanceName, "_blank");
}; };
private selectInstance = (e: any) => {
this.props.selectAndLoadInstance(e.target.innerText);
};
} }
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => {
const match = domainMatchSelector(state);
return {
graph: state.data.graph, graph: state.data.graph,
instanceDetails: state.currentInstance.currentInstanceDetails, instanceDetails: state.currentInstance.currentInstanceDetails,
instanceLoadError: state.currentInstance.error, instanceLoadError: state.currentInstance.error,
instanceName: state.currentInstance.currentInstanceName, instanceName: match && match.params.domain,
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
}); };
const mapDispatchToProps = (dispatch: Dispatch) => ({ };
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any) const Sidebar = connect(mapStateToProps)(SidebarImpl);
}); export default Sidebar;
export const Sidebar = connect(
mapStateToProps,
mapDispatchToProps
)(SidebarImpl);

View file

@ -0,0 +1,4 @@
export { default as Graph } from "./Graph";
export { default as Sidebar } from "./Sidebar";
export { default as Nav } from "./Nav";
export { default as InstanceSearch } from "./InstanceSearch";

View file

@ -1,8 +1,8 @@
import { Classes, Code, H1, H2, H4 } from "@blueprintjs/core"; import { Classes, Code, H1, H2, H4 } from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import { Page } from "../Page"; import { Page } from "../atoms/";
export const AboutScreen: React.FC = () => ( const AboutScreen: React.FC = () => (
<Page> <Page>
<H1>About</H1> <H1>About</H1>
<p className={Classes.RUNNING_TEXT}> <p className={Classes.RUNNING_TEXT}>
@ -85,3 +85,4 @@ export const AboutScreen: React.FC = () => (
</p> </p>
</Page> </Page>
); );
export default AboutScreen;

View file

@ -1,60 +1,82 @@
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import styled from "styled-components";
import { NonIdealState, Spinner } from "@blueprintjs/core"; import { NonIdealState, Spinner } from "@blueprintjs/core";
import { fetchGraph, fetchInstances } from "../../redux/actions"; import { fetchGraph, fetchInstances, loadInstance } from "../../redux/actions";
import { IAppState, IGraph, IInstance } from "../../redux/types"; import { IAppState } from "../../redux/types";
import { ErrorState } from "../ErrorState"; import { domainMatchSelector } from "../../util";
import Graph from "../Graph"; import { ErrorState } from "../molecules/";
import { Sidebar } from "../Sidebar"; import { Graph, Sidebar } from "../organisms/";
const GraphContainer = styled.div`
display: flex;
height: 100%;
width: 100%;
`;
const FullDiv = styled.div`
position: absolute;
top: 50px;
bottom: 0;
left: 0;
right: 0;
`;
interface IGraphScreenProps { interface IGraphScreenProps {
graph?: IGraph; currentInstanceName: string | null;
instances?: IInstance[]; pathname: string;
isLoadingGraph: boolean; isLoadingGraph: boolean;
isLoadingInstances: boolean; isLoadingInstances: boolean;
graphLoadError: boolean; graphLoadError: boolean;
loadInstance: (domain: string | null) => void;
fetchInstances: () => void; fetchInstances: () => void;
fetchGraph: () => void; fetchGraph: () => void;
} }
/**
* This component takes care of loading or deselecting the current instance when the URL path changes.
*/
class GraphScreenImpl extends React.Component<IGraphScreenProps> { class GraphScreenImpl extends React.Component<IGraphScreenProps> {
public render() { public render() {
let body = <div />; let content;
if (this.props.isLoadingInstances || this.props.isLoadingGraph) { if (this.props.isLoadingInstances || this.props.isLoadingGraph) {
body = this.loadingState("Loading..."); content = this.loadingState("Loading...");
} else if (!!this.props.graphLoadError) {
content = <ErrorState />;
} else { } else {
body = this.graphState(); content = (
<GraphContainer>
<Graph />
<Sidebar />
</GraphContainer>
);
} }
return <div>{body}</div>; return <FullDiv>{content}</FullDiv>;
} }
public componentDidMount() { public componentDidMount() {
this.load(); this.loadInstancesAndGraph();
this.loadCurrentInstance();
} }
public componentDidUpdate() { public componentDidUpdate(prevProps: IGraphScreenProps) {
this.load(); this.loadCurrentInstance(prevProps.currentInstanceName);
} }
private load = () => { private loadInstancesAndGraph = () => {
if (!this.props.instances && !this.props.isLoadingInstances && !this.props.graphLoadError) { if (!this.props.isLoadingGraph && !this.props.graphLoadError) {
this.props.fetchInstances();
}
if (!this.props.graph && !this.props.isLoadingGraph && !this.props.graphLoadError) {
this.props.fetchGraph(); this.props.fetchGraph();
} }
if (!this.props.isLoadingInstances && !this.props.graphLoadError) {
this.props.fetchInstances();
}
}; };
private graphState = () => { private loadCurrentInstance = (prevInstanceName?: string | null) => {
const content = this.props.graphLoadError ? <ErrorState /> : <Graph />; if (prevInstanceName !== this.props.currentInstanceName) {
return ( this.props.loadInstance(this.props.currentInstanceName);
<div> }
<Sidebar />
{content}
</div>
);
}; };
private loadingState = (title?: string) => { private loadingState = (title?: string) => {
@ -62,18 +84,25 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps> {
}; };
} }
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => {
const match = domainMatchSelector(state);
return {
currentInstanceName: match && match.params.domain,
graph: state.data.graph, graph: state.data.graph,
graphLoadError: state.data.error, graphLoadError: state.data.error,
instances: state.data.instances, instances: state.data.instances,
isLoadingGraph: state.data.isLoadingGraph, isLoadingGraph: state.data.isLoadingGraph,
isLoadingInstances: state.data.isLoadingInstances isLoadingInstances: state.data.isLoadingInstances,
}); pathname: state.router.location.pathname
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
fetchGraph: () => dispatch(fetchGraph() as any), fetchGraph: () => dispatch(fetchGraph() as any),
fetchInstances: () => dispatch(fetchInstances() as any) fetchInstances: () => dispatch(fetchInstances() as any),
loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any)
}); });
export const GraphScreen = connect( const GraphScreen = connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(GraphScreenImpl); )(GraphScreenImpl);
export default GraphScreen;

View file

@ -0,0 +1,2 @@
export { default as AboutScreen } from "./AboutScreen";
export { default as GraphScreen } from "./GraphScreen";

View file

@ -1,6 +1,6 @@
import styled from "styled-components"; import styled from "styled-components";
export default styled.div` export const FullDiv = styled.div`
width: 100%; width: 100%;
height: 100%; height: 100%;
`; `;

View file

@ -3,3 +3,8 @@ export const DESKTOP_WIDTH_THRESHOLD = 800;
export const DEFAULT_NODE_COLOR = "#CED9E0"; export const DEFAULT_NODE_COLOR = "#CED9E0";
export const SELECTED_NODE_COLOR = "#48AFF0"; export const SELECTED_NODE_COLOR = "#48AFF0";
export const INSTANCE_DOMAIN_PATH = "/instance/:domain";
export interface IInstanceDomainPath {
domain: string;
}

View file

@ -3,7 +3,6 @@ body {
margin: 0; margin: 0;
padding: 50px 0 0 0; padding: 50px 0 0 0;
font-family: sans-serif; font-family: sans-serif;
/*background-color: #30404D;*/
background-color: #293742; background-color: #293742;
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue,
@ -15,50 +14,3 @@ body {
min-width: 300px; min-width: 300px;
overflow-x: hidden; overflow-x: hidden;
} }
.fediverse-sidebar {
position: fixed;
top: 50px;
bottom: 0;
right: 0;
min-width: 400px;
width: 25%;
z-index: 20;
overflow: scroll;
overflow-x: hidden;
transition-property: all;
transition-duration: 0.5s;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
.fediverse-sidebar.closed {
right: -400px;
}
.fediverse-sidebar-toggle-button {
position: absolute;
top: 50px;
right: 400px;
z-index: 20;
transition-property: all;
transition-duration: 0.5s;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
.fediverse-sidebar-toggle-button.closed {
right: 0;
}
.fediverse-sidebar-table {
width: 100%;
}
@media screen and (min-width: 1600px) {
.fediverse-sidebar.closed {
right: -25%;
}
.fediverse-sidebar-toggle-button {
right: 25%;
}
}

View file

@ -4,6 +4,8 @@ import "../node_modules/@blueprintjs/select/lib/css/blueprint-select.css";
import "../node_modules/normalize.css/normalize.css"; import "../node_modules/normalize.css/normalize.css";
import "./index.css"; import "./index.css";
import cytoscape from "cytoscape";
import popper from "cytoscape-popper";
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
@ -12,16 +14,26 @@ import thunk from "redux-thunk";
import { FocusStyleManager } from "@blueprintjs/core"; import { FocusStyleManager } from "@blueprintjs/core";
import { routerMiddleware } from "connected-react-router";
import { createBrowserHistory } from "history";
import { AppRouter } from "./AppRouter"; import { AppRouter } from "./AppRouter";
import { rootReducer } from "./redux/reducers"; import createRootReducer from "./redux/reducers";
// https://blueprintjs.com/docs/#core/accessibility.focus-management // https://blueprintjs.com/docs/#core/accessibility.focus-management
FocusStyleManager.onlyShowFocusOnTabs(); FocusStyleManager.onlyShowFocusOnTabs();
export const history = createBrowserHistory();
// Initialize redux // Initialize redux
// @ts-ignore // @ts-ignore
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk))); const store = createStore(
createRootReducer(history),
composeEnhancers(applyMiddleware(routerMiddleware(history), thunk))
);
// Initialize cytoscape plugins
cytoscape.use(popper as any);
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
@ -30,8 +42,8 @@ ReactDOM.render(
document.getElementById("root") as HTMLElement document.getElementById("root") as HTMLElement
); );
if (process.env.NODE_ENV !== "production") { // if (process.env.NODE_ENV !== "production") {
// tslint:disable-next-line:no-var-requires // // tslint:disable-next-line:no-var-requires
const axe = require("react-axe"); // const axe = require("react-axe");
axe(React, ReactDOM, 5000); // axe(React, ReactDOM, 5000);
} // }

View file

@ -1,13 +1,14 @@
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { push } from "connected-react-router";
import { getFromApi } from "../util"; import { getFromApi } from "../util";
import { ActionType, IGraph, IInstance, IInstanceDetails } from "./types"; import { ActionType, IAppState, IGraph, IInstance, IInstanceDetails } from "./types";
// selectInstance and deselectInstance are not exported since we only call them from selectAndLoadInstance() // requestInstanceDetails and deselectInstance are not exported since we only call them from loadInstance()
const selectInstance = (instanceName: string) => { const requestInstanceDetails = (instanceName: string) => {
return { return {
payload: instanceName, payload: instanceName,
type: ActionType.SELECT_INSTANCE type: ActionType.REQUEST_INSTANCE_DETAILS
}; };
}; };
const deselectInstance = () => { const deselectInstance = () => {
@ -71,13 +72,16 @@ export const fetchInstances = () => {
}; };
}; };
export const selectAndLoadInstance = (instanceName: string) => { export const loadInstance = (instanceName: string | null) => {
return (dispatch: Dispatch) => { return (dispatch: Dispatch, getState: () => IAppState) => {
if (!instanceName) { if (!instanceName) {
dispatch(deselectInstance()); dispatch(deselectInstance());
if (getState().router.location.pathname.startsWith("/instance/")) {
dispatch(push("/"));
}
return; return;
} }
dispatch(selectInstance(instanceName)); dispatch(requestInstanceDetails(instanceName));
return getFromApi("instances/" + instanceName) return getFromApi("instances/" + instanceName)
.then(details => dispatch(receiveInstanceDetails(details))) .then(details => dispatch(receiveInstanceDetails(details)))
.catch(e => dispatch(instanceLoadFailed())); .catch(e => dispatch(instanceLoadFailed()));

View file

@ -1,5 +1,7 @@
import { connectRouter } from "connected-react-router";
import { combineReducers } from "redux"; import { combineReducers } from "redux";
import { History } from "history";
import { ActionType, IAction, ICurrentInstanceState, IDataState } from "./types"; import { ActionType, IAction, ICurrentInstanceState, IDataState } from "./types";
const initialDataState = { const initialDataState = {
@ -46,29 +48,29 @@ const data = (state: IDataState = initialDataState, action: IAction) => {
const initialCurrentInstanceState: ICurrentInstanceState = { const initialCurrentInstanceState: ICurrentInstanceState = {
currentInstanceDetails: null, currentInstanceDetails: null,
currentInstanceName: null,
error: false, error: false,
isLoadingInstanceDetails: false isLoadingInstanceDetails: false
}; };
const currentInstance = (state = initialCurrentInstanceState, action: IAction): ICurrentInstanceState => { const currentInstance = (state = initialCurrentInstanceState, action: IAction): ICurrentInstanceState => {
switch (action.type) { switch (action.type) {
case ActionType.SELECT_INSTANCE: case ActionType.REQUEST_INSTANCE_DETAILS:
return { return {
...state, ...state,
currentInstanceName: action.payload, error: false,
isLoadingInstanceDetails: true isLoadingInstanceDetails: true
}; };
case ActionType.RECEIVE_INSTANCE_DETAILS: case ActionType.RECEIVE_INSTANCE_DETAILS:
return { return {
...state, ...state,
currentInstanceDetails: action.payload, currentInstanceDetails: action.payload,
error: false,
isLoadingInstanceDetails: false isLoadingInstanceDetails: false
}; };
case ActionType.DESELECT_INSTANCE: case ActionType.DESELECT_INSTANCE:
return { return {
...state, ...state,
currentInstanceDetails: null, currentInstanceDetails: null,
currentInstanceName: null error: false
}; };
case ActionType.INSTANCE_LOAD_ERROR: case ActionType.INSTANCE_LOAD_ERROR:
return { return {
@ -81,7 +83,10 @@ const currentInstance = (state = initialCurrentInstanceState, action: IAction):
} }
}; };
export const rootReducer = combineReducers({ export default (history: History) =>
combineReducers({
router: connectRouter(history),
// tslint:disable-next-line:object-literal-sort-keys
currentInstance, currentInstance,
data data
}); });

View file

@ -1,5 +1,7 @@
import { RouterState } from "connected-react-router";
export enum ActionType { export enum ActionType {
SELECT_INSTANCE = "SELECT_INSTANCE", REQUEST_INSTANCE_DETAILS = "REQUEST_INSTANCE_DETAILS",
REQUEST_INSTANCES = "REQUEST_INSTANCES", REQUEST_INSTANCES = "REQUEST_INSTANCES",
RECEIVE_INSTANCES = "RECEIVE_INSTANCES", RECEIVE_INSTANCES = "RECEIVE_INSTANCES",
REQUEST_GRAPH = "REQUEST_GRAPH", REQUEST_GRAPH = "REQUEST_GRAPH",
@ -60,9 +62,9 @@ export interface IGraph {
// Redux state // Redux state
// The current instance name is stored in the URL. See state -> router -> location
export interface ICurrentInstanceState { export interface ICurrentInstanceState {
currentInstanceDetails: IInstanceDetails | null; currentInstanceDetails: IInstanceDetails | null;
currentInstanceName: string | null;
isLoadingInstanceDetails: boolean; isLoadingInstanceDetails: boolean;
error: boolean; error: boolean;
} }
@ -76,6 +78,7 @@ export interface IDataState {
} }
export interface IAppState { export interface IAppState {
router: RouterState;
currentInstance: ICurrentInstanceState; currentInstance: ICurrentInstanceState;
data: IDataState; data: IDataState;
} }

View file

@ -1,4 +1,7 @@
import { createMatchSelector } from "connected-react-router";
import fetch from "cross-fetch"; import fetch from "cross-fetch";
import { IInstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants";
import { IAppState } from "./redux/types";
let API_ROOT = "http://localhost:4000/api/"; let API_ROOT = "http://localhost:4000/api/";
if (["true", true, 1, "1"].indexOf(process.env.REACT_APP_STAGING || "") > -1) { if (["true", true, 1, "1"].indexOf(process.env.REACT_APP_STAGING || "") > -1) {
@ -12,3 +15,5 @@ export const getFromApi = (path: string): Promise<any> => {
path = path.endsWith("/") ? path : path + "/"; path = path.endsWith("/") ? path : path + "/";
return fetch(domain + path).then(response => response.json()); return fetch(domain + path).then(response => response.json());
}; };
export const domainMatchSelector = createMatchSelector<IAppState, IInstanceDomainPath>(INSTANCE_DOMAIN_PATH);

View file

@ -2966,6 +2966,15 @@ connect-history-api-fallback@^1.3.0:
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
connected-react-router@^6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.5.2.tgz#422af70f86cb276681e20ab4295cf27dd9b6c7e3"
integrity sha512-qzsLPZCofSI80fwy+HgxtEgSGS4ndYUUZAWaw1dqaOGPLKX/FVwIOEb7q+hjHdnZ4v5pKZcNv5GG4urjujIoyA==
dependencies:
immutable "^3.8.1"
prop-types "^15.7.2"
seamless-immutable "^7.1.3"
console-browserify@^1.1.0: console-browserify@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
@ -5245,6 +5254,11 @@ immer@1.10.0:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
immutable@^3.8.1:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
import-cwd@^2.0.0: import-cwd@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@ -9556,6 +9570,11 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0" ajv-errors "^1.0.0"
ajv-keywords "^3.1.0" ajv-keywords "^3.1.0"
seamless-immutable@^7.1.3:
version "7.1.4"
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8"
integrity sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==
select-hose@^2.0.0: select-hose@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"