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",
"version": "0.1.0",
"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": {
"@blueprintjs/core": "^3.4.0",
"@blueprintjs/icons": "^3.1.0",
@ -21,13 +46,8 @@
"redux-thunk": "^2.3.0",
"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": {
"@blueprintjs/tslint-config": "^1.8.0",
"@types/classnames": "^2.2.6",
"@types/jest": "^23.3.1",
"@types/lodash": "^4.14.116",
@ -37,6 +57,9 @@
"@types/react-redux": "^6.0.6",
"@types/react-virtualized": "^9.18.7",
"@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"
}
}

View file

@ -1,24 +1,24 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { Button, Classes, Dialog, NonIdealState, Spinner } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Button, Classes, Dialog, NonIdealState, Spinner } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import ErrorState from './components/ErrorState';
import { Graph } from './components/Graph';
import { Nav } from './components/Nav';
import { Sidebar } from './components/Sidebar';
import { DESKTOP_WIDTH_THRESHOLD } from './constants';
import { fetchGraph, fetchInstances } from './redux/actions';
import { IAppState, IGraph, IInstance } from './redux/types';
import { ErrorState } from "./components/ErrorState";
import { Graph } from "./components/Graph";
import { Nav } from "./components/Nav";
import { Sidebar } from "./components/Sidebar";
import { DESKTOP_WIDTH_THRESHOLD } from "./constants";
import { fetchGraph, fetchInstances } from "./redux/actions";
import { IAppState, IGraph, IInstance } from "./redux/types";
interface IAppProps {
graph?: IGraph;
instances?: IInstance[],
instances?: IInstance[];
isLoadingGraph: boolean;
isLoadingInstances: boolean,
graphLoadError: boolean,
isLoadingInstances: boolean;
graphLoadError: boolean;
fetchInstances: () => void;
fetchGraph: () => void;
}
@ -26,7 +26,6 @@ interface IAppLocalState {
mobileDialogOpen: boolean;
}
class AppImpl extends React.Component<IAppProps, IAppLocalState> {
constructor(props: IAppProps) {
super(props);
this.state = { mobileDialogOpen: false };
@ -40,7 +39,7 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
body = this.graphState();
}
return (
<div className="App bp3-dark">
<div className={`${Classes.DARK} App`}>
<Nav />
{body}
{this.renderMobileDialog()}
@ -66,26 +65,21 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
if (!this.props.graph && !this.props.isLoadingGraph && !this.props.graphLoadError) {
this.props.fetchGraph();
}
}
};
private graphState = () => {
const content = this.props.graphLoadError ? <ErrorState /> : <Graph />
const content = this.props.graphLoadError ? <ErrorState /> : <Graph />;
return (
<div>
<Sidebar />
{content}
</div>
)
}
);
};
private loadingState = (title?: string) => {
return (
<NonIdealState
icon={<Spinner />}
title={title || "Loading..."}
/>
)
}
return <NonIdealState icon={<Spinner />} title={title || "Loading..."} />;
};
private renderMobileDialog = () => {
return (
@ -94,34 +88,30 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
title="Desktop-optimized site"
onClose={this.handleMobileDialogClose}
isOpen={this.state.mobileDialogOpen}
className={Classes.DARK + ' fediverse-about-dialog'}
className={Classes.DARK + " fediverse-about-dialog"}
>
<div className={Classes.DIALOG_BODY}>
<p className={Classes.RUNNING_TEXT}>
fediverse.space is optimized for desktop computers. Feel free to check it out on your phone
(ideally in landscape mode) but for best results, open it on a computer.
fediverse.space is optimized for desktop computers. Feel free to check it out on your phone (ideally in
landscape mode) but for best results, open it on a computer.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
icon={IconNames.THUMBS_UP}
text="OK!"
onClick={this.handleMobileDialogClose}
/>
<Button icon={IconNames.THUMBS_UP} text="OK!" onClick={this.handleMobileDialogClose} />
</div>
</div>
</Dialog>
);
}
};
private handleMobileDialogOpen = () => {
this.setState({ mobileDialogOpen: true });
}
};
private handleMobileDialogClose = () => {
this.setState({ mobileDialogOpen: false });
}
};
}
const mapStateToProps = (state: IAppState) => ({
@ -129,10 +119,13 @@ const mapStateToProps = (state: IAppState) => ({
graphLoadError: state.data.error,
instances: state.data.instances,
isLoadingGraph: state.data.isLoadingGraph,
isLoadingInstances: state.data.isLoadingInstances,
})
isLoadingInstances: state.data.isLoadingInstances
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
fetchGraph: () => dispatch(fetchGraph() as any),
fetchInstances: () => dispatch(fetchInstances() as any),
})
export const App = connect(mapStateToProps, mapDispatchToProps)(AppImpl)
fetchInstances: () => dispatch(fetchInstances() as any)
});
export const App = connect(
mapStateToProps,
mapDispatchToProps
)(AppImpl);

