style(linting): set up automatic linting
This commit is contained in:
parent
1c1536e71d
commit
49fe41128a
|
@ -2,6 +2,31 @@
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "NODE_ENV=development react-scripts-ts start",
|
||||||
|
"build": "react-scripts-ts build",
|
||||||
|
"lint": "tslint -p tsconfig.json -c tslint.json \"src/**/*.{ts,tsx}\"",
|
||||||
|
"lint:fix": "yarn lint --fix",
|
||||||
|
"pretty": "prettier --write \"src/**/*.{ts,tsx}\"",
|
||||||
|
"test": "yarn lint && react-scripts-ts test --env=jsdom",
|
||||||
|
"eject": "react-scripts-ts eject"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"src/**/*.{ts,tsx,json,css}": [
|
||||||
|
"yarn pretty",
|
||||||
|
"yarn lint:fix",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 120,
|
||||||
|
"parser": "typescript"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blueprintjs/core": "^3.4.0",
|
"@blueprintjs/core": "^3.4.0",
|
||||||
"@blueprintjs/icons": "^3.1.0",
|
"@blueprintjs/icons": "^3.1.0",
|
||||||
|
@ -21,13 +46,8 @@
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"sanitize-html": "^1.18.4"
|
"sanitize-html": "^1.18.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"start": "NODE_ENV=development react-scripts-ts start",
|
|
||||||
"build": "react-scripts-ts build",
|
|
||||||
"test": "react-scripts-ts test --env=jsdom",
|
|
||||||
"eject": "react-scripts-ts eject"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@blueprintjs/tslint-config": "^1.8.0",
|
||||||
"@types/classnames": "^2.2.6",
|
"@types/classnames": "^2.2.6",
|
||||||
"@types/jest": "^23.3.1",
|
"@types/jest": "^23.3.1",
|
||||||
"@types/lodash": "^4.14.116",
|
"@types/lodash": "^4.14.116",
|
||||||
|
@ -37,6 +57,9 @@
|
||||||
"@types/react-redux": "^6.0.6",
|
"@types/react-redux": "^6.0.6",
|
||||||
"@types/react-virtualized": "^9.18.7",
|
"@types/react-virtualized": "^9.18.7",
|
||||||
"@types/sanitize-html": "^1.18.0",
|
"@types/sanitize-html": "^1.18.0",
|
||||||
|
"husky": "^1.3.1",
|
||||||
|
"lint-staged": "^8.1.5",
|
||||||
|
"tslint-eslint-rules": "^5.4.0",
|
||||||
"typescript": "^3.0.1"
|
"typescript": "^3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
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 { Button, Classes, Dialog, NonIdealState, Spinner } from '@blueprintjs/core';
|
import { Button, Classes, Dialog, NonIdealState, Spinner } from "@blueprintjs/core";
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
import ErrorState from './components/ErrorState';
|
import { ErrorState } from "./components/ErrorState";
|
||||||
import { Graph } from './components/Graph';
|
import { Graph } from "./components/Graph";
|
||||||
import { Nav } from './components/Nav';
|
import { Nav } from "./components/Nav";
|
||||||
import { Sidebar } from './components/Sidebar';
|
import { Sidebar } from "./components/Sidebar";
|
||||||
import { DESKTOP_WIDTH_THRESHOLD } from './constants';
|
import { DESKTOP_WIDTH_THRESHOLD } from "./constants";
|
||||||
import { fetchGraph, fetchInstances } from './redux/actions';
|
import { fetchGraph, fetchInstances } from "./redux/actions";
|
||||||
import { IAppState, IGraph, IInstance } from './redux/types';
|
import { IAppState, IGraph, IInstance } from "./redux/types";
|
||||||
|
|
||||||
interface IAppProps {
|
interface IAppProps {
|
||||||
graph?: IGraph;
|
graph?: IGraph;
|
||||||
instances?: IInstance[],
|
instances?: IInstance[];
|
||||||
isLoadingGraph: boolean;
|
isLoadingGraph: boolean;
|
||||||
isLoadingInstances: boolean,
|
isLoadingInstances: boolean;
|
||||||
graphLoadError: boolean,
|
graphLoadError: boolean;
|
||||||
fetchInstances: () => void;
|
fetchInstances: () => void;
|
||||||
fetchGraph: () => void;
|
fetchGraph: () => void;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,6 @@ interface IAppLocalState {
|
||||||
mobileDialogOpen: boolean;
|
mobileDialogOpen: boolean;
|
||||||
}
|
}
|
||||||
class AppImpl extends React.Component<IAppProps, IAppLocalState> {
|
class AppImpl extends React.Component<IAppProps, IAppLocalState> {
|
||||||
|
|
||||||
constructor(props: IAppProps) {
|
constructor(props: IAppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { mobileDialogOpen: false };
|
this.state = { mobileDialogOpen: false };
|
||||||
|
@ -40,7 +39,7 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
|
||||||
body = this.graphState();
|
body = this.graphState();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="App bp3-dark">
|
<div className={`${Classes.DARK} App`}>
|
||||||
<Nav />
|
<Nav />
|
||||||
{body}
|
{body}
|
||||||
{this.renderMobileDialog()}
|
{this.renderMobileDialog()}
|
||||||
|
@ -66,62 +65,53 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
|
||||||
if (!this.props.graph && !this.props.isLoadingGraph && !this.props.graphLoadError) {
|
if (!this.props.graph && !this.props.isLoadingGraph && !this.props.graphLoadError) {
|
||||||
this.props.fetchGraph();
|
this.props.fetchGraph();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
private graphState = () => {
|
private graphState = () => {
|
||||||
const content = this.props.graphLoadError ? <ErrorState /> : <Graph />
|
const content = this.props.graphLoadError ? <ErrorState /> : <Graph />;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
private loadingState = (title?: string) => {
|
private loadingState = (title?: string) => {
|
||||||
return (
|
return <NonIdealState icon={<Spinner />} title={title || "Loading..."} />;
|
||||||
<NonIdealState
|
};
|
||||||
icon={<Spinner />}
|
|
||||||
title={title || "Loading..."}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderMobileDialog = () => {
|
private renderMobileDialog = () => {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
icon={IconNames.DESKTOP}
|
icon={IconNames.DESKTOP}
|
||||||
title="Desktop-optimized site"
|
title="Desktop-optimized site"
|
||||||
onClose={this.handleMobileDialogClose}
|
onClose={this.handleMobileDialogClose}
|
||||||
isOpen={this.state.mobileDialogOpen}
|
isOpen={this.state.mobileDialogOpen}
|
||||||
className={Classes.DARK + ' fediverse-about-dialog'}
|
className={Classes.DARK + " fediverse-about-dialog"}
|
||||||
>
|
>
|
||||||
<div className={Classes.DIALOG_BODY}>
|
<div className={Classes.DIALOG_BODY}>
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
<p className={Classes.RUNNING_TEXT}>
|
||||||
fediverse.space is optimized for desktop computers. Feel free to check it out on your phone
|
fediverse.space is optimized for desktop computers. Feel free to check it out on your phone (ideally in
|
||||||
(ideally in landscape mode) but for best results, open it on a computer.
|
landscape mode) but for best results, open it on a computer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
<Button
|
<Button icon={IconNames.THUMBS_UP} text="OK!" onClick={this.handleMobileDialogClose} />
|
||||||
icon={IconNames.THUMBS_UP}
|
|
||||||
text="OK!"
|
|
||||||
onClick={this.handleMobileDialogClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
private handleMobileDialogOpen = () => {
|
private handleMobileDialogOpen = () => {
|
||||||
this.setState({ mobileDialogOpen: true });
|
this.setState({ mobileDialogOpen: true });
|
||||||
}
|
};
|
||||||
|
|
||||||
private handleMobileDialogClose = () => {
|
private handleMobileDialogClose = () => {
|
||||||
this.setState({ mobileDialogOpen: false });
|
this.setState({ mobileDialogOpen: false });
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => ({
|
const mapStateToProps = (state: IAppState) => ({
|
||||||
|
@ -129,10 +119,13 @@ const mapStateToProps = (state: IAppState) => ({
|
||||||
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
|
||||||
})
|
});
|
||||||
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)
|
||||||
})
|
});
|
||||||
export const App = connect(mapStateToProps, mapDispatchToProps)(AppImpl)
|
export const App = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(AppImpl);
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
import { NonIdealState } from '@blueprintjs/core';
|
import { NonIdealState } from "@blueprintjs/core";
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
|
export const ErrorState: React.SFC = () => <NonIdealState icon={IconNames.ERROR} title={"Something went wrong."} />;
|
||||||
const ErrorState: React.SFC = () => (
|
|
||||||
<NonIdealState
|
|
||||||
icon={IconNames.ERROR}
|
|
||||||
title={"Something went wrong."}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
export default ErrorState;
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||||
import { Sigma, SigmaEnableWebGL, Filter, ForceAtlas2 } from 'react-sigma';
|
import { Sigma, SigmaEnableWebGL, Filter, ForceAtlas2 } from 'react-sigma';
|
||||||
|
|
||||||
import { selectAndLoadInstance } from '../redux/actions';
|
import { selectAndLoadInstance } from '../redux/actions';
|
||||||
import ErrorState from './ErrorState';
|
import { ErrorState } from './ErrorState';
|
||||||
|
|
||||||
const STYLE = {
|
const STYLE = {
|
||||||
bottom: "0",
|
bottom: "0",
|
||||||
|
|
|
@ -1,89 +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 { Button, MenuItem } from '@blueprintjs/core';
|
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 { selectAndLoadInstance } from '../redux/actions';
|
import { selectAndLoadInstance } from "../redux/actions";
|
||||||
import { IAppState, IInstance } from '../redux/types';
|
import { IAppState, IInstance } from "../redux/types";
|
||||||
|
|
||||||
interface IInstanceSearchProps {
|
interface IInstanceSearchProps {
|
||||||
currentInstanceName: string | null;
|
currentInstanceName: string | null;
|
||||||
instances?: IInstance[];
|
instances?: IInstance[];
|
||||||
selectAndLoadInstance: (instanceName: string) => void;
|
selectAndLoadInstance: (instanceName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InstanceSelect = Select.ofType<IInstance>();
|
const InstanceSelect = Select.ofType<IInstance>();
|
||||||
|
|
||||||
class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
|
class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<InstanceSelect
|
||||||
|
items={this.props.instances || []}
|
||||||
|
itemRenderer={this.itemRenderer}
|
||||||
|
onItemSelect={this.onItemSelect}
|
||||||
|
itemPredicate={this.itemPredicate}
|
||||||
|
disabled={!this.props.instances}
|
||||||
|
initialContent={this.renderInitialContent()}
|
||||||
|
noResults={this.renderNoResults()}
|
||||||
|
popoverProps={{ popoverClassName: "fediverse-instance-search-popover" }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={IconNames.SELECTION}
|
||||||
|
rightIcon={IconNames.CARET_DOWN}
|
||||||
|
text={this.props.currentInstanceName || "Select an instance"}
|
||||||
|
disabled={!this.props.instances}
|
||||||
|
/>
|
||||||
|
</InstanceSelect>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
private renderInitialContent = () => {
|
||||||
return (
|
return <MenuItem disabled={true} text={"Start typing"} />;
|
||||||
<InstanceSelect
|
};
|
||||||
items={this.props.instances || []}
|
|
||||||
itemRenderer={this.itemRenderer}
|
|
||||||
onItemSelect={this.onItemSelect}
|
|
||||||
itemPredicate={this.itemPredicate}
|
|
||||||
disabled={!this.props.instances}
|
|
||||||
initialContent={this.renderInitialContent()}
|
|
||||||
noResults={this.renderNoResults()}
|
|
||||||
popoverProps={{popoverClassName: "fediverse-instance-search-popover"}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon={IconNames.SELECTION}
|
|
||||||
rightIcon={IconNames.CARET_DOWN}
|
|
||||||
text={this.props.currentInstanceName || ("Select an instance")}
|
|
||||||
disabled={!this.props.instances}
|
|
||||||
/>
|
|
||||||
</InstanceSelect>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderInitialContent = () => {
|
private renderNoResults = () => {
|
||||||
return (
|
return <MenuItem disabled={true} text={"Keep typing"} />;
|
||||||
<MenuItem disabled={true} text={"Start typing"} />
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderNoResults = () => {
|
private itemRenderer = (item: IInstance, itemProps: IItemRendererProps) => {
|
||||||
return (
|
if (!itemProps.modifiers.matchesPredicate) {
|
||||||
<MenuItem disabled={true} text={"Keep typing"} />
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<MenuItem text={item.name} key={item.name} active={itemProps.modifiers.active} onClick={itemProps.handleClick} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private itemRenderer = (item: IInstance, itemProps: IItemRendererProps) => {
|
private itemPredicate: ItemPredicate<IInstance> = (query, item, index) => {
|
||||||
if (!itemProps.modifiers.matchesPredicate) {
|
if (!item.name || query.length < 4) {
|
||||||
return null;
|
return false;
|
||||||
}
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
text={item.name}
|
|
||||||
key={item.name}
|
|
||||||
active={itemProps.modifiers.active}
|
|
||||||
onClick={itemProps.handleClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return item.name.toLowerCase().indexOf(query.toLowerCase()) >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
private itemPredicate: ItemPredicate<IInstance> = (query, item, index) => {
|
private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => {
|
||||||
if (!item.name || query.length < 4) {
|
this.props.selectAndLoadInstance(item.name);
|
||||||
return false;
|
};
|
||||||
}
|
|
||||||
return item.name.toLowerCase().indexOf(query.toLowerCase()) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => {
|
|
||||||
this.props.selectAndLoadInstance(item.name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => ({
|
const mapStateToProps = (state: IAppState) => ({
|
||||||
currentInstanceName: state.currentInstance.currentInstanceName,
|
currentInstanceName: state.currentInstance.currentInstanceName,
|
||||||
instances: state.data.instances,
|
instances: state.data.instances
|
||||||
})
|
});
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any),
|
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any)
|
||||||
})
|
});
|
||||||
export const InstanceSearch = connect(mapStateToProps, mapDispatchToProps)(InstanceSearchImpl)
|
export const InstanceSearch = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(InstanceSearchImpl);
|
||||||
|
|
|
@ -1,41 +1,31 @@
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
import { Alignment, Button, Classes, Dialog, Icon, Navbar } from '@blueprintjs/core';
|
import { Alignment, Button, Classes, Code, Dialog, H2, H4, Icon, Navbar } from "@blueprintjs/core";
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
import { InstanceSearch } from './InstanceSearch';
|
import { InstanceSearch } from "./InstanceSearch";
|
||||||
|
|
||||||
interface INavState {
|
interface INavState {
|
||||||
aboutIsOpen: boolean;
|
aboutIsOpen: boolean;
|
||||||
}
|
}
|
||||||
export class Nav extends React.Component<{}, INavState> {
|
export class Nav extends React.Component<{}, INavState> {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.state = { aboutIsOpen: false };
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any) {
|
public render() {
|
||||||
super(props);
|
return (
|
||||||
this.state = {aboutIsOpen: false};
|
<Navbar fixedToTop={true}>
|
||||||
}
|
<Navbar.Group align={Alignment.LEFT}>
|
||||||
|
<Navbar.Heading>
|
||||||
public render() {
|
<Icon icon={IconNames.GLOBE_NETWORK} iconSize={Icon.SIZE_LARGE} className="fediverse-heading-icon" />
|
||||||
return (
|
fediverse.space
|
||||||
<Navbar fixedToTop={true}>
|
</Navbar.Heading>
|
||||||
<Navbar.Group align={Alignment.LEFT}>
|
<Navbar.Divider />
|
||||||
<Navbar.Heading>
|
<Button icon={IconNames.INFO_SIGN} text="About" minimal={true} onClick={this.handleAboutOpen} />
|
||||||
<Icon
|
{this.renderAboutDialog()}
|
||||||
icon={IconNames.GLOBE_NETWORK}
|
{/* <Button
|
||||||
iconSize={Icon.SIZE_LARGE}
|
|
||||||
className="fediverse-heading-icon"
|
|
||||||
/>
|
|
||||||
fediverse.space
|
|
||||||
</Navbar.Heading>
|
|
||||||
<Navbar.Divider />
|
|
||||||
<Button
|
|
||||||
icon={IconNames.INFO_SIGN}
|
|
||||||
text="About"
|
|
||||||
minimal={true}
|
|
||||||
onClick={this.handleAboutOpen}
|
|
||||||
/>
|
|
||||||
{this.renderAboutDialog()}
|
|
||||||
{/* <Button
|
|
||||||
icon={<Icon icon={IconNames.GLOBE_NETWORK} />}
|
icon={<Icon icon={IconNames.GLOBE_NETWORK} />}
|
||||||
text="Network"
|
text="Network"
|
||||||
minimal={true}
|
minimal={true}
|
||||||
|
@ -45,89 +35,91 @@ export class Nav extends React.Component<{}, INavState> {
|
||||||
text="Map"
|
text="Map"
|
||||||
minimal={true}
|
minimal={true}
|
||||||
/> */}
|
/> */}
|
||||||
</Navbar.Group>
|
</Navbar.Group>
|
||||||
<Navbar.Group align={Alignment.RIGHT}>
|
<Navbar.Group align={Alignment.RIGHT}>
|
||||||
<InstanceSearch />
|
<InstanceSearch />
|
||||||
</Navbar.Group>
|
</Navbar.Group>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderAboutDialog = () => {
|
private renderAboutDialog = () => {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
icon={IconNames.INFO_SIGN}
|
icon={IconNames.INFO_SIGN}
|
||||||
title="About"
|
title="About"
|
||||||
onClose={this.handleAboutClose}
|
onClose={this.handleAboutClose}
|
||||||
isOpen={this.state.aboutIsOpen}
|
isOpen={this.state.aboutIsOpen}
|
||||||
className={Classes.DARK + ' fediverse-about-dialog'}
|
className={Classes.DARK + " fediverse-about-dialog"}
|
||||||
>
|
>
|
||||||
<div className={Classes.DIALOG_BODY}>
|
<div className={Classes.DIALOG_BODY}>
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
<p className={Classes.RUNNING_TEXT}>
|
||||||
fediverse.space is a tool to visualize networks and communities on the
|
fediverse.space is a tool to visualize networks and communities on the{" "}
|
||||||
{' '}<a href="https://en.wikipedia.org/wiki/Fediverse" target="_blank">fediverse</a>.
|
<a href="https://en.wikipedia.org/wiki/Fediverse" target="_blank">
|
||||||
It works by scraping every instance it can find and aggregating statistics on communication
|
fediverse
|
||||||
between these.
|
</a>
|
||||||
</p>
|
. It works by scraping every instance it can find and aggregating statistics on communication between these.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>FAQ</h2>
|
<H2>FAQ</H2>
|
||||||
<h4>Why can't I see details about my instance?</h4>
|
<H4>Why can't I see details about my instance?</H4>
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
<p className={Classes.RUNNING_TEXT}>
|
||||||
Currently, fediverse.space only supports Mastodon and Pleroma instances. In addition, instances
|
Currently, fediverse.space only supports Mastodon and Pleroma instances. In addition, instances with 5 or
|
||||||
with 5 or fewer users won't be scraped -- it's a tool for understanding communities, not
|
fewer users won't be scraped -- it's a tool for understanding communities, not individuals.
|
||||||
individuals.
|
</p>
|
||||||
</p>
|
<H4>How do you calculate the strength of relationships between instances?</H4>
|
||||||
<h4>How do you calculate the strength of relationships between instances?</h4>
|
<p className={Classes.RUNNING_TEXT}>
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
fediverse.space scrapes the last 5000 statuses from within the last month on the public timeline of each
|
||||||
fediverse.space scrapes the last 5000 statuses from within the last month on the public
|
instance. It looks at the ratio of
|
||||||
timeline of each instance. It looks at the ratio of
|
<Code>mentions of an instance / total statuses</Code>. It uses a ratio rather than an absolute number of
|
||||||
<code>mentions of an instance / total statuses</code>.
|
mentions to reflect that smaller instances can play a large role in a community.
|
||||||
It uses a ratio rather than an absolute number of mentions to reflect that smaller instances
|
</p>
|
||||||
can play a large role in a community.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Credits</h2>
|
<H2>Credits</H2>
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
<p className={Classes.RUNNING_TEXT}>
|
||||||
This site is inspired by several other sites in the same vein:
|
This site is inspired by several other sites in the same vein:
|
||||||
<ul className={Classes.LIST}>
|
<ul className={Classes.LIST}>
|
||||||
<li><a href="https://the-federation.info/" target="_blank">the-federation.info</a></li>
|
<li>
|
||||||
<li><a href="http://fediverse.network/" target="_blank">fediverse.network</a></li>
|
<a href="https://the-federation.info/" target="_blank">
|
||||||
<li>
|
the-federation.info
|
||||||
<a
|
</a>
|
||||||
href="https://lucahammer.at/vis/fediverse/2018-08-30-mastoverse_hashtags/"
|
</li>
|
||||||
target="_blank"
|
<li>
|
||||||
>
|
<a href="http://fediverse.network/" target="_blank">
|
||||||
Mastodon hashtag network
|
fediverse.network
|
||||||
</a>
|
</a>
|
||||||
{' by '}
|
</li>
|
||||||
<a href="https://vis.social/web/statuses/100634284168959187" target="_blank">
|
<li>
|
||||||
@Luca@vis.social
|
<a href="https://lucahammer.at/vis/fediverse/2018-08-30-mastoverse_hashtags/" target="_blank">
|
||||||
</a>
|
Mastodon hashtag network
|
||||||
</li>
|
</a>
|
||||||
</ul>
|
{" by "}
|
||||||
The source code for fediverse.space is available on{' '}
|
<a href="https://vis.social/web/statuses/100634284168959187" target="_blank">
|
||||||
<a href="https://gitlab.com/taobojlen/fediverse.space" target="_blank">GitLab</a>;{' '}
|
@Luca@vis.social
|
||||||
issues and pull requests are welcome!
|
</a>
|
||||||
</p>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
The source code for fediverse.space is available on{" "}
|
||||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
<a href="https://gitlab.com/taobojlen/fediverse.space" target="_blank">
|
||||||
<Button
|
GitLab
|
||||||
icon={IconNames.THUMBS_UP}
|
</a>
|
||||||
text="OK!"
|
; issues and pull requests are welcome!
|
||||||
onClick={this.handleAboutClose}
|
</p>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
</div>
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
</Dialog>
|
<Button icon={IconNames.THUMBS_UP} text="OK!" onClick={this.handleAboutClose} />
|
||||||
)
|
</div>
|
||||||
}
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private handleAboutOpen = () => {
|
private handleAboutOpen = () => {
|
||||||
this.setState({aboutIsOpen: true});
|
this.setState({ aboutIsOpen: true });
|
||||||
}
|
};
|
||||||
|
|
||||||
private handleAboutClose = () => {
|
private handleAboutClose = () => {
|
||||||
this.setState({aboutIsOpen: false});
|
this.setState({ aboutIsOpen: false });
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,306 +1,324 @@
|
||||||
import { orderBy } from 'lodash';
|
import { orderBy } from "lodash";
|
||||||
import * as moment from 'moment';
|
import * as moment from "moment";
|
||||||
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 * as sanitize from 'sanitize-html';
|
import * as sanitize from "sanitize-html";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnchorButton, Button, Card, Classes, Divider, Elevation, HTMLTable, NonIdealState, Position,
|
AnchorButton,
|
||||||
Tab, Tabs, Tooltip
|
Button,
|
||||||
} from '@blueprintjs/core';
|
Card,
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
Classes,
|
||||||
|
Code,
|
||||||
|
Divider,
|
||||||
|
Elevation,
|
||||||
|
H2,
|
||||||
|
H4,
|
||||||
|
HTMLTable,
|
||||||
|
NonIdealState,
|
||||||
|
Position,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
Tooltip
|
||||||
|
} from "@blueprintjs/core";
|
||||||
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
import { selectAndLoadInstance } from '../redux/actions';
|
import { selectAndLoadInstance } from "../redux/actions";
|
||||||
import { IAppState, IGraph, IInstanceDetails } from '../redux/types';
|
import { IAppState, IGraph, IInstanceDetails } from "../redux/types";
|
||||||
import ErrorState from './ErrorState';
|
import { ErrorState } from "./ErrorState";
|
||||||
|
|
||||||
interface ISidebarProps {
|
interface ISidebarProps {
|
||||||
graph?: IGraph,
|
graph?: IGraph;
|
||||||
instanceName: string | null,
|
instanceName: string | null;
|
||||||
instanceLoadError: boolean,
|
instanceLoadError: boolean;
|
||||||
instanceDetails: IInstanceDetails | null,
|
instanceDetails: IInstanceDetails | null;
|
||||||
isLoadingInstanceDetails: boolean;
|
isLoadingInstanceDetails: boolean;
|
||||||
selectAndLoadInstance: (instanceName: string) => void;
|
selectAndLoadInstance: (instanceName: string) => void;
|
||||||
}
|
}
|
||||||
interface ISidebarState {
|
interface ISidebarState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
|
constructor(props: ISidebarProps) {
|
||||||
|
super(props);
|
||||||
|
const isOpen = window.innerWidth >= 900 ? true : false;
|
||||||
|
this.state = { isOpen };
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: ISidebarProps) {
|
public render() {
|
||||||
super(props);
|
const closedClass = this.state.isOpen ? "" : " closed";
|
||||||
const isOpen = window.innerWidth >= 900 ? true : false;
|
const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT;
|
||||||
this.state = { isOpen };
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={this.handleToggle}
|
||||||
|
large={true}
|
||||||
|
icon={buttonIcon}
|
||||||
|
className={"fediverse-sidebar-toggle-button" + closedClass}
|
||||||
|
minimal={true}
|
||||||
|
/>
|
||||||
|
<Card className={"fediverse-sidebar" + closedClass} elevation={Elevation.THREE}>
|
||||||
|
{this.renderSidebarContents()}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleToggle = () => {
|
||||||
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderSidebarContents = () => {
|
||||||
|
if (this.props.isLoadingInstanceDetails) {
|
||||||
|
return this.renderLoadingState();
|
||||||
|
} else if (!this.props.instanceDetails) {
|
||||||
|
return this.renderEmptyState();
|
||||||
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personalinstance") > -1) {
|
||||||
|
return this.renderPersonalInstanceErrorState();
|
||||||
|
} else if (this.props.instanceDetails.status !== "success") {
|
||||||
|
return this.renderMissingDataState();
|
||||||
|
} else if (this.props.instanceLoadError) {
|
||||||
|
return <ErrorState />;
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.renderHeading()}
|
||||||
|
<Tabs>
|
||||||
|
{this.props.instanceDetails.description && (
|
||||||
|
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
||||||
|
)}
|
||||||
|
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
||||||
|
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
|
||||||
|
<Tab id="peers" title="Known peers" panel={this.renderPeers()} />
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
private shouldRenderStats = () => {
|
||||||
const closedClass = this.state.isOpen ? "" : " closed";
|
const details = this.props.instanceDetails;
|
||||||
const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT;
|
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
|
||||||
return (
|
};
|
||||||
<div>
|
|
||||||
<Button
|
private renderHeading = () => {
|
||||||
onClick={this.handleToggle}
|
let content: JSX.Element;
|
||||||
large={true}
|
if (!this.props.instanceName) {
|
||||||
icon={buttonIcon}
|
content = <span>{"No instance selected"}</span>;
|
||||||
className={"fediverse-sidebar-toggle-button" + closedClass}
|
} else {
|
||||||
minimal={true}
|
content = (
|
||||||
/>
|
<span>
|
||||||
<Card className={"fediverse-sidebar" + closedClass} elevation={Elevation.THREE}>
|
{this.props.instanceName + " "}
|
||||||
{this.renderSidebarContents()}
|
<Tooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
|
||||||
</Card>
|
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
|
||||||
</div>
|
</Tooltip>
|
||||||
)
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<H2>{content}</H2>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private handleToggle = () => {
|
private renderDescription = () => {
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
const description = this.props.instanceDetails!.description;
|
||||||
|
if (!description) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
return <p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{ __html: sanitize(description) }} />;
|
||||||
|
};
|
||||||
|
|
||||||
private renderSidebarContents = () => {
|
private renderVersionAndCounts = () => {
|
||||||
if (this.props.isLoadingInstanceDetails) {
|
const version = this.props.instanceDetails!.version;
|
||||||
return this.renderLoadingState();
|
const userCount = this.props.instanceDetails!.userCount;
|
||||||
} else if (!this.props.instanceDetails) {
|
const statusCount = this.props.instanceDetails!.statusCount;
|
||||||
return this.renderEmptyState();
|
const domainCount = this.props.instanceDetails!.domainCount;
|
||||||
} else if (this.props.instanceDetails.status.toLowerCase().indexOf('personalinstance') > -1) {
|
const lastUpdated = this.props.instanceDetails!.lastUpdated;
|
||||||
return this.renderPersonalInstanceErrorState();
|
return (
|
||||||
} else if (this.props.instanceDetails.status !== 'success') {
|
<div>
|
||||||
return this.renderMissingDataState();
|
<HTMLTable small={true} striped={true} className="fediverse-sidebar-table">
|
||||||
} else if (this.props.instanceLoadError) {
|
<tbody>
|
||||||
return <ErrorState />;
|
<tr>
|
||||||
}
|
<td>Version</td>
|
||||||
return (
|
<td>{<Code>{version}</Code> || "Unknown"}</td>
|
||||||
<div>
|
|
||||||
{this.renderHeading()}
|
|
||||||
<Tabs>
|
|
||||||
{this.props.instanceDetails.description &&
|
|
||||||
<Tab id="description" title="Description" panel={this.renderDescription()} />}
|
|
||||||
{this.shouldRenderStats() &&
|
|
||||||
<Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
|
||||||
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
|
|
||||||
<Tab id="peers" title="Known peers" panel={this.renderPeers()} />
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldRenderStats = () => {
|
|
||||||
const details = this.props.instanceDetails;
|
|
||||||
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderHeading = () => {
|
|
||||||
let content: JSX.Element;
|
|
||||||
if (!this.props.instanceName) {
|
|
||||||
content = <span>{"No instance selected"}</span>;
|
|
||||||
} else {
|
|
||||||
content = (
|
|
||||||
<span>
|
|
||||||
{this.props.instanceName + ' '}
|
|
||||||
<Tooltip
|
|
||||||
content="Open link in new tab"
|
|
||||||
position={Position.TOP}
|
|
||||||
className={Classes.DARK}
|
|
||||||
>
|
|
||||||
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>{content}</h2>
|
|
||||||
<Divider />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderDescription = () => {
|
|
||||||
const description = this.props.instanceDetails!.description;
|
|
||||||
if (!description) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{__html: sanitize(description)}} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderVersionAndCounts = () => {
|
|
||||||
const version = this.props.instanceDetails!.version;
|
|
||||||
const userCount = this.props.instanceDetails!.userCount;
|
|
||||||
const statusCount = this.props.instanceDetails!.statusCount;
|
|
||||||
const domainCount = this.props.instanceDetails!.domainCount;
|
|
||||||
const lastUpdated = this.props.instanceDetails!.lastUpdated;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<HTMLTable small={true} striped={true} className="fediverse-sidebar-table">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Version</td>
|
|
||||||
<td>{<code>{version}</code> || "Unknown"}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Users</td>
|
|
||||||
<td>{userCount || "Unknown"}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Statuses</td>
|
|
||||||
<td>{statusCount || "Unknown"}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Known peers</td>
|
|
||||||
<td>{domainCount || "Unknown"}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Last updated</td>
|
|
||||||
<td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</HTMLTable>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderNeighbors = () => {
|
|
||||||
if (!this.props.graph || !this.props.instanceName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const edges = this.props.graph.edges.filter(e => [e.source, e.target].indexOf(this.props.instanceName!) > -1);
|
|
||||||
const neighbors: any[] = [];
|
|
||||||
edges.forEach(e => {
|
|
||||||
if (e.source === this.props.instanceName) {
|
|
||||||
neighbors.push({neighbor: e.target, weight: e.size});
|
|
||||||
} else {
|
|
||||||
neighbors.push({neighbor: e.source, weight: e.size});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const neighborRows = orderBy(neighbors, ['weight'], ['desc']).map((neighborDetails: any, idx: number) => (
|
|
||||||
<tr key={idx}>
|
|
||||||
<td><AnchorButton minimal={true} onClick={this.selectInstance}>{neighborDetails.neighbor}</AnchorButton></td>
|
|
||||||
<td>{neighborDetails.weight.toFixed(4)}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
));
|
<tr>
|
||||||
return (
|
<td>Users</td>
|
||||||
<div>
|
<td>{userCount || "Unknown"}</td>
|
||||||
<p className={Classes.TEXT_MUTED}>
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Instance</th>
|
|
||||||
<th>Mention ratio</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{neighborRows}
|
|
||||||
</tbody>
|
|
||||||
</HTMLTable>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPeers = () => {
|
|
||||||
const peers = this.props.instanceDetails!.peers;
|
|
||||||
if (!peers || peers.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const peerRows = peers.map(instance => (
|
|
||||||
<tr key={instance.name} onClick={this.selectInstance}>
|
|
||||||
<td><AnchorButton minimal={true} onClick={this.selectInstance}>{instance.name}</AnchorButton></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
));
|
<tr>
|
||||||
return (
|
<td>Statuses</td>
|
||||||
<div>
|
<td>{statusCount || "Unknown"}</td>
|
||||||
<p className={Classes.TEXT_MUTED}>
|
</tr>
|
||||||
All the instances, past and present, that {this.props.instanceName} knows about.
|
<tr>
|
||||||
</p>
|
<td>Known peers</td>
|
||||||
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
<td>{domainCount || "Unknown"}</td>
|
||||||
<tbody>
|
</tr>
|
||||||
{peerRows}
|
<tr>
|
||||||
</tbody>
|
<td>Last updated</td>
|
||||||
</HTMLTable>
|
<td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
|
||||||
</div>
|
</tr>
|
||||||
)
|
</tbody>
|
||||||
}
|
</HTMLTable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private renderEmptyState = () => {
|
private renderNeighbors = () => {
|
||||||
return (
|
if (!this.props.graph || !this.props.instanceName) {
|
||||||
<NonIdealState
|
return;
|
||||||
icon={IconNames.CIRCLE}
|
|
||||||
title="No instance selected"
|
|
||||||
description="Select an instance from the graph or the top-right dropdown to see its details."
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
const edges = this.props.graph.edges.filter(e => [e.source, e.target].indexOf(this.props.instanceName!) > -1);
|
||||||
|
const neighbors: any[] = [];
|
||||||
|
edges.forEach(e => {
|
||||||
|
if (e.source === this.props.instanceName) {
|
||||||
|
neighbors.push({ neighbor: e.target, weight: e.size });
|
||||||
|
} else {
|
||||||
|
neighbors.push({ neighbor: e.source, weight: e.size });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const neighborRows = orderBy(neighbors, ["weight"], ["desc"]).map((neighborDetails: any, idx: number) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td>
|
||||||
|
<AnchorButton minimal={true} onClick={this.selectInstance}>
|
||||||
|
{neighborDetails.neighbor}
|
||||||
|
</AnchorButton>
|
||||||
|
</td>
|
||||||
|
<td>{neighborDetails.weight.toFixed(4)}</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className={Classes.TEXT_MUTED}>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Instance</th>
|
||||||
|
<th>Mention ratio</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{neighborRows}</tbody>
|
||||||
|
</HTMLTable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private renderLoadingState = () => {
|
private renderPeers = () => {
|
||||||
return (
|
const peers = this.props.instanceDetails!.peers;
|
||||||
<div>
|
if (!peers || peers.length === 0) {
|
||||||
<h4><span className={Classes.SKELETON}>Description</span></h4>
|
return;
|
||||||
<p className={Classes.SKELETON}>
|
|
||||||
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt.
|
|
||||||
Cupiditate sit voluptates quia nulla et saepe id suscipit.
|
|
||||||
Voluptas sed rerum placeat consectetur pariatur necessitatibus tempora.
|
|
||||||
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt.
|
|
||||||
Cupiditate sit voluptates quia nulla et saepe id suscipit.
|
|
||||||
Voluptas sed rerum placeat consectetur pariatur necessitatibus tempora.
|
|
||||||
</p>
|
|
||||||
<h4><span className={Classes.SKELETON}>Version</span></h4>
|
|
||||||
<p className={Classes.SKELETON}>
|
|
||||||
Eaque rerum sequi unde omnis voluptatibus non quia fugit.
|
|
||||||
</p>
|
|
||||||
<h4><span className={Classes.SKELETON}>Stats</span></h4>
|
|
||||||
<p className={Classes.SKELETON}>
|
|
||||||
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt.
|
|
||||||
Cupiditate sit voluptates quia nulla et saepe id suscipit.
|
|
||||||
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt.
|
|
||||||
Cupiditate sit voluptates quia nulla et saepe id suscipit.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const peerRows = peers.map(instance => (
|
||||||
|
<tr key={instance.name} onClick={this.selectInstance}>
|
||||||
|
<td>
|
||||||
|
<AnchorButton minimal={true} onClick={this.selectInstance}>
|
||||||
|
{instance.name}
|
||||||
|
</AnchorButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className={Classes.TEXT_MUTED}>
|
||||||
|
All the instances, past and present, that {this.props.instanceName} knows about.
|
||||||
|
</p>
|
||||||
|
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
||||||
|
<tbody>{peerRows}</tbody>
|
||||||
|
</HTMLTable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private renderPersonalInstanceErrorState = () => {
|
private renderEmptyState = () => {
|
||||||
return (
|
return (
|
||||||
<NonIdealState
|
<NonIdealState
|
||||||
icon={IconNames.BLOCKED_PERSON}
|
icon={IconNames.CIRCLE}
|
||||||
title="No data"
|
title="No instance selected"
|
||||||
description="This instance has fewer than 10 users. It was not crawled in order to protect their privacy, but if it's your instance you can opt in."
|
description="Select an instance from the graph or the top-right dropdown to see its details."
|
||||||
action={<AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@tao" target="_blank">
|
/>
|
||||||
Message @tao to opt in</AnchorButton>}
|
);
|
||||||
/>
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderMissingDataState = () => {
|
private renderLoadingState = () => {
|
||||||
return (
|
return (
|
||||||
<NonIdealState
|
<div>
|
||||||
icon={IconNames.ERROR}
|
<H4>
|
||||||
title="No data"
|
<span className={Classes.SKELETON}>Description</span>
|
||||||
description="This instance could not be crawled. Either it was down or it's an instance type we don't support yet."
|
</H4>
|
||||||
/>
|
<p className={Classes.SKELETON}>
|
||||||
)
|
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit
|
||||||
}
|
voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur necessitatibus
|
||||||
|
tempora. Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt.
|
||||||
|
Cupiditate sit voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur
|
||||||
|
necessitatibus tempora.
|
||||||
|
</p>
|
||||||
|
<H4>
|
||||||
|
<span className={Classes.SKELETON}>Version</span>
|
||||||
|
</H4>
|
||||||
|
<p className={Classes.SKELETON}>Eaque rerum sequi unde omnis voluptatibus non quia fugit.</p>
|
||||||
|
<H4>
|
||||||
|
<span className={Classes.SKELETON}>Stats</span>
|
||||||
|
</H4>
|
||||||
|
<p className={Classes.SKELETON}>
|
||||||
|
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit
|
||||||
|
voluptates quia nulla et saepe id suscipit. Eaque rerum sequi unde omnis voluptatibus non quia fugit.
|
||||||
|
Dignissimos asperiores aut incidunt. Cupiditate sit voluptates quia nulla et saepe id suscipit.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private openInstanceLink = () => {
|
private renderPersonalInstanceErrorState = () => {
|
||||||
window.open("https://" + this.props.instanceName, "_blank");
|
return (
|
||||||
}
|
<NonIdealState
|
||||||
|
icon={IconNames.BLOCKED_PERSON}
|
||||||
|
title="No data"
|
||||||
|
description="This instance has fewer than 10 users. It was not crawled in order to protect their privacy, but if it's your instance you can opt in."
|
||||||
|
action={
|
||||||
|
<AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@tao" target="_blank">
|
||||||
|
Message @tao to opt in
|
||||||
|
</AnchorButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private selectInstance = (e: any) => {
|
private renderMissingDataState = () => {
|
||||||
this.props.selectAndLoadInstance(e.target.innerText);
|
return (
|
||||||
}
|
<NonIdealState
|
||||||
|
icon={IconNames.ERROR}
|
||||||
|
title="No data"
|
||||||
|
description="This instance could not be crawled. Either it was down or it's an instance type we don't support yet."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private openInstanceLink = () => {
|
||||||
|
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) => ({
|
||||||
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: state.currentInstance.currentInstanceName,
|
||||||
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails,
|
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any),
|
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any)
|
||||||
});
|
});
|
||||||
export const Sidebar = connect(mapStateToProps, mapDispatchToProps)(SidebarImpl);
|
export const Sidebar = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(SidebarImpl);
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import '../node_modules/@blueprintjs/core/lib/css/blueprint.css';
|
import "../node_modules/@blueprintjs/core/lib/css/blueprint.css";
|
||||||
import '../node_modules/@blueprintjs/icons/lib/css/blueprint-icons.css';
|
import "../node_modules/@blueprintjs/icons/lib/css/blueprint-icons.css";
|
||||||
import '../node_modules/@blueprintjs/select/lib/css/blueprint-select.css';
|
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 * 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";
|
||||||
import { applyMiddleware, compose, createStore } from 'redux';
|
import { applyMiddleware, compose, createStore } from "redux";
|
||||||
import thunk from 'redux-thunk';
|
import thunk from "redux-thunk";
|
||||||
|
|
||||||
import { FocusStyleManager } from '@blueprintjs/core';
|
import { FocusStyleManager } from "@blueprintjs/core";
|
||||||
|
|
||||||
import { App } from './App';
|
import { App } from "./App";
|
||||||
import { rootReducer } from './redux/reducers';
|
import { rootReducer } from "./redux/reducers";
|
||||||
|
|
||||||
// https://blueprintjs.com/docs/#core/accessibility.focus-management
|
// https://blueprintjs.com/docs/#core/accessibility.focus-management
|
||||||
FocusStyleManager.onlyShowFocusOnTabs();
|
FocusStyleManager.onlyShowFocusOnTabs();
|
||||||
|
@ -21,13 +21,11 @@ FocusStyleManager.onlyShowFocusOnTabs();
|
||||||
// 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(
|
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
|
||||||
applyMiddleware(thunk)
|
|
||||||
));
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App />
|
<App />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById("root") as HTMLElement
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,101 +1,100 @@
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from "redux";
|
||||||
|
|
||||||
import { getFromApi } from '../util';
|
import { getFromApi } from "../util";
|
||||||
import { ActionType, IGraph, IInstance, IInstanceDetails } from './types';
|
import { ActionType, IGraph, IInstance, IInstanceDetails } from "./types";
|
||||||
|
|
||||||
// selectInstance and deselectInstance are not exported since we only call them from selectAndLoadInstance()
|
// selectInstance and deselectInstance are not exported since we only call them from selectAndLoadInstance()
|
||||||
const selectInstance = (instanceName: string) => {
|
const selectInstance = (instanceName: string) => {
|
||||||
return {
|
return {
|
||||||
payload: instanceName,
|
payload: instanceName,
|
||||||
type: ActionType.SELECT_INSTANCE,
|
type: ActionType.SELECT_INSTANCE
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
const deselectInstance = () => {
|
const deselectInstance = () => {
|
||||||
return {
|
return {
|
||||||
type: ActionType.DESELECT_INSTANCE,
|
type: ActionType.DESELECT_INSTANCE
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const requestInstances = () => {
|
export const requestInstances = () => {
|
||||||
return {
|
return {
|
||||||
type: ActionType.REQUEST_INSTANCES,
|
type: ActionType.REQUEST_INSTANCES
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const receiveInstances = (instances: IInstance[]) => {
|
export const receiveInstances = (instances: IInstance[]) => {
|
||||||
return {
|
return {
|
||||||
payload: instances,
|
payload: instances,
|
||||||
type: ActionType.RECEIVE_INSTANCES,
|
type: ActionType.RECEIVE_INSTANCES
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
export const requestGraph = () => {
|
export const requestGraph = () => {
|
||||||
return {
|
return {
|
||||||
type: ActionType.REQUEST_GRAPH,
|
type: ActionType.REQUEST_GRAPH
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const receiveGraph = (graph: IGraph) => {
|
export const receiveGraph = (graph: IGraph) => {
|
||||||
return {
|
return {
|
||||||
payload: graph,
|
payload: graph,
|
||||||
type: ActionType.RECEIVE_GRAPH,
|
type: ActionType.RECEIVE_GRAPH
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const graphLoadFailed = () => {
|
const graphLoadFailed = () => {
|
||||||
return {
|
return {
|
||||||
type: ActionType.GRAPH_LOAD_ERROR,
|
type: ActionType.GRAPH_LOAD_ERROR
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const instanceLoadFailed = () => {
|
const instanceLoadFailed = () => {
|
||||||
return {
|
return {
|
||||||
type: ActionType.INSTANCE_LOAD_ERROR,
|
type: ActionType.INSTANCE_LOAD_ERROR
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
|
export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
|
||||||
return {
|
return {
|
||||||
payload: instanceDetails,
|
payload: instanceDetails,
|
||||||
type: ActionType.RECEIVE_INSTANCE_DETAILS,
|
type: ActionType.RECEIVE_INSTANCE_DETAILS
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/** Async actions: https://redux.js.org/advanced/asyncactions */
|
/** Async actions: https://redux.js.org/advanced/asyncactions */
|
||||||
|
|
||||||
export const fetchInstances = () => {
|
export const fetchInstances = () => {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch) => {
|
||||||
dispatch(requestInstances());
|
dispatch(requestInstances());
|
||||||
return getFromApi("instances")
|
return getFromApi("instances")
|
||||||
.then(instances => dispatch(receiveInstances(instances)))
|
.then(instances => dispatch(receiveInstances(instances)))
|
||||||
.catch(e => dispatch(graphLoadFailed()));
|
.catch(e => dispatch(graphLoadFailed()));
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const selectAndLoadInstance = (instanceName: string) => {
|
export const selectAndLoadInstance = (instanceName: string) => {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch) => {
|
||||||
if (!instanceName) {
|
if (!instanceName) {
|
||||||
dispatch(deselectInstance());
|
dispatch(deselectInstance());
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
dispatch(selectInstance(instanceName));
|
|
||||||
return getFromApi("instances/" + instanceName)
|
|
||||||
.then(details => dispatch(receiveInstanceDetails(details)))
|
|
||||||
.catch(e => dispatch(instanceLoadFailed()));
|
|
||||||
}
|
}
|
||||||
}
|
dispatch(selectInstance(instanceName));
|
||||||
|
return getFromApi("instances/" + instanceName)
|
||||||
|
.then(details => dispatch(receiveInstanceDetails(details)))
|
||||||
|
.catch(e => dispatch(instanceLoadFailed()));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchGraph = () => {
|
export const fetchGraph = () => {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch) => {
|
||||||
dispatch(requestGraph());
|
dispatch(requestGraph());
|
||||||
return Promise.all([getFromApi("graph/edges"), getFromApi("graph/nodes")])
|
return Promise.all([getFromApi("graph/edges"), getFromApi("graph/nodes")])
|
||||||
.then(responses => {
|
.then(responses => {
|
||||||
return {
|
return {
|
||||||
edges: responses[0],
|
edges: responses[0],
|
||||||
nodes: responses[1],
|
nodes: responses[1]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.then(graph => dispatch(receiveGraph(graph)))
|
.then(graph => dispatch(receiveGraph(graph)))
|
||||||
.catch(e => dispatch(graphLoadFailed()));
|
.catch(e => dispatch(graphLoadFailed()));
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,87 +1,87 @@
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from "redux";
|
||||||
|
|
||||||
import { ActionType, IAction, ICurrentInstanceState, IDataState } from './types';
|
import { ActionType, IAction, ICurrentInstanceState, IDataState } from "./types";
|
||||||
|
|
||||||
const initialDataState = {
|
const initialDataState = {
|
||||||
error: false,
|
error: false,
|
||||||
isLoadingGraph: false,
|
isLoadingGraph: false,
|
||||||
isLoadingInstances: false,
|
isLoadingInstances: false
|
||||||
}
|
};
|
||||||
const data = (state: IDataState = initialDataState, action: IAction) => {
|
const data = (state: IDataState = initialDataState, action: IAction) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.REQUEST_INSTANCES:
|
case ActionType.REQUEST_INSTANCES:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
instances: [],
|
instances: [],
|
||||||
isLoadingInstances: true,
|
isLoadingInstances: true
|
||||||
};
|
};
|
||||||
case ActionType.RECEIVE_INSTANCES:
|
case ActionType.RECEIVE_INSTANCES:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
instances: action.payload,
|
instances: action.payload,
|
||||||
isLoadingInstances: false,
|
isLoadingInstances: false
|
||||||
};
|
};
|
||||||
case ActionType.REQUEST_GRAPH:
|
case ActionType.REQUEST_GRAPH:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isLoadingGraph: true,
|
isLoadingGraph: true
|
||||||
};
|
};
|
||||||
case ActionType.RECEIVE_GRAPH:
|
case ActionType.RECEIVE_GRAPH:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graph: action.payload,
|
graph: action.payload,
|
||||||
isLoadingGraph: false,
|
isLoadingGraph: false
|
||||||
};
|
};
|
||||||
case ActionType.GRAPH_LOAD_ERROR:
|
case ActionType.GRAPH_LOAD_ERROR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: true,
|
error: true,
|
||||||
isLoadingGraph: false,
|
isLoadingGraph: false,
|
||||||
isLoadingInstances: false,
|
isLoadingInstances: false
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const initialCurrentInstanceState = {
|
const initialCurrentInstanceState = {
|
||||||
currentInstanceDetails: null,
|
currentInstanceDetails: null,
|
||||||
currentInstanceName: null,
|
currentInstanceName: null,
|
||||||
error: false,
|
error: false,
|
||||||
isLoadingInstanceDetails: false,
|
isLoadingInstanceDetails: false
|
||||||
|
};
|
||||||
|
const currentInstance = (state = initialCurrentInstanceState, action: IAction): ICurrentInstanceState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionType.SELECT_INSTANCE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentInstanceName: action.payload,
|
||||||
|
isLoadingInstanceDetails: true
|
||||||
|
};
|
||||||
|
case ActionType.RECEIVE_INSTANCE_DETAILS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentInstanceDetails: action.payload,
|
||||||
|
isLoadingInstanceDetails: false
|
||||||
|
};
|
||||||
|
case ActionType.DESELECT_INSTANCE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentInstanceDetails: null,
|
||||||
|
currentInstanceName: null
|
||||||
|
};
|
||||||
|
case ActionType.INSTANCE_LOAD_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: true,
|
||||||
|
isLoadingInstanceDetails: false
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const currentInstance = (state = initialCurrentInstanceState , action: IAction): ICurrentInstanceState => {
|
|
||||||
switch (action.type) {
|
|
||||||
case ActionType.SELECT_INSTANCE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentInstanceName: action.payload,
|
|
||||||
isLoadingInstanceDetails: true,
|
|
||||||
};
|
|
||||||
case ActionType.RECEIVE_INSTANCE_DETAILS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentInstanceDetails: action.payload,
|
|
||||||
isLoadingInstanceDetails: false,
|
|
||||||
}
|
|
||||||
case ActionType.DESELECT_INSTANCE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentInstanceDetails: null,
|
|
||||||
currentInstanceName: null,
|
|
||||||
}
|
|
||||||
case ActionType.INSTANCE_LOAD_ERROR:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
error: true,
|
|
||||||
isLoadingInstanceDetails: false,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
export const rootReducer = combineReducers({
|
||||||
currentInstance,
|
currentInstance,
|
||||||
data,
|
data
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,76 +1,76 @@
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
SELECT_INSTANCE = 'SELECT_INSTANCE',
|
SELECT_INSTANCE = "SELECT_INSTANCE",
|
||||||
REQUEST_INSTANCES = 'REQUEST_INSTANCES',
|
REQUEST_INSTANCES = "REQUEST_INSTANCES",
|
||||||
RECEIVE_INSTANCES = 'RECEIVE_INSTANCES',
|
RECEIVE_INSTANCES = "RECEIVE_INSTANCES",
|
||||||
REQUEST_GRAPH = 'REQUEST_GRAPH',
|
REQUEST_GRAPH = "REQUEST_GRAPH",
|
||||||
RECEIVE_GRAPH = 'RECEIVE_GRAPH',
|
RECEIVE_GRAPH = "RECEIVE_GRAPH",
|
||||||
RECEIVE_INSTANCE_DETAILS = 'RECEIVE_INSTANCE_DETAILS',
|
RECEIVE_INSTANCE_DETAILS = "RECEIVE_INSTANCE_DETAILS",
|
||||||
DESELECT_INSTANCE = 'DESELECT_INSTANCE',
|
DESELECT_INSTANCE = "DESELECT_INSTANCE",
|
||||||
GRAPH_LOAD_ERROR = 'GRAPH_LOAD_ERROR',
|
GRAPH_LOAD_ERROR = "GRAPH_LOAD_ERROR",
|
||||||
INSTANCE_LOAD_ERROR = 'INSTANCE_LOAD_ERROR'
|
INSTANCE_LOAD_ERROR = "INSTANCE_LOAD_ERROR"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAction {
|
export interface IAction {
|
||||||
type: ActionType,
|
type: ActionType;
|
||||||
payload: any,
|
payload: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInstance {
|
export interface IInstance {
|
||||||
name: string,
|
name: string;
|
||||||
numUsers?: number,
|
numUsers?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInstanceDetails {
|
export interface IInstanceDetails {
|
||||||
name: string;
|
name: string;
|
||||||
peers?: IInstance[];
|
peers?: IInstance[];
|
||||||
description?: string;
|
description?: string;
|
||||||
domainCount?: number;
|
domainCount?: number;
|
||||||
statusCount?: number;
|
statusCount?: number;
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
version?: string;
|
version?: string;
|
||||||
lastUpdated?: string;
|
lastUpdated?: string;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGraphNode {
|
interface IGraphNode {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGraphEdge {
|
interface IGraphEdge {
|
||||||
source: string;
|
source: string;
|
||||||
target: string;
|
target: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGraph {
|
export interface IGraph {
|
||||||
nodes: IGraphNode[];
|
nodes: IGraphNode[];
|
||||||
edges: IGraphEdge[];
|
edges: IGraphEdge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redux state
|
// Redux state
|
||||||
|
|
||||||
export interface ICurrentInstanceState {
|
export interface ICurrentInstanceState {
|
||||||
currentInstanceDetails: IInstanceDetails | null,
|
currentInstanceDetails: IInstanceDetails | null;
|
||||||
currentInstanceName: string | null,
|
currentInstanceName: string | null;
|
||||||
isLoadingInstanceDetails: boolean,
|
isLoadingInstanceDetails: boolean;
|
||||||
error: boolean,
|
error: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDataState {
|
export interface IDataState {
|
||||||
instances?: IInstance[],
|
instances?: IInstance[];
|
||||||
graph?: IGraph,
|
graph?: IGraph;
|
||||||
isLoadingInstances: boolean,
|
isLoadingInstances: boolean;
|
||||||
isLoadingGraph: boolean,
|
isLoadingGraph: boolean;
|
||||||
error: boolean,
|
error: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAppState {
|
export interface IAppState {
|
||||||
currentInstance: ICurrentInstanceState;
|
currentInstance: ICurrentInstanceState;
|
||||||
data: IDataState,
|
data: IDataState;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
{
|
{
|
||||||
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
|
"extends": [
|
||||||
"linterOptions": {
|
"tslint:recommended",
|
||||||
"exclude": [
|
"tslint-eslint-rules",
|
||||||
"config/**/*.js",
|
"tslint-react",
|
||||||
"node_modules/**/*.ts",
|
"@blueprintjs/tslint-config/blueprint-rules",
|
||||||
"coverage/lcov-report/*.js"
|
"tslint-config-prettier"
|
||||||
]
|
]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue