add routing support for selected instance
This commit is contained in:
parent
17488ff8a0
commit
39d279debd
|
@ -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
|
||||||
|
|
|
@ -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"]}}
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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."} />;
|
|
|
@ -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;
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
export const Page = styled.div`
|
|
||||||
max-width: 800px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 2em;
|
|
||||||
`;
|
|
26
frontend/src/components/atoms/Page.tsx
Normal file
26
frontend/src/components/atoms/Page.tsx
Normal 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;
|
2
frontend/src/components/atoms/index.ts
Normal file
2
frontend/src/components/atoms/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Page } from "./Page";
|
||||||
|
export { default as FloatingCard } from "./FloatingCard";
|
|
@ -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;
|
7
frontend/src/components/molecules/ErrorState.tsx
Normal file
7
frontend/src/components/molecules/ErrorState.tsx
Normal 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;
|
|
@ -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;
|
3
frontend/src/components/molecules/index.ts
Normal file
3
frontend/src/components/molecules/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as Cytoscape } from "./Cytoscape";
|
||||||
|
export { default as ErrorState } from "./ErrorState";
|
||||||
|
export { default as FloatingResetButton } from "./FloatingResetButton";
|
76
frontend/src/components/organisms/Graph.tsx
Normal file
76
frontend/src/components/organisms/Graph.tsx
Normal 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;
|
|
@ -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;
|
56
frontend/src/components/organisms/Nav.tsx
Normal file
56
frontend/src/components/organisms/Nav.tsx
Normal 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;
|
|
@ -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);
|
|
4
frontend/src/components/organisms/index.ts
Normal file
4
frontend/src/components/organisms/index.ts
Normal 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";
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
2
frontend/src/components/screens/index.ts
Normal file
2
frontend/src/components/screens/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as AboutScreen } from "./AboutScreen";
|
||||||
|
export { default as GraphScreen } from "./GraphScreen";
|
|
@ -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%;
|
||||||
`;
|
`;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
// }
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue