diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..193d895 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "elixirLS.projectDir": "backend/" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd16aa..b759a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,38 +1,61 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + ### Added + - It's now shown in the front-end if an instance wasn't crawled because of its robots.txt. - You can now link directly to instances at e.g. /instance/mastodon.social. - Instance details now have a link to the corresponding fediverse.network page. +- The reset-graph-view button now explains what it's for when you hover over it. + ### Changed + - You no longer have to zoom completely in to see labels. - Label size is now dependent on the instance size. +- The instance lookup field is now front-and-center. Is also uses the backend for faster lookups. This is to improve + performance, and it lays the groundwork for full-text search over instance names and descriptions. + ### Deprecated + ### Removed + ### Fixed + - Previously, direct links to /about would return a 404 on Netlify's infrastructure. Now it's fixed. + ### Security ## [2.0.0] - 2019-07-20 + ### Added + - The backend has been completely rewritten in Elixir for improved stability and performance. - An "insularity score" was added to show the percentage of mentions to users on the same instance. - The crawler now respects robots.txt. + ### Changed + - Migrated the frontend graph from Sigma.js to Cytoscape.js. - To improve performance, instances with no neighbors are no longer shown on the graph. + ### Deprecated + - The /api/v1 endpoint no longer exists; now there's a new /api. + ### Security + - Spam domains can be blacklisted in the backend crawler's config. - Add basic automated security scanning (using [Sobelow](https://github.com/andmarti1424/sc-im.git) and Gitlab's dependency scanning). ## [1.0.0] - 2018-09-01 + ### Added + - Initial release. The date above is inaccurate; this first version was released sometime in the fall of 2018. - This release had a Django backend and a [Sigma.js](http://sigmajs.org/) graph. diff --git a/backend/lib/backend/api.ex b/backend/lib/backend/api.ex index 860c1a5..c4cff71 100644 --- a/backend/lib/backend/api.ex +++ b/backend/lib/backend/api.ex @@ -52,4 +52,16 @@ defmodule Backend.Api do ) |> Repo.all() end + + def search_instances(query, cursor_after \\ nil) do + ilike_query = "%#{query}%" + + %{entries: instances, metadata: metadata} = + Instance + |> where([i], ilike(i.domain, ^ilike_query)) + |> order_by(asc: :id) + |> Repo.paginate(after: cursor_after, cursor_fields: [:id], limit: 50) + + %{instances: instances, next: metadata.after} + end end diff --git a/backend/lib/backend/repo.ex b/backend/lib/backend/repo.ex index ec5228f..acf5779 100644 --- a/backend/lib/backend/repo.ex +++ b/backend/lib/backend/repo.ex @@ -3,6 +3,8 @@ defmodule Backend.Repo do otp_app: :backend, adapter: Ecto.Adapters.Postgres + use Paginator + def init(_type, config) do {:ok, Keyword.put(config, :url, System.get_env("DATABASE_URL"))} end diff --git a/backend/lib/backend_web/controllers/search_controller.ex b/backend/lib/backend_web/controllers/search_controller.ex new file mode 100644 index 0000000..e9bbe96 --- /dev/null +++ b/backend/lib/backend_web/controllers/search_controller.ex @@ -0,0 +1,13 @@ +defmodule BackendWeb.SearchController do + use BackendWeb, :controller + alias Backend.Api + + action_fallback(BackendWeb.FallbackController) + + def index(conn, params) do + query = Map.get(params, "query") + cursor_after = Map.get(params, "after", nil) + %{instances: instances, next: next} = Api.search_instances(query, cursor_after) + render(conn, "index.json", instances: instances, next: next) + end +end diff --git a/backend/lib/backend_web/router.ex b/backend/lib/backend_web/router.ex index 93b0668..282cef7 100644 --- a/backend/lib/backend_web/router.ex +++ b/backend/lib/backend_web/router.ex @@ -2,13 +2,14 @@ defmodule BackendWeb.Router do use BackendWeb, :router pipeline :api do - plug :accepts, ["json"] + plug(:accepts, ["json"]) end scope "/api", BackendWeb do - pipe_through :api + pipe_through(:api) - resources "/instances", InstanceController, only: [:index, :show] - resources "/graph", GraphController, only: [:index] + resources("/instances", InstanceController, only: [:index, :show]) + resources("/graph", GraphController, only: [:index]) + resources("/search", SearchController, only: [:index]) end end diff --git a/backend/lib/backend_web/views/search_view.ex b/backend/lib/backend_web/views/search_view.ex new file mode 100644 index 0000000..a44ab5a --- /dev/null +++ b/backend/lib/backend_web/views/search_view.ex @@ -0,0 +1,20 @@ +defmodule BackendWeb.SearchView do + use BackendWeb, :view + alias BackendWeb.SearchView + require Logger + + def render("index.json", %{instances: instances, next: next}) do + %{ + results: render_many(instances, SearchView, "instance.json", as: :instance), + next: next + } + end + + def render("instance.json", %{instance: instance}) do + %{ + name: instance.domain, + description: instance.description, + userCount: instance.user_count + } + end +end diff --git a/backend/mix.exs b/backend/mix.exs index 2298eca..9f4b58a 100644 --- a/backend/mix.exs +++ b/backend/mix.exs @@ -47,7 +47,8 @@ defmodule Backend.MixProject do {:quantum, "~> 2.3"}, {:corsica, "~> 1.1.2"}, {:sobelow, "~> 0.8", only: :dev}, - {:gollum, "~> 0.3.2"} + {:gollum, "~> 0.3.2"}, + {:paginator, "~> 0.6.0"} ] end diff --git a/backend/mix.lock b/backend/mix.lock index 4c81fc3..76eccf1 100644 --- a/backend/mix.lock +++ b/backend/mix.lock @@ -25,6 +25,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, + "paginator": {:hex, :paginator, "0.6.0", "bc2c01abdd98281ff39b6a7439cf540091122a7927bdaabc167c61d4508f9cbb", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.4.9", "746d098e10741c334d88143d3c94cab1756435f94387a63441792e66ec0ee974", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/frontend/package.json b/frontend/package.json index 3b2b09b..40c2e94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,12 +36,13 @@ "cross-fetch": "^3.0.4", "cytoscape": "^3.8.1", "cytoscape-popper": "^1.0.4", - "lodash": "^4.17.14", + "inflection": "^1.12.0", + "lodash": "^4.17.15", "moment": "^2.22.2", "normalize.css": "^8.0.0", "numeral": "^2.0.6", - "react": "^16.4.2", - "react-dom": "^16.4.2", + "react": "^16.8.0", + "react-dom": "^16.8.0", "react-redux": "^7.1.0", "react-router-dom": "^5.0.1", "react-scripts": "^3.0.1", @@ -56,19 +57,20 @@ "devDependencies": { "@blueprintjs/tslint-config": "^1.8.1", "@types/classnames": "^2.2.9", - "@types/cytoscape": "^3.4.3", + "@types/cytoscape": "^3.8.0", + "@types/inflection": "^1.5.28", "@types/jest": "^24.0.15", "@types/lodash": "^4.14.136", - "@types/node": "^12.6.2", + "@types/node": "^12.6.8", "@types/numeral": "^0.0.25", "@types/react": "^16.8.23", "@types/react-dom": "^16.8.4", "@types/react-redux": "^7.1.1", "@types/react-router-dom": "^4.3.4", - "@types/react-virtualized": "^9.21.2", + "@types/react-virtualized": "^9.21.3", "@types/sanitize-html": "^1.20.1", "@types/styled-components": "4.1.18", - "husky": "^3.0.0", + "husky": "^3.0.1", "lint-staged": "^9.2.0", "react-axe": "^3.2.0", "tslint": "^5.18.0", diff --git a/frontend/src/AppRouter.tsx b/frontend/src/AppRouter.tsx index a709401..0b642e2 100644 --- a/frontend/src/AppRouter.tsx +++ b/frontend/src/AppRouter.tsx @@ -4,10 +4,10 @@ import { Button, Classes, Dialog } from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; import { ConnectedRouter } from "connected-react-router"; -import { Route, RouteComponentProps } from "react-router-dom"; +import { Route } from "react-router-dom"; import { Nav } from "./components/organisms/"; import { AboutScreen, GraphScreen } from "./components/screens/"; -import { DESKTOP_WIDTH_THRESHOLD, IInstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants"; +import { DESKTOP_WIDTH_THRESHOLD } from "./constants"; import { history } from "./index"; interface IAppLocalState { @@ -24,13 +24,9 @@ export class AppRouter extends React.Component<{}, IAppLocalState> {
diff --git a/frontend/src/components/molecules/Cytoscape.tsx b/frontend/src/components/molecules/Cytoscape.tsx index 046765e..c518cfa 100644 --- a/frontend/src/components/molecules/Cytoscape.tsx +++ b/frontend/src/components/molecules/Cytoscape.tsx @@ -154,7 +154,7 @@ class Cytoscape extends React.Component { public resetGraphPosition() { if (!this.cy) { - return; + throw new Error("Expected cytoscape, but there wasn't one!"); } const { currentNodeId } = this.props; if (currentNodeId) { @@ -175,7 +175,7 @@ class Cytoscape extends React.Component { */ private setNodeSelection = (prevNodeId?: string | null) => { if (!this.cy) { - return; + throw new Error("Expected cytoscape, but there wasn't one!"); } if (prevNodeId) { this.cy.$id(prevNodeId).unselect(); diff --git a/frontend/src/components/molecules/FloatingResetButton.tsx b/frontend/src/components/molecules/FloatingResetButton.tsx index a8b8cf5..428a9ef 100644 --- a/frontend/src/components/molecules/FloatingResetButton.tsx +++ b/frontend/src/components/molecules/FloatingResetButton.tsx @@ -7,7 +7,7 @@ interface IFloatingResetButtonProps { } const FloatingResetButton: React.FC = ({ onClick }) => ( - + )} + + + ); + } + + private handleInputChange = (event: React.ChangeEvent) => { + this.setState({ currentQuery: event.currentTarget.value }); + }; + + private handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + this.search(); + } + }; + + private search = () => { + this.props.handleSearch(this.state.currentQuery); + }; + + private selectInstanceFactory = (domain: string) => () => { + this.props.navigateToInstance(domain); + }; + + private renderSearchBar = () => ( + + + + + ); +} + +const mapStateToProps = (state: IAppState) => ({ + error: state.search.error, + hasMoreResults: !!state.search.next, + isLoadingResults: state.search.isLoadingResults, + query: state.search.query, + results: state.search.results +}); +const mapDispatchToProps = (dispatch: Dispatch) => ({ + handleSearch: (query: string) => dispatch(updateSearch(query) as any), + navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(SearchScreen); diff --git a/frontend/src/components/screens/index.ts b/frontend/src/components/screens/index.ts index 622eed8..3b0bb21 100644 --- a/frontend/src/components/screens/index.ts +++ b/frontend/src/components/screens/index.ts @@ -1,2 +1,4 @@ export { default as AboutScreen } from "./AboutScreen"; export { default as GraphScreen } from "./GraphScreen"; +export { default as SearchScreen } from "./SearchScreen"; +export { default as InstanceScreen } from "./InstanceScreen"; diff --git a/frontend/src/components/styled-components.ts b/frontend/src/components/styled-components.ts deleted file mode 100644 index 38a0738..0000000 --- a/frontend/src/components/styled-components.ts +++ /dev/null @@ -1,6 +0,0 @@ -import styled from "styled-components"; - -export const FullDiv = styled.div` - width: 100%; - height: 100%; -`; diff --git a/frontend/src/redux/actions.ts b/frontend/src/redux/actions.ts index 519fdc2..7dd34be 100644 --- a/frontend/src/redux/actions.ts +++ b/frontend/src/redux/actions.ts @@ -2,76 +2,77 @@ import { Dispatch } from "redux"; import { push } from "connected-react-router"; import { getFromApi } from "../util"; -import { ActionType, IAppState, IGraph, IInstance, IInstanceDetails } from "./types"; +import { ActionType, IAppState, IGraph, IInstanceDetails, ISearchResponse } from "./types"; -// requestInstanceDetails and deselectInstance are not exported since we only call them from loadInstance() +// 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 }; }; -export const requestInstances = () => { - return { - type: ActionType.REQUEST_INSTANCES - }; -}; - -export const receiveInstances = (instances: IInstance[]) => { - return { - payload: instances, - type: ActionType.RECEIVE_INSTANCES - }; -}; -export const requestGraph = () => { +// Graph +const requestGraph = () => { return { type: ActionType.REQUEST_GRAPH }; }; - -export const receiveGraph = (graph: IGraph) => { +const receiveGraph = (graph: IGraph) => { return { payload: graph, type: ActionType.RECEIVE_GRAPH }; }; - const graphLoadFailed = () => { return { type: ActionType.GRAPH_LOAD_ERROR }; }; -const instanceLoadFailed = () => { +// Search +const requestSearchResult = (query: string) => { return { - type: ActionType.INSTANCE_LOAD_ERROR + payload: query, + 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 }; }; -export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => { +const resetSearch = () => { return { - payload: instanceDetails, - type: ActionType.RECEIVE_INSTANCE_DETAILS + type: ActionType.RESET_SEARCH }; }; /** Async actions: https://redux.js.org/advanced/asyncactions */ -export const fetchInstances = () => { - return (dispatch: Dispatch) => { - dispatch(requestInstances()); - return getFromApi("instances") - .then(instances => dispatch(receiveInstances(instances))) - .catch(e => dispatch(graphLoadFailed())); - }; -}; - export const loadInstance = (instanceName: string | null) => { return (dispatch: Dispatch, getState: () => IAppState) => { if (!instanceName) { @@ -84,7 +85,26 @@ export const loadInstance = (instanceName: string | null) => { dispatch(requestInstanceDetails(instanceName)); return getFromApi("instances/" + instanceName) .then(details => dispatch(receiveInstanceDetails(details))) - .catch(e => dispatch(instanceLoadFailed())); + .catch(() => dispatch(instanceLoadFailed())); + }; +}; + +export const updateSearch = (query: string) => { + return (dispatch: Dispatch, getState: () => IAppState) => { + if (!query) { + dispatch(resetSearch()); + return; + } + + const next = getState().search.next; + let url = `search/?query=${query}`; + if (next) { + url += `&after=${next}`; + } + dispatch(requestSearchResult(query)); + return getFromApi(url) + .then(result => dispatch(receiveSearchResults(result))) + .catch(() => dispatch(searchFailed())); }; }; @@ -93,6 +113,6 @@ export const fetchGraph = () => { dispatch(requestGraph()); return getFromApi("graph") .then(graph => dispatch(receiveGraph(graph))) - .catch(e => dispatch(graphLoadFailed())); + .catch(() => dispatch(graphLoadFailed())); }; }; diff --git a/frontend/src/redux/reducers.ts b/frontend/src/redux/reducers.ts index 80a0541..91f121d 100644 --- a/frontend/src/redux/reducers.ts +++ b/frontend/src/redux/reducers.ts @@ -2,27 +2,14 @@ import { connectRouter } from "connected-react-router"; import { combineReducers } from "redux"; import { History } from "history"; -import { ActionType, IAction, ICurrentInstanceState, IDataState } from "./types"; +import { ActionType, IAction, ICurrentInstanceState, IDataState, ISearchState } from "./types"; const initialDataState = { error: false, - isLoadingGraph: false, - isLoadingInstances: false + isLoadingGraph: false }; const data = (state: IDataState = initialDataState, action: IAction) => { switch (action.type) { - case ActionType.REQUEST_INSTANCES: - return { - ...state, - instances: [], - isLoadingInstances: true - }; - case ActionType.RECEIVE_INSTANCES: - return { - ...state, - instances: action.payload, - isLoadingInstances: false - }; case ActionType.REQUEST_GRAPH: return { ...state, @@ -38,8 +25,7 @@ const data = (state: IDataState = initialDataState, action: IAction) => { return { ...state, error: true, - isLoadingGraph: false, - isLoadingInstances: false + isLoadingGraph: false }; default: return state; @@ -83,10 +69,54 @@ const currentInstance = (state = initialCurrentInstanceState, action: IAction): } }; +const initialSearchState: ISearchState = { + error: false, + isLoadingResults: false, + next: "", + query: "", + results: [] +}; +const search = (state = initialSearchState, action: IAction): ISearchState => { + switch (action.type) { + case ActionType.REQUEST_SEARCH_RESULTS: + const query = action.payload; + const isNewQuery = state.query !== query; + return { + ...state, + error: false, + isLoadingResults: true, + query, + results: isNewQuery ? [] : state.results + }; + case ActionType.RECEIVE_SEARCH_RESULTS: + return { + ...state, + error: false, + isLoadingResults: false, + next: action.payload.next, + results: state.results.concat(action.payload.results) + }; + case ActionType.SEARCH_RESULTS_ERROR: + return { + ...state, + error: true, + isLoadingResults: false, + next: "", + query: "", + results: [] + }; + case ActionType.RESET_SEARCH: + return initialSearchState; + default: + return state; + } +}; + export default (history: History) => combineReducers({ router: connectRouter(history), // tslint:disable-next-line:object-literal-sort-keys currentInstance, - data + data, + search }); diff --git a/frontend/src/redux/types.ts b/frontend/src/redux/types.ts index de435de..e5f177f 100644 --- a/frontend/src/redux/types.ts +++ b/frontend/src/redux/types.ts @@ -1,15 +1,23 @@ import { RouterState } from "connected-react-router"; export enum ActionType { + // Instance details REQUEST_INSTANCE_DETAILS = "REQUEST_INSTANCE_DETAILS", - REQUEST_INSTANCES = "REQUEST_INSTANCES", - RECEIVE_INSTANCES = "RECEIVE_INSTANCES", + RECEIVE_INSTANCE_DETAILS = "RECEIVE_INSTANCE_DETAILS", + INSTANCE_LOAD_ERROR = "INSTANCE_LOAD_ERROR", + // Graph REQUEST_GRAPH = "REQUEST_GRAPH", RECEIVE_GRAPH = "RECEIVE_GRAPH", - RECEIVE_INSTANCE_DETAILS = "RECEIVE_INSTANCE_DETAILS", - DESELECT_INSTANCE = "DESELECT_INSTANCE", GRAPH_LOAD_ERROR = "GRAPH_LOAD_ERROR", - INSTANCE_LOAD_ERROR = "INSTANCE_LOAD_ERROR" + // Nav + DESELECT_INSTANCE = "DESELECT_INSTANCE", + // Search + REQUEST_SEARCH_RESULTS = "REQUEST_SEARCH_RESULTS", + RECEIVE_SEARCH_RESULTS = "RECEIVE_SEARCH_RESULTS", + SEARCH_RESULTS_ERROR = "SEARCH_RESULTS_ERROR", + RESET_SEARCH = "RESET_SEARCH" + // REQUEST_INSTANCES = "REQUEST_INSTANCES", + // RECEIVE_INSTANCES = "RECEIVE_INSTANCES", } export interface IAction { @@ -21,6 +29,12 @@ export interface IInstance { name: string; } +export interface ISearchResultInstance { + name: string; + description?: string; + userCount?: number; +} + export interface IInstanceDetails { name: string; description?: string; @@ -60,6 +74,11 @@ export interface IGraph { edges: IGraphEdge[]; } +export interface ISearchResponse { + results: ISearchResultInstance[]; + next: string | null; +} + // Redux state // The current instance name is stored in the URL. See state -> router -> location @@ -70,15 +89,22 @@ export interface ICurrentInstanceState { } export interface IDataState { - instances?: IInstance[]; graph?: IGraph; - isLoadingInstances: boolean; isLoadingGraph: boolean; error: boolean; } +export interface ISearchState { + error: boolean; + isLoadingResults: boolean; + next: string; + query: string; + results: ISearchResultInstance[]; +} + export interface IAppState { router: RouterState; currentInstance: ICurrentInstanceState; data: IDataState; + search: ISearchState; } diff --git a/frontend/src/util.ts b/frontend/src/util.ts index 60d7613..bedf53f 100644 --- a/frontend/src/util.ts +++ b/frontend/src/util.ts @@ -12,8 +12,7 @@ if (["true", true, 1, "1"].indexOf(process.env.REACT_APP_STAGING || "") > -1) { export const getFromApi = (path: string): Promise => { const domain = API_ROOT.endsWith("/") ? API_ROOT : API_ROOT + "/"; - path = path.endsWith("/") ? path : path + "/"; - return fetch(domain + path).then(response => response.json()); + return fetch(encodeURI(domain + path)).then(response => response.json()); }; export const domainMatchSelector = createMatchSelector(INSTANCE_DOMAIN_PATH); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 3824597..f5db90f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1318,10 +1318,10 @@ resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b" integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A== -"@types/cytoscape@^3.4.3": - version "3.4.3" - resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.4.3.tgz#8b9353154dc895231cd344ed1c7eff2d1391c103" - integrity sha512-uADb/vBj/xTeNNRvtYlzPz1rftMR4Jf6ipq4jqKfYibMZ173sAbdFM3Fl2fPbGfP28CWJpqhcpHp4+NUq3Ma4g== +"@types/cytoscape@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.8.0.tgz#334006612fc6285dac83ee3665132743d7651f58" + integrity sha512-8TJL7HuMEgjQRCcUC3xKenb7Y6Ra3ZJ3LvYDlpxlt5LlAatzRyTBtIuE1JOdRelqAla8r87XJUzTgi92mlUlQQ== "@types/dom4@^2.0.1": version "2.0.1" @@ -1376,6 +1376,11 @@ "@types/domutils" "*" "@types/node" "*" +"@types/inflection@^1.5.28": + version "1.5.28" + resolved "https://registry.yarnpkg.com/@types/inflection/-/inflection-1.5.28.tgz#43d55e0d72cf333a2dffd9c4ec0407455a1b0931" + integrity sha1-Q9VeDXLPMzot/9nE7AQHRVobCTE= + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1423,10 +1428,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.4.tgz#f83ec3c3e05b174b7241fadeb6688267fe5b22ca" integrity sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ== -"@types/node@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.2.tgz#a5ccec6abb6060d5f20d256fb03ed743e9774999" - integrity sha512-gojym4tX0FWeV2gsW4Xmzo5wxGjXGm550oVUII7f7G5o4BV6c7DBdiG1RRQd+y1bvqRyYtPfMK85UM95vsapqQ== +"@types/node@^12.6.8": + version "12.6.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.8.tgz#e469b4bf9d1c9832aee4907ba8a051494357c12c" + integrity sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1490,10 +1495,10 @@ "@types/history" "*" "@types/react" "*" -"@types/react-virtualized@^9.21.2": - version "9.21.2" - resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.2.tgz#c5e4293409593814c35466913e83fb856e2053d0" - integrity sha512-Q6geJaDd8FlBw3ilD4ODferTyVtYAmDE3d7+GacfwN0jPt9rD9XkeuPjcHmyIwTrMXuLv1VIJmRxU9WQoQFBJw== +"@types/react-virtualized@^9.21.3": + version "9.21.3" + resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.3.tgz#79a44b870a4848cbc7cc04ff4bc06e5a10955262" + integrity sha512-QhXeiVwXrshVAoq2Cy3SGZEDiFdeFfup2ciQya5RTgr5uycQ2alIKzLfy4X38UCrxonwxe8byk5q8fYV0U87Zg== dependencies: "@types/prop-types" "*" "@types/react" "*" @@ -5173,11 +5178,12 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -husky@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.0.tgz#de63821a7049dc412b1afd753c259e2f6e227562" - integrity sha512-lKMEn7bRK+7f5eWPNGclDVciYNQt0GIkAQmhKl+uHP1qFzoN0h92kmH9HZ8PCwyVA2EQPD8KHf0FYWqnTxau+Q== +husky@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.1.tgz#06152c28e129622b05fa09c494209de8cf2dfb59" + integrity sha512-PXBv+iGKw23GHUlgELRlVX9932feFL407/wHFwtsGeArp0dDM4u+/QusSQwPKxmNgjpSL+ustbOdQ2jetgAZbA== dependencies: + chalk "^2.4.2" cosmiconfig "^5.2.1" execa "^1.0.0" get-stdin "^7.0.0" @@ -5317,6 +5323,11 @@ indexof@0.0.1: resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= +inflection@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -6676,11 +6687,16 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== -lodash@^4.17.12, lodash@^4.17.14: +lodash@^4.17.12: version "4.17.14" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== +lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" @@ -8889,7 +8905,7 @@ react-dev-utils@^9.0.1: strip-ansi "5.2.0" text-table "0.2.0" -react-dom@^16.4.2: +react-dom@^16.8.0: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== @@ -9055,7 +9071,7 @@ react-virtualized@^9.21.1: prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" -react@^16.4.2: +react@^16.8.0: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==