View file

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

View file

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { Sigma, SigmaEnableWebGL, Filter, ForceAtlas2 } from 'react-sigma';
import { selectAndLoadInstance } from '../redux/actions';
import ErrorState from './ErrorState';
import { ErrorState } from './ErrorState';
const STYLE = {
bottom: "0",

View file

@ -1,13 +1,13 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { Button, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { IItemRendererProps, ItemPredicate, Select } from '@blueprintjs/select';
import { Button, MenuItem } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { IItemRendererProps, ItemPredicate, Select } from "@blueprintjs/select";
import { selectAndLoadInstance } from '../redux/actions';
import { IAppState, IInstance } from '../redux/types';
import { selectAndLoadInstance } from "../redux/actions";
import { IAppState, IInstance } from "../redux/types";
interface IInstanceSearchProps {
currentInstanceName: string | null;
@ -18,7 +18,6 @@ interface IInstanceSearchProps {
const InstanceSelect = Select.ofType<IInstance>();
class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
public render() {
return (
<InstanceSelect
@ -29,12 +28,12 @@ class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
disabled={!this.props.instances}
initialContent={this.renderInitialContent()}
noResults={this.renderNoResults()}
popoverProps={{popoverClassName: "fediverse-instance-search-popover"}}
popoverProps={{ popoverClassName: "fediverse-instance-search-popover" }}
>
<Button
icon={IconNames.SELECTION}
rightIcon={IconNames.CARET_DOWN}
text={this.props.currentInstanceName || ("Select an instance")}
text={this.props.currentInstanceName || "Select an instance"}
disabled={!this.props.instances}
/>
</InstanceSelect>
@ -42,48 +41,42 @@ class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
}
private renderInitialContent = () => {
return (
<MenuItem disabled={true} text={"Start typing"} />
);
}
return <MenuItem disabled={true} text={"Start typing"} />;
};
private renderNoResults = () => {
return (
<MenuItem disabled={true} text={"Keep typing"} />
);
}
return <MenuItem disabled={true} text={"Keep typing"} />;
};
private itemRenderer = (item: IInstance, itemProps: IItemRendererProps) => {
if (!itemProps.modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
text={item.name}
key={item.name}
active={itemProps.modifiers.active}
onClick={itemProps.handleClick}
/>
<MenuItem text={item.name} key={item.name} active={itemProps.modifiers.active} onClick={itemProps.handleClick} />
);
}
};
private itemPredicate: ItemPredicate<IInstance> = (query, item, index) => {
if (!item.name || query.length < 4) {
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) => ({
currentInstanceName: state.currentInstance.currentInstanceName,
instances: state.data.instances,
})
instances: state.data.instances
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any),
})
export const InstanceSearch = connect(mapStateToProps, mapDispatchToProps)(InstanceSearchImpl)
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any)
});
export const InstanceSearch = connect(
mapStateToProps,
mapDispatchToProps
)(InstanceSearchImpl);

View file

