diff --git a/.vscode/extensions.json b/.vscode/extensions.json index df38b8d..9a49c32 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { "recommendations": [ "jakebecker.elixir-ls", - "ms-vscode.vscode-typescript-tslint-plugin", "kevinmcgowan.typescriptimport", "msjsdiag.debugger-for-chrome" ] diff --git a/frontend/.eslintignore b/frontend/.eslintignore new file mode 100644 index 0000000..385644c --- /dev/null +++ b/frontend/.eslintignore @@ -0,0 +1,4 @@ +node_modules +dist +build +coverage \ No newline at end of file diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 0000000..56ee49e --- /dev/null +++ b/frontend/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + parserOptions: { + tsconfigRootDir: __dirname, + project: ["./tsconfig.json"], + }, + plugins: ["@typescript-eslint", "prettier"], + extends: [ + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "prettier/@typescript-eslint", + "prettier", + ], + rules: { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "react/prop-types": 0, + "@typescript-eslint/no-non-null-assertion": 0 + }, +}; diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js new file mode 100644 index 0000000..a1204a7 --- /dev/null +++ b/frontend/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + printWidth: 100 +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 34a3c94..9f973de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "start": "NODE_ENV=development react-scripts start", "build": "react-scripts build", "typecheck": "tsc --noemit", - "lint": "yarn typecheck && tslint -p tsconfig.json -c tslint.json \"src/**/*.{ts,tsx}\"", + "lint": "yarn typecheck && yarn eslint src/ --ext .js,.jsx,.ts,.tsx", "lint:fix": "yarn lint --fix", "pretty": "prettier --write \"src/**/*.{ts,tsx}\"", "test": "yarn lint && react-scripts test --ci", @@ -54,11 +54,10 @@ "tippy.js": "^4.3.5" }, "devDependencies": { - "@blueprintjs/tslint-config": "^3.0.0", "@types/classnames": "^2.2.9", "@types/cytoscape": "^3.8.3", "@types/inflection": "^1.5.28", - "@types/jest": "^25.2.2", + "@types/jest": "^25.2.3", "@types/lodash": "^4.14.151", "@types/node": "^14.0.1", "@types/numeral": "^0.0.28", @@ -68,12 +67,19 @@ "@types/react-router-dom": "^5.1.5", "@types/sanitize-html": "^1.23.0", "@types/styled-components": "5.1.0", + "@typescript-eslint/eslint-plugin": "^2.24.0", + "@typescript-eslint/parser": "^2.34.0", + "eslint-config-airbnb-typescript": "^7.2.1", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.1.3", + "eslint-plugin-react": "^7.20.0", + "eslint-plugin-react-hooks": "^4.0.2", "husky": "^4.2.5", "lint-staged": "^10.2.4", + "prettier": "^2.0.5", "react-axe": "^3.3.0", - "tslint": "^6.1.2", - "tslint-config-security": "^1.16.0", - "tslint-eslint-rules": "^5.4.0", "typescript": "^3.9.2" }, "browserslist": [ diff --git a/frontend/src/AppRouter.tsx b/frontend/src/AppRouter.tsx index 4ddda2b..149e11b 100644 --- a/frontend/src/AppRouter.tsx +++ b/frontend/src/AppRouter.tsx @@ -4,26 +4,26 @@ import { Classes } from "@blueprintjs/core"; import { ConnectedRouter } from "connected-react-router"; import { Route } from "react-router-dom"; -import { Nav } from "./components/organisms/"; +import { Nav } from "./components/organisms"; import { AboutScreen, AdminScreen, GraphScreen, LoginScreen, TableScreen, - VerifyLoginScreen -} from "./components/screens/"; + VerifyLoginScreen, +} from "./components/screens"; import { history } from "./index"; const AppRouter: React.FC = () => (
diff --git a/frontend/src/components/atoms/FloatingCard.tsx b/frontend/src/components/atoms/FloatingCard.tsx index 53f928c..938618b 100644 --- a/frontend/src/components/atoms/FloatingCard.tsx +++ b/frontend/src/components/atoms/FloatingCard.tsx @@ -11,7 +11,7 @@ const FloatingCardElement = styled(Card)` z-index: 2; `; -const FloatingCard: React.FC = props => ( +const FloatingCard: React.FC = (props) => ( diff --git a/frontend/src/components/atoms/GraphHideEdgesButton.tsx b/frontend/src/components/atoms/GraphHideEdgesButton.tsx index f901b44..3453bc4 100644 --- a/frontend/src/components/atoms/GraphHideEdgesButton.tsx +++ b/frontend/src/components/atoms/GraphHideEdgesButton.tsx @@ -7,11 +7,11 @@ const StyledSwitch = styled(Switch)` margin: 0; `; -interface IGraphHideEdgesButtonProps { +interface GraphHideEdgesButtonProps { isShowingEdges: boolean; toggleEdges: () => void; } -const GraphHideEdgesButton: React.FC = ({ isShowingEdges, toggleEdges }) => ( +const GraphHideEdgesButton: React.FC = ({ isShowingEdges, toggleEdges }) => ( diff --git a/frontend/src/components/atoms/GraphKey.tsx b/frontend/src/components/atoms/GraphKey.tsx index 10d15f0..d6272f2 100644 --- a/frontend/src/components/atoms/GraphKey.tsx +++ b/frontend/src/components/atoms/GraphKey.tsx @@ -6,9 +6,9 @@ import React from "react"; import styled from "styled-components"; import { FloatingCard, InstanceType } from "."; import { QUANTITATIVE_COLOR_SCHEME } from "../../constants"; -import { IColorScheme } from "../../types"; +import { ColorScheme } from "../../types"; -const ColorSchemeSelect = Select.ofType(); +const ColorSchemeSelect = Select.ofType(); const StyledLi = styled.li` margin-top: 2px; @@ -27,12 +27,12 @@ const ColorBarContainer = styled.div` flex-direction: column; margin-right: 10px; `; -interface IColorBarProps { +interface ColorBarProps { color: string; } -const ColorBar = styled.div` +const ColorBar = styled.div` width: 10px; - background-color: ${props => props.color}; + background-color: ${(props) => props.color}; flex: 1; `; const TextContainer = styled.div` @@ -41,13 +41,46 @@ const TextContainer = styled.div` justify-content: space-between; `; -interface IGraphKeyProps { - current?: IColorScheme; - colorSchemes: IColorScheme[]; +const renderItem: ItemRenderer = (colorScheme, { handleClick, modifiers }) => { + if (!modifiers.matchesPredicate) { + return null; + } + return ; +}; + +const renderQualitativeKey = (values: string[]) => ( +
    + {values.map((v) => ( + + + + ))} +
+); + +const renderQuantitativeKey = (range: number[]) => { + const [min, max] = range; + return ( + + + {QUANTITATIVE_COLOR_SCHEME.map((color) => ( + + ))} + + + {numeral.default(min).format("0")} + {numeral.default(max).format("0")} + + + ); +}; +interface GraphKeyProps { + current?: ColorScheme; + colorSchemes: ColorScheme[]; ranges?: { [key: string]: [number, number] }; - onItemSelect: (colorScheme?: IColorScheme) => void; + onItemSelect: (colorScheme?: ColorScheme) => void; } -const GraphKey: React.FC = ({ current, colorSchemes, ranges, onItemSelect }) => { +const GraphKey: React.FC = ({ current, colorSchemes, ranges, onItemSelect }) => { const unsetColorScheme = () => { onItemSelect(undefined); }; @@ -76,13 +109,7 @@ const GraphKey: React.FC = ({ current, colorSchemes, ranges, onI rightIcon={IconNames.CARET_DOWN} tabIndex={-1} /> - {isEndOfSection && ( - )} @@ -187,20 +188,19 @@ class InstanceTable extends React.PureComponent { const { instanceListSort } = this.props; if (instanceListSort.field !== field) { return IconNames.SORT; - } else if (instanceListSort.direction === "asc") { - return IconNames.SORT_ASC; - } else { - return IconNames.SORT_DESC; } + if (instanceListSort.direction === "asc") { + return IconNames.SORT_ASC; + } + return IconNames.SORT_DESC; }; private getSortIntent = (field: SortField) => { const { instanceListSort } = this.props; if (instanceListSort.field === field) { return Intent.PRIMARY; - } else { - return Intent.NONE; } + return Intent.NONE; }; private getPagesToDisplay = (totalPages: number, currentPage: number) => { @@ -214,24 +214,19 @@ class InstanceTable extends React.PureComponent { const pagesToDisplay = firstPages.concat(surroundingPages).concat(lastPages); - return sortedUniq(sortBy(pagesToDisplay, n => n)); + return sortedUniq(sortBy(pagesToDisplay, (n) => n)); }; } -const mapStateToProps = (state: IAppState) => { - return { - instanceListSort: state.data.instanceListSort, - instancesResponse: state.data.instancesResponse, - isLoading: state.data.isLoadingInstanceList, - loadError: state.data.instanceListLoadError - }; -}; +const mapStateToProps = (state: AppState) => ({ + instanceListSort: state.data.instanceListSort, + instancesResponse: state.data.instancesResponse, + isLoading: state.data.isLoadingInstanceList, + loadError: state.data.instanceListLoadError, +}); const mapDispatchToProps = (dispatch: Dispatch) => ({ - loadInstanceList: (page?: number, sort?: IInstanceSort) => dispatch(loadInstanceList(page, sort) as any), - navigate: (path: string) => dispatch(push(path)) + loadInstanceList: (page?: number, sort?: InstanceSort) => dispatch(loadInstanceList(page, sort) as any), + navigate: (path: string) => dispatch(push(path)), }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(InstanceTable); +export default connect(mapStateToProps, mapDispatchToProps)(InstanceTable); diff --git a/frontend/src/components/organisms/Nav.tsx b/frontend/src/components/organisms/Nav.tsx index d6e4d39..b19caf0 100644 --- a/frontend/src/components/organisms/Nav.tsx +++ b/frontend/src/components/organisms/Nav.tsx @@ -1,21 +1,19 @@ import * as React from "react"; -import { Alignment, Navbar } from "@blueprintjs/core"; +import { Alignment, Navbar, Classes } from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; -import { Classes } from "@blueprintjs/core"; import { match, NavLink } from "react-router-dom"; -import { IInstanceDomainPath } from "../../constants"; +import { InstanceDomainPath } from "../../constants"; -interface INavState { +interface NavState { aboutIsOpen: boolean; } -const graphIsActive = (currMatch: match, location: Location) => { - return location.pathname === "/" || location.pathname.startsWith("/instance/"); -}; +const graphIsActive = (currMatch: match, location: Location) => + location.pathname === "/" || location.pathname.startsWith("/instance/"); -class Nav extends React.Component<{}, INavState> { +class Nav extends React.Component<{}, NavState> { constructor(props: any) { super(props); this.state = { aboutIsOpen: false }; @@ -23,7 +21,7 @@ class Nav extends React.Component<{}, INavState> { public render() { return ( - + fediverse.space @@ -46,7 +44,7 @@ class Nav extends React.Component<{}, INavState> { to="/about" className={`${Classes.BUTTON} ${Classes.MINIMAL} bp3-icon-${IconNames.INFO_SIGN}`} activeClassName={Classes.INTENT_PRIMARY} - exact={true} + exact > About diff --git a/frontend/src/components/organisms/SearchFilters.tsx b/frontend/src/components/organisms/SearchFilters.tsx index 190e729..fd6a9c2 100644 --- a/frontend/src/components/organisms/SearchFilters.tsx +++ b/frontend/src/components/organisms/SearchFilters.tsx @@ -3,7 +3,7 @@ import { IconNames } from "@blueprintjs/icons"; import React, { MouseEvent } from "react"; import styled from "styled-components"; import { INSTANCE_TYPES } from "../../constants"; -import { getSearchFilterDisplayValue, ISearchFilter } from "../../searchFilters"; +import { getSearchFilterDisplayValue, SearchFilter } from "../../searchFilters"; import { getTypeDisplayString } from "../../util"; const SearchFilterContainer = styled.div` @@ -19,30 +19,30 @@ const StyledTag = styled(Tag)` margin-left: 5px; `; -interface ISearchFiltersProps { - selectedFilters: ISearchFilter[]; - selectFilter: (filter: ISearchFilter) => void; +interface SearchFiltersProps { + selectedFilters: SearchFilter[]; + selectFilter: (filter: SearchFilter) => void; deselectFilter: (e: MouseEvent, props: ITagProps) => void; } -const SearchFilters: React.FC = ({ selectedFilters, selectFilter, deselectFilter }) => { - const hasInstanceTypeFilter = selectedFilters.some(sf => sf.field === "type"); +const SearchFilters: React.FC = ({ selectedFilters, selectFilter, deselectFilter }) => { + const hasInstanceTypeFilter = selectedFilters.some((sf) => sf.field === "type"); const handleSelectInstanceType = (e: MouseEvent) => { const field = "type"; const relation = "eq"; const value = e.currentTarget.innerText.toLowerCase().replace(" ", ""); - const filter: ISearchFilter = { + const filter: SearchFilter = { displayValue: getSearchFilterDisplayValue(field, relation, value), field, relation, - value + value, }; selectFilter(filter); }; const renderMenu = () => ( - {INSTANCE_TYPES.map(t => ( + {INSTANCE_TYPES.map((t) => ( ))} @@ -51,15 +51,15 @@ const SearchFilters: React.FC = ({ selectedFilters, selectF return ( - {selectedFilters.map(filter => ( - + {selectedFilters.map((filter) => ( + {filter.displayValue} ))} - diff --git a/frontend/src/components/organisms/SidebarContainer.tsx b/frontend/src/components/organisms/SidebarContainer.tsx index 6af77a4..c9f882c 100644 --- a/frontend/src/components/organisms/SidebarContainer.tsx +++ b/frontend/src/components/organisms/SidebarContainer.tsx @@ -17,11 +17,9 @@ const StyledCard = styled(Card)` display: flex; flex-direction: column; `; -const SidebarContainer: React.FC = ({ children }) => { - return ( - - {children} - - ); -}; +const SidebarContainer: React.FC = ({ children }) => ( + + {children} + +); export default SidebarContainer; diff --git a/frontend/src/components/screens/AboutScreen.tsx b/frontend/src/components/screens/AboutScreen.tsx index fad79c0..319b167 100644 --- a/frontend/src/components/screens/AboutScreen.tsx +++ b/frontend/src/components/screens/AboutScreen.tsx @@ -2,9 +2,9 @@ import { Classes, Code, H1, H2, H4 } from "@blueprintjs/core"; import * as React from "react"; import styled from "styled-components"; // import appsignalLogo from "../../assets/appsignal.svg"; -import gitlabLogo from "../../assets/gitlab.png"; -import nlnetLogo from "../../assets/nlnet.png"; -import { Page } from "../atoms/"; +import * as gitlabLogo from "../../assets/gitlab.png"; +import * as nlnetLogo from "../../assets/nlnet.png"; +import { Page } from "../atoms"; const SponsorContainer = styled.div` margin-bottom: 20px; @@ -36,10 +36,11 @@ const AboutScreen: React.FC = () => (

FAQ

-

Why can't I see details about my instance?

+

Why can't I see details about my instance?

fediverse.space only supports servers using the Mastodon API, the Misskey API, the GNU Social API, or Nodeinfo. - Instances with 10 or fewer users won't be scraped -- it's a tool for understanding communities, not individuals. + Instances with 10 or fewer users won't be scraped -- it's a tool for understanding communities, not + individuals.

diff --git a/frontend/src/components/screens/AdminScreen.tsx b/frontend/src/components/screens/AdminScreen.tsx index feb122a..52e0821 100644 --- a/frontend/src/components/screens/AdminScreen.tsx +++ b/frontend/src/components/screens/AdminScreen.tsx @@ -17,7 +17,7 @@ const ButtonContainer = styled.div` justify-content: space-between; `; -interface IAdminSettings { +interface AdminSettings { domain: string; optIn: boolean; optOut: boolean; @@ -25,26 +25,26 @@ interface IAdminSettings { statusCount: number; } -interface IAdminScreenProps { +interface AdminScreenProps { navigate: (path: string) => void; } -interface IAdminScreenState { - settings?: IAdminSettings; +interface AdminScreenState { + settings?: AdminSettings; isUpdating: boolean; } -class AdminScreen extends React.PureComponent { +class AdminScreen extends React.PureComponent { private authToken = getAuthToken(); - public constructor(props: IAdminScreenProps) { + public constructor(props: AdminScreenProps) { super(props); this.state = { isUpdating: false }; } public componentDidMount() { // Load instance settings from server - if (!!this.authToken) { - getFromApi(`admin`, this.authToken!) - .then(response => { + if (this.authToken) { + getFromApi(`admin`, this.authToken) + .then((response) => { this.setState({ settings: response }); }) .catch(() => { @@ -52,7 +52,7 @@ class AdminScreen extends React.PureComponent) => { - const settings = this.state.settings as IAdminSettings; + const settings = this.state.settings as AdminSettings; const optIn = e.currentTarget.checked; - let optOut = settings.optOut; + let { optOut } = settings; if (optIn) { optOut = false; } @@ -126,9 +126,9 @@ class AdminScreen extends React.PureComponent) => { - const settings = this.state.settings as IAdminSettings; + const settings = this.state.settings as AdminSettings; const optOut = e.currentTarget.checked; - let optIn = settings.optIn; + let { optIn } = settings; if (optOut) { optIn = false; } @@ -140,15 +140,15 @@ class AdminScreen extends React.PureComponent { + .then((response) => { this.setState({ settings: response, isUpdating: false }); AppToaster.show({ icon: IconNames.TICK, intent: Intent.SUCCESS, - message: "Successfully updated settings." + message: "Successfully updated settings.", }); }) .catch(() => { @@ -161,16 +161,13 @@ class AdminScreen extends React.PureComponent ({ - navigate: (path: string) => dispatch(push(path)) + navigate: (path: string) => dispatch(push(path)), }); -export default connect( - undefined, - mapDispatchToProps -)(AdminScreen); +export default connect(undefined, mapDispatchToProps)(AdminScreen); diff --git a/frontend/src/components/screens/GraphScreen.tsx b/frontend/src/components/screens/GraphScreen.tsx index 85ea9c4..2fb5fd0 100644 --- a/frontend/src/components/screens/GraphScreen.tsx +++ b/frontend/src/components/screens/GraphScreen.tsx @@ -7,9 +7,9 @@ import { Route, RouteComponentProps, Switch, withRouter } from "react-router"; import { InstanceScreen, SearchScreen } from "."; import { INSTANCE_DOMAIN_PATH } from "../../constants"; import { loadInstance } from "../../redux/actions"; -import { IAppState } from "../../redux/types"; +import { AppState } from "../../redux/types"; import { domainMatchSelector, isSmallScreen } from "../../util"; -import { Graph, SidebarContainer } from "../organisms/"; +import { Graph, SidebarContainer } from "../organisms"; const GraphContainer = styled.div` display: flex; @@ -24,13 +24,13 @@ const FullDiv = styled.div` right: 0; `; -interface IGraphScreenProps extends RouteComponentProps { +interface GraphScreenProps extends RouteComponentProps { currentInstanceName: string | null; pathname: string; graphLoadError: boolean; loadInstance: (domain: string | null) => void; } -interface IGraphScreenState { +interface GraphScreenState { hasBeenViewed: boolean; } /** @@ -41,8 +41,8 @@ interface IGraphScreenState { * However, if it's not the first page viewed (e.g. if someone opens directly on /about) we don't want to render the * graph since it slows down everything else! */ -class GraphScreenImpl extends React.Component { - public constructor(props: IGraphScreenProps) { +class GraphScreenImpl extends React.Component { + public constructor(props: GraphScreenProps) { super(props); this.state = { hasBeenViewed: false }; } @@ -56,7 +56,7 @@ class GraphScreenImpl extends React.Component ( + private renderRoutes = () => ( {/* Smaller screens never load the entire graph. Instead, `InstanceScreen` shows only the neighborhood. */} @@ -80,7 +80,7 @@ class GraphScreenImpl extends React.Component - + @@ -94,19 +94,16 @@ class GraphScreenImpl extends React.Component { +const mapStateToProps = (state: AppState) => { const match = domainMatchSelector(state); return { currentInstanceName: match && match.params.domain, graphLoadError: state.data.graphLoadError, - pathname: state.router.location.pathname + pathname: state.router.location.pathname, }; }; const mapDispatchToProps = (dispatch: Dispatch) => ({ - loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any) + loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any), }); -const GraphScreen = connect( - mapStateToProps, - mapDispatchToProps -)(GraphScreenImpl); +const GraphScreen = connect(mapStateToProps, mapDispatchToProps)(GraphScreenImpl); export default withRouter(GraphScreen); diff --git a/frontend/src/components/screens/InstanceScreen.tsx b/frontend/src/components/screens/InstanceScreen.tsx index 0991faf..4c2b2a2 100644 --- a/frontend/src/components/screens/InstanceScreen.tsx +++ b/frontend/src/components/screens/InstanceScreen.tsx @@ -20,7 +20,7 @@ import { Spinner, Tab, Tabs, - Tooltip + Tooltip, } from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; @@ -28,10 +28,10 @@ import { push } from "connected-react-router"; import { Link } from "react-router-dom"; import { Dispatch } from "redux"; import styled from "styled-components"; -import { IAppState, IGraph, IGraphResponse, IInstanceDetails } from "../../redux/types"; +import { AppState, Graph, GraphResponse, InstanceDetails, Peer } from "../../redux/types"; import { domainMatchSelector, getFromApi, isSmallScreen } from "../../util"; import { InstanceType } from "../atoms"; -import { Cytoscape, ErrorState } from "../molecules/"; +import { Cytoscape, ErrorState } from "../molecules"; import { FederationTab } from "../organisms"; const InstanceScreenContainer = styled.div` @@ -82,25 +82,25 @@ const StyledGraphContainer = styled.div` flex-direction: column; margin-bottom: 10px; `; -interface IInstanceScreenProps { - graph?: IGraph; +interface InstanceScreenProps { + graph?: Graph; instanceName: string | null; instanceLoadError: boolean; - instanceDetails: IInstanceDetails | null; + instanceDetails: InstanceDetails | null; isLoadingInstanceDetails: boolean; navigateToRoot: () => void; navigateToInstance: (domain: string) => void; } -interface IInstanceScreenState { +interface InstanceScreenState { neighbors?: string[]; isProcessingNeighbors: boolean; // Local (neighborhood) graph. Used only on small screens (mobile devices). isLoadingLocalGraph: boolean; - localGraph?: IGraph; + localGraph?: Graph; localGraphLoadError?: boolean; } -class InstanceScreenImpl extends React.PureComponent { - public constructor(props: IInstanceScreenProps) { +class InstanceScreenImpl extends React.PureComponent { + public constructor(props: InstanceScreenProps) { super(props); this.state = { isProcessingNeighbors: false, isLoadingLocalGraph: false, localGraphLoadError: false }; } @@ -116,9 +116,9 @@ class InstanceScreenImpl extends React.PureComponent; - } else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) { + } else if (this.props.instanceDetails.status.toLowerCase().includes("personal instance")) { content = this.renderPersonalInstanceErrorState(); - } else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) { + } else if (this.props.instanceDetails.status.toLowerCase().includes("robots.txt")) { content = this.renderRobotsTxtState(); } else if (this.props.instanceDetails.status !== "success") { content = this.renderMissingDataState(); @@ -130,7 +130,7 @@ class InstanceScreenImpl extends React.PureComponent {this.props.instanceName} - + @@ -145,7 +145,7 @@ class InstanceScreenImpl extends React.PureComponent [e.data.source, e.data.target].indexOf(instanceName!) > -1); + const graphToUse = graph || localGraph; + if (!graphToUse) { + return; + } + const edges = graphToUse.edges.filter((e) => [e.data.source, e.data.target].includes(instanceName)); const neighbors: any[] = []; - edges.forEach(e => { + edges.forEach((e) => { if (e.data.source === instanceName) { neighbors.push({ neighbor: e.data.target, weight: e.data.weight }); } else { @@ -183,39 +186,38 @@ class InstanceScreenImpl extends React.PureComponent { + .then((response: GraphResponse) => { // We do some processing of edges here to make sure that every edge's source and target are in the neighborhood // We could (and should) be doing this in the backend, but I don't want to mess around with complex SQL // queries. // TODO: think more about moving the backend to a graph database that would make this easier. - const graph = response.graph; - const nodeIds = new Set(graph.nodes.map(n => n.data.id)); - const edges = graph.edges.filter(e => nodeIds.has(e.data.source) && nodeIds.has(e.data.target)); + const { graph } = response; + const nodeIds = new Set(graph.nodes.map((n) => n.data.id)); + const edges = graph.edges.filter((e) => nodeIds.has(e.data.source) && nodeIds.has(e.data.target)); this.setState({ isLoadingLocalGraph: false, localGraph: { ...graph, edges } }); }) .catch(() => this.setState({ isLoadingLocalGraph: false, localGraphLoadError: true })); }; private renderTabs = () => { + const { instanceDetails } = this.props; const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0; - const federationRestrictions = this.props.instanceDetails && this.props.instanceDetails.federationRestrictions; + const federationRestrictions = instanceDetails && instanceDetails.federationRestrictions; const hasLocalGraph = !!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0; const insularCallout = this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors && !hasLocalGraph ? ( -

This instance doesn't have any neighbors that we know of, so it's hidden from the graph.

+

This instance doesn't have any neighbors that we know of, so it's hidden from the graph.

- ) : ( - undefined - ); + ) : undefined; return ( <> {insularCallout} {this.maybeRenderLocalGraph()} - {this.props.instanceDetails!.description && ( + {instanceDetails && instanceDetails.description && ( )} {this.shouldRenderStats() && } @@ -232,7 +234,7 @@ class InstanceScreenImpl extends React.PureComponent { const { localGraph } = this.state; - const hasLocalGraph = - !!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0; - if (!hasLocalGraph) { + const hasLocalGraph = !!localGraph && localGraph.nodes.length > 0 && localGraph.edges.length > 0; + if (!hasLocalGraph || !localGraph) { return; } return ( - + @@ -268,7 +269,11 @@ class InstanceScreenImpl extends React.PureComponent { - const description = this.props.instanceDetails!.description; + const { instanceDetails } = this.props; + if (!instanceDetails) { + return; + } + const { description } = instanceDetails; if (!description) { return; } @@ -288,10 +293,10 @@ class InstanceScreenImpl extends React.PureComponent + Version @@ -299,7 +304,7 @@ class InstanceScreenImpl extends React.PureComponent Instance type - {(type && ) || "Unknown"} + {(type && ) || "Unknown"} Users @@ -311,7 +316,8 @@ class InstanceScreenImpl extends React.PureComponent - Insularity{" "} + Insularity + {" "} @@ -330,7 +336,8 @@ class InstanceScreenImpl extends React.PureComponent - Statuses / day{" "} + Statuses / day + {" "} @@ -349,7 +356,8 @@ class InstanceScreenImpl extends React.PureComponent - Statuses / person / day{" "} + Statuses / person / day + {" "} @@ -372,7 +380,7 @@ class InstanceScreenImpl extends React.PureComponent Last updated - {moment(lastUpdated + "Z").fromNow() || "Unknown"} + {moment(`${lastUpdated}Z`).fromNow() || "Unknown"} @@ -412,7 +420,7 @@ class InstanceScreenImpl extends React.PureComponent - + Instance @@ -426,11 +434,15 @@ class InstanceScreenImpl extends React.PureComponent { - const peers = this.props.instanceDetails!.peers; + const { instanceDetails } = this.props; + if (!instanceDetails) { + return; + } + const { peers } = instanceDetails; if (!peers || peers.length === 0) { return; } - const peerRows = peers.map(instance => ( + const peerRows = peers.map((instance: Peer) => ( @@ -444,7 +456,7 @@ class InstanceScreenImpl extends React.PureComponent All the instances, past and present, that {this.props.instanceName} knows about.

- + {peerRows} @@ -453,71 +465,62 @@ class InstanceScreenImpl extends React.PureComponent } />; - private renderPersonalInstanceErrorState = () => { - return ( - - {"Opt in"} - - } - /> - ); - }; + private renderPersonalInstanceErrorState = () => ( + + Opt in + + } + /> + ); - private renderMissingDataState = () => { - return ( - <> - - - {this.props.instanceDetails && this.props.instanceDetails.status} + private renderMissingDataState = () => ( + <> + + + {this.props.instanceDetails && this.props.instanceDetails.status} + + + ); + + private renderRobotsTxtState = () => ( + + 🤖 - - ); - }; - - private renderRobotsTxtState = () => { - return ( - - 🤖 - - } - title="No data" - description="This instance was not crawled because its robots.txt did not allow us to." - /> - ); - }; + } + title="No data" + description="This instance was not crawled because its robots.txt did not allow us to." + /> + ); private openInstanceLink = () => { - window.open("https://" + this.props.instanceName, "_blank"); + window.open(`https://${this.props.instanceName}`, "_blank"); }; } -const mapStateToProps = (state: IAppState) => { +const mapStateToProps = (state: AppState) => { const match = domainMatchSelector(state); return { graph: state.data.graphResponse && state.data.graphResponse.graph, instanceDetails: state.currentInstance.currentInstanceDetails, instanceLoadError: state.currentInstance.error, instanceName: match && match.params.domain, - isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails + isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails, }; }; const mapDispatchToProps = (dispatch: Dispatch) => ({ navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)), - navigateToRoot: () => dispatch(push("/")) + navigateToRoot: () => dispatch(push("/")), }); -const InstanceScreen = connect( - mapStateToProps, - mapDispatchToProps -)(InstanceScreenImpl); +const InstanceScreen = connect(mapStateToProps, mapDispatchToProps)(InstanceScreenImpl); export default InstanceScreen; diff --git a/frontend/src/components/screens/LoginScreen.tsx b/frontend/src/components/screens/LoginScreen.tsx index 57f0569..a33c27a 100644 --- a/frontend/src/components/screens/LoginScreen.tsx +++ b/frontend/src/components/screens/LoginScreen.tsx @@ -8,11 +8,11 @@ import { getAuthToken, getFromApi, postToApi } from "../../util"; import { Page } from "../atoms"; import { ErrorState } from "../molecules"; -interface IFormContainerProps { +interface FormContainerProps { error: boolean; } -const FormContainer = styled.div` - ${props => (props.error ? "margin: 20px auto 0 auto;" : "margin-top: 20px;")} +const FormContainer = styled.div` + ${(props) => (props.error ? "margin: 20px auto 0 auto;" : "margin-top: 20px;")} `; const LoginTypeContainer = styled.div` display: flex; @@ -31,23 +31,28 @@ const StyledIcon = styled(Icon)` margin-bottom: 10px; `; -interface ILoginTypes { +interface LoginTypes { domain: string; email?: string; fediverseAccount?: string; } -interface ILoginScreenState { +interface LoginScreenState { domain: string; isGettingLoginTypes: boolean; isSendingLoginRequest: boolean; - loginTypes?: ILoginTypes; + loginTypes?: LoginTypes; selectedLoginType?: "email" | "fediverseAccount"; error: boolean; } -class LoginScreen extends React.PureComponent<{}, ILoginScreenState> { +class LoginScreen extends React.PureComponent<{}, LoginScreenState> { public constructor(props: any) { super(props); - this.state = { domain: "", error: false, isGettingLoginTypes: false, isSendingLoginRequest: false }; + this.state = { + domain: "", + error: false, + isGettingLoginTypes: false, + isSendingLoginRequest: false, + }; } public render() { @@ -59,13 +64,13 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> { const { error, loginTypes, isSendingLoginRequest, selectedLoginType } = this.state; let content; - if (!!error) { + if (error) { content = ( ); } else if (!!selectedLoginType && !isSendingLoginRequest) { content = this.renderPostLogin(); - } else if (!!loginTypes) { + } else if (loginTypes) { content = this.renderChooseLoginType(); } else { content = this.renderChooseInstance(); @@ -78,7 +83,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> { You must be the instance admin to manage how fediverse.space interacts with your instance.

- It's currently only possible to administrate Mastodon and Pleroma instances. If you want to login with a + It's currently only possible to administrate Mastodon and Pleroma instances. If you want to login with a direct message, your instance must federate with mastodon.social and vice versa.

@@ -95,7 +100,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> { const onButtonClick = () => this.getLoginTypes(); return (

- + { rightElement={ )} @@ -104,13 +104,11 @@ class SearchScreen extends React.PureComponent; } else if (query || error) { - rightSearchBarElement = ( -