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,26 +65,21 @@ 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 (
@ -94,34 +88,30 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
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,13 +1,13 @@
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;
@ -18,7 +18,6 @@ interface IInstanceSearchProps {
const InstanceSelect = Select.ofType<IInstance>(); const InstanceSelect = Select.ofType<IInstance>();
class InstanceSearchImpl extends React.Component<IInstanceSearchProps> { class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
public render() { public render() {
return ( return (
<InstanceSelect <InstanceSelect
@ -29,12 +28,12 @@ class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
disabled={!this.props.instances} disabled={!this.props.instances}
initialContent={this.renderInitialContent()} initialContent={this.renderInitialContent()}
noResults={this.renderNoResults()} noResults={this.renderNoResults()}
popoverProps={{popoverClassName: "fediverse-instance-search-popover"}} popoverProps={{ popoverClassName: "fediverse-instance-search-popover" }}
> >
<Button <Button
icon={IconNames.SELECTION} icon={IconNames.SELECTION}
rightIcon={IconNames.CARET_DOWN} rightIcon={IconNames.CARET_DOWN}
text={this.props.currentInstanceName || ("Select an instance")} text={this.props.currentInstanceName || "Select an instance"}
disabled={!this.props.instances} disabled={!this.props.instances}
/> />
</InstanceSelect> </InstanceSelect>
@ -42,48 +41,42 @@ class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
} }
private renderInitialContent = () => { private renderInitialContent = () => {
return ( return <MenuItem disabled={true} text={"Start typing"} />;
<MenuItem disabled={true} text={"Start typing"} /> };
);
}
private renderNoResults = () => { private renderNoResults = () => {
return ( return <MenuItem disabled={true} text={"Keep typing"} />;
<MenuItem disabled={true} text={"Keep typing"} /> };
);
}
private itemRenderer = (item: IInstance, itemProps: IItemRendererProps) => { private itemRenderer = (item: IInstance, itemProps: IItemRendererProps) => {
if (!itemProps.modifiers.matchesPredicate) { if (!itemProps.modifiers.matchesPredicate) {
return null; return null;
} }
return ( return (
<MenuItem <MenuItem text={item.name} key={item.name} active={itemProps.modifiers.active} onClick={itemProps.handleClick} />
text={item.name}
key={item.name}
active={itemProps.modifiers.active}
onClick={itemProps.handleClick}
/>
); );
} };
private itemPredicate: ItemPredicate<IInstance> = (query, item, index) => { private itemPredicate: ItemPredicate<IInstance> = (query, item, index) => {
if (!item.name || query.length < 4) { if (!item.name || query.length < 4) {
return false; return false;
} }
return item.name.toLowerCase().indexOf(query.toLowerCase()) >= 0; return item.name.toLowerCase().indexOf(query.toLowerCase()) >= 0;
} };
private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => { private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => {
this.props.selectAndLoadInstance(item.name); 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,18 +1,17 @@
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) { constructor(props: any) {
super(props); super(props);
this.state = {aboutIsOpen: false}; this.state = { aboutIsOpen: false };
} }
public render() { public render() {
@ -20,20 +19,11 @@ export class Nav extends React.Component<{}, INavState> {
<Navbar fixedToTop={true}> <Navbar fixedToTop={true}>
<Navbar.Group align={Alignment.LEFT}> <Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading> <Navbar.Heading>
<Icon <Icon icon={IconNames.GLOBE_NETWORK} iconSize={Icon.SIZE_LARGE} className="fediverse-heading-icon" />
icon={IconNames.GLOBE_NETWORK}
iconSize={Icon.SIZE_LARGE}
className="fediverse-heading-icon"
/>
fediverse.space fediverse.space
</Navbar.Heading> </Navbar.Heading>
<Navbar.Divider /> <Navbar.Divider />
<Button <Button icon={IconNames.INFO_SIGN} text="About" minimal={true} onClick={this.handleAboutOpen} />
icon={IconNames.INFO_SIGN}
text="About"
minimal={true}
onClick={this.handleAboutOpen}
/>
{this.renderAboutDialog()} {this.renderAboutDialog()}
{/* <Button {/* <Button
icon={<Icon icon={IconNames.GLOBE_NETWORK} />} icon={<Icon icon={IconNames.GLOBE_NETWORK} />}
@ -50,7 +40,7 @@ export class Nav extends React.Component<{}, INavState> {
<InstanceSearch /> <InstanceSearch />
</Navbar.Group> </Navbar.Group>
</Navbar> </Navbar>
) );
} }
private renderAboutDialog = () => { private renderAboutDialog = () => {
@ -60,74 +50,76 @@ export class Nav extends React.Component<{}, INavState> {
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>
. It works by scraping every instance it can find and aggregating statistics on communication between these.
</p> </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 fediverse.space scrapes the last 5000 statuses from within the last month on the public timeline of each
timeline of each instance. It looks at the ratio of instance. It looks at the ratio of
<code>mentions of an instance / total statuses</code>. <Code>mentions of an instance / total statuses</Code>. It uses a ratio rather than an absolute number of
It uses a ratio rather than an absolute number of mentions to reflect that smaller instances mentions to reflect that smaller instances can play a large role in a community.
can play a large role in a community.
</p> </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><a href="http://fediverse.network/" target="_blank">fediverse.network</a></li>
<li> <li>
<a <a href="https://the-federation.info/" target="_blank">
href="https://lucahammer.at/vis/fediverse/2018-08-30-mastoverse_hashtags/" the-federation.info
target="_blank" </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 Mastodon hashtag network
</a> </a>
{' by '} {" by "}
<a href="https://vis.social/web/statuses/100634284168959187" target="_blank"> <a href="https://vis.social/web/statuses/100634284168959187" target="_blank">
@Luca@vis.social @Luca@vis.social
</a> </a>
</li> </li>
</ul> </ul>
The source code for fediverse.space is available on{' '} The source code for fediverse.space is available on{" "}
<a href="https://gitlab.com/taobojlen/fediverse.space" target="_blank">GitLab</a>;{' '} <a href="https://gitlab.com/taobojlen/fediverse.space" target="_blank">
issues and pull requests are welcome! GitLab
</a>
; issues and pull requests are welcome!
</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.handleAboutClose} />
icon={IconNames.THUMBS_UP}
text="OK!"
onClick={this.handleAboutClose}
/>
</div> </div>
</div> </div>
</Dialog> </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,25 +1,38 @@
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;
} }
@ -27,7 +40,6 @@ interface ISidebarState {
isOpen: boolean; isOpen: boolean;
} }
class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> { class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
constructor(props: ISidebarProps) { constructor(props: ISidebarProps) {
super(props); super(props);
const isOpen = window.innerWidth >= 900 ? true : false; const isOpen = window.innerWidth >= 900 ? true : false;
@ -50,21 +62,21 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
{this.renderSidebarContents()} {this.renderSidebarContents()}
</Card> </Card>
</div> </div>
) );
} }
private handleToggle = () => { private handleToggle = () => {
this.setState({ isOpen: !this.state.isOpen }); this.setState({ isOpen: !this.state.isOpen });
} };
private renderSidebarContents = () => { private renderSidebarContents = () => {
if (this.props.isLoadingInstanceDetails) { if (this.props.isLoadingInstanceDetails) {
return this.renderLoadingState(); return this.renderLoadingState();
} else if (!this.props.instanceDetails) { } else if (!this.props.instanceDetails) {
return this.renderEmptyState(); 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(); return this.renderPersonalInstanceErrorState();
} else if (this.props.instanceDetails.status !== 'success') { } else if (this.props.instanceDetails.status !== "success") {
return this.renderMissingDataState(); return this.renderMissingDataState();
} else if (this.props.instanceLoadError) { } else if (this.props.instanceLoadError) {
return <ErrorState />; return <ErrorState />;
@ -73,21 +85,21 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<div> <div>
{this.renderHeading()} {this.renderHeading()}
<Tabs> <Tabs>
{this.props.instanceDetails.description && {this.props.instanceDetails.description && (
<Tab id="description" title="Description" panel={this.renderDescription()} />} <Tab id="description" title="Description" panel={this.renderDescription()} />
{this.shouldRenderStats() && )}
<Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />} {this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} /> <Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
<Tab id="peers" title="Known peers" panel={this.renderPeers()} /> <Tab id="peers" title="Known peers" panel={this.renderPeers()} />
</Tabs> </Tabs>
</div> </div>
); );
} };
private shouldRenderStats = () => { private shouldRenderStats = () => {
const details = this.props.instanceDetails; const details = this.props.instanceDetails;
return details && (details.version || details.userCount || details.statusCount || details.domainCount); return details && (details.version || details.userCount || details.statusCount || details.domainCount);
} };
private renderHeading = () => { private renderHeading = () => {
let content: JSX.Element; let content: JSX.Element;
@ -96,12 +108,8 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
} else { } else {
content = ( content = (
<span> <span>
{this.props.instanceName + ' '} {this.props.instanceName + " "}
<Tooltip <Tooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
content="Open link in new tab"
position={Position.TOP}
className={Classes.DARK}
>
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} /> <AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
</Tooltip> </Tooltip>
</span> </span>
@ -109,21 +117,19 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
} }
return ( return (
<div> <div>
<h2>{content}</h2> <H2>{content}</H2>
<Divider /> <Divider />
</div> </div>
); );
} };
private renderDescription = () => { private renderDescription = () => {
const description = this.props.instanceDetails!.description; const description = this.props.instanceDetails!.description;
if (!description) { if (!description) {
return; return;
} }
return ( return <p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{ __html: sanitize(description) }} />;
<p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{__html: sanitize(description)}} /> };
)
}
private renderVersionAndCounts = () => { private renderVersionAndCounts = () => {
const version = this.props.instanceDetails!.version; const version = this.props.instanceDetails!.version;
@ -137,7 +143,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<tbody> <tbody>
<tr> <tr>
<td>Version</td> <td>Version</td>
<td>{<code>{version}</code> || "Unknown"}</td> <td>{<Code>{version}</Code> || "Unknown"}</td>
</tr> </tr>
<tr> <tr>
<td>Users</td> <td>Users</td>
@ -158,8 +164,8 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
</tbody> </tbody>
</HTMLTable> </HTMLTable>
</div> </div>
) );
} };
private renderNeighbors = () => { private renderNeighbors = () => {
if (!this.props.graph || !this.props.instanceName) { if (!this.props.graph || !this.props.instanceName) {
@ -169,22 +175,26 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
const neighbors: any[] = []; const neighbors: any[] = [];
edges.forEach(e => { edges.forEach(e => {
if (e.source === this.props.instanceName) { if (e.source === this.props.instanceName) {
neighbors.push({neighbor: e.target, weight: e.size}); neighbors.push({ neighbor: e.target, weight: e.size });
} else { } 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}> <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> <td>{neighborDetails.weight.toFixed(4)}</td>
</tr> </tr>
)); ));
return ( return (
<div> <div>
<p className={Classes.TEXT_MUTED}> <p className={Classes.TEXT_MUTED}>
The mention ratio is the average of how many times the two instances mention each other per status. The mention ratio is the average of how many times the two instances mention each other per status. A mention
A mention ratio of 1 would mean that every single status contained a mention of a user on the other instance. ratio of 1 would mean that every single status contained a mention of a user on the other instance.
</p> </p>
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table"> <HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
<thead> <thead>
@ -193,13 +203,11 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
<th>Mention ratio</th> <th>Mention ratio</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{neighborRows}</tbody>
{neighborRows}
</tbody>
</HTMLTable> </HTMLTable>
</div> </div>
); );
} };
private renderPeers = () => { private renderPeers = () => {
const peers = this.props.instanceDetails!.peers; const peers = this.props.instanceDetails!.peers;
@ -208,7 +216,11 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
} }
const peerRows = peers.map(instance => ( const peerRows = peers.map(instance => (
<tr key={instance.name} onClick={this.selectInstance}> <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> </tr>
)); ));
return ( return (
@ -217,13 +229,11 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
All the instances, past and present, that {this.props.instanceName} knows about. All the instances, past and present, that {this.props.instanceName} knows about.
</p> </p>
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table"> <HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
<tbody> <tbody>{peerRows}</tbody>
{peerRows}
</tbody>
</HTMLTable> </HTMLTable>
</div> </div>
) );
} };
private renderEmptyState = () => { private renderEmptyState = () => {
return ( return (
@ -232,35 +242,37 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
title="No instance selected" title="No instance selected"
description="Select an instance from the graph or the top-right dropdown to see its details." description="Select an instance from the graph or the top-right dropdown to see its details."
/> />
) );
} };
private renderLoadingState = () => { private renderLoadingState = () => {
return ( return (
<div> <div>
<h4><span className={Classes.SKELETON}>Description</span></h4> <H4>
<span className={Classes.SKELETON}>Description</span>
</H4>
<p className={Classes.SKELETON}> <p className={Classes.SKELETON}>
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit
Cupiditate sit voluptates quia nulla et saepe id suscipit. voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur necessitatibus
Voluptas sed rerum placeat consectetur pariatur necessitatibus tempora. tempora. Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt.
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
Cupiditate sit voluptates quia nulla et saepe id suscipit. necessitatibus tempora.
Voluptas sed rerum placeat consectetur pariatur necessitatibus tempora.
</p> </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}> <p className={Classes.SKELETON}>
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit
</p> voluptates quia nulla et saepe id suscipit. Eaque rerum sequi unde omnis voluptatibus non quia fugit.
<h4><span className={Classes.SKELETON}>Stats</span></h4> Dignissimos asperiores aut incidunt. Cupiditate sit voluptates quia nulla et saepe id suscipit.
<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> </p>
</div> </div>
); );
} };
private renderPersonalInstanceErrorState = () => { private renderPersonalInstanceErrorState = () => {
return ( return (
@ -268,11 +280,14 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
icon={IconNames.BLOCKED_PERSON} icon={IconNames.BLOCKED_PERSON}
title="No data" 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." 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"> action={
Message @tao to opt in</AnchorButton>} <AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@tao" target="_blank">
/> Message @tao to opt in
) </AnchorButton>
} }
/>
);
};
private renderMissingDataState = () => { private renderMissingDataState = () => {
return ( return (
@ -281,16 +296,16 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
title="No data" 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." description="This instance could not be crawled. Either it was down or it's an instance type we don't support yet."
/> />
) );
} };
private openInstanceLink = () => { private openInstanceLink = () => {
window.open("https://" + this.props.instanceName, "_blank"); window.open("https://" + this.props.instanceName, "_blank");
} };
private selectInstance = (e: any) => { private selectInstance = (e: any) => {
this.props.selectAndLoadInstance(e.target.innerText); this.props.selectAndLoadInstance(e.target.innerText);
} };
} }
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => ({
@ -298,9 +313,12 @@ const mapStateToProps = (state: IAppState) => ({
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,65 +1,64 @@
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 */
@ -69,8 +68,8 @@ export const fetchInstances = () => {
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) => {
@ -82,8 +81,8 @@ export const selectAndLoadInstance = (instanceName: string) => {
return getFromApi("instances/" + instanceName) return getFromApi("instances/" + instanceName)
.then(details => dispatch(receiveInstanceDetails(details))) .then(details => dispatch(receiveInstanceDetails(details)))
.catch(e => dispatch(instanceLoadFailed())); .catch(e => dispatch(instanceLoadFailed()));
} };
} };
export const fetchGraph = () => { export const fetchGraph = () => {
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
@ -92,10 +91,10 @@ export const fetchGraph = () => {
.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 => { const currentInstance = (state = initialCurrentInstanceState, action: IAction): ICurrentInstanceState => {
switch (action.type) { switch (action.type) {
case ActionType.SELECT_INSTANCE: case ActionType.SELECT_INSTANCE:
return { return {
...state, ...state,
currentInstanceName: action.payload, currentInstanceName: action.payload,
isLoadingInstanceDetails: true, isLoadingInstanceDetails: true
}; };
case ActionType.RECEIVE_INSTANCE_DETAILS: case ActionType.RECEIVE_INSTANCE_DETAILS:
return { return {
...state, ...state,
currentInstanceDetails: action.payload, currentInstanceDetails: action.payload,
isLoadingInstanceDetails: false, isLoadingInstanceDetails: false
} };
case ActionType.DESELECT_INSTANCE: case ActionType.DESELECT_INSTANCE:
return { return {
...state, ...state,
currentInstanceDetails: null, currentInstanceDetails: null,
currentInstanceName: null, currentInstanceName: null
} };
case ActionType.INSTANCE_LOAD_ERROR: case ActionType.INSTANCE_LOAD_ERROR:
return { return {
...state, ...state,
error: true, error: true,
isLoadingInstanceDetails: false, isLoadingInstanceDetails: false
}; };
default: default:
return state; return state;
} }
} };
export const rootReducer = combineReducers({ export const rootReducer = combineReducers({
currentInstance, currentInstance,
data, data
}) });

View file

@ -1,23 +1,23 @@
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 {
@ -56,21 +56,21 @@ export interface IGraph {
// 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