@ -1,18 +1,17 @@
import * as React from 'react';
import * as React from "react";
import { Alignment, Button, Classes, Dialog, Icon, Navbar } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Alignment, Button, Classes, Code, Dialog, H2, H4, Icon, Navbar } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { InstanceSearch } from './InstanceSearch';
import { InstanceSearch } from "./InstanceSearch";
interface INavState {
aboutIsOpen: boolean;
}
export class Nav extends React.Component<{}, INavState> {
constructor(props: any) {
super(props);
this.state = {aboutIsOpen: false};
this.state = { aboutIsOpen: false };
}
public render() {
@ -20,20 +19,11 @@ export class Nav extends React.Component<{}, INavState> {
<Navbar fixedToTop={true}>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>
<Icon
icon={IconNames.GLOBE_NETWORK}
iconSize={Icon.SIZE_LARGE}
className="fediverse-heading-icon"
/>
<Icon icon={IconNames.GLOBE_NETWORK} 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}
/>
<Button icon={IconNames.INFO_SIGN} text="About" minimal={true} onClick={this.handleAboutOpen} />
{this.renderAboutDialog()}
{/* <Button
icon={<Icon icon={IconNames.GLOBE_NETWORK} />}
@ -50,7 +40,7 @@ export class Nav extends React.Component<{}, INavState> {
<InstanceSearch />
</Navbar.Group>
</Navbar>
)
);
}
private renderAboutDialog = () => {
@ -60,74 +50,76 @@ export class Nav extends React.Component<{}, INavState> {
title="About"
onClose={this.handleAboutClose}
isOpen={this.state.aboutIsOpen}
className={Classes.DARK + ' fediverse-about-dialog'}
className={Classes.DARK + " fediverse-about-dialog"}
>
<div className={Classes.DIALOG_BODY}>
<p className={Classes.RUNNING_TEXT}>
fediverse.space is a tool to visualize networks and communities on the
{' '}<a href="https://en.wikipedia.org/wiki/Fediverse" target="_blank">fediverse</a>.
It works by scraping every instance it can find and aggregating statistics on communication
between these.
fediverse.space is a tool to visualize networks and communities on the{" "}
<a href="https://en.wikipedia.org/wiki/Fediverse" target="_blank">
fediverse
</a>
. It works by scraping every instance it can find and aggregating statistics on communication between these.
</p>
<h2>FAQ</h2>
<h4>Why can't I see details about my instance?</h4>
<H2>FAQ</H2>
<H4>Why can't I see details about my instance?</H4>
<p className={Classes.RUNNING_TEXT}>
Currently, fediverse.space only supports Mastodon and Pleroma instances. In addition, instances
with 5 or fewer users won't be scraped -- it's a tool for understanding communities, not
individuals.
Currently, fediverse.space only supports Mastodon and Pleroma instances. In addition, instances with 5 or
fewer users won't be scraped -- it's a tool for understanding communities, not individuals.
</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}>
fediverse.space scrapes the last 5000 statuses from within the last month on the public
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 mentions to reflect that smaller instances
can play a large role in a community.
fediverse.space scrapes the last 5000 statuses from within the last month on the public 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
mentions to reflect that smaller instances can play a large role in a community.
</p>
<h2>Credits</h2>
<H2>Credits</H2>
<p className={Classes.RUNNING_TEXT}>
This site is inspired by several other sites in the same vein:
<ul className={Classes.LIST}>
<li><a href="https://the-federation.info/" target="_blank">the-federation.info</a></li>
<li><a href="http://fediverse.network/" target="_blank">fediverse.network</a></li>
<li>
<a
href="https://lucahammer.at/vis/fediverse/2018-08-30-mastoverse_hashtags/"
target="_blank"
>
<a href="https://the-federation.info/" target="_blank">
the-federation.info
</a>
</li>
<li>
<a href="http://fediverse.network/" target="_blank">
fediverse.network
</a>
</li>
<li>
<a href="https://lucahammer.at/vis/fediverse/2018-08-30-mastoverse_hashtags/" target="_blank">
Mastodon hashtag network
</a>
{' by '}
{" by "}
<a href="https://vis.social/web/statuses/100634284168959187" target="_blank">
@Luca@vis.social
</a>
</li>
</ul>
The source code for fediverse.space is available on{' '}
<a href="https://gitlab.com/taobojlen/fediverse.space" target="_blank">GitLab</a>;{' '}
issues and pull requests are welcome!
The source code for fediverse.space is available on{" "}
<a href="https://gitlab.com/taobojlen/fediverse.space" target="_blank">
GitLab
</a>
; issues and pull requests are welcome!
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
icon={IconNames.THUMBS_UP}
text="OK!"
onClick={this.handleAboutClose}
/>
<Button icon={IconNames.THUMBS_UP} text="OK!" onClick={this.handleAboutClose} />
</div>
</div>
</Dialog>
)
}
);
};
private handleAboutOpen = () => {
this.setState({aboutIsOpen: true});
}
this.setState({ aboutIsOpen: true });
};
private handleAboutClose = () => {
this.setState({aboutIsOpen: false});
}
this.setState({ aboutIsOpen: false });
};
}

View file

@ -1,25 +1,38 @@
import { orderBy } from 'lodash';
import * as moment from 'moment';
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import * as sanitize from 'sanitize-html';
import { orderBy } from "lodash";
import * as moment from "moment";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import * as sanitize from "sanitize-html";
import {
AnchorButton, Button, Card, Classes, Divider, Elevation, HTMLTable, NonIdealState, Position,
Tab, Tabs, Tooltip
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
AnchorButton,
Button,
Card,
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 { IAppState, IGraph, IInstanceDetails } from '../redux/types';
import ErrorState from './ErrorState';
import { selectAndLoadInstance } from "../redux/actions";
import { IAppState, IGraph, IInstanceDetails } from "../redux/types";
import { ErrorState } from "./ErrorState";
interface ISidebarProps {
graph?: IGraph,
instanceName: string | null,
instanceLoadError: boolean,
instanceDetails: IInstanceDetails | null,
graph?: IGraph;
instanceName: string | null;
instanceLoadError: boolean;
instanceDetails: IInstanceDetails | null;
isLoadingInstanceDetails: boolean;
selectAndLoadInstance: (instanceName: string) => void;
}
@ -27,7 +40,6 @@ interface ISidebarState {
isOpen: boolean;
}
class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
constructor(props: ISidebarProps) {
super(props);
const isOpen = window.innerWidth >= 900 ? true : false;
@ -50,21 +62,21 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
{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) {
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personalinstance") > -1) {
return this.renderPersonalInstanceErrorState();
} else if (this.props.instanceDetails.status !== 'success') {
} else if (this.props.instanceDetails.status !== "success") {
return this.renderMissingDataState();
} else if (this.props.instanceLoadError) {
return <ErrorState />;
@ -73,21 +85,21 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<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()} />}
{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;
@ -96,12 +108,8 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
} else {
content = (
<span>
{this.props.instanceName + ' '}
<Tooltip
content="Open link in new tab"
position={Position.TOP}
className={Classes.DARK}
>
{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>
@ -109,21 +117,19 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
}
return (
<div>
<h2>{content}</h2>
<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)}} />
)
}
return <p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{ __html: sanitize(description) }} />;
};
private renderVersionAndCounts = () => {
const version = this.props.instanceDetails!.version;
@ -137,7 +143,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<tbody>
<tr>
<td>Version</td>
<td>{<code>{version}</code> || "Unknown"}</td>
<td>{<Code>{version}</Code> || "Unknown"}</td>
</tr>
<tr>
<td>Users</td>
@ -158,8 +164,8 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
</tbody>
</HTMLTable>
</div>
)
}
);
};
private renderNeighbors = () => {
if (!this.props.graph || !this.props.instanceName) {
@ -169,22 +175,26 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
const neighbors: any[] = [];
edges.forEach(e => {
if (e.source === this.props.instanceName) {
neighbors.push({neighbor: e.target, weight: e.size});
neighbors.push({ neighbor: e.target, weight: e.size });
} else {
neighbors.push({neighbor: e.source, weight: e.size});
neighbors.push({ neighbor: e.source, weight: e.size });
}
})
const neighborRows = orderBy(neighbors, ['weight'], ['desc']).map((neighborDetails: any, idx: number) => (
});
const neighborRows = orderBy(neighbors, ["weight"], ["desc"]).map((neighborDetails: any, idx: number) => (
<tr key={idx}>
<td><AnchorButton minimal={true} onClick={this.selectInstance}>{neighborDetails.neighbor}</AnchorButton></td>
<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.
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>
@ -193,13 +203,11 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<th>Mention ratio</th>
</tr>
</thead>
<tbody>
{neighborRows}
</tbody>
<tbody>{neighborRows}</tbody>
</HTMLTable>
</div>
);
}
};
private renderPeers = () => {
const peers = this.props.instanceDetails!.peers;
@ -208,7 +216,11 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
}
const peerRows = peers.map(instance => (
<tr key={instance.name} onClick={this.selectInstance}>
<td><AnchorButton minimal={true} onClick={this.selectInstance}>{instance.name}</AnchorButton></td>
<td>
<AnchorButton minimal={true} onClick={this.selectInstance}>
{instance.name}
</AnchorButton>
</td>
</tr>
));
return (
@ -217,13 +229,11 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
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>
<tbody>{peerRows}</tbody>
</HTMLTable>
</div>
)
}
);
};
private renderEmptyState = () => {
return (
@ -232,35 +242,37 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
title="No instance selected"
description="Select an instance from the graph or the top-right dropdown to see its details."
/>
)
}
);
};
private renderLoadingState = () => {
return (
<div>
<h4><span className={Classes.SKELETON}>Description</span></h4>
<H4>
<span className={Classes.SKELETON}>Description</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.
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.
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>
<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.
</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.
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 renderPersonalInstanceErrorState = () => {
return (
@ -268,11 +280,14 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
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>}
/>
)
action={
<AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@tao" target="_blank">
Message @tao to opt in
</AnchorButton>
}
/>
);
};
private renderMissingDataState = () => {
return (
@ -281,16 +296,16 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
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) => ({
@ -298,9 +313,12 @@ const mapStateToProps = (state: IAppState) => ({
instanceDetails: state.currentInstance.currentInstanceDetails,
instanceLoadError: state.currentInstance.error,
instanceName: state.currentInstance.currentInstanceName,
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails,
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
});
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/icons/lib/css/blueprint-icons.css';
import '../node_modules/@blueprintjs/select/lib/css/blueprint-select.css';
import '../node_modules/normalize.css/normalize.css';
import './index.css';
import "../node_modules/@blueprintjs/core/lib/css/blueprint.css";
import "../node_modules/@blueprintjs/icons/lib/css/blueprint-icons.css";
import "../node_modules/@blueprintjs/select/lib/css/blueprint-select.css";
import "../node_modules/normalize.css/normalize.css";
import "./index.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { applyMiddleware, compose, createStore } from 'redux';
import thunk from 'redux-thunk';
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { applyMiddleware, compose, createStore } from "redux";
import thunk from "redux-thunk";
import { FocusStyleManager } from '@blueprintjs/core';
import { FocusStyleManager } from "@blueprintjs/core";
import { App } from './App';
import { rootReducer } from './redux/reducers';
import { App } from "./App";
import { rootReducer } from "./redux/reducers";
// https://blueprintjs.com/docs/#core/accessibility.focus-management
FocusStyleManager.onlyShowFocusOnTabs();
@ -21,13 +21,11 @@ FocusStyleManager.onlyShowFocusOnTabs();
// Initialize redux
// @ts-ignore
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, composeEnhancers(
applyMiddleware(thunk)
));
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
document.getElementById("root") as HTMLElement
);

View file

@ -1,65 +1,64 @@
import { Dispatch } from 'redux';
import { Dispatch } from "redux";
import { getFromApi } from '../util';
import { ActionType, IGraph, IInstance, IInstanceDetails } from './types';
import { getFromApi } from "../util";
import { ActionType, IGraph, IInstance, IInstanceDetails } from "./types";
// selectInstance and deselectInstance are not exported since we only call them from selectAndLoadInstance()
const selectInstance = (instanceName: string) => {
return {
payload: instanceName,
type: ActionType.SELECT_INSTANCE,
}
}
type: ActionType.SELECT_INSTANCE
};
};
const deselectInstance = () => {
return {
type: ActionType.DESELECT_INSTANCE,
}
}
type: ActionType.DESELECT_INSTANCE
};
};
export const requestInstances = () => {
return {
type: ActionType.REQUEST_INSTANCES,
}
}
type: ActionType.REQUEST_INSTANCES
};
};
export const receiveInstances = (instances: IInstance[]) => {
return {
payload: instances,
type: ActionType.RECEIVE_INSTANCES,
}
}
type: ActionType.RECEIVE_INSTANCES
};
};
export const requestGraph = () => {
return {
type: ActionType.REQUEST_GRAPH,
}
}
type: ActionType.REQUEST_GRAPH
};
};
export const receiveGraph = (graph: IGraph) => {
return {
payload: graph,
type: ActionType.RECEIVE_GRAPH,
}
}
type: ActionType.RECEIVE_GRAPH
};
};
const graphLoadFailed = () => {
return {
type: ActionType.GRAPH_LOAD_ERROR,
}
}
type: ActionType.GRAPH_LOAD_ERROR
};
};
const instanceLoadFailed = () => {
return {
type: ActionType.INSTANCE_LOAD_ERROR,
}
}
type: ActionType.INSTANCE_LOAD_ERROR
};
};
export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
return {
payload: instanceDetails,
type: ActionType.RECEIVE_INSTANCE_DETAILS,
}
}
type: ActionType.RECEIVE_INSTANCE_DETAILS
};
};
/** Async actions: https://redux.js.org/advanced/asyncactions */
@ -69,8 +68,8 @@ export const fetchInstances = () => {
return getFromApi("instances")
.then(instances => dispatch(receiveInstances(instances)))
.catch(e => dispatch(graphLoadFailed()));
}
}
};
};
export const selectAndLoadInstance = (instanceName: string) => {
return (dispatch: Dispatch) => {
@ -82,8 +81,8 @@ export const selectAndLoadInstance = (instanceName: string) => {
return getFromApi("instances/" + instanceName)
.then(details => dispatch(receiveInstanceDetails(details)))
.catch(e => dispatch(instanceLoadFailed()));
}
}
};
};
export const fetchGraph = () => {
return (dispatch: Dispatch) => {
@ -92,10 +91,10 @@ export const fetchGraph = () => {
.then(responses => {
return {
edges: responses[0],
nodes: responses[1],
nodes: responses[1]
};
})
.then(graph => dispatch(receiveGraph(graph)))
.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 = {
error: false,
isLoadingGraph: false,
isLoadingInstances: false,
}
isLoadingInstances: false
};
const data = (state: IDataState = initialDataState, action: IAction) => {
switch (action.type) {
case ActionType.REQUEST_INSTANCES:
return {
...state,
instances: [],
isLoadingInstances: true,
isLoadingInstances: true
};
case ActionType.RECEIVE_INSTANCES:
return {
...state,
instances: action.payload,
isLoadingInstances: false,
isLoadingInstances: false
};
case ActionType.REQUEST_GRAPH:
return {
...state,
isLoadingGraph: true,
isLoadingGraph: true
};
case ActionType.RECEIVE_GRAPH:
return {
...state,
graph: action.payload,
isLoadingGraph: false,
isLoadingGraph: false
};
case ActionType.GRAPH_LOAD_ERROR:
return {
...state,
error: true,
isLoadingGraph: false,
isLoadingInstances: false,
isLoadingInstances: false
};
default:
return state;
}
}
};
const initialCurrentInstanceState = {
currentInstanceDetails: null,
currentInstanceName: null,
error: false,
isLoadingInstanceDetails: false,
isLoadingInstanceDetails: false
};
const currentInstance = (state = initialCurrentInstanceState , action: IAction): ICurrentInstanceState => {
const currentInstance = (state = initialCurrentInstanceState, action: IAction): ICurrentInstanceState => {
switch (action.type) {
case ActionType.SELECT_INSTANCE:
return {
...state,
currentInstanceName: action.payload,
isLoadingInstanceDetails: true,
isLoadingInstanceDetails: true
};
case ActionType.RECEIVE_INSTANCE_DETAILS:
return {
...state,
currentInstanceDetails: action.payload,
isLoadingInstanceDetails: false,
}
isLoadingInstanceDetails: false
};
case ActionType.DESELECT_INSTANCE:
return {
...state,
currentInstanceDetails: null,
currentInstanceName: null,
}
currentInstanceName: null
};
case ActionType.INSTANCE_LOAD_ERROR:
return {
...state,
error: true,
isLoadingInstanceDetails: false,
isLoadingInstanceDetails: false
};
default:
return state;
}
}
};
export const rootReducer = combineReducers({
currentInstance,
data,
})
data
});

