style(linting): set up automatic linting
This commit is contained in:
parent
1c1536e71d
commit
49fe41128a
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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."} />;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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()));
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue