style(linting): set up automatic linting

This commit is contained in:
Tao Bror Bojlén 2019-04-17 11:44:48 +01:00
parent 1c1536e71d
commit 49fe41128a
No known key found for this signature in database
GPG key ID: C6EC7AAB905F9E6F
13 changed files with 1388 additions and 760 deletions

View file

@ -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"
} }
} }

View file

@ -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);

View file

@ -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;

View file

@ -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",

View file

@ -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);

View file

@ -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 });
} };
} }

View file

@ -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);

View file

@ -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
); );

View file

@ -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()));
} };
} };

View file

@ -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
}) });

View file

@ -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;
} }

View file

@ -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