View file

@ -1,23 +1,23 @@
export enum ActionType {
SELECT_INSTANCE = 'SELECT_INSTANCE',
REQUEST_INSTANCES = 'REQUEST_INSTANCES',
RECEIVE_INSTANCES = 'RECEIVE_INSTANCES',
REQUEST_GRAPH = 'REQUEST_GRAPH',
RECEIVE_GRAPH = 'RECEIVE_GRAPH',
RECEIVE_INSTANCE_DETAILS = 'RECEIVE_INSTANCE_DETAILS',
DESELECT_INSTANCE = 'DESELECT_INSTANCE',
GRAPH_LOAD_ERROR = 'GRAPH_LOAD_ERROR',
INSTANCE_LOAD_ERROR = 'INSTANCE_LOAD_ERROR'
SELECT_INSTANCE = "SELECT_INSTANCE",
REQUEST_INSTANCES = "REQUEST_INSTANCES",
RECEIVE_INSTANCES = "RECEIVE_INSTANCES",
REQUEST_GRAPH = "REQUEST_GRAPH",
RECEIVE_GRAPH = "RECEIVE_GRAPH",
RECEIVE_INSTANCE_DETAILS = "RECEIVE_INSTANCE_DETAILS",
DESELECT_INSTANCE = "DESELECT_INSTANCE",
GRAPH_LOAD_ERROR = "GRAPH_LOAD_ERROR",
INSTANCE_LOAD_ERROR = "INSTANCE_LOAD_ERROR"
}
export interface IAction {
type: ActionType,
payload: any,
type: ActionType;
payload: any;
}
export interface IInstance {
name: string,
numUsers?: number,
name: string;
numUsers?: number;
}
export interface IInstanceDetails {
@ -56,21 +56,21 @@ export interface IGraph {
// Redux state
export interface ICurrentInstanceState {
currentInstanceDetails: IInstanceDetails | null,
currentInstanceName: string | null,
isLoadingInstanceDetails: boolean,
error: boolean,
currentInstanceDetails: IInstanceDetails | null;
currentInstanceName: string | null;
isLoadingInstanceDetails: boolean;
error: boolean;
}
export interface IDataState {
instances?: IInstance[],
graph?: IGraph,
isLoadingInstances: boolean,
isLoadingGraph: boolean,
error: boolean,
instances?: IInstance[];
graph?: IGraph;
isLoadingInstances: boolean;
isLoadingGraph: boolean;
error: boolean;
}
export interface IAppState {
currentInstance: ICurrentInstanceState;
data: IDataState,
data: IDataState;
}

View file

@ -1,10 +1,9 @@
{
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts",
"coverage/lcov-report/*.js"
"extends": [
"tslint:recommended",
"tslint-eslint-rules",
"tslint-react",
"@blueprintjs/tslint-config/blueprint-rules",
"tslint-config-prettier"
]
}
}

File diff suppressed because it is too large Load diff