move from tslint to eslint

This commit is contained in:
Tao Bojlén 2020-05-19 14:36:22 +01:00
parent fe3525a427
commit d05f737fc4
44 changed files with 1160 additions and 1219 deletions

View File

@ -1,7 +1,6 @@
{
"recommendations": [
"jakebecker.elixir-ls",
"ms-vscode.vscode-typescript-tslint-plugin",
"kevinmcgowan.typescriptimport",
"msjsdiag.debugger-for-chrome"
]

4
frontend/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
build
coverage

24
frontend/.eslintrc.js Normal file
View File

@ -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
},
};

3
frontend/.prettierrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
printWidth: 100
}

View File

@ -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": [

View File

@ -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 = () => (
<ConnectedRouter history={history}>
<div className={`${Classes.DARK} App`}>
<Nav />
<Route path="/instances" exact={true} component={TableScreen} />
<Route path="/about" exact={true} component={AboutScreen} />
<Route path="/admin/login" exact={true} component={LoginScreen} />
<Route path="/admin/verify" exact={true} component={VerifyLoginScreen} />
<Route path="/admin" exact={true} component={AdminScreen} />
<Route path="/instances" exact component={TableScreen} />
<Route path="/about" exact component={AboutScreen} />
<Route path="/admin/login" exact component={LoginScreen} />
<Route path="/admin/verify" exact component={VerifyLoginScreen} />
<Route path="/admin" exact component={AdminScreen} />
{/* We always want the GraphScreen to be rendered (since un- and re-mounting it is expensive */}
<GraphScreen />
</div>

View File

@ -11,7 +11,7 @@ const FloatingCardElement = styled(Card)`
z-index: 2;
`;
const FloatingCard: React.FC<ICardProps> = props => (
const FloatingCard: React.FC<ICardProps> = (props) => (
<FloatingCardRow>
<FloatingCardElement elevation={Elevation.ONE} {...props} />
</FloatingCardRow>

View File

@ -7,11 +7,11 @@ const StyledSwitch = styled(Switch)`
margin: 0;
`;
interface IGraphHideEdgesButtonProps {
interface GraphHideEdgesButtonProps {
isShowingEdges: boolean;
toggleEdges: () => void;
}
const GraphHideEdgesButton: React.FC<IGraphHideEdgesButtonProps> = ({ isShowingEdges, toggleEdges }) => (
const GraphHideEdgesButton: React.FC<GraphHideEdgesButtonProps> = ({ isShowingEdges, toggleEdges }) => (
<FloatingCard>
<StyledSwitch checked={isShowingEdges} label="Show connections" onChange={toggleEdges} tabIndex={-1} />
</FloatingCard>

View File

@ -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<IColorScheme>();
const ColorSchemeSelect = Select.ofType<ColorScheme>();
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<IColorBarProps>`
const ColorBar = styled.div<ColorBarProps>`
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> = (colorScheme, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return <MenuItem active={modifiers.active} key={colorScheme.name} onClick={handleClick} text={colorScheme.name} />;
};
const renderQualitativeKey = (values: string[]) => (
<ul className={Classes.LIST_UNSTYLED}>
{values.map((v) => (
<StyledLi key={v}>
<InstanceType type={v} />
</StyledLi>
))}
</ul>
);
const renderQuantitativeKey = (range: number[]) => {
const [min, max] = range;
return (
<ColorKeyContainer>
<ColorBarContainer>
{QUANTITATIVE_COLOR_SCHEME.map((color) => (
<ColorBar color={color} key={color} />
))}
</ColorBarContainer>
<TextContainer>
<span className={Classes.TEXT_SMALL}>{numeral.default(min).format("0")}</span>
<span className={Classes.TEXT_SMALL}>{numeral.default(max).format("0")}</span>
</TextContainer>
</ColorKeyContainer>
);
};
interface GraphKeyProps {
current?: ColorScheme;
colorSchemes: ColorScheme[];
ranges?: { [key: string]: [number, number] };
onItemSelect: (colorScheme?: IColorScheme) => void;
onItemSelect: (colorScheme?: ColorScheme) => void;
}
const GraphKey: React.FC<IGraphKeyProps> = ({ current, colorSchemes, ranges, onItemSelect }) => {
const GraphKey: React.FC<GraphKeyProps> = ({ current, colorSchemes, ranges, onItemSelect }) => {
const unsetColorScheme = () => {
onItemSelect(undefined);
};
@ -76,13 +109,7 @@ const GraphKey: React.FC<IGraphKeyProps> = ({ current, colorSchemes, ranges, onI
rightIcon={IconNames.CARET_DOWN}
tabIndex={-1}
/>
<Button
icon={IconNames.SMALL_CROSS}
minimal={true}
onClick={unsetColorScheme}
disabled={!current}
tabIndex={-1}
/>
<Button icon={IconNames.SMALL_CROSS} minimal onClick={unsetColorScheme} disabled={!current} tabIndex={-1} />
</ColorSchemeSelect>
<br />
{!!current && !!key && (
@ -95,38 +122,4 @@ const GraphKey: React.FC<IGraphKeyProps> = ({ current, colorSchemes, ranges, onI
);
};
const renderItem: ItemRenderer<IColorScheme> = (colorScheme, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return <MenuItem active={modifiers.active} key={colorScheme.name} onClick={handleClick} text={colorScheme.name} />;
};
const renderQualitativeKey = (values: string[]) => (
<ul className={Classes.LIST_UNSTYLED}>
{values.map(v => (
<StyledLi key={v}>
<InstanceType type={v} />
</StyledLi>
))}
</ul>
);
const renderQuantitativeKey = (range: number[]) => {
const [min, max] = range;
return (
<ColorKeyContainer>
<ColorBarContainer>
{QUANTITATIVE_COLOR_SCHEME.map((color, idx) => (
<ColorBar color={color} key={color} />
))}
</ColorBarContainer>
<TextContainer>
<span className={Classes.TEXT_SMALL}>{numeral.default(min).format("0")}</span>
<span className={Classes.TEXT_SMALL}>{numeral.default(max).format("0")}</span>
</TextContainer>
</ColorKeyContainer>
);
};
export default GraphKey;

View File

@ -2,10 +2,10 @@ import { Button } from "@blueprintjs/core";
import * as React from "react";
import FloatingCard from "./FloatingCard";
interface IGraphResetButtonProps {
interface GraphResetButtonProps {
onClick: () => void;
}
const GraphResetButton: React.FC<IGraphResetButtonProps> = ({ onClick }) => (
const GraphResetButton: React.FC<GraphResetButtonProps> = ({ onClick }) => (
<FloatingCard>
<Button icon="compass" title="Reset graph view" onClick={onClick} tabIndex={-1} />
</FloatingCard>

View File

@ -5,7 +5,7 @@ import { QUALITATIVE_COLOR_SCHEME } from "../../constants";
import { typeColorScheme } from "../../types";
import { getTypeDisplayString } from "../../util";
interface IInstanceTypeProps {
interface InstanceTypeProps {
type: string;
colorAfterName?: boolean;
}
@ -13,9 +13,9 @@ interface IInstanceTypeProps {
* By default, renders the color followed by the name of the instance type.
* You can change this by passing `colorAfterName={true}`.
*/
const InstanceType: React.FC<IInstanceTypeProps> = ({ type, colorAfterName }) => {
const InstanceType: React.FC<InstanceTypeProps> = ({ type, colorAfterName }) => {
const idx = typeColorScheme.values.indexOf(type);
const name = " " + getTypeDisplayString(type);
const name = ` ${getTypeDisplayString(type)}`;
return (
<>
{!!colorAfterName && name}

View File

@ -11,19 +11,19 @@ const Backdrop = styled.div`
z-index: 3;
`;
interface IContainerProps {
interface ContainerProps {
fullWidth?: boolean;
}
const Container = styled.div<IContainerProps>`
max-width: ${props => (props.fullWidth ? "100%" : "800px")};
const Container = styled.div<ContainerProps>`
max-width: ${(props) => (props.fullWidth ? "100%" : "800px")};
margin: auto;
padding: 2em;
`;
interface IPageProps {
interface PageProps {
fullWidth?: boolean;
}
const Page: React.FC<IPageProps> = ({ children, fullWidth }) => (
const Page: React.FC<PageProps> = ({ children, fullWidth }) => (
<Backdrop>
<Container fullWidth={fullWidth}>{children}</Container>
</Backdrop>

View File

@ -10,9 +10,9 @@ import {
QUALITATIVE_COLOR_SCHEME,
QUANTITATIVE_COLOR_SCHEME,
SEARCH_RESULT_COLOR,
SELECTED_NODE_COLOR
SELECTED_NODE_COLOR,
} from "../../constants";
import { IColorScheme } from "../../types";
import { ColorScheme } from "../../types";
import { getBuckets, getTypeDisplayString } from "../../util";
const CytoscapeContainer = styled.div`
@ -21,8 +21,8 @@ const CytoscapeContainer = styled.div`
flex: 1;
`;
interface ICytoscapeProps {
colorScheme?: IColorScheme;
interface CytoscapeProps {
colorScheme?: ColorScheme;
currentNodeId: string | null;
elements: cytoscape.ElementsDefinition;
hoveringOver?: string;
@ -32,10 +32,11 @@ interface ICytoscapeProps {
navigateToInstancePath?: (domain: string) => void;
navigateToRoot?: () => void;
}
class Cytoscape extends React.PureComponent<ICytoscapeProps> {
class Cytoscape extends React.PureComponent<CytoscapeProps> {
private cy?: cytoscape.Core;
public componentDidMount() {
// eslint-disable-next-line react/no-find-dom-node
const container = ReactDOM.findDOMNode(this);
this.cy = cytoscape({
autoungrabify: true,
@ -44,16 +45,16 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
hideEdgesOnViewport: true,
hideLabelsOnViewport: true,
layout: {
name: "preset"
name: "preset",
},
maxZoom: 2,
minZoom: 0.01,
pixelRatio: 1.0,
selectionType: "single"
selectionType: "single",
});
// Setup node tooltip on hover
this.cy.nodes().forEach(n => {
this.cy.nodes().forEach((n) => {
const tooltipContent = `${n.data("id")} (${getTypeDisplayString(n.data("type"))})`;
const ref = (n as any).popperRef();
const t = tippy(ref, {
@ -61,12 +62,12 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
animation: "fade",
content: tooltipContent,
duration: 100,
trigger: "manual"
trigger: "manual",
});
n.on("mouseover", e => {
n.on("mouseover", () => {
(t as Instance).show();
});
n.on("mouseout", e => {
n.on("mouseout", () => {
(t as Instance).hide();
});
});
@ -78,25 +79,25 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
.style({
"curve-style": "haystack", // fast edges
"line-color": DEFAULT_NODE_COLOR,
width: "mapData(weight, 0, 0.5, 1, 20)"
width: "mapData(weight, 0, 0.5, 1, 20)",
})
.selector("node[label]")
.style({
color: DEFAULT_NODE_COLOR,
"font-size": "mapData(size, 1, 6, 10, 100)",
"min-zoomed-font-size": 16
"min-zoomed-font-size": 16,
})
.selector(".hidden") // used to hide nodes not in the neighborhood of the selected, or to hide edges
.style({
display: "none"
display: "none",
})
.selector(".thickEdge") // when a node is selected, make edges thicker so you can actually see them
.style({
width: 2
width: 2,
});
this.resetNodeColorScheme(style); // this function also called `update()`
this.cy.nodes().on("select", e => {
this.cy.nodes().on("select", (e) => {
const instanceId = e.target.data("id");
if (instanceId && instanceId !== this.props.currentNodeId) {
if (this.props.navigateToInstancePath) {
@ -110,21 +111,19 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
this.cy!.nodes().removeClass("hidden");
this.cy!.edges().removeClass("thickEdge");
// Then hide everything except neighborhood
this.cy!.nodes()
.diff(neighborhood)
.left.addClass("hidden");
this.cy!.nodes().diff(neighborhood).left.addClass("hidden");
neighborhood.connectedEdges().addClass("thickEdge");
});
});
this.cy.nodes().on("unselect", e => {
this.cy.nodes().on("unselect", () => {
this.cy!.batch(() => {
this.cy!.nodes().removeClass("hidden");
this.cy!.edges().removeClass("thickEdge");
});
});
this.cy.on("click", e => {
this.cy.on("click", (e) => {
// Clicking on the background should also deselect
const target = e.target;
const { target } = e;
if (!target || target === this.cy || target.isEdge()) {
if (this.props.navigateToRoot) {
// Go to the URL "/"
@ -136,7 +135,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
this.setNodeSelection();
}
public componentDidUpdate(prevProps: ICytoscapeProps) {
public componentDidUpdate(prevProps: CytoscapeProps) {
this.setNodeSelection(prevProps.currentNodeId);
if (prevProps.colorScheme !== this.props.colorScheme) {
this.updateColorScheme();
@ -174,12 +173,12 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
if (currentNodeId) {
this.cy.zoom({
level: 0.2,
position: this.cy.$id(currentNodeId).position()
position: this.cy.$id(currentNodeId).position(),
});
} else {
this.cy.zoom({
level: 0.2,
position: { x: 0, y: 0 }
position: { x: 0, y: 0 },
});
}
}
@ -221,7 +220,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
// quite good as it is, so...
height: "mapData(size, 1, 6, 20, 200)",
label: "data(id)",
width: "mapData(size, 1, 6, 20, 200)"
width: "mapData(size, 1, 6, 20, 200)",
});
this.setNodeSearchColorScheme(style);
@ -240,16 +239,16 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
"background-color": SEARCH_RESULT_COLOR,
"border-color": SEARCH_RESULT_COLOR,
"border-opacity": 0.7,
"border-width": 250
"border-width": 250,
})
.selector("node.hovered")
.style({
"border-color": HOVERED_NODE_COLOR,
"border-width": 1000
"border-width": 1000,
})
.selector("node:selected")
.style({
"background-color": SELECTED_NODE_COLOR
"background-color": SELECTED_NODE_COLOR,
})
.update();
};
@ -263,10 +262,11 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
if (!colorScheme) {
this.resetNodeColorScheme();
return;
} else if (colorScheme.type === "qualitative") {
}
if (colorScheme.type === "qualitative") {
colorScheme.values.forEach((v, idx) => {
style = style.selector(`node[${colorScheme.cytoscapeDataKey} = '${v}']`).style({
"background-color": QUALITATIVE_COLOR_SCHEME[idx]
"background-color": QUALITATIVE_COLOR_SCHEME[idx],
});
});
} else if (colorScheme.type === "quantitative") {
@ -284,7 +284,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
const max = idx === QUANTITATIVE_COLOR_SCHEME.length - 1 ? maxVal + 1 : buckets[idx + 1];
const selector = `node[${dataKey} >= ${min}][${dataKey} < ${max}]`;
style = style.selector(selector).style({
"background-color": color
"background-color": color,
});
});
}
@ -304,10 +304,10 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
}
const { hoveringOver } = this.props;
if (!!prevHoveredId) {
if (prevHoveredId) {
this.cy.$id(prevHoveredId).removeClass("hovered");
}
if (!!hoveringOver) {
if (hoveringOver) {
this.cy.$id(hoveringOver).addClass("hovered");
}
};
@ -322,7 +322,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
this.cy!.nodes().removeClass("searchResult");
if (!!searchResultIds && searchResultIds.length > 0) {
const currentResultSelector = searchResultIds.map(id => `node[id = "${id}"]`).join(", ");
const currentResultSelector = searchResultIds.map((id) => `node[id = "${id}"]`).join(", ");
this.cy!.$(currentResultSelector).addClass("searchResult");
}
});
@ -344,11 +344,11 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
/* Helper function to remove edges if source or target node is missing */
private cleanElements = (elements: cytoscape.ElementsDefinition): cytoscape.ElementsDefinition => {
const domains = new Set(elements.nodes.map(n => n.data.id));
const edges = elements.edges.filter(e => domains.has(e.data.source) && domains.has(e.data.target));
const domains = new Set(elements.nodes.map((n) => n.data.id));
const edges = elements.edges.filter((e) => domains.has(e.data.source) && domains.has(e.data.target));
return {
edges,
nodes: elements.nodes
nodes: elements.nodes,
};
};
}

View File

@ -2,11 +2,11 @@ import { NonIdealState } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import * as React from "react";
interface IErrorStateProps {
interface ErrorStateProps {
description?: string;
}
const ErrorState: React.FC<IErrorStateProps> = ({ description }) => (
<NonIdealState icon={IconNames.ERROR} title={"Something went wrong."} description={description} />
const ErrorState: React.FC<ErrorStateProps> = ({ description }) => (
<NonIdealState icon={IconNames.ERROR} title="Something went wrong." description={description} />
);
export default ErrorState;

View File

@ -1,6 +1,6 @@
import React from "react";
import styled from "styled-components";
import { IColorScheme } from "../../types";
import { ColorScheme } from "../../types";
import { GraphHideEdgesButton, GraphKey, GraphResetButton } from "../atoms";
const GraphToolsContainer = styled.div`
@ -11,35 +11,33 @@ const GraphToolsContainer = styled.div`
flex-direction: column;
`;
interface IGraphToolsProps {
currentColorScheme?: IColorScheme;
colorSchemes: IColorScheme[];
interface GraphToolsProps {
currentColorScheme?: ColorScheme;
colorSchemes: ColorScheme[];
isShowingEdges: boolean;
ranges?: { [key: string]: [number, number] };
onColorSchemeSelect: (colorScheme?: IColorScheme) => void;
onColorSchemeSelect: (colorScheme?: ColorScheme) => void;
onResetButtonClick: () => void;
toggleEdges: () => void;
}
const GraphTools: React.FC<IGraphToolsProps> = ({
const GraphTools: React.FC<GraphToolsProps> = ({
currentColorScheme,
colorSchemes,
isShowingEdges,
ranges,
onColorSchemeSelect,
onResetButtonClick,
toggleEdges
}) => {
return (
<GraphToolsContainer>
<GraphResetButton onClick={onResetButtonClick} />
<GraphHideEdgesButton isShowingEdges={isShowingEdges} toggleEdges={toggleEdges} />
<GraphKey
current={currentColorScheme}
colorSchemes={colorSchemes}
onItemSelect={onColorSchemeSelect}
ranges={ranges}
/>
</GraphToolsContainer>
);
};
toggleEdges,
}) => (
<GraphToolsContainer>
<GraphResetButton onClick={onResetButtonClick} />
<GraphHideEdgesButton isShowingEdges={isShowingEdges} toggleEdges={toggleEdges} />
<GraphKey
current={currentColorScheme}
colorSchemes={colorSchemes}
onItemSelect={onColorSchemeSelect}
ranges={ranges}
/>
</GraphToolsContainer>
);
export default GraphTools;

View File

@ -4,7 +4,7 @@ import * as numeral from "numeral";
import React from "react";
import sanitize from "sanitize-html";
import styled from "styled-components";
import { ISearchResultInstance } from "../../redux/types";
import { SearchResultInstance } from "../../redux/types";
import { InstanceType } from "../atoms";
const StyledCard = styled(Card)`
@ -32,18 +32,18 @@ const StyledUserCount = styled.div`
const StyledDescription = styled.div`
margin-top: 10px;
`;
interface ISearchResultProps {
result: ISearchResultInstance;
interface SearchResultProps {
result: SearchResultInstance;
onClick: () => void;
onMouseEnter: () => void;
onMouseLeave: () => void;
}
const SearchResult: React.FC<ISearchResultProps> = ({ result, onClick, onMouseEnter, onMouseLeave }) => {
const SearchResult: React.FC<SearchResultProps> = ({ result, onClick, onMouseEnter, onMouseLeave }) => {
let shortenedDescription;
if (result.description) {
shortenedDescription = result.description && sanitize(result.description);
if (shortenedDescription.length > 100) {
shortenedDescription = shortenedDescription.substring(0, 100) + "...";
shortenedDescription = `${shortenedDescription.substring(0, 100)}...`;
}
}
@ -59,7 +59,7 @@ const SearchResult: React.FC<ISearchResultProps> = ({ result, onClick, onMouseEn
return (
<StyledCard
elevation={Elevation.ONE}
interactive={true}
interactive
key={result.name}
onClick={onClick}
onMouseEnter={onMouseEnter}

View File

@ -1,12 +1,12 @@
import { Classes, H3 } from "@blueprintjs/core";
import React from "react";
import { Link } from "react-router-dom";
import { IFederationRestrictions } from "../../redux/types";
import { FederationRestrictions } from "../../redux/types";
const maybeGetList = (domains?: string[]) =>
domains && (
<ul>
{domains.sort().map(domain => (
{domains.sort().map((domain) => (
<li key={domain}>
<Link to={`/instance/${domain}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
{domain}
@ -16,10 +16,10 @@ const maybeGetList = (domains?: string[]) =>
</ul>
);
interface IFederationTabProps {
restrictions?: IFederationRestrictions;
interface FederationTabProps {
restrictions?: FederationRestrictions;
}
const FederationTab: React.FC<IFederationTabProps> = ({ restrictions }) => {
const FederationTab: React.FC<FederationTabProps> = ({ restrictions }) => {
if (!restrictions) {
return null;
}

View File

@ -6,33 +6,33 @@ import { push } from "connected-react-router";
import { Dispatch } from "redux";
import styled from "styled-components";
import { fetchGraph } from "../../redux/actions";
import { IAppState, IGraphResponse } from "../../redux/types";
import { colorSchemes, IColorScheme } from "../../types";
import { AppState, GraphResponse } from "../../redux/types";
import { colorSchemes, ColorScheme } from "../../types";
import { domainMatchSelector } from "../../util";
import { Cytoscape, ErrorState, GraphTools } from "../molecules/";
import { Cytoscape, ErrorState, GraphTools } from "../molecules";
const GraphDiv = styled.div`
flex: 2;
`;
interface IGraphProps {
interface GraphProps {
currentInstanceName: string | null;
fetchGraph: () => void;
graphResponse?: IGraphResponse;
graphResponse?: GraphResponse;
graphLoadError: boolean;
hoveringOverResult?: string;
isLoadingGraph: boolean;
searchResultDomains: string[];
navigate: (path: string) => void;
}
interface IGraphState {
colorScheme?: IColorScheme;
interface GraphState {
colorScheme?: ColorScheme;
isShowingEdges: boolean;
}
class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
class GraphImpl extends React.PureComponent<GraphProps, GraphState> {
private cytoscapeComponent: React.RefObject<Cytoscape>;
public constructor(props: IGraphProps) {
public constructor(props: GraphProps) {
super(props);
this.cytoscapeComponent = React.createRef();
this.state = { colorScheme: undefined, isShowingEdges: true };
@ -76,7 +76,7 @@ class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
);
}
return <GraphDiv aria-hidden={true}>{content}</GraphDiv>;
return <GraphDiv aria-hidden>{content}</GraphDiv>;
}
private loadGraph = () => {
@ -95,7 +95,7 @@ class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
this.setState({ isShowingEdges: !this.state.isShowingEdges });
};
private setColorScheme = (colorScheme?: IColorScheme) => {
private setColorScheme = (colorScheme?: ColorScheme) => {
this.setState({ colorScheme });
};
@ -107,7 +107,7 @@ class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
this.props.navigate("/");
};
}
const mapStateToProps = (state: IAppState) => {
const mapStateToProps = (state: AppState) => {
const match = domainMatchSelector(state);
return {
currentInstanceName: match && match.params.domain,
@ -115,15 +115,12 @@ const mapStateToProps = (state: IAppState) => {
graphResponse: state.data.graphResponse,
hoveringOverResult: state.search.hoveringOverResult,
isLoadingGraph: state.data.isLoadingGraph,
searchResultDomains: state.search.results.map(r => r.name)
searchResultDomains: state.search.results.map((r) => r.name),
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
fetchGraph: () => dispatch(fetchGraph() as any),
navigate: (path: string) => dispatch(push(path))
navigate: (path: string) => dispatch(push(path)),
});
const Graph = connect(
mapStateToProps,
mapDispatchToProps
)(GraphImpl);
const Graph = connect(mapStateToProps, mapDispatchToProps)(GraphImpl);
export default Graph;

View File

@ -8,7 +8,7 @@ import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
import { loadInstanceList } from "../../redux/actions";
import { IAppState, IInstanceListResponse, IInstanceSort, SortField } from "../../redux/types";
import { AppState, InstanceListResponse, InstanceSort, SortField, InstanceDetails } from "../../redux/types";
import { InstanceType } from "../atoms";
import { ErrorState } from "../molecules";
@ -41,15 +41,15 @@ const InsularityColumn = styled.th`
width: 15%;
`;
interface IInstanceTableProps {
interface InstanceTableProps {
loadError: boolean;
instancesResponse?: IInstanceListResponse;
instanceListSort: IInstanceSort;
instancesResponse?: InstanceListResponse;
instanceListSort: InstanceSort;
isLoading: boolean;
loadInstanceList: (page?: number, sort?: IInstanceSort) => void;
loadInstanceList: (page?: number, sort?: InstanceSort) => void;
navigate: (path: string) => void;
}
class InstanceTable extends React.PureComponent<IInstanceTableProps> {
class InstanceTable extends React.PureComponent<InstanceTableProps> {
public componentDidMount() {
const { isLoading, instancesResponse, loadError } = this.props;
if (!isLoading && !instancesResponse && !loadError) {
@ -61,22 +61,23 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
const { isLoading, instancesResponse, loadError } = this.props;
if (loadError) {
return <ErrorState />;
} else if (isLoading || !instancesResponse) {
}
if (isLoading || !instancesResponse) {
return <NonIdealState icon={<Spinner />} />;
}
const { instances, pageNumber: currentPage, totalPages, totalEntries, pageSize } = instancesResponse!;
const { instances, pageNumber: currentPage, totalPages, totalEntries, pageSize } = instancesResponse;
const pagesToDisplay = this.getPagesToDisplay(totalPages, currentPage);
return (
<>
<StyledTable striped={true} bordered={true} interactive={true}>
<StyledTable striped bordered interactive>
<thead>
<tr>
<InstanceColumn>
Instance
<Button
minimal={true}
minimal
icon={this.getSortIcon("domain")}
onClick={this.sortByFactory("domain")}
intent={this.getSortIntent("domain")}
@ -87,7 +88,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
<UserCountColumn>
Users
<Button
minimal={true}
minimal
icon={this.getSortIcon("userCount")}
onClick={this.sortByFactory("userCount")}
intent={this.getSortIntent("userCount")}
@ -96,7 +97,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
<StatusCountColumn>
Statuses
<Button
minimal={true}
minimal
icon={this.getSortIcon("statusCount")}
onClick={this.sortByFactory("statusCount")}
intent={this.getSortIntent("statusCount")}
@ -105,7 +106,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
<InsularityColumn>
Insularity
<Button
minimal={true}
minimal
icon={this.getSortIcon("insularity")}
onClick={this.sortByFactory("insularity")}
intent={this.getSortIntent("insularity")}
@ -114,7 +115,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
</tr>
</thead>
<tbody>
{instances.map(i => (
{instances.map((i: InstanceDetails) => (
<tr key={i.name} onClick={this.goToInstanceFactory(i.name)}>
<td>{i.name}</td>
<td>{i.type && <InstanceType type={i.type} />}</td>
@ -134,7 +135,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
</p>
<ButtonGroup>
{zip(pagesToDisplay, pagesToDisplay.slice(1)).map(([page, nextPage], idx) => {
{zip(pagesToDisplay, pagesToDisplay.slice(1)).map(([page, nextPage]) => {
if (page === undefined) {
return null;
}
@ -152,8 +153,8 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
{page}
</Button>
{isEndOfSection && (
<Button disabled={true} key={"..."}>
{"..."}
<Button disabled key="...">
...
</Button>
)}
</>
@ -187,20 +188,19 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
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<IInstanceTableProps> {
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);

View File

@ -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<IInstanceDomainPath>, location: Location) => {
return location.pathname === "/" || location.pathname.startsWith("/instance/");
};
const graphIsActive = (currMatch: match<InstanceDomainPath>, 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 (
<Navbar fixedToTop={true}>
<Navbar fixedToTop>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>fediverse.space</Navbar.Heading>
<Navbar.Divider />
@ -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
</NavLink>

View File

@ -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<HTMLButtonElement>, props: ITagProps) => void;
}
const SearchFilters: React.FC<ISearchFiltersProps> = ({ selectedFilters, selectFilter, deselectFilter }) => {
const hasInstanceTypeFilter = selectedFilters.some(sf => sf.field === "type");
const SearchFilters: React.FC<SearchFiltersProps> = ({ selectedFilters, selectFilter, deselectFilter }) => {
const hasInstanceTypeFilter = selectedFilters.some((sf) => sf.field === "type");
const handleSelectInstanceType = (e: MouseEvent<HTMLElement>) => {
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 = () => (
<Menu>
<MenuItem icon={IconNames.SYMBOL_CIRCLE} text="Instance type" disabled={hasInstanceTypeFilter}>
{INSTANCE_TYPES.map(t => (
{INSTANCE_TYPES.map((t) => (
<MenuItem key={t} text={getTypeDisplayString(t)} onClick={handleSelectInstanceType} />
))}
</MenuItem>
@ -51,15 +51,15 @@ const SearchFilters: React.FC<ISearchFiltersProps> = ({ selectedFilters, selectF
return (
<SearchFilterContainer>
<TagContainer>
{selectedFilters.map(filter => (
<StyledTag key={filter.displayValue} minimal={true} onRemove={deselectFilter}>
{selectedFilters.map((filter) => (
<StyledTag key={filter.displayValue} minimal onRemove={deselectFilter}>
{filter.displayValue}
</StyledTag>
))}
</TagContainer>
<Popover autoFocus={false} content={renderMenu()} position={Position.BOTTOM}>
<Button minimal={true} icon={IconNames.FILTER}>
{"Add filter"}
<Button minimal icon={IconNames.FILTER}>
Add filter
</Button>
</Popover>
</SearchFilterContainer>

View File

@ -17,11 +17,9 @@ const StyledCard = styled(Card)`
display: flex;
flex-direction: column;
`;
const SidebarContainer: React.FC = ({ children }) => {
return (
<RightDiv>
<StyledCard elevation={Elevation.TWO}>{children}</StyledCard>
</RightDiv>
);
};
const SidebarContainer: React.FC = ({ children }) => (
<RightDiv>
<StyledCard elevation={Elevation.TWO}>{children}</StyledCard>
</RightDiv>
);
export default SidebarContainer;

View File

@ -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 = () => (
<br />
<H2>FAQ</H2>
<H4>Why can't I see details about my instance?</H4>
<H4>Why can&apos;t I see details about my instance?</H4>
<p className={Classes.RUNNING_TEXT}>
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&apos;t be scraped -- it&apos;s a tool for understanding communities, not
individuals.
</p>
<H4>

View File

@ -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<IAdminScreenProps, IAdminScreenState> {
class AdminScreen extends React.PureComponent<AdminScreenProps, AdminScreenState> {
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<IAdminScreenProps, IAdminScreenSta
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: "Failed to load settings.",
timeout: 0
timeout: 0,
});
unsetAuthToken();
});
@ -78,7 +78,7 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
<Switch
id="opt-in-switch"
checked={!!settings.optIn}
large={true}
large
label="Opt in"
disabled={!!isUpdating}
onChange={this.updateOptIn}
@ -89,7 +89,7 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
<Switch
id="opt-out-switch"
checked={!!settings.optOut}
large={true}
large
label="Opt out"
disabled={!!isUpdating}
onChange={this.updateOptOut}
@ -116,9 +116,9 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
}
private updateOptIn = (e: React.FormEvent<HTMLInputElement>) => {
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<IAdminScreenProps, IAdminScreenSta
};
private updateOptOut = (e: React.FormEvent<HTMLInputElement>) => {
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<IAdminScreenProps, IAdminScreenSta
this.setState({ isUpdating: true });
const body = {
optIn: this.state.settings!.optIn,
optOut: this.state.settings!.optOut
optOut: this.state.settings!.optOut,
};
postToApi(`admin`, body, this.authToken!)
.then(response => {
.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<IAdminScreenProps, IAdminScreenSta
unsetAuthToken();
AppToaster.show({
icon: IconNames.LOG_OUT,
message: "Logged out."
message: "Logged out.",
});
this.props.navigate("/admin/login");
};
}
const mapDispatchToProps = (dispatch: Dispatch) => ({
navigate: (path: string) => dispatch(push(path))
navigate: (path: string) => dispatch(push(path)),
});
export default connect(
undefined,
mapDispatchToProps
)(AdminScreen);
export default connect(undefined, mapDispatchToProps)(AdminScreen);

View File

@ -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<IGraphScreenProps, IGraphScreenState> {
public constructor(props: IGraphScreenProps) {
class GraphScreenImpl extends React.Component<GraphScreenProps, GraphScreenState> {
public constructor(props: GraphScreenProps) {
super(props);
this.state = { hasBeenViewed: false };
}
@ -56,7 +56,7 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenSta
this.loadCurrentInstance();
}
public componentDidUpdate(prevProps: IGraphScreenProps) {
public componentDidUpdate(prevProps: GraphScreenProps) {
this.setHasBeenViewed();
this.loadCurrentInstance(prevProps.currentInstanceName);
}
@ -72,7 +72,7 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenSta
}
};
private renderRoutes = ({ location }: RouteComponentProps) => (
private renderRoutes = () => (
<FullDiv>
<GraphContainer>
{/* Smaller screens never load the entire graph. Instead, `InstanceScreen` shows only the neighborhood. */}
@ -80,7 +80,7 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenSta
<SidebarContainer>
<Switch>
<Route path={INSTANCE_DOMAIN_PATH} component={InstanceScreen} />
<Route exact={true} path="/" component={SearchScreen} />
<Route exact path="/" component={SearchScreen} />
</Switch>
</SidebarContainer>
</GraphContainer>
@ -94,19 +94,16 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenSta
};
}
const mapStateToProps = (state: IAppState) => {
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);

View File

@ -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<IInstanceScreenProps, IInstanceScreenState> {
public constructor(props: IInstanceScreenProps) {
class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, InstanceScreenState> {
public constructor(props: InstanceScreenProps) {
super(props);
this.state = { isProcessingNeighbors: false, isLoadingLocalGraph: false, localGraphLoadError: false };
}
@ -116,9 +116,9 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
!this.props.instanceDetails.status
) {
content = <ErrorState />;
} 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<IInstanceScreenProps, IInst
<HeadingContainer>
<StyledHeadingH2>{this.props.instanceName}</StyledHeadingH2>
<StyledHeadingTooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
<AnchorButton icon={IconNames.LINK} minimal onClick={this.openInstanceLink} />
</StyledHeadingTooltip>
<StyledCloseButton icon={IconNames.CROSS} onClick={this.props.navigateToRoot} />
</HeadingContainer>
@ -145,7 +145,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
this.processEdgesToFindNeighbors();
}
public componentDidUpdate(prevProps: IInstanceScreenProps, prevState: IInstanceScreenState) {
public componentDidUpdate(prevProps: InstanceScreenProps, prevState: InstanceScreenState) {
const isNewInstance = prevProps.instanceName !== this.props.instanceName;
const receivedNewEdges = !!this.props.graph && !this.state.isProcessingNeighbors && !this.state.neighbors;
const receivedNewLocalGraph = !!this.state.localGraph && !prevState.localGraph;
@ -164,10 +164,13 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
}
this.setState({ isProcessingNeighbors: true });
const graphToUse = !!graph ? graph : localGraph;
const edges = graphToUse!.edges.filter(e => [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<IInstanceScreenProps, IInst
}
this.setState({ isLoadingLocalGraph: true });
getFromApi(`graph/${this.props.instanceName}`)
.then((response: IGraphResponse) => {
.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 ? (
<StyledCallout icon={IconNames.INFO_SIGN} title="Insular instance">
<p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
<p>This instance doesn&apos;t have any neighbors that we know of, so it&apos;s hidden from the graph.</p>
</StyledCallout>
) : (
undefined
);
) : undefined;
return (
<>
{insularCallout}
{this.maybeRenderLocalGraph()}
<StyledTabs>
{this.props.instanceDetails!.description && (
{instanceDetails && instanceDetails.description && (
<Tab id="description" title="Description" panel={this.renderDescription()} />
)}
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
@ -232,7 +234,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
<StyledLinkToFdNetwork>
<AnchorButton
href={`https://fediverse.network/${this.props.instanceName}`}
minimal={true}
minimal
rightIcon={IconNames.SHARE}
target="_blank"
text="See more statistics at fediverse.network"
@ -244,18 +246,17 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
private maybeRenderLocalGraph = () => {
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 (
<StyledGraphContainer aria-hidden={true}>
<StyledGraphContainer aria-hidden>
<Cytoscape
elements={localGraph!}
elements={localGraph}
currentNodeId={this.props.instanceName}
navigateToInstancePath={this.props.navigateToInstance}
showEdges={true}
showEdges
/>
<Divider />
</StyledGraphContainer>
@ -268,7 +269,11 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
};
private renderDescription = () => {
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<IInstanceScreenProps, IInst
insularity,
type,
statusesPerDay,
statusesPerUserPerDay
statusesPerUserPerDay,
} = this.props.instanceDetails;
return (
<StyledHTMLTable small={true} striped={true}>
<StyledHTMLTable small striped>
<tbody>
<tr>
<td>Version</td>
@ -299,7 +304,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
</tr>
<tr>
<td>Instance type</td>
<td>{(type && <InstanceType type={type} colorAfterName={true} />) || "Unknown"}</td>
<td>{(type && <InstanceType type={type} colorAfterName />) || "Unknown"}</td>
</tr>
<tr>
<td>Users</td>
@ -311,7 +316,8 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
</tr>
<tr>
<td>
Insularity{" "}
Insularity
{" "}
<Tooltip
content={
<span>
@ -330,7 +336,8 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
</tr>
<tr>
<td>
Statuses / day{" "}
Statuses / day
{" "}
<Tooltip
content={
<span>
@ -349,7 +356,8 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
</tr>
<tr>
<td>
Statuses / person / day{" "}
Statuses / person / day
{" "}
<Tooltip
content={
<span>
@ -372,7 +380,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
</tr>
<tr>
<td>Last updated</td>
<td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
<td>{moment(`${lastUpdated}Z`).fromNow() || "Unknown"}</td>
</tr>
</tbody>
</StyledHTMLTable>
@ -412,7 +420,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
would mean that every single status on {this.props.instanceName} contained a mention of someone on the other
instance, and vice versa.
</p>
<StyledHTMLTable small={true} striped={true} interactive={false}>
<StyledHTMLTable small striped interactive={false}>
<thead>
<tr>
<th>Instance</th>
@ -426,11 +434,15 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
};
private renderPeers = () => {
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) => (
<tr key={instance.name}>
<td>
<Link to={`/instance/${instance.name}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
@ -444,7 +456,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
<p className={Classes.TEXT_MUTED}>
All the instances, past and present, that {this.props.instanceName} knows about.
</p>
<StyledHTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
<StyledHTMLTable small striped interactive={false} className="fediverse-sidebar-table">
<tbody>{peerRows}</tbody>
</StyledHTMLTable>
</div>
@ -453,71 +465,62 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;
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={
<Link to={"/admin"} className={Classes.BUTTON} role="button">
{"Opt in"}
</Link>
}
/>
);
};
private renderPersonalInstanceErrorState = () => (
<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={
<Link to="/admin" className={Classes.BUTTON} role="button">
Opt in
</Link>
}
/>
);
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."
/>
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
{this.props.instanceDetails && this.props.instanceDetails.status}
private renderMissingDataState = () => (
<>
<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."
/>
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
{this.props.instanceDetails && this.props.instanceDetails.status}
</span>
</>
);
private renderRobotsTxtState = () => (
<NonIdealState
icon={
<span role="img" aria-label="robot">
🤖
</span>
</>
);
};
private renderRobotsTxtState = () => {
return (
<NonIdealState
icon={
<span role="img" aria-label="robot">
🤖
</span>
}
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;

View File

@ -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<IFormContainerProps>`
${props => (props.error ? "margin: 20px auto 0 auto;" : "margin-top: 20px;")}
const FormContainer = styled.div<FormContainerProps>`
${(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 = (
<ErrorState description="This could be because the instance is down. If not, please reload the page and try again." />
);
} 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.
</p>
<p className={Classes.RUNNING_TEXT}>
It's currently only possible to administrate Mastodon and Pleroma instances. If you want to login with a
It&apos;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.
</p>
<p className={Classes.RUNNING_TEXT}>
@ -95,7 +100,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
const onButtonClick = () => this.getLoginTypes();
return (
<form onSubmit={this.getLoginTypes}>
<FormGroup label="Instance domain" labelFor="domain-input" disabled={isGettingLoginTypes} inline={true}>
<FormGroup label="Instance domain" labelFor="domain-input" disabled={isGettingLoginTypes} inline>
<InputGroup
disabled={isGettingLoginTypes}
id="domain-input"
@ -104,7 +109,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
rightElement={
<Button
intent={Intent.PRIMARY}
minimal={true}
minimal
rightIcon={IconNames.ARROW_RIGHT}
title="submit"
loading={isGettingLoginTypes}
@ -130,18 +135,13 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
<H4>Choose an authentication method</H4>
<LoginTypeContainer>
{loginTypes.email && (
<LoginTypeButton
large={true}
icon={IconNames.ENVELOPE}
onClick={loginWithEmail}
loading={!!isSendingLoginRequest}
>
<LoginTypeButton large icon={IconNames.ENVELOPE} onClick={loginWithEmail} loading={!!isSendingLoginRequest}>
{`Email ${loginTypes.email}`}
</LoginTypeButton>
)}
{loginTypes.fediverseAccount && (
<LoginTypeButton
large={true}
large
icon={IconNames.GLOBE_NETWORK}
onClick={loginWithDm}
loading={!!isSendingLoginRequest}
@ -175,7 +175,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
};
private getLoginTypes = (e?: React.FormEvent<HTMLFormElement>) => {
if (!!e) {
if (e) {
e.preventDefault();
}
this.setState({ isGettingLoginTypes: true });
@ -184,8 +184,8 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
domain = domain.slice(8);
}
getFromApi(`admin/login/${domain.trim()}`)
.then(response => {
if (!!response.error) {
.then((response) => {
if (response.error) {
// Go to catch() below
throw new Error(response.error);
} else {
@ -196,7 +196,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: err.message
message: err.message,
});
this.setState({ isGettingLoginTypes: false });
});
@ -205,7 +205,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
private login = (type: "email" | "fediverseAccount") => {
this.setState({ isSendingLoginRequest: true, selectedLoginType: type });
postToApi("admin/login", { domain: this.state.loginTypes!.domain, type })
.then(response => {
.then((response) => {
if ("error" in response || "errors" in response) {
// Go to catch() below
throw new Error();

View File

@ -7,20 +7,20 @@ import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
import { setResultHover, updateSearch } from "../../redux/actions";
import { IAppState, ISearchResultInstance } from "../../redux/types";
import { ISearchFilter } from "../../searchFilters";
import { AppState, SearchResultInstance } from "../../redux/types";
import { SearchFilter } from "../../searchFilters";
import { isSmallScreen } from "../../util";
import { SearchResult } from "../molecules";
import { SearchFilters } from "../organisms";
interface ISearchBarContainerProps {
interface SearchBarContainerProps {
hasSearchResults: boolean;
hasError: boolean;
}
const SearchBarContainer = styled.div<ISearchBarContainerProps>`
const SearchBarContainer = styled.div<SearchBarContainerProps>`
width: 80%;
text-align: center;
margin: ${props => (props.hasSearchResults || props.hasError ? "0 auto" : "auto")};
margin: ${(props) => (props.hasSearchResults || props.hasError ? "0 auto" : "auto")};
align-self: center;
`;
const SearchResults = styled.div`
@ -38,22 +38,22 @@ const CalloutContainer = styled.div`
text-align: left;
`;
interface ISearchScreenProps {
interface SearchScreenProps {
error: boolean;
isLoadingResults: boolean;
query: string;
hasMoreResults: boolean;
results: ISearchResultInstance[];
handleSearch: (query: string, filters: ISearchFilter[]) => void;
results: SearchResultInstance[];
handleSearch: (query: string, filters: SearchFilter[]) => void;
navigateToInstance: (domain: string) => void;
setIsHoveringOver: (domain?: string) => void;
}
interface ISearchScreenState {
interface SearchScreenState {
currentQuery: string;
searchFilters: ISearchFilter[];
searchFilters: SearchFilter[];
}
class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreenState> {
public constructor(props: ISearchScreenProps) {
class SearchScreen extends React.PureComponent<SearchScreenProps, SearchScreenState> {
public constructor(props: SearchScreenProps) {
super(props);
this.state = { currentQuery: "", searchFilters: [] };
}
@ -81,7 +81,7 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
} else if (!!results && results.length > 0) {
content = (
<SearchResults>
{results.map(result => (
{results.map((result) => (
<SearchResult
result={result}
key={result.name}
@ -92,7 +92,7 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
))}
{isLoadingResults && <StyledSpinner size={Spinner.SIZE_SMALL} />}
{!isLoadingResults && hasMoreResults && (
<Button onClick={this.search} minimal={true}>
<Button onClick={this.search} minimal>
Load more results
</Button>
)}
@ -104,13 +104,11 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
if (isLoadingResults) {
rightSearchBarElement = <Spinner size={Spinner.SIZE_SMALL} />;
} else if (query || error) {
rightSearchBarElement = (
<Button minimal={true} icon={IconNames.CROSS} onClick={this.clearQuery} aria-label="Search" />
);
rightSearchBarElement = <Button minimal icon={IconNames.CROSS} onClick={this.clearQuery} aria-label="Search" />;
} else {
rightSearchBarElement = (
<Button
minimal={true}
minimal
icon={IconNames.ARROW_RIGHT}
intent={Intent.PRIMARY}
onClick={this.search}
@ -127,7 +125,7 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
<InputGroup
leftIcon={IconNames.SEARCH}
rightElement={rightSearchBarElement}
large={true}
large
placeholder="Search instance names and descriptions"
aria-label="Search instance names and descriptions"
type="search"
@ -164,10 +162,10 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
this.setState({ currentQuery: "" }, () => this.props.handleSearch("", []));
};
private selectSearchFilter = (filter: ISearchFilter) => {
private selectSearchFilter = (filter: SearchFilter) => {
const { searchFilters } = this.state;
// Don't add the same filters twice
if (searchFilters.some(sf => isEqual(sf, filter))) {
if (searchFilters.some((sf) => isEqual(sf, filter))) {
return;
}
this.setState({ searchFilters: [...searchFilters, filter] }, this.search);
@ -176,9 +174,9 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
private deselectSearchFilter = (e: MouseEvent<HTMLButtonElement>) => {
const { searchFilters } = this.state;
const displayValueToRemove = get(e, "currentTarget.parentElement.innerText", "");
if (!!displayValueToRemove) {
if (displayValueToRemove) {
this.setState(
{ searchFilters: searchFilters.filter(sf => sf.displayValue !== displayValueToRemove) },
{ searchFilters: searchFilters.filter((sf) => sf.displayValue !== displayValueToRemove) },
this.search
);
}
@ -207,19 +205,16 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
);
}
const mapStateToProps = (state: IAppState) => ({
const mapStateToProps = (state: AppState) => ({
error: state.search.error,
hasMoreResults: !!state.search.next,
isLoadingResults: state.search.isLoadingResults,
query: state.search.query,
results: state.search.results
results: state.search.results,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
handleSearch: (query: string, filters: ISearchFilter[]) => dispatch(updateSearch(query, filters) as any),
handleSearch: (query: string, filters: SearchFilter[]) => dispatch(updateSearch(query, filters) as any),
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
setIsHoveringOver: (domain?: string) => dispatch(setResultHover(domain))
setIsHoveringOver: (domain?: string) => dispatch(setResultHover(domain)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(SearchScreen);
export default connect(mapStateToProps, mapDispatchToProps)(SearchScreen);

View File

@ -6,8 +6,8 @@ import { InstanceTable } from "../organisms";
class TableScreen extends React.PureComponent {
public render() {
return (
<Page fullWidth={true}>
<H1>{"Instances"}</H1>
<Page fullWidth>
<H1>Instances</H1>
<InstanceTable />
</Page>
);

View File

@ -1,20 +1,20 @@
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router";
import { IAppState } from "../../redux/types";
import { AppState } from "../../redux/types";
import { setAuthToken } from "../../util";
import { Page } from "../atoms";
interface IVerifyLoginScreenProps {
interface VerifyLoginScreenProps {
search: string;
}
const VerifyLoginScreen: React.FC<IVerifyLoginScreenProps> = ({ search }) => {
const VerifyLoginScreen: React.FC<VerifyLoginScreenProps> = ({ search }) => {
const [didSaveToken, setDidSaveToken] = useState(false);
const token = new URLSearchParams(search).get("token");
useEffect(() => {
// Save the auth token
if (!!token) {
if (token) {
setAuthToken(token);
setDidSaveToken(true);
}
@ -22,15 +22,14 @@ const VerifyLoginScreen: React.FC<IVerifyLoginScreenProps> = ({ search }) => {
if (!token) {
return <Redirect to="/admin/login" />;
} else if (!didSaveToken) {
}
if (!didSaveToken) {
return <Page />;
}
return <Redirect to="/admin" />;
};
const mapStateToProps = (state: IAppState) => {
return {
search: state.router.location.search
};
};
const mapStateToProps = (state: AppState) => ({
search: state.router.location.search,
});
export default connect(mapStateToProps)(VerifyLoginScreen);

View File

@ -21,7 +21,7 @@ export const QUALITATIVE_COLOR_SCHEME = [
"#0E5A8A",
"#0A6640",
"#A66321",
"#A82A2A"
"#A82A2A",
];
// From https://blueprintjs.com/docs/#core/colors.sequential-color-schemes
@ -35,11 +35,11 @@ export const QUANTITATIVE_COLOR_SCHEME = [
"#C15B3F",
"#B64C2F",
"#AA3C1F",
"#9E2B0E"
"#9E2B0E",
];
export const INSTANCE_DOMAIN_PATH = "/instance/:domain";
export interface IInstanceDomainPath {
export interface InstanceDomainPath {
domain: string;
}
@ -55,5 +55,5 @@ export const INSTANCE_TYPES = [
"friendica",
"hubzilla",
"plume",
"wordpress"
"wordpress",
];

View File

@ -25,8 +25,7 @@ FocusStyleManager.onlyShowFocusOnTabs();
export const history = createBrowserHistory();
// Initialize redux
// @ts-ignore
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
createRootReducer(history),
composeEnhancers(applyMiddleware(routerMiddleware(history), thunk))

View File

@ -1 +1 @@
/// <reference types="react-scripts" />
// / <reference types="react-scripts" />

View File

@ -2,171 +2,145 @@ import { isEqual } from "lodash";
import { Dispatch } from "redux";
import { push } from "connected-react-router";
import { ISearchFilter } from "../searchFilters";
import { SearchFilter } from "../searchFilters";
import { getFromApi } from "../util";
import { ActionType, IAppState, IGraph, IInstanceDetails, IInstanceSort, ISearchResponse } from "./types";
import { ActionType, AppState, Graph, InstanceDetails, InstanceSort, SearchResponse } from "./types";
// Instance details
const requestInstanceDetails = (instanceName: string) => {
return {
payload: instanceName,
type: ActionType.REQUEST_INSTANCE_DETAILS
};
};
const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
return {
payload: instanceDetails,
type: ActionType.RECEIVE_INSTANCE_DETAILS
};
};
const instanceLoadFailed = () => {
return {
type: ActionType.INSTANCE_LOAD_ERROR
};
};
const deselectInstance = () => {
return {
type: ActionType.DESELECT_INSTANCE
};
};
const requestInstanceDetails = (instanceName: string) => ({
payload: instanceName,
type: ActionType.REQUEST_INSTANCE_DETAILS,
});
const receiveInstanceDetails = (instanceDetails: InstanceDetails) => ({
payload: instanceDetails,
type: ActionType.RECEIVE_INSTANCE_DETAILS,
});
const instanceLoadFailed = () => ({
type: ActionType.INSTANCE_LOAD_ERROR,
});
const deselectInstance = () => ({
type: ActionType.DESELECT_INSTANCE,
});
// Graph
const requestGraph = () => {
return {
type: ActionType.REQUEST_GRAPH
};
};
const receiveGraph = (graph: IGraph) => {
return {
payload: graph,
type: ActionType.RECEIVE_GRAPH
};
};
const graphLoadFailed = () => {
return {
type: ActionType.GRAPH_LOAD_ERROR
};
};
const requestGraph = () => ({
type: ActionType.REQUEST_GRAPH,
});
const receiveGraph = (graph: Graph) => ({
payload: graph,
type: ActionType.RECEIVE_GRAPH,
});
const graphLoadFailed = () => ({
type: ActionType.GRAPH_LOAD_ERROR,
});
// Instance list
const requestInstanceList = (sort?: IInstanceSort) => ({
const requestInstanceList = (sort?: InstanceSort) => ({
payload: sort,
type: ActionType.REQUEST_INSTANCES
type: ActionType.REQUEST_INSTANCES,
});
const receiveInstanceList = (instances: IInstanceDetails[]) => ({
const receiveInstanceList = (instances: InstanceDetails[]) => ({
payload: instances,
type: ActionType.RECEIVE_INSTANCES
type: ActionType.RECEIVE_INSTANCES,
});
const instanceListLoadFailed = () => ({
type: ActionType.INSTANCE_LIST_LOAD_ERROR
type: ActionType.INSTANCE_LIST_LOAD_ERROR,
});
// Search
const requestSearchResult = (query: string, filters: ISearchFilter[]) => {
return {
payload: { query, filters },
type: ActionType.REQUEST_SEARCH_RESULTS
};
};
const receiveSearchResults = (result: ISearchResponse) => {
return {
payload: result,
type: ActionType.RECEIVE_SEARCH_RESULTS
};
};
const searchFailed = () => {
return {
type: ActionType.SEARCH_RESULTS_ERROR
};
};
const requestSearchResult = (query: string, filters: SearchFilter[]) => ({
payload: { query, filters },
type: ActionType.REQUEST_SEARCH_RESULTS,
});
const receiveSearchResults = (result: SearchResponse) => ({
payload: result,
type: ActionType.RECEIVE_SEARCH_RESULTS,
});
const searchFailed = () => ({
type: ActionType.SEARCH_RESULTS_ERROR,
});
const resetSearch = () => {
return {
type: ActionType.RESET_SEARCH
};
};
const resetSearch = () => ({
type: ActionType.RESET_SEARCH,
});
export const setResultHover = (domain?: string) => {
return {
payload: domain,
type: ActionType.SET_SEARCH_RESULT_HOVER
};
};
export const setResultHover = (domain?: string) => ({
payload: domain,
type: ActionType.SET_SEARCH_RESULT_HOVER,
});
/** Async actions: https://redux.js.org/advanced/asyncactions */
export const loadInstance = (instanceName: string | null) => {
return (dispatch: Dispatch, getState: () => IAppState) => {
if (!instanceName) {
dispatch(deselectInstance());
if (getState().router.location.pathname.startsWith("/instance/")) {
dispatch(push("/"));
}
return;
export const loadInstance = (instanceName: string | null) => (dispatch: Dispatch, getState: () => AppState) => {
if (!instanceName) {
dispatch(deselectInstance());
if (getState().router.location.pathname.startsWith("/instance/")) {
dispatch(push("/"));
}
dispatch(requestInstanceDetails(instanceName));
return getFromApi("instances/" + instanceName)
.then(details => dispatch(receiveInstanceDetails(details)))
.catch(() => dispatch(instanceLoadFailed()));
};
return;
}
dispatch(requestInstanceDetails(instanceName));
return getFromApi(`instances/${instanceName}`)
.then((details) => dispatch(receiveInstanceDetails(details)))
.catch(() => dispatch(instanceLoadFailed()));
};
export const updateSearch = (query: string, filters: ISearchFilter[]) => {
return (dispatch: Dispatch, getState: () => IAppState) => {
query = query.trim();
export const updateSearch = (query: string, filters: SearchFilter[]) => (
dispatch: Dispatch,
getState: () => AppState
) => {
query = query.trim();
if (!query) {
dispatch(resetSearch());
return;
}
if (!query) {
dispatch(resetSearch());
return;
}
const prevQuery = getState().search.query;
const prevFilters = getState().search.filters;
const isNewQuery = prevQuery !== query || !isEqual(prevFilters, filters);
const prevQuery = getState().search.query;
const prevFilters = getState().search.filters;
const isNewQuery = prevQuery !== query || !isEqual(prevFilters, filters);
const next = getState().search.next;
let url = `search/?query=${query}`;
if (!isNewQuery && next) {
url += `&after=${next}`;
}
const { next } = getState().search;
let url = `search/?query=${query}`;
if (!isNewQuery && next) {
url += `&after=${next}`;
}
// Add filters
// The format is e.g. type_eq=mastodon or user_count_gt=1000
filters.forEach(filter => {
url += `&${filter.field}_${filter.relation}=${filter.value}`;
});
// Add filters
// The format is e.g. type_eq=mastodon or user_count_gt=1000
filters.forEach((filter) => {
url += `&${filter.field}_${filter.relation}=${filter.value}`;
});
dispatch(requestSearchResult(query, filters));
return getFromApi(url)
.then(result => dispatch(receiveSearchResults(result)))
.catch(() => dispatch(searchFailed()));
};
dispatch(requestSearchResult(query, filters));
return getFromApi(url)
.then((result) => dispatch(receiveSearchResults(result)))
.catch(() => dispatch(searchFailed()));
};
export const fetchGraph = () => {
return (dispatch: Dispatch) => {
dispatch(requestGraph());
return getFromApi("graph")
.then(graph => dispatch(receiveGraph(graph)))
.catch(() => dispatch(graphLoadFailed()));
};
export const fetchGraph = () => (dispatch: Dispatch) => {
dispatch(requestGraph());
return getFromApi("graph")
.then((graph) => dispatch(receiveGraph(graph)))
.catch(() => dispatch(graphLoadFailed()));
};
export const loadInstanceList = (page?: number, sort?: IInstanceSort) => {
return (dispatch: Dispatch, getState: () => IAppState) => {
sort = sort ? sort : getState().data.instanceListSort;
dispatch(requestInstanceList(sort));
const params: string[] = [];
if (!!page) {
params.push(`page=${page}`);
}
if (!!sort) {
params.push(`sortField=${sort.field}`);
params.push(`sortDirection=${sort.direction}`);
}
const path = !!params ? `instances?${params.join("&")}` : "instances";
return getFromApi(path)
.then(instancesListResponse => dispatch(receiveInstanceList(instancesListResponse)))
.catch(() => dispatch(instanceListLoadFailed()));
};
export const loadInstanceList = (page?: number, sort?: InstanceSort) => (
dispatch: Dispatch,
getState: () => AppState
) => {
sort = sort || getState().data.instanceListSort;
dispatch(requestInstanceList(sort));
const params: string[] = [];
if (page) {
params.push(`page=${page}`);
}
if (sort) {
params.push(`sortField=${sort.field}`);
params.push(`sortDirection=${sort.direction}`);
}
const path = params ? `instances?${params.join("&")}` : "instances";
return getFromApi(path)
.then((instancesListResponse) => dispatch(receiveInstanceList(instancesListResponse)))
.catch(() => dispatch(instanceListLoadFailed()));
};

View File

@ -3,34 +3,34 @@ import { isEqual } from "lodash";
import { combineReducers } from "redux";
import { History } from "history";
import { ActionType, IAction, ICurrentInstanceState, IDataState, ISearchState } from "./types";
import { ActionType, Action, CurrentInstanceState, DataState, SearchState } from "./types";
const initialDataState: IDataState = {
const initialDataState: DataState = {
graphLoadError: false,
instanceListLoadError: false,
instanceListSort: { field: "userCount", direction: "desc" },
isLoadingGraph: false,
isLoadingInstanceList: false
isLoadingInstanceList: false,
};
const data = (state: IDataState = initialDataState, action: IAction): IDataState => {
const data = (state: DataState = initialDataState, action: Action): DataState => {
switch (action.type) {
case ActionType.REQUEST_GRAPH:
return {
...state,
graphResponse: undefined,
isLoadingGraph: true
isLoadingGraph: true,
};
case ActionType.RECEIVE_GRAPH:
return {
...state,
graphResponse: action.payload,
isLoadingGraph: false
isLoadingGraph: false,
};
case ActionType.GRAPH_LOAD_ERROR:
return {
...state,
graphLoadError: true,
isLoadingGraph: false
isLoadingGraph: false,
};
case ActionType.REQUEST_INSTANCES:
return {
@ -38,71 +38,71 @@ const data = (state: IDataState = initialDataState, action: IAction): IDataState
instanceListLoadError: false,
instanceListSort: action.payload,
instancesResponse: undefined,
isLoadingInstanceList: true
isLoadingInstanceList: true,
};
case ActionType.RECEIVE_INSTANCES:
return {
...state,
instancesResponse: action.payload,
isLoadingInstanceList: false
isLoadingInstanceList: false,
};
case ActionType.INSTANCE_LIST_LOAD_ERROR:
return {
...state,
instanceListLoadError: true,
isLoadingInstanceList: false
isLoadingInstanceList: false,
};
default:
return state;
}
};
const initialCurrentInstanceState: ICurrentInstanceState = {
const initialCurrentInstanceState: CurrentInstanceState = {
currentInstanceDetails: null,
error: false,
isLoadingInstanceDetails: false
isLoadingInstanceDetails: false,
};
const currentInstance = (state = initialCurrentInstanceState, action: IAction): ICurrentInstanceState => {
const currentInstance = (state = initialCurrentInstanceState, action: Action): CurrentInstanceState => {
switch (action.type) {
case ActionType.REQUEST_INSTANCE_DETAILS:
return {
...state,
error: false,
isLoadingInstanceDetails: true
isLoadingInstanceDetails: true,
};
case ActionType.RECEIVE_INSTANCE_DETAILS:
return {
...state,
currentInstanceDetails: action.payload,
error: false,
isLoadingInstanceDetails: false
isLoadingInstanceDetails: false,
};
case ActionType.DESELECT_INSTANCE:
return {
...state,
currentInstanceDetails: null,
error: false
error: false,
};
case ActionType.INSTANCE_LOAD_ERROR:
return {
...state,
error: true,
isLoadingInstanceDetails: false
isLoadingInstanceDetails: false,
};
default:
return state;
}
};
const initialSearchState: ISearchState = {
const initialSearchState: SearchState = {
error: false,
filters: [],
isLoadingResults: false,
next: "",
query: "",
results: []
results: [],
};
const search = (state = initialSearchState, action: IAction): ISearchState => {
const search = (state = initialSearchState, action: Action): SearchState => {
switch (action.type) {
case ActionType.REQUEST_SEARCH_RESULTS:
const { query, filters } = action.payload;
@ -114,7 +114,7 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
isLoadingResults: true,
next: isNewQuery ? "" : state.next,
query,
results: isNewQuery ? [] : state.results
results: isNewQuery ? [] : state.results,
};
case ActionType.RECEIVE_SEARCH_RESULTS:
return {
@ -122,7 +122,7 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
error: false,
isLoadingResults: false,
next: action.payload.next,
results: state.results.concat(action.payload.results)
results: state.results.concat(action.payload.results),
};
case ActionType.SEARCH_RESULTS_ERROR:
return { ...initialSearchState, error: true };
@ -131,7 +131,7 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
case ActionType.SET_SEARCH_RESULT_HOVER:
return {
...state,
hoveringOverResult: action.payload
hoveringOverResult: action.payload,
};
default:
return state;
@ -141,8 +141,7 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
export default (history: History) =>
combineReducers({
router: connectRouter(history),
// tslint:disable-next-line:object-literal-sort-keys
currentInstance,
data,
search
search,
});

View File

@ -1,5 +1,5 @@
import { RouterState } from "connected-react-router";
import { ISearchFilter } from "../searchFilters";
import { SearchFilter } from "../searchFilters";
export enum ActionType {
// Instance details
@ -22,33 +22,33 @@ export enum ActionType {
SEARCH_RESULTS_ERROR = "SEARCH_RESULTS_ERROR",
RESET_SEARCH = "RESET_SEARCH",
// Search -- hovering over results
SET_SEARCH_RESULT_HOVER = "SET_SEARCH_RESULT_HOVER"
SET_SEARCH_RESULT_HOVER = "SET_SEARCH_RESULT_HOVER",
}
export interface IAction {
export interface Action {
type: ActionType;
payload: any;
}
export type SortField = "domain" | "userCount" | "statusCount" | "insularity";
export type SortDirection = "asc" | "desc";
export interface IInstanceSort {
export interface InstanceSort {
field: SortField;
direction: SortDirection;
}
export interface IPeer {
export interface Peer {
name: string;
}
export interface ISearchResultInstance {
export interface SearchResultInstance {
name: string;
description?: string;
userCount?: number;
type?: string;
}
export interface IFederationRestrictions {
export interface FederationRestrictions {
reportRemoval?: string[];
reject?: string[];
mediaRemoval?: string[];
@ -59,7 +59,7 @@ export interface IFederationRestrictions {
accept?: string[];
}
export interface IInstanceDetails {
export interface InstanceDetails {
name: string;
description?: string;
version?: string;
@ -67,8 +67,8 @@ export interface IInstanceDetails {
insularity?: number;
statusCount?: number;
domainCount?: number;
peers?: IPeer[];
federationRestrictions: IFederationRestrictions;
peers?: Peer[];
federationRestrictions: FederationRestrictions;
lastUpdated?: string;
status: string;
type?: string;
@ -76,7 +76,7 @@ export interface IInstanceDetails {
statusesPerUserPerDay?: number;
}
interface IGraphNode {
interface GraphNode {
data: {
id: string;
label: string;
@ -88,7 +88,7 @@ interface IGraphNode {
};
}
interface IGraphEdge {
interface GraphEdge {
data: {
source: string;
target: string;
@ -97,65 +97,65 @@ interface IGraphEdge {
};
}
interface IGraphMetadata {
interface GraphMetadata {
ranges: { [key: string]: [number, number] };
}
export interface IGraph {
nodes: IGraphNode[];
edges: IGraphEdge[];
export interface Graph {
nodes: GraphNode[];
edges: GraphEdge[];
}
export interface IGraphResponse {
graph: IGraph;
metadata: IGraphMetadata;
export interface GraphResponse {
graph: Graph;
metadata: GraphMetadata;
}
export interface ISearchResponse {
results: ISearchResultInstance[];
export interface SearchResponse {
results: SearchResultInstance[];
next: string | null;
}
export interface IInstanceListResponse {
export interface InstanceListResponse {
pageNumber: number;
totalPages: number;
totalEntries: number;
pageSize: number;
instances: IInstanceDetails[];
instances: InstanceDetails[];
}
// Redux state
// The current instance name is stored in the URL. See state -> router -> location
export interface ICurrentInstanceState {
currentInstanceDetails: IInstanceDetails | null;
export interface CurrentInstanceState {
currentInstanceDetails: InstanceDetails | null;
isLoadingInstanceDetails: boolean;
error: boolean;
}
export interface IDataState {
graphResponse?: IGraphResponse;
instancesResponse?: IInstanceListResponse;
instanceListSort: IInstanceSort;
export interface DataState {
graphResponse?: GraphResponse;
instancesResponse?: InstanceListResponse;
instanceListSort: InstanceSort;
isLoadingGraph: boolean;
isLoadingInstanceList: boolean;
graphLoadError: boolean;
instanceListLoadError: boolean;
}
export interface ISearchState {
export interface SearchState {
error: boolean;
isLoadingResults: boolean;
next: string;
query: string;
results: ISearchResultInstance[];
filters: ISearchFilter[];
results: SearchResultInstance[];
filters: SearchFilter[];
hoveringOverResult?: string;
}
export interface IAppState {
export interface AppState {
router: RouterState;
currentInstance: ICurrentInstanceState;
data: IDataState;
search: ISearchState;
currentInstance: CurrentInstanceState;
data: DataState;
search: SearchState;
}

View File

@ -1,8 +1,8 @@
type ISearchFilterRelation = "eq" | "gt" | "gte" | "lt" | "lte";
export interface ISearchFilter {
type SearchFilterRelation = "eq" | "gt" | "gte" | "lt" | "lte";
export interface SearchFilter {
// The ES field to filter on
field: string;
relation: ISearchFilterRelation;
relation: SearchFilterRelation;
// The value we want to filter to
value: string;
// Human-meaningful text that we're showing in the UI
@ -10,17 +10,19 @@ export interface ISearchFilter {
}
// Maps to translate this to user-friendly text
type SearchFilterField = "type" | "user_count";
const searchFilterFieldTranslations = {
type: "Instance type",
user_count: "User count"
// eslint-disable-next-line @typescript-eslint/camelcase
user_count: "User count",
};
const searchFilterRelationTranslations = {
eq: "=",
gt: ">",
gte: ">=",
lt: "<",
lte: "<="
lte: "<=",
};
export const getSearchFilterDisplayValue = (field: string, relation: ISearchFilterRelation, value: string) =>
export const getSearchFilterDisplayValue = (field: SearchFilterField, relation: SearchFilterRelation, value: string) =>
`${searchFilterFieldTranslations[field]} ${searchFilterRelationTranslations[relation]} ${value}`;

View File

@ -2,5 +2,5 @@ import { Position, Toaster } from "@blueprintjs/core";
export default Toaster.create({
className: "app-toaster",
position: Position.TOP
position: Position.TOP,
});

View File

@ -1,6 +1,6 @@
import { INSTANCE_TYPES } from "./constants";
interface IColorSchemeBase {
interface ColorSchemeBase {
// The name of the coloring, e.g. "Instance type"
name: string;
// The name of the key in a cytoscape node's `data` field to color by.
@ -9,30 +9,30 @@ interface IColorSchemeBase {
description?: string;
type: "qualitative" | "quantitative";
}
interface IQualitativeColorScheme extends IColorSchemeBase {
interface QualitativeColorScheme extends ColorSchemeBase {
// The values the color scheme is used for. E.g. ["mastodon", "pleroma", "misskey"].
values: string[];
type: "qualitative";
}
interface IQuantitativeColorScheme extends IColorSchemeBase {
interface QuantitativeColorScheme extends ColorSchemeBase {
type: "quantitative";
exponential: boolean;
}
export type IColorScheme = IQualitativeColorScheme | IQuantitativeColorScheme;
export type ColorScheme = QualitativeColorScheme | QuantitativeColorScheme;
export const typeColorScheme: IQualitativeColorScheme = {
export const typeColorScheme: QualitativeColorScheme = {
cytoscapeDataKey: "type",
name: "Instance type",
type: "qualitative",
values: INSTANCE_TYPES
values: INSTANCE_TYPES,
};
export const activityColorScheme: IQuantitativeColorScheme = {
export const activityColorScheme: QuantitativeColorScheme = {
cytoscapeDataKey: "statusesPerDay",
description: "The average number of statuses posted per day. This is an exponential scale.",
exponential: true,
name: "Activity",
type: "quantitative"
type: "quantitative",
};
export const colorSchemes: IColorScheme[] = [typeColorScheme, activityColorScheme];
export const colorSchemes: ColorScheme[] = [typeColorScheme, activityColorScheme];

1
frontend/src/typings/globals.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "*.png";

View File

@ -1,41 +1,39 @@
import { createMatchSelector } from "connected-react-router";
import fetch from "cross-fetch";
import { range } from "lodash";
import { DESKTOP_WIDTH_THRESHOLD, IInstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants";
import { IAppState } from "./redux/types";
import { DESKTOP_WIDTH_THRESHOLD, InstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants";
import { AppState } from "./redux/types";
let API_ROOT = "http://localhost:4000/api/";
if (["true", true, 1, "1"].indexOf(process.env.REACT_APP_STAGING || "") > -1) {
if (["true", true, 1, "1"].includes(process.env.REACT_APP_STAGING || "")) {
API_ROOT = "https://phoenix.api-develop.fediverse.space/api/";
} else if (process.env.NODE_ENV === "production") {
API_ROOT = "https://phoenix.api.fediverse.space/api/";
}
export const getFromApi = (path: string, token?: string): Promise<any> => {
const domain = API_ROOT.endsWith("/") ? API_ROOT : API_ROOT + "/";
const domain = API_ROOT.endsWith("/") ? API_ROOT : `${API_ROOT}/`;
const headers = token ? { token } : undefined;
return fetch(encodeURI(domain + path), {
headers
}).then(response => response.json());
headers,
}).then((response) => response.json());
};
export const postToApi = (path: string, body: any, token?: string): Promise<any> => {
const domain = API_ROOT.endsWith("/") ? API_ROOT : API_ROOT + "/";
const domain = API_ROOT.endsWith("/") ? API_ROOT : `${API_ROOT}/`;
const defaultHeaders = {
Accept: "application/json",
"Content-Type": "application/json"
"Content-Type": "application/json",
};
const headers = token ? { ...defaultHeaders, token } : defaultHeaders;
return fetch(encodeURI(domain + path), {
body: JSON.stringify(body),
headers,
method: "POST"
}).then(response => {
return response.json();
});
method: "POST",
}).then((response) => response.json());
};
export const domainMatchSelector = createMatchSelector<IAppState, IInstanceDomainPath>(INSTANCE_DOMAIN_PATH);
export const domainMatchSelector = createMatchSelector<AppState, InstanceDomainPath>(INSTANCE_DOMAIN_PATH);
export const isSmallScreen = window.innerWidth < DESKTOP_WIDTH_THRESHOLD;
@ -49,28 +47,25 @@ export const unsetAuthToken = () => {
sessionStorage.removeItem("adminToken");
};
export const getAuthToken = () => {
return sessionStorage.getItem("adminToken");
};
export const getAuthToken = () => sessionStorage.getItem("adminToken");
export const getBuckets = (min: number, max: number, steps: number, exponential: boolean) => {
if (exponential) {
const logSpace = range(steps).map(i => Math.E ** i);
const logSpace = range(steps).map((i) => Math.E ** i);
// Scale the log space to the linear range
const logRange = logSpace[logSpace.length - 1] - logSpace[0];
const linearRange = max - min;
const scalingFactor = linearRange / logRange;
const translation = min - logSpace[0];
return logSpace.map(i => (i + translation) * scalingFactor);
} else {
// Linear
const bucketSize = (max - min) / steps;
return range(min, max, bucketSize);
return logSpace.map((i) => (i + translation) * scalingFactor);
}
// Linear
const bucketSize = (max - min) / steps;
return range(min, max, bucketSize);
};
const typeToDisplay = {
gnusocial: "GNU Social"
const typeToDisplay: { [field: string]: string } = {
gnusocial: "GNU Social",
};
export const getTypeDisplayString = (key: string) => {
if (key in typeToDisplay) {

View File

@ -1,40 +1,32 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"jsx": "preserve",
"lib": [
"es2015",
"dom"
],
"module": "esnext",
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"outDir": "build",
"rootDir": "src",
"sourceMap": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"sourceMap": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"jsx": "react",
"typeRoots": [
"./node_modules/@types",
"./src/typings"
],
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"exclude": [
"node_modules",
"build"
],
"include": [
"src"
]

View File

@ -1,11 +0,0 @@
{
"extends": [
"tslint:recommended",
"tslint-eslint-rules",
"tslint-react",
"@blueprintjs/tslint-config/blueprint-rules",
"tslint-config-prettier",
"tslint-config-security"
],
"exclude": ["**/*.css"]
}

File diff suppressed because it is too large Load Diff