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,62 +65,53 @@ 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 (
<Dialog
icon={IconNames.DESKTOP}
title="Desktop-optimized site"
onClose={this.handleMobileDialogClose}
isOpen={this.state.mobileDialogOpen}
className={Classes.DARK + ' fediverse-about-dialog'}
icon={IconNames.DESKTOP}
title="Desktop-optimized site"
onClose={this.handleMobileDialogClose}
isOpen={this.state.mobileDialogOpen}
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.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
icon={IconNames.THUMBS_UP}
text="OK!"
onClick={this.handleMobileDialogClose}
/>
</div>
<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.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<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,89 +1,82 @@
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;
instances?: IInstance[];
selectAndLoadInstance: (instanceName: string) => void;
currentInstanceName: string | null;
instances?: IInstance[];
selectAndLoadInstance: (instanceName: string) => void;
}
const InstanceSelect = Select.ofType<IInstance>();
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() {
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>
);
}
private renderInitialContent = () => {
return <MenuItem disabled={true} text={"Start typing"} />;
};
private renderInitialContent = () => {
return (
<MenuItem disabled={true} text={"Start typing"} />
);
}
private renderNoResults = () => {
return <MenuItem disabled={true} text={"Keep typing"} />;
};
private renderNoResults = () => {
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} />
);
};
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}
/>
);
private itemPredicate: ItemPredicate<IInstance> = (query, item, index) => {
if (!item.name || query.length < 4) {
return false;
}
return item.name.toLowerCase().indexOf(query.toLowerCase()) >= 0;
};
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);
}
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,
})
currentInstanceName: state.currentInstance.currentInstanceName,
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,41 +1,31 @@
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;
aboutIsOpen: boolean;
}
export class Nav extends React.Component<{}, INavState> {
constructor(props: any) {
super(props);
this.state = { aboutIsOpen: false };
}
constructor(props: any) {
super(props);
this.state = {aboutIsOpen: false};
}
public render() {
return (
<Navbar fixedToTop={true}>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>
<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}
/>
{this.renderAboutDialog()}
{/* <Button
public render() {
return (
<Navbar fixedToTop={true}>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>
<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} />
{this.renderAboutDialog()}
{/* <Button
icon={<Icon icon={IconNames.GLOBE_NETWORK} />}
text="Network"
minimal={true}
@ -45,89 +35,91 @@ export class Nav extends React.Component<{}, INavState> {
text="Map"
minimal={true}
/> */}
</Navbar.Group>
<Navbar.Group align={Alignment.RIGHT}>
<InstanceSearch />
</Navbar.Group>
</Navbar>
)
}
</Navbar.Group>
<Navbar.Group align={Alignment.RIGHT}>
<InstanceSearch />
</Navbar.Group>
</Navbar>
);
}
private renderAboutDialog = () => {
return (
<Dialog
icon={IconNames.INFO_SIGN}
title="About"
onClose={this.handleAboutClose}
isOpen={this.state.aboutIsOpen}
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.
</p>
private renderAboutDialog = () => {
return (
<Dialog
icon={IconNames.INFO_SIGN}
title="About"
onClose={this.handleAboutClose}
isOpen={this.state.aboutIsOpen}
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.
</p>
<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.
</p>
<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.
</p>
<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.
</p>
<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.
</p>
<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"
>
Mastodon hashtag network
</a>
{' 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!
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
icon={IconNames.THUMBS_UP}
text="OK!"
onClick={this.handleAboutClose}
/>
</div>
</div>
</Dialog>
)
}
<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">
Mastodon hashtag network
</a>
{" 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!
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button icon={IconNames.THUMBS_UP} text="OK!" onClick={this.handleAboutClose} />
</div>
</div>
</Dialog>
);
};
private handleAboutOpen = () => {
this.setState({aboutIsOpen: true});
}
private handleAboutOpen = () => {
this.setState({ aboutIsOpen: true });
};
private handleAboutClose = () => {
this.setState({aboutIsOpen: false});
}
private handleAboutClose = () => {
this.setState({ aboutIsOpen: false });
};
}

View File

@ -1,306 +1,324 @@
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,
isLoadingInstanceDetails: boolean;
selectAndLoadInstance: (instanceName: string) => void;
graph?: IGraph;
instanceName: string | null;
instanceLoadError: boolean;
instanceDetails: IInstanceDetails | null;
isLoadingInstanceDetails: boolean;
selectAndLoadInstance: (instanceName: string) => void;
}
interface ISidebarState {
isOpen: boolean;
isOpen: boolean;
}
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) {
super(props);
const isOpen = window.innerWidth >= 900 ? true : false;
this.state = { isOpen };
public render() {
const closedClass = this.state.isOpen ? "" : " closed";
const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT;
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() {
const closedClass = this.state.isOpen ? "" : " closed";
const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT;
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 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 handleToggle = () => {
this.setState({ isOpen: !this.state.isOpen });
private renderDescription = () => {
const description = this.props.instanceDetails!.description;
if (!description) {
return;
}
return <p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{ __html: sanitize(description) }} />;
};
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>
);
}
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>
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>
));
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 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>
<td>Users</td>
<td>{userCount || "Unknown"}</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>
)
}
<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 renderEmptyState = () => {
return (
<NonIdealState
icon={IconNames.CIRCLE}
title="No instance selected"
description="Select an instance from the graph or the top-right dropdown to see its details."
/>
)
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>
));
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 = () => {
return (
<div>
<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.
</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 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>
));
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 = () => {
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 renderEmptyState = () => {
return (
<NonIdealState
icon={IconNames.CIRCLE}
title="No instance selected"
description="Select an instance from the graph or the top-right dropdown to see its details."
/>
);
};
private renderMissingDataState = () => {
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 renderLoadingState = () => {
return (
<div>
<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.
</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 = () => {
window.open("https://" + this.props.instanceName, "_blank");
}
private renderPersonalInstanceErrorState = () => {
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) => {
this.props.selectAndLoadInstance(e.target.innerText);
}
private renderMissingDataState = () => {
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) => ({
graph: state.data.graph,
instanceDetails: state.currentInstance.currentInstanceDetails,
instanceLoadError: state.currentInstance.error,
instanceName: state.currentInstance.currentInstanceName,
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails,
graph: state.data.graph,
instanceDetails: state.currentInstance.currentInstanceDetails,
instanceLoadError: state.currentInstance.error,
instanceName: state.currentInstance.currentInstanceName,
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,101 +1,100 @@
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,
}
}
return {
payload: instanceName,
type: ActionType.SELECT_INSTANCE
};
};
const deselectInstance = () => {
return {
type: ActionType.DESELECT_INSTANCE,
}
}
return {
type: ActionType.DESELECT_INSTANCE
};
};
export const requestInstances = () => {
return {
type: ActionType.REQUEST_INSTANCES,
}
}
return {
type: ActionType.REQUEST_INSTANCES
};
};
export const receiveInstances = (instances: IInstance[]) => {
return {
payload: instances,
type: ActionType.RECEIVE_INSTANCES,
}
}
return {
payload: instances,
type: ActionType.RECEIVE_INSTANCES
};
};
export const requestGraph = () => {
return {
type: ActionType.REQUEST_GRAPH,
}
}
return {
type: ActionType.REQUEST_GRAPH
};
};
export const receiveGraph = (graph: IGraph) => {
return {
payload: graph,
type: ActionType.RECEIVE_GRAPH,
}
}
return {
payload: graph,
type: ActionType.RECEIVE_GRAPH
};
};
const graphLoadFailed = () => {
return {
type: ActionType.GRAPH_LOAD_ERROR,
}
}
return {
type: ActionType.GRAPH_LOAD_ERROR
};
};
const instanceLoadFailed = () => {
return {
type: ActionType.INSTANCE_LOAD_ERROR,
}
}
return {
type: ActionType.INSTANCE_LOAD_ERROR
};
};
export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
return {
payload: instanceDetails,
type: ActionType.RECEIVE_INSTANCE_DETAILS,
}
}
return {
payload: instanceDetails,
type: ActionType.RECEIVE_INSTANCE_DETAILS
};
};
/** Async actions: https://redux.js.org/advanced/asyncactions */
export const fetchInstances = () => {
return (dispatch: Dispatch) => {
dispatch(requestInstances());
return getFromApi("instances")
.then(instances => dispatch(receiveInstances(instances)))
.catch(e => dispatch(graphLoadFailed()));
}
}
return (dispatch: Dispatch) => {
dispatch(requestInstances());
return getFromApi("instances")
.then(instances => dispatch(receiveInstances(instances)))
.catch(e => dispatch(graphLoadFailed()));
};
};
export const selectAndLoadInstance = (instanceName: string) => {
return (dispatch: Dispatch) => {
if (!instanceName) {
dispatch(deselectInstance());
return;
}
dispatch(selectInstance(instanceName));
return getFromApi("instances/" + instanceName)
.then(details => dispatch(receiveInstanceDetails(details)))
.catch(e => dispatch(instanceLoadFailed()));
return (dispatch: Dispatch) => {
if (!instanceName) {
dispatch(deselectInstance());
return;
}
}
dispatch(selectInstance(instanceName));
return getFromApi("instances/" + instanceName)
.then(details => dispatch(receiveInstanceDetails(details)))
.catch(e => dispatch(instanceLoadFailed()));
};
};
export const fetchGraph = () => {
return (dispatch: Dispatch) => {
dispatch(requestGraph());
return Promise.all([getFromApi("graph/edges"), getFromApi("graph/nodes")])
.then(responses => {
return {
edges: responses[0],
nodes: responses[1],
};
})
.then(graph => dispatch(receiveGraph(graph)))
.catch(e => dispatch(graphLoadFailed()));
}
}
return (dispatch: Dispatch) => {
dispatch(requestGraph());
return Promise.all([getFromApi("graph/edges"), getFromApi("graph/nodes")])
.then(responses => {
return {
edges: responses[0],
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,
}
error: false,
isLoadingGraph: false,
isLoadingInstances: false
};
const data = (state: IDataState = initialDataState, action: IAction) => {
switch (action.type) {
case ActionType.REQUEST_INSTANCES:
return {
...state,
instances: [],
isLoadingInstances: true,
};
case ActionType.RECEIVE_INSTANCES:
return {
...state,
instances: action.payload,
isLoadingInstances: false,
};
case ActionType.REQUEST_GRAPH:
return {
...state,
isLoadingGraph: true,
};
case ActionType.RECEIVE_GRAPH:
return {
...state,
graph: action.payload,
isLoadingGraph: false,
};
case ActionType.GRAPH_LOAD_ERROR:
return {
...state,
error: true,
isLoadingGraph: false,
isLoadingInstances: false,
};
default:
return state;
}
}
switch (action.type) {
case ActionType.REQUEST_INSTANCES:
return {
...state,
instances: [],
isLoadingInstances: true
};
case ActionType.RECEIVE_INSTANCES:
return {
...state,
instances: action.payload,
isLoadingInstances: false
};
case ActionType.REQUEST_GRAPH:
return {
...state,
isLoadingGraph: true
};
case ActionType.RECEIVE_GRAPH:
return {
...state,
graph: action.payload,
isLoadingGraph: false
};
case ActionType.GRAPH_LOAD_ERROR:
return {
...state,
error: true,
isLoadingGraph: false,
isLoadingInstances: false
};
default:
return state;
}
};
const initialCurrentInstanceState = {
currentInstanceDetails: null,
currentInstanceName: null,
error: false,
isLoadingInstanceDetails: false,
currentInstanceDetails: null,
currentInstanceName: null,
error: 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({
currentInstance,
data,
})
currentInstance,
data
});

View File

@ -1,76 +1,76 @@
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 {
name: string;
peers?: IInstance[];
description?: string;
domainCount?: number;
statusCount?: number;
userCount?: number;
version?: string;
lastUpdated?: string;
status: string;
name: string;
peers?: IInstance[];
description?: string;
domainCount?: number;
statusCount?: number;
userCount?: number;
version?: string;
lastUpdated?: string;
status: string;
}
interface IGraphNode {
id: string;
label: string;
x: number;
y: number;
size?: number;
color?: string;
id: string;
label: string;
x: number;
y: number;
size?: number;
color?: string;
}
interface IGraphEdge {
source: string;
target: string;
id?: string;
size?: number;
source: string;
target: string;
id?: string;
size?: number;
}
export interface IGraph {
nodes: IGraphNode[];
edges: IGraphEdge[];
nodes: IGraphNode[];
edges: IGraphEdge[];
}
// 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,
currentInstance: ICurrentInstanceState;
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