Refactor graph sidebar, move to backend search
This commit is contained in:
parent
3ebab9d452
commit
6673a24466
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"elixirLS.projectDir": "backend/"
|
||||||
|
}
|
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -1,38 +1,61 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- It's now shown in the front-end if an instance wasn't crawled because of its robots.txt.
|
- 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.
|
- 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.
|
- 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
|
### Changed
|
||||||
|
|
||||||
- You no longer have to zoom completely in to see labels.
|
- You no longer have to zoom completely in to see labels.
|
||||||
- Label size is now dependent on the instance size.
|
- 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
|
### Deprecated
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Previously, direct links to /about would return a 404 on Netlify's infrastructure. Now it's fixed.
|
- Previously, direct links to /about would return a 404 on Netlify's infrastructure. Now it's fixed.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
## [2.0.0] - 2019-07-20
|
## [2.0.0] - 2019-07-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- The backend has been completely rewritten in Elixir for improved stability and performance.
|
- 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.
|
- An "insularity score" was added to show the percentage of mentions to users on the same instance.
|
||||||
- The crawler now respects robots.txt.
|
- The crawler now respects robots.txt.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Migrated the frontend graph from Sigma.js to Cytoscape.js.
|
- Migrated the frontend graph from Sigma.js to Cytoscape.js.
|
||||||
- To improve performance, instances with no neighbors are no longer shown on the graph.
|
- To improve performance, instances with no neighbors are no longer shown on the graph.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
- The /api/v1 endpoint no longer exists; now there's a new /api.
|
- The /api/v1 endpoint no longer exists; now there's a new /api.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Spam domains can be blacklisted in the backend crawler's config.
|
- 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).
|
- 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
|
## [1.0.0] - 2018-09-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Initial release. The date above is inaccurate; this first version was released sometime in the fall of 2018.
|
- 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.
|
- This release had a Django backend and a [Sigma.js](http://sigmajs.org/) graph.
|
||||||
|
|
|
@ -52,4 +52,16 @@ defmodule Backend.Api do
|
||||||
)
|
)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@ defmodule Backend.Repo do
|
||||||
otp_app: :backend,
|
otp_app: :backend,
|
||||||
adapter: Ecto.Adapters.Postgres
|
adapter: Ecto.Adapters.Postgres
|
||||||
|
|
||||||
|
use Paginator
|
||||||
|
|
||||||
def init(_type, config) do
|
def init(_type, config) do
|
||||||
{:ok, Keyword.put(config, :url, System.get_env("DATABASE_URL"))}
|
{:ok, Keyword.put(config, :url, System.get_env("DATABASE_URL"))}
|
||||||
end
|
end
|
||||||
|
|
13
backend/lib/backend_web/controllers/search_controller.ex
Normal file
13
backend/lib/backend_web/controllers/search_controller.ex
Normal file
|
@ -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
|
|
@ -2,13 +2,14 @@ defmodule BackendWeb.Router do
|
||||||
use BackendWeb, :router
|
use BackendWeb, :router
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :api do
|
||||||
plug :accepts, ["json"]
|
plug(:accepts, ["json"])
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api", BackendWeb do
|
scope "/api", BackendWeb do
|
||||||
pipe_through :api
|
pipe_through(:api)
|
||||||
|
|
||||||
resources "/instances", InstanceController, only: [:index, :show]
|
resources("/instances", InstanceController, only: [:index, :show])
|
||||||
resources "/graph", GraphController, only: [:index]
|
resources("/graph", GraphController, only: [:index])
|
||||||
|
resources("/search", SearchController, only: [:index])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
20
backend/lib/backend_web/views/search_view.ex
Normal file
20
backend/lib/backend_web/views/search_view.ex
Normal file
|
@ -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
|
|
@ -47,7 +47,8 @@ defmodule Backend.MixProject do
|
||||||
{:quantum, "~> 2.3"},
|
{:quantum, "~> 2.3"},
|
||||||
{:corsica, "~> 1.1.2"},
|
{:corsica, "~> 1.1.2"},
|
||||||
{:sobelow, "~> 0.8", only: :dev},
|
{:sobelow, "~> 0.8", only: :dev},
|
||||||
{:gollum, "~> 0.3.2"}
|
{:gollum, "~> 0.3.2"},
|
||||||
|
{:paginator, "~> 0.6.0"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
|
||||||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
|
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
|
||||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "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"},
|
"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": {: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"},
|
"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"},
|
||||||
|
|
|
@ -36,12 +36,13 @@
|
||||||
"cross-fetch": "^3.0.4",
|
"cross-fetch": "^3.0.4",
|
||||||
"cytoscape": "^3.8.1",
|
"cytoscape": "^3.8.1",
|
||||||
"cytoscape-popper": "^1.0.4",
|
"cytoscape-popper": "^1.0.4",
|
||||||
"lodash": "^4.17.14",
|
"inflection": "^1.12.0",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"normalize.css": "^8.0.0",
|
"normalize.css": "^8.0.0",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"react": "^16.4.2",
|
"react": "^16.8.0",
|
||||||
"react-dom": "^16.4.2",
|
"react-dom": "^16.8.0",
|
||||||
"react-redux": "^7.1.0",
|
"react-redux": "^7.1.0",
|
||||||
"react-router-dom": "^5.0.1",
|
"react-router-dom": "^5.0.1",
|
||||||
"react-scripts": "^3.0.1",
|
"react-scripts": "^3.0.1",
|
||||||
|
@ -56,19 +57,20 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blueprintjs/tslint-config": "^1.8.1",
|
"@blueprintjs/tslint-config": "^1.8.1",
|
||||||
"@types/classnames": "^2.2.9",
|
"@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/jest": "^24.0.15",
|
||||||
"@types/lodash": "^4.14.136",
|
"@types/lodash": "^4.14.136",
|
||||||
"@types/node": "^12.6.2",
|
"@types/node": "^12.6.8",
|
||||||
"@types/numeral": "^0.0.25",
|
"@types/numeral": "^0.0.25",
|
||||||
"@types/react": "^16.8.23",
|
"@types/react": "^16.8.23",
|
||||||
"@types/react-dom": "^16.8.4",
|
"@types/react-dom": "^16.8.4",
|
||||||
"@types/react-redux": "^7.1.1",
|
"@types/react-redux": "^7.1.1",
|
||||||
"@types/react-router-dom": "^4.3.4",
|
"@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/sanitize-html": "^1.20.1",
|
||||||
"@types/styled-components": "4.1.18",
|
"@types/styled-components": "4.1.18",
|
||||||
"husky": "^3.0.0",
|
"husky": "^3.0.1",
|
||||||
"lint-staged": "^9.2.0",
|
"lint-staged": "^9.2.0",
|
||||||
"react-axe": "^3.2.0",
|
"react-axe": "^3.2.0",
|
||||||
"tslint": "^5.18.0",
|
"tslint": "^5.18.0",
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { Button, Classes, Dialog } from "@blueprintjs/core";
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
import { ConnectedRouter } from "connected-react-router";
|
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 { Nav } from "./components/organisms/";
|
||||||
import { AboutScreen, GraphScreen } from "./components/screens/";
|
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";
|
import { history } from "./index";
|
||||||
|
|
||||||
interface IAppLocalState {
|
interface IAppLocalState {
|
||||||
|
@ -24,13 +24,9 @@ export class AppRouter extends React.Component<{}, IAppLocalState> {
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<div className={`${Classes.DARK} App`}>
|
<div className={`${Classes.DARK} App`}>
|
||||||
<Nav />
|
<Nav />
|
||||||
{/* We use `children={}` instead of `component={}` such that the graph is never unmounted */}
|
|
||||||
<Route exact={true} path="/">
|
|
||||||
<Route path={INSTANCE_DOMAIN_PATH}>
|
|
||||||
{(routeProps: RouteComponentProps<IInstanceDomainPath>) => <GraphScreen {...routeProps} />}
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
<Route path="/about" component={AboutScreen} />
|
<Route path="/about" component={AboutScreen} />
|
||||||
|
{/* We always want the GraphScreen to be rendered (since un- and re-mounting it is expensive */}
|
||||||
|
<GraphScreen />
|
||||||
{this.renderMobileDialog()}
|
{this.renderMobileDialog()}
|
||||||
</div>
|
</div>
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
|
|
|
@ -154,7 +154,7 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
|
||||||
|
|
||||||
public resetGraphPosition() {
|
public resetGraphPosition() {
|
||||||
if (!this.cy) {
|
if (!this.cy) {
|
||||||
return;
|
throw new Error("Expected cytoscape, but there wasn't one!");
|
||||||
}
|
}
|
||||||
const { currentNodeId } = this.props;
|
const { currentNodeId } = this.props;
|
||||||
if (currentNodeId) {
|
if (currentNodeId) {
|
||||||
|
@ -175,7 +175,7 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
|
||||||
*/
|
*/
|
||||||
private setNodeSelection = (prevNodeId?: string | null) => {
|
private setNodeSelection = (prevNodeId?: string | null) => {
|
||||||
if (!this.cy) {
|
if (!this.cy) {
|
||||||
return;
|
throw new Error("Expected cytoscape, but there wasn't one!");
|
||||||
}
|
}
|
||||||
if (prevNodeId) {
|
if (prevNodeId) {
|
||||||
this.cy.$id(prevNodeId).unselect();
|
this.cy.$id(prevNodeId).unselect();
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface IFloatingResetButtonProps {
|
||||||
}
|
}
|
||||||
const FloatingResetButton: React.FC<IFloatingResetButtonProps> = ({ onClick }) => (
|
const FloatingResetButton: React.FC<IFloatingResetButtonProps> = ({ onClick }) => (
|
||||||
<FloatingCard>
|
<FloatingCard>
|
||||||
<Button icon="compass" onClick={onClick} />
|
<Button icon="compass" title="Reset graph view" onClick={onClick} />
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
);
|
);
|
||||||
export default FloatingResetButton;
|
export default FloatingResetButton;
|
||||||
|
|
48
frontend/src/components/molecules/SearchResult.tsx
Normal file
48
frontend/src/components/molecules/SearchResult.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { Card, Classes, Elevation, H4 } from "@blueprintjs/core";
|
||||||
|
import inflection from "inflection";
|
||||||
|
import * as numeral from "numeral";
|
||||||
|
import React from "react";
|
||||||
|
import sanitize from "sanitize-html";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { ISearchResultInstance } from "../../redux/types";
|
||||||
|
|
||||||
|
const StyledCard = styled(Card)`
|
||||||
|
width: 80%;
|
||||||
|
margin: 1em auto;
|
||||||
|
background-color: #394b59 !important;
|
||||||
|
text-align: left;
|
||||||
|
`;
|
||||||
|
const StyledH4 = styled(H4)`
|
||||||
|
margin-bottom: 5px;
|
||||||
|
`;
|
||||||
|
const StyledUserCount = styled.div`
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
const StyledDescription = styled.div`
|
||||||
|
margin-top: 10px;
|
||||||
|
`;
|
||||||
|
interface ISearchResultProps {
|
||||||
|
result: ISearchResultInstance;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
const SearchResult: React.FC<ISearchResultProps> = ({ result, onClick }) => {
|
||||||
|
let shortenedDescription;
|
||||||
|
if (result.description) {
|
||||||
|
shortenedDescription = result.description && sanitize(result.description);
|
||||||
|
if (shortenedDescription.length > 100) {
|
||||||
|
shortenedDescription = shortenedDescription.substring(0, 100) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<StyledCard elevation={Elevation.ONE} interactive={true} key={result.name} onClick={onClick}>
|
||||||
|
<StyledH4>{result.name}</StyledH4>
|
||||||
|
{result.userCount && (
|
||||||
|
<StyledUserCount className={Classes.TEXT_MUTED}>
|
||||||
|
{numeral.default(result.userCount).format("0,0")} {inflection.inflect("people", result.userCount, "person")}
|
||||||
|
</StyledUserCount>
|
||||||
|
)}
|
||||||
|
{shortenedDescription && <StyledDescription dangerouslySetInnerHTML={{ __html: shortenedDescription }} />}
|
||||||
|
</StyledCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SearchResult;
|
|
@ -1,3 +1,4 @@
|
||||||
export { default as Cytoscape } from "./Cytoscape";
|
export { default as Cytoscape } from "./Cytoscape";
|
||||||
export { default as ErrorState } from "./ErrorState";
|
export { default as ErrorState } from "./ErrorState";
|
||||||
export { default as FloatingResetButton } from "./FloatingResetButton";
|
export { default as FloatingResetButton } from "./FloatingResetButton";
|
||||||
|
export { default as SearchResult } from "./SearchResult";
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
import { NonIdealState, Spinner } from "@blueprintjs/core";
|
||||||
import { push } from "connected-react-router";
|
import { push } from "connected-react-router";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { fetchGraph } from "../../redux/actions";
|
||||||
import { IAppState, IGraph } from "../../redux/types";
|
import { IAppState, IGraph } from "../../redux/types";
|
||||||
import { domainMatchSelector } from "../../util";
|
import { domainMatchSelector } from "../../util";
|
||||||
import { Cytoscape, ErrorState, FloatingResetButton } from "../molecules/";
|
import { Cytoscape, ErrorState, FloatingResetButton } from "../molecules/";
|
||||||
|
|
||||||
const GraphDiv = styled.div`
|
const GraphDiv = styled.div`
|
||||||
flex: 3;
|
flex: 2;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TODO: merge this component with Cytoscape.tsx
|
|
||||||
interface IGraphProps {
|
interface IGraphProps {
|
||||||
graph?: IGraph;
|
|
||||||
currentInstanceName: string | null;
|
currentInstanceName: string | null;
|
||||||
|
fetchGraph: () => void;
|
||||||
|
graph?: IGraph;
|
||||||
|
graphLoadError: boolean;
|
||||||
|
isLoadingGraph: boolean;
|
||||||
navigate: (path: string) => void;
|
navigate: (path: string) => void;
|
||||||
}
|
}
|
||||||
class GraphImpl extends React.Component<IGraphProps> {
|
class GraphImpl extends React.Component<IGraphProps> {
|
||||||
|
@ -26,13 +30,19 @@ class GraphImpl extends React.Component<IGraphProps> {
|
||||||
this.cytoscapeComponent = React.createRef();
|
this.cytoscapeComponent = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public componentDidMount() {
|
||||||
if (!this.props.graph) {
|
this.loadGraph();
|
||||||
return <ErrorState />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
public render() {
|
||||||
<GraphDiv>
|
let content;
|
||||||
|
if (this.props.isLoadingGraph) {
|
||||||
|
content = <NonIdealState icon={<Spinner />} title="Loading..." />;
|
||||||
|
} else if (this.props.graphLoadError || !this.props.graph) {
|
||||||
|
content = <ErrorState />;
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
<Cytoscape
|
<Cytoscape
|
||||||
currentNodeId={this.props.currentInstanceName}
|
currentNodeId={this.props.currentInstanceName}
|
||||||
elements={this.props.graph}
|
elements={this.props.graph}
|
||||||
|
@ -41,10 +51,19 @@ class GraphImpl extends React.Component<IGraphProps> {
|
||||||
ref={this.cytoscapeComponent}
|
ref={this.cytoscapeComponent}
|
||||||
/>
|
/>
|
||||||
<FloatingResetButton onClick={this.resetGraphPosition} />
|
<FloatingResetButton onClick={this.resetGraphPosition} />
|
||||||
</GraphDiv>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <GraphDiv>{content}</GraphDiv>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadGraph = () => {
|
||||||
|
if (!this.props.isLoadingGraph && !this.props.graphLoadError) {
|
||||||
|
this.props.fetchGraph();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private resetGraphPosition = () => {
|
private resetGraphPosition = () => {
|
||||||
if (this.cytoscapeComponent.current) {
|
if (this.cytoscapeComponent.current) {
|
||||||
this.cytoscapeComponent.current.resetGraphPosition();
|
this.cytoscapeComponent.current.resetGraphPosition();
|
||||||
|
@ -63,10 +82,13 @@ const mapStateToProps = (state: IAppState) => {
|
||||||
const match = domainMatchSelector(state);
|
const match = domainMatchSelector(state);
|
||||||
return {
|
return {
|
||||||
currentInstanceName: match && match.params.domain,
|
currentInstanceName: match && match.params.domain,
|
||||||
graph: state.data.graph
|
graph: state.data.graph,
|
||||||
|
graphLoadError: state.data.error,
|
||||||
|
isLoadingGraph: state.data.isLoadingGraph
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
|
fetchGraph: () => dispatch(fetchGraph() as any),
|
||||||
navigate: (path: string) => dispatch(push(path))
|
navigate: (path: string) => dispatch(push(path))
|
||||||
});
|
});
|
||||||
const Graph = connect(
|
const Graph = connect(
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
|
|
||||||
import { Button, MenuItem } from "@blueprintjs/core";
|
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
|
||||||
import { IItemRendererProps, ItemPredicate, Select } from "@blueprintjs/select";
|
|
||||||
|
|
||||||
import { push } from "connected-react-router";
|
|
||||||
import { IAppState, IInstance } from "../../redux/types";
|
|
||||||
import { domainMatchSelector } from "../../util";
|
|
||||||
|
|
||||||
interface IInstanceSearchProps {
|
|
||||||
currentInstanceName: string | null;
|
|
||||||
pathname: string;
|
|
||||||
instances?: IInstance[];
|
|
||||||
selectInstance: (instanceName: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InstanceSelect = Select.ofType<IInstance>();
|
|
||||||
|
|
||||||
class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<InstanceSelect
|
|
||||||
items={this.props.instances || []}
|
|
||||||
itemRenderer={this.itemRenderer}
|
|
||||||
onItemSelect={this.onItemSelect}
|
|
||||||
itemPredicate={this.itemPredicate}
|
|
||||||
disabled={
|
|
||||||
!this.props.instances || (!this.props.pathname.startsWith("/instance/") && this.props.pathname !== "/")
|
|
||||||
}
|
|
||||||
initialContent={this.renderInitialContent()}
|
|
||||||
noResults={this.renderNoResults()}
|
|
||||||
popoverProps={{ popoverClassName: "fediverse-instance-search-popover" }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon={IconNames.SELECTION}
|
|
||||||
rightIcon={IconNames.CARET_DOWN}
|
|
||||||
text={this.props.currentInstanceName || "Select an instance"}
|
|
||||||
disabled={!this.props.instances}
|
|
||||||
/>
|
|
||||||
</InstanceSelect>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderInitialContent = () => {
|
|
||||||
return <MenuItem disabled={true} text={"Start typing"} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderNoResults = () => {
|
|
||||||
return <MenuItem disabled={true} text={"Keep typing"} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
private itemRenderer = (item: IInstance, itemProps: IItemRendererProps) => {
|
|
||||||
if (!itemProps.modifiers.matchesPredicate) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<MenuItem text={item.name} key={item.name} active={itemProps.modifiers.active} onClick={itemProps.handleClick} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private itemPredicate: ItemPredicate<IInstance> = (query, item, index) => {
|
|
||||||
if (!item.name || query.length < 4) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return item.name.toLowerCase().indexOf(query.toLowerCase()) >= 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => {
|
|
||||||
this.props.selectInstance(item.name);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => {
|
|
||||||
const match = domainMatchSelector(state);
|
|
||||||
return {
|
|
||||||
currentInstanceName: match && match.params.domain,
|
|
||||||
instances: state.data.instances,
|
|
||||||
pathname: state.router.location.pathname
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|
||||||
selectInstance: (domain: string) => dispatch(push(`/instance/${domain}`))
|
|
||||||
});
|
|
||||||
const InstanceSearch = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(InstanceSearchImpl);
|
|
||||||
export default InstanceSearch;
|
|
|
@ -5,7 +5,6 @@ import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
import { Classes } from "@blueprintjs/core";
|
import { Classes } from "@blueprintjs/core";
|
||||||
import { match, NavLink } from "react-router-dom";
|
import { match, NavLink } from "react-router-dom";
|
||||||
import { InstanceSearch } from ".";
|
|
||||||
import { IInstanceDomainPath } from "../../constants";
|
import { IInstanceDomainPath } from "../../constants";
|
||||||
|
|
||||||
interface INavState {
|
interface INavState {
|
||||||
|
@ -45,9 +44,6 @@ class Nav extends React.Component<{}, INavState> {
|
||||||
About
|
About
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Navbar.Group>
|
</Navbar.Group>
|
||||||
<Navbar.Group align={Alignment.RIGHT}>
|
|
||||||
<InstanceSearch />
|
|
||||||
</Navbar.Group>
|
|
||||||
</Navbar>
|
</Navbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
27
frontend/src/components/organisms/SidebarContainer.tsx
Normal file
27
frontend/src/components/organisms/SidebarContainer.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Card, Elevation } from "@blueprintjs/core";
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const RightDiv = styled.div`
|
||||||
|
align-self: right;
|
||||||
|
background-color: grey;
|
||||||
|
flex: 1;
|
||||||
|
overflow: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
`;
|
||||||
|
const StyledCard = styled(Card)`
|
||||||
|
min-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
const SidebarContainer: React.FC = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<RightDiv>
|
||||||
|
<StyledCard elevation={Elevation.TWO}>{children}</StyledCard>
|
||||||
|
</RightDiv>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SidebarContainer;
|
|
@ -1,4 +1,3 @@
|
||||||
export { default as Graph } from "./Graph";
|
export { default as Graph } from "./Graph";
|
||||||
export { default as Sidebar } from "./Sidebar";
|
|
||||||
export { default as Nav } from "./Nav";
|
export { default as Nav } from "./Nav";
|
||||||
export { default as InstanceSearch } from "./InstanceSearch";
|
export { default as SidebarContainer } from "./SidebarContainer";
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { NonIdealState, Spinner } from "@blueprintjs/core";
|
import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
|
||||||
|
import { InstanceScreen, SearchScreen } from ".";
|
||||||
import { fetchGraph, fetchInstances, loadInstance } from "../../redux/actions";
|
import { INSTANCE_DOMAIN_PATH } from "../../constants";
|
||||||
|
import { loadInstance } from "../../redux/actions";
|
||||||
import { IAppState } from "../../redux/types";
|
import { IAppState } from "../../redux/types";
|
||||||
import { domainMatchSelector } from "../../util";
|
import { domainMatchSelector } from "../../util";
|
||||||
import { ErrorState } from "../molecules/";
|
import { Graph, SidebarContainer } from "../organisms/";
|
||||||
import { Graph, Sidebar } from "../organisms/";
|
|
||||||
|
|
||||||
const GraphContainer = styled.div`
|
const GraphContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -24,39 +24,22 @@ const FullDiv = styled.div`
|
||||||
right: 0;
|
right: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IGraphScreenProps {
|
interface IGraphScreenProps extends RouteComponentProps {
|
||||||
currentInstanceName: string | null;
|
currentInstanceName: string | null;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
isLoadingGraph: boolean;
|
|
||||||
isLoadingInstances: boolean;
|
|
||||||
graphLoadError: boolean;
|
graphLoadError: boolean;
|
||||||
loadInstance: (domain: string | null) => void;
|
loadInstance: (domain: string | null) => void;
|
||||||
fetchInstances: () => void;
|
|
||||||
fetchGraph: () => void;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This component takes care of loading or deselecting the current instance when the URL path changes.
|
* This component takes care of loading or deselecting the current instance when the URL path changes.
|
||||||
|
* It also handles changing and animating the screen shown in the sidebar.
|
||||||
*/
|
*/
|
||||||
class GraphScreenImpl extends React.Component<IGraphScreenProps> {
|
class GraphScreenImpl extends React.Component<IGraphScreenProps> {
|
||||||
public render() {
|
public render() {
|
||||||
let content;
|
return <Route render={this.renderRoutes} />;
|
||||||
if (this.props.isLoadingInstances || this.props.isLoadingGraph) {
|
|
||||||
content = this.loadingState("Loading...");
|
|
||||||
} else if (!!this.props.graphLoadError) {
|
|
||||||
content = <ErrorState />;
|
|
||||||
} else {
|
|
||||||
content = (
|
|
||||||
<GraphContainer>
|
|
||||||
<Graph />
|
|
||||||
<Sidebar />
|
|
||||||
</GraphContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <FullDiv>{content}</FullDiv>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.loadInstancesAndGraph();
|
|
||||||
this.loadCurrentInstance();
|
this.loadCurrentInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,45 +47,40 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps> {
|
||||||
this.loadCurrentInstance(prevProps.currentInstanceName);
|
this.loadCurrentInstance(prevProps.currentInstanceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadInstancesAndGraph = () => {
|
private renderRoutes = ({ location }: RouteComponentProps) => (
|
||||||
if (!this.props.isLoadingGraph && !this.props.graphLoadError) {
|
<FullDiv>
|
||||||
this.props.fetchGraph();
|
<GraphContainer>
|
||||||
}
|
<Graph />
|
||||||
if (!this.props.isLoadingInstances && !this.props.graphLoadError) {
|
<SidebarContainer>
|
||||||
this.props.fetchInstances();
|
<Switch>
|
||||||
}
|
<Route path={INSTANCE_DOMAIN_PATH} component={InstanceScreen} />
|
||||||
};
|
<Route exact={true} path="/" component={SearchScreen} />
|
||||||
|
</Switch>
|
||||||
|
</SidebarContainer>
|
||||||
|
</GraphContainer>
|
||||||
|
</FullDiv>
|
||||||
|
);
|
||||||
|
|
||||||
private loadCurrentInstance = (prevInstanceName?: string | null) => {
|
private loadCurrentInstance = (prevInstanceName?: string | null) => {
|
||||||
if (prevInstanceName !== this.props.currentInstanceName) {
|
if (prevInstanceName !== this.props.currentInstanceName) {
|
||||||
this.props.loadInstance(this.props.currentInstanceName);
|
this.props.loadInstance(this.props.currentInstanceName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private loadingState = (title?: string) => {
|
|
||||||
return <NonIdealState icon={<Spinner />} title={title || "Loading..."} />;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => {
|
const mapStateToProps = (state: IAppState) => {
|
||||||
const match = domainMatchSelector(state);
|
const match = domainMatchSelector(state);
|
||||||
return {
|
return {
|
||||||
currentInstanceName: match && match.params.domain,
|
currentInstanceName: match && match.params.domain,
|
||||||
graph: state.data.graph,
|
|
||||||
graphLoadError: state.data.error,
|
graphLoadError: state.data.error,
|
||||||
instances: state.data.instances,
|
|
||||||
isLoadingGraph: state.data.isLoadingGraph,
|
|
||||||
isLoadingInstances: state.data.isLoadingInstances,
|
|
||||||
pathname: state.router.location.pathname
|
pathname: state.router.location.pathname
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
fetchGraph: () => dispatch(fetchGraph() as any),
|
|
||||||
fetchInstances: () => dispatch(fetchInstances() as any),
|
|
||||||
loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any)
|
loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any)
|
||||||
});
|
});
|
||||||
const GraphScreen = connect(
|
const GraphScreen = connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(GraphScreenImpl);
|
)(GraphScreenImpl);
|
||||||
export default GraphScreen;
|
export default withRouter(GraphScreen);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { orderBy } from "lodash";
|
import { orderBy } from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import * as numeral from "numeral";
|
import * as numeral from "numeral";
|
||||||
import * as React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import sanitize from "sanitize-html";
|
import sanitize from "sanitize-html";
|
||||||
|
|
||||||
|
@ -9,114 +9,122 @@ import {
|
||||||
AnchorButton,
|
AnchorButton,
|
||||||
Button,
|
Button,
|
||||||
Callout,
|
Callout,
|
||||||
Card,
|
|
||||||
Classes,
|
Classes,
|
||||||
Code,
|
Code,
|
||||||
Divider,
|
Divider,
|
||||||
Elevation,
|
|
||||||
H2,
|
H2,
|
||||||
H4,
|
|
||||||
HTMLTable,
|
HTMLTable,
|
||||||
Icon,
|
Icon,
|
||||||
NonIdealState,
|
NonIdealState,
|
||||||
Position,
|
Position,
|
||||||
|
Spinner,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@blueprintjs/core";
|
} from "@blueprintjs/core";
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
|
import { push } from "connected-react-router";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { IAppState, IGraph, IInstanceDetails } from "../../redux/types";
|
import { IAppState, IGraph, IInstanceDetails } from "../../redux/types";
|
||||||
import { domainMatchSelector } from "../../util";
|
import { domainMatchSelector } from "../../util";
|
||||||
import { ErrorState } from "../molecules/";
|
import { ErrorState } from "../molecules/";
|
||||||
import { FullDiv } from "../styled-components";
|
|
||||||
|
|
||||||
interface IClosedProp {
|
const InstanceScreenContainer = styled.div`
|
||||||
closed?: boolean;
|
margin-bottom: auto;
|
||||||
}
|
display: flex;
|
||||||
const SidebarContainer = styled.div<IClosedProp>`
|
flex-direction: column;
|
||||||
position: fixed;
|
flex: 1;
|
||||||
top: 50px;
|
|
||||||
bottom: 0;
|
|
||||||
right: ${props => (props.closed ? "-400px" : 0)};
|
|
||||||
min-width: 400px;
|
|
||||||
width: 25%;
|
|
||||||
z-index: 20;
|
|
||||||
overflow: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
transition-property: all;
|
|
||||||
transition-duration: 0.5s;
|
|
||||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
|
||||||
@media screen and (min-width: 1600px) {
|
|
||||||
right: ${props => (props.closed ? "-25%" : 0)};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
const StyledCard = styled(Card)`
|
const HeadingContainer = styled.div`
|
||||||
min-height: 100%;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
const StyledButton = styled(Button)`
|
const StyledHeadingH2 = styled(H2)`
|
||||||
position: absolute;
|
margin: 0;
|
||||||
top: 0;
|
`;
|
||||||
left: -40px;
|
const StyledCloseButton = styled(Button)`
|
||||||
z-index: 20;
|
justify-self: flex-end;
|
||||||
transition-property: all;
|
`;
|
||||||
transition-duration: 0.5s;
|
const StyledHeadingTooltip = styled(Tooltip)`
|
||||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
margin-left: 5px;
|
||||||
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
const StyledHTMLTable = styled(HTMLTable)`
|
const StyledHTMLTable = styled(HTMLTable)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
const StyledLinkToFdNetwork = styled.div`
|
const StyledLinkToFdNetwork = styled.div`
|
||||||
margin-top: 3em;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-top: auto;
|
||||||
`;
|
`;
|
||||||
|
const StyledTabs = styled(Tabs)`
|
||||||
interface ISidebarProps {
|
width: 100%;
|
||||||
|
`;
|
||||||
|
interface IInstanceScreenProps {
|
||||||
graph?: IGraph;
|
graph?: IGraph;
|
||||||
instanceName: string | null;
|
instanceName: string | null;
|
||||||
instanceLoadError: boolean;
|
instanceLoadError: boolean;
|
||||||
instanceDetails: IInstanceDetails | null;
|
instanceDetails: IInstanceDetails | null;
|
||||||
isLoadingInstanceDetails: boolean;
|
isLoadingInstanceDetails: boolean;
|
||||||
|
navigateToRoot: () => void;
|
||||||
}
|
}
|
||||||
interface ISidebarState {
|
|
||||||
isOpen: boolean;
|
interface IInstanceScreenState {
|
||||||
neighbors?: string[];
|
neighbors?: string[];
|
||||||
isProcessingNeighbors: boolean;
|
isProcessingNeighbors: boolean;
|
||||||
}
|
}
|
||||||
class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInstanceScreenState> {
|
||||||
constructor(props: ISidebarProps) {
|
public constructor(props: IInstanceScreenProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const isOpen = window.innerWidth >= 900 ? true : false;
|
this.state = { isProcessingNeighbors: false };
|
||||||
this.state = { isOpen, isProcessingNeighbors: false };
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
let content;
|
||||||
|
if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors) {
|
||||||
|
content = this.renderLoadingState();
|
||||||
|
} else if (!this.props.instanceDetails) {
|
||||||
|
return this.renderEmptyState();
|
||||||
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
|
||||||
|
content = this.renderPersonalInstanceErrorState();
|
||||||
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) {
|
||||||
|
content = this.renderRobotsTxtState();
|
||||||
|
} else if (this.props.instanceDetails.status !== "success") {
|
||||||
|
content = this.renderMissingDataState();
|
||||||
|
} else if (this.props.instanceLoadError) {
|
||||||
|
return (content = <ErrorState />);
|
||||||
|
} else {
|
||||||
|
content = this.renderTabs();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<InstanceScreenContainer>
|
||||||
|
<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} />
|
||||||
|
</StyledHeadingTooltip>
|
||||||
|
<StyledCloseButton icon={IconNames.CROSS} onClick={this.props.navigateToRoot} />
|
||||||
|
</HeadingContainer>
|
||||||
|
<Divider />
|
||||||
|
{content}
|
||||||
|
</InstanceScreenContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.processEdgesToFindNeighbors();
|
this.processEdgesToFindNeighbors();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: ISidebarProps, prevState: ISidebarState) {
|
public componentDidUpdate(prevProps: IInstanceScreenProps, prevState: IInstanceScreenState) {
|
||||||
if (prevProps.instanceName !== this.props.instanceName) {
|
if (prevProps.instanceName !== this.props.instanceName) {
|
||||||
this.processEdgesToFindNeighbors();
|
this.processEdgesToFindNeighbors();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
|
||||||
const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT;
|
|
||||||
return (
|
|
||||||
<SidebarContainer closed={!this.state.isOpen}>
|
|
||||||
<StyledButton onClick={this.handleToggle} large={true} icon={buttonIcon} minimal={true} />
|
|
||||||
<StyledCard elevation={Elevation.TWO}>{this.renderSidebarContents()}</StyledCard>
|
|
||||||
</SidebarContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleToggle = () => {
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
private processEdgesToFindNeighbors = () => {
|
private processEdgesToFindNeighbors = () => {
|
||||||
const { graph, instanceName } = this.props;
|
const { graph, instanceName } = this.props;
|
||||||
if (!graph || !instanceName) {
|
if (!graph || !instanceName) {
|
||||||
|
@ -135,63 +143,38 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
this.setState({ neighbors, isProcessingNeighbors: false });
|
this.setState({ neighbors, isProcessingNeighbors: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderSidebarContents = () => {
|
|
||||||
let content;
|
|
||||||
if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors) {
|
|
||||||
content = this.renderLoadingState();
|
|
||||||
} else if (!this.props.instanceDetails) {
|
|
||||||
return this.renderEmptyState();
|
|
||||||
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
|
|
||||||
content = this.renderPersonalInstanceErrorState();
|
|
||||||
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) {
|
|
||||||
content = this.renderRobotsTxtState();
|
|
||||||
} else if (this.props.instanceDetails.status !== "success") {
|
|
||||||
content = this.renderMissingDataState();
|
|
||||||
} else if (this.props.instanceLoadError) {
|
|
||||||
return (content = <ErrorState />);
|
|
||||||
} else {
|
|
||||||
content = this.renderTabs();
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FullDiv>
|
|
||||||
{this.renderHeading()}
|
|
||||||
{content}
|
|
||||||
</FullDiv>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderTabs = () => {
|
private renderTabs = () => {
|
||||||
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
||||||
|
|
||||||
const insularCallout = hasNeighbors ? (
|
const insularCallout =
|
||||||
undefined
|
this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors ? (
|
||||||
) : (
|
|
||||||
<Callout icon={IconNames.INFO_SIGN} title="Insular instance">
|
<Callout 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't have any neighbors that we know of, so it's hidden from the graph.</p>
|
||||||
</Callout>
|
</Callout>
|
||||||
|
) : (
|
||||||
|
undefined
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{insularCallout}
|
{insularCallout}
|
||||||
<Tabs>
|
<StyledTabs>
|
||||||
{this.props.instanceDetails!.description && (
|
{this.props.instanceDetails!.description && (
|
||||||
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
||||||
)}
|
)}
|
||||||
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
||||||
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
|
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
|
||||||
<Tab id="peers" title="Known peers" panel={this.renderPeers()} />
|
<Tab id="peers" title="Known peers" panel={this.renderPeers()} />
|
||||||
</Tabs>
|
</StyledTabs>
|
||||||
<StyledLinkToFdNetwork>
|
<StyledLinkToFdNetwork>
|
||||||
<a
|
<AnchorButton
|
||||||
href={`https://fediverse.network/${this.props.instanceName}`}
|
href={`https://fediverse.network/${this.props.instanceName}`}
|
||||||
|
minimal={true}
|
||||||
|
rightIcon={IconNames.SHARE}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
text="See more statistics at fediverse.network"
|
||||||
className={`${Classes.BUTTON} bp3-icon-${IconNames.LINK}`}
|
/>
|
||||||
>
|
|
||||||
See more statistics at fediverse.network
|
|
||||||
</a>
|
|
||||||
</StyledLinkToFdNetwork>
|
</StyledLinkToFdNetwork>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -200,29 +183,6 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
|
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderHeading = () => {
|
|
||||||
let content: JSX.Element;
|
|
||||||
if (!this.props.instanceName) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
content = (
|
|
||||||
<span>
|
|
||||||
{this.props.instanceName + " "}
|
|
||||||
<Tooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
|
|
||||||
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<H2>{content}</H2>
|
|
||||||
<Divider />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderDescription = () => {
|
private renderDescription = () => {
|
||||||
const description = this.props.instanceDetails!.description;
|
const description = this.props.instanceDetails!.description;
|
||||||
if (!description) {
|
if (!description) {
|
||||||
|
@ -237,7 +197,6 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
}
|
}
|
||||||
const { version, userCount, statusCount, domainCount, lastUpdated, insularity } = this.props.instanceDetails;
|
const { version, userCount, statusCount, domainCount, lastUpdated, insularity } = this.props.instanceDetails;
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
<StyledHTMLTable small={true} striped={true}>
|
<StyledHTMLTable small={true} striped={true}>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -281,7 +240,6 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</StyledHTMLTable>
|
</StyledHTMLTable>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -369,34 +327,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderLoadingState = () => {
|
private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<H4>
|
|
||||||
<span className={Classes.SKELETON}>Description</span>
|
|
||||||
</H4>
|
|
||||||
<p className={Classes.SKELETON}>
|
|
||||||
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit
|
|
||||||
voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur necessitatibus
|
|
||||||
tempora. Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt.
|
|
||||||
Cupiditate sit voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur
|
|
||||||
necessitatibus tempora.
|
|
||||||
</p>
|
|
||||||
<H4>
|
|
||||||
<span className={Classes.SKELETON}>Version</span>
|
|
||||||
</H4>
|
|
||||||
<p className={Classes.SKELETON}>Eaque rerum sequi unde omnis voluptatibus non quia fugit.</p>
|
|
||||||
<H4>
|
|
||||||
<span className={Classes.SKELETON}>Stats</span>
|
|
||||||
</H4>
|
|
||||||
<p className={Classes.SKELETON}>
|
|
||||||
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit
|
|
||||||
voluptates quia nulla et saepe id suscipit. Eaque rerum sequi unde omnis voluptatibus non quia fugit.
|
|
||||||
Dignissimos asperiores aut incidunt. Cupiditate sit voluptates quia nulla et saepe id suscipit.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderPersonalInstanceErrorState = () => {
|
private renderPersonalInstanceErrorState = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -415,7 +346,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
|
|
||||||
private renderMissingDataState = () => {
|
private renderMissingDataState = () => {
|
||||||
return (
|
return (
|
||||||
<FullDiv>
|
<>
|
||||||
<NonIdealState
|
<NonIdealState
|
||||||
icon={IconNames.ERROR}
|
icon={IconNames.ERROR}
|
||||||
title="No data"
|
title="No data"
|
||||||
|
@ -424,7 +355,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
|
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
|
||||||
{this.props.instanceDetails && this.props.instanceDetails.status}
|
{this.props.instanceDetails && this.props.instanceDetails.status}
|
||||||
</span>
|
</span>
|
||||||
</FullDiv>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -457,5 +388,11 @@ const mapStateToProps = (state: IAppState) => {
|
||||||
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
|
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const Sidebar = connect(mapStateToProps)(SidebarImpl);
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
export default Sidebar;
|
navigateToRoot: () => dispatch(push("/"))
|
||||||
|
});
|
||||||
|
const InstanceScreen = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(InstanceScreenImpl);
|
||||||
|
export default InstanceScreen;
|
136
frontend/src/components/screens/SearchScreen.tsx
Normal file
136
frontend/src/components/screens/SearchScreen.tsx
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import { Button, Classes, H2, NonIdealState, Spinner } from "@blueprintjs/core";
|
||||||
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
import { push } from "connected-react-router";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { updateSearch } from "../../redux/actions";
|
||||||
|
import { IAppState, ISearchResultInstance } from "../../redux/types";
|
||||||
|
import { SearchResult } from "../molecules";
|
||||||
|
|
||||||
|
const SearchContainer = styled.div`
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
const SearchBarContainer = styled.div`
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
const SearchResults = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
const StyledSpinner = styled(Spinner)`
|
||||||
|
margin-top: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ISearchScreenProps {
|
||||||
|
error: boolean;
|
||||||
|
isLoadingResults: boolean;
|
||||||
|
query: string;
|
||||||
|
hasMoreResults: boolean;
|
||||||
|
results: ISearchResultInstance[];
|
||||||
|
handleSearch: (query: string) => void;
|
||||||
|
navigateToInstance: (domain: string) => void;
|
||||||
|
}
|
||||||
|
interface ISearchScreenState {
|
||||||
|
currentQuery: string;
|
||||||
|
}
|
||||||
|
class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreenState> {
|
||||||
|
public constructor(props: ISearchScreenProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { currentQuery: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
if (this.props.query) {
|
||||||
|
this.setState({ currentQuery: this.props.query });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { error, hasMoreResults, results, isLoadingResults, query } = this.props;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <NonIdealState icon={IconNames.ERROR} title="Something went wrong." action={this.renderSearchBar()} />;
|
||||||
|
} else if (!isLoadingResults && query && results.length === 0) {
|
||||||
|
return (
|
||||||
|
<NonIdealState
|
||||||
|
icon={IconNames.SEARCH}
|
||||||
|
title="No search results"
|
||||||
|
description="Try searching for something else."
|
||||||
|
action={this.renderSearchBar()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchContainer>
|
||||||
|
<H2>Find an instance</H2>
|
||||||
|
{this.renderSearchBar()}
|
||||||
|
<SearchResults>
|
||||||
|
{results.map(result => (
|
||||||
|
<SearchResult result={result} key={result.name} onClick={this.selectInstanceFactory(result.name)} />
|
||||||
|
))}
|
||||||
|
{isLoadingResults && <StyledSpinner size={Spinner.SIZE_SMALL} />}
|
||||||
|
{!isLoadingResults && hasMoreResults && (
|
||||||
|
<Button onClick={this.search} minimal={true}>
|
||||||
|
Load more results
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</SearchResults>
|
||||||
|
</SearchContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ currentQuery: event.currentTarget.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private search = () => {
|
||||||
|
this.props.handleSearch(this.state.currentQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
private selectInstanceFactory = (domain: string) => () => {
|
||||||
|
this.props.navigateToInstance(domain);
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderSearchBar = () => (
|
||||||
|
<SearchBarContainer className={`${Classes.INPUT_GROUP} ${Classes.LARGE}`}>
|
||||||
|
<span className={`${Classes.ICON} bp3-icon-${IconNames.SEARCH}`} />
|
||||||
|
<input
|
||||||
|
className={Classes.INPUT}
|
||||||
|
type="search"
|
||||||
|
placeholder="Instance name"
|
||||||
|
dir="auto"
|
||||||
|
value={this.state.currentQuery}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
|
/>
|
||||||
|
</SearchBarContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
|
@ -1,2 +1,4 @@
|
||||||
export { default as AboutScreen } from "./AboutScreen";
|
export { default as AboutScreen } from "./AboutScreen";
|
||||||
export { default as GraphScreen } from "./GraphScreen";
|
export { default as GraphScreen } from "./GraphScreen";
|
||||||
|
export { default as SearchScreen } from "./SearchScreen";
|
||||||
|
export { default as InstanceScreen } from "./InstanceScreen";
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
export const FullDiv = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
|
@ -2,76 +2,77 @@ import { Dispatch } from "redux";
|
||||||
|
|
||||||
import { push } from "connected-react-router";
|
import { push } from "connected-react-router";
|
||||||
import { getFromApi } from "../util";
|
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) => {
|
const requestInstanceDetails = (instanceName: string) => {
|
||||||
return {
|
return {
|
||||||
payload: instanceName,
|
payload: instanceName,
|
||||||
type: ActionType.REQUEST_INSTANCE_DETAILS
|
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 = () => {
|
const deselectInstance = () => {
|
||||||
return {
|
return {
|
||||||
type: ActionType.DESELECT_INSTANCE
|
type: ActionType.DESELECT_INSTANCE
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestInstances = () => {
|
// Graph
|
||||||
return {
|
const requestGraph = () => {
|
||||||
type: ActionType.REQUEST_INSTANCES
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const receiveInstances = (instances: IInstance[]) => {
|
|
||||||
return {
|
|
||||||
payload: instances,
|
|
||||||
type: ActionType.RECEIVE_INSTANCES
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export const requestGraph = () => {
|
|
||||||
return {
|
return {
|
||||||
type: ActionType.REQUEST_GRAPH
|
type: ActionType.REQUEST_GRAPH
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const receiveGraph = (graph: IGraph) => {
|
||||||
export const receiveGraph = (graph: IGraph) => {
|
|
||||||
return {
|
return {
|
||||||
payload: graph,
|
payload: graph,
|
||||||
type: ActionType.RECEIVE_GRAPH
|
type: ActionType.RECEIVE_GRAPH
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const graphLoadFailed = () => {
|
const graphLoadFailed = () => {
|
||||||
return {
|
return {
|
||||||
type: ActionType.GRAPH_LOAD_ERROR
|
type: ActionType.GRAPH_LOAD_ERROR
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const instanceLoadFailed = () => {
|
// Search
|
||||||
|
const requestSearchResult = (query: string) => {
|
||||||
return {
|
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 {
|
return {
|
||||||
payload: instanceDetails,
|
type: ActionType.RESET_SEARCH
|
||||||
type: ActionType.RECEIVE_INSTANCE_DETAILS
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Async actions: https://redux.js.org/advanced/asyncactions */
|
/** 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) => {
|
export const loadInstance = (instanceName: string | null) => {
|
||||||
return (dispatch: Dispatch, getState: () => IAppState) => {
|
return (dispatch: Dispatch, getState: () => IAppState) => {
|
||||||
if (!instanceName) {
|
if (!instanceName) {
|
||||||
|
@ -84,7 +85,26 @@ export const loadInstance = (instanceName: string | null) => {
|
||||||
dispatch(requestInstanceDetails(instanceName));
|
dispatch(requestInstanceDetails(instanceName));
|
||||||
return getFromApi("instances/" + instanceName)
|
return getFromApi("instances/" + instanceName)
|
||||||
.then(details => dispatch(receiveInstanceDetails(details)))
|
.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());
|
dispatch(requestGraph());
|
||||||
return getFromApi("graph")
|
return getFromApi("graph")
|
||||||
.then(graph => dispatch(receiveGraph(graph)))
|
.then(graph => dispatch(receiveGraph(graph)))
|
||||||
.catch(e => dispatch(graphLoadFailed()));
|
.catch(() => dispatch(graphLoadFailed()));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,27 +2,14 @@ import { connectRouter } from "connected-react-router";
|
||||||
import { combineReducers } from "redux";
|
import { combineReducers } from "redux";
|
||||||
|
|
||||||
import { History } from "history";
|
import { History } from "history";
|
||||||
import { ActionType, IAction, ICurrentInstanceState, IDataState } from "./types";
|
import { ActionType, IAction, ICurrentInstanceState, IDataState, ISearchState } from "./types";
|
||||||
|
|
||||||
const initialDataState = {
|
const initialDataState = {
|
||||||
error: false,
|
error: false,
|
||||||
isLoadingGraph: false,
|
isLoadingGraph: false
|
||||||
isLoadingInstances: false
|
|
||||||
};
|
};
|
||||||
const data = (state: IDataState = initialDataState, action: IAction) => {
|
const data = (state: IDataState = initialDataState, action: IAction) => {
|
||||||
switch (action.type) {
|
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:
|
case ActionType.REQUEST_GRAPH:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -38,8 +25,7 @@ const data = (state: IDataState = initialDataState, action: IAction) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: true,
|
error: true,
|
||||||
isLoadingGraph: false,
|
isLoadingGraph: false
|
||||||
isLoadingInstances: false
|
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
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) =>
|
export default (history: History) =>
|
||||||
combineReducers({
|
combineReducers({
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
||||||
// tslint:disable-next-line:object-literal-sort-keys
|
// tslint:disable-next-line:object-literal-sort-keys
|
||||||
currentInstance,
|
currentInstance,
|
||||||
data
|
data,
|
||||||
|
search
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
import { RouterState } from "connected-react-router";
|
import { RouterState } from "connected-react-router";
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
|
// Instance details
|
||||||
REQUEST_INSTANCE_DETAILS = "REQUEST_INSTANCE_DETAILS",
|
REQUEST_INSTANCE_DETAILS = "REQUEST_INSTANCE_DETAILS",
|
||||||
REQUEST_INSTANCES = "REQUEST_INSTANCES",
|
RECEIVE_INSTANCE_DETAILS = "RECEIVE_INSTANCE_DETAILS",
|
||||||
RECEIVE_INSTANCES = "RECEIVE_INSTANCES",
|
INSTANCE_LOAD_ERROR = "INSTANCE_LOAD_ERROR",
|
||||||
|
// Graph
|
||||||
REQUEST_GRAPH = "REQUEST_GRAPH",
|
REQUEST_GRAPH = "REQUEST_GRAPH",
|
||||||
RECEIVE_GRAPH = "RECEIVE_GRAPH",
|
RECEIVE_GRAPH = "RECEIVE_GRAPH",
|
||||||
RECEIVE_INSTANCE_DETAILS = "RECEIVE_INSTANCE_DETAILS",
|
|
||||||
DESELECT_INSTANCE = "DESELECT_INSTANCE",
|
|
||||||
GRAPH_LOAD_ERROR = "GRAPH_LOAD_ERROR",
|
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 {
|
export interface IAction {
|
||||||
|
@ -21,6 +29,12 @@ export interface IInstance {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISearchResultInstance {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
userCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IInstanceDetails {
|
export interface IInstanceDetails {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -60,6 +74,11 @@ export interface IGraph {
|
||||||
edges: IGraphEdge[];
|
edges: IGraphEdge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISearchResponse {
|
||||||
|
results: ISearchResultInstance[];
|
||||||
|
next: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
// Redux state
|
// Redux state
|
||||||
|
|
||||||
// The current instance name is stored in the URL. See state -> router -> location
|
// The current instance name is stored in the URL. See state -> router -> location
|
||||||
|
@ -70,15 +89,22 @@ export interface ICurrentInstanceState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDataState {
|
export interface IDataState {
|
||||||
instances?: IInstance[];
|
|
||||||
graph?: IGraph;
|
graph?: IGraph;
|
||||||
isLoadingInstances: boolean;
|
|
||||||
isLoadingGraph: boolean;
|
isLoadingGraph: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISearchState {
|
||||||
|
error: boolean;
|
||||||
|
isLoadingResults: boolean;
|
||||||
|
next: string;
|
||||||
|
query: string;
|
||||||
|
results: ISearchResultInstance[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAppState {
|
export interface IAppState {
|
||||||
router: RouterState;
|
router: RouterState;
|
||||||
currentInstance: ICurrentInstanceState;
|
currentInstance: ICurrentInstanceState;
|
||||||
data: IDataState;
|
data: IDataState;
|
||||||
|
search: ISearchState;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,7 @@ if (["true", true, 1, "1"].indexOf(process.env.REACT_APP_STAGING || "") > -1) {
|
||||||
|
|
||||||
export const getFromApi = (path: string): Promise<any> => {
|
export const getFromApi = (path: string): Promise<any> => {
|
||||||
const domain = API_ROOT.endsWith("/") ? API_ROOT : API_ROOT + "/";
|
const domain = API_ROOT.endsWith("/") ? API_ROOT : API_ROOT + "/";
|
||||||
path = path.endsWith("/") ? path : path + "/";
|
return fetch(encodeURI(domain + path)).then(response => response.json());
|
||||||
return fetch(domain + path).then(response => response.json());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const domainMatchSelector = createMatchSelector<IAppState, IInstanceDomainPath>(INSTANCE_DOMAIN_PATH);
|
export const domainMatchSelector = createMatchSelector<IAppState, IInstanceDomainPath>(INSTANCE_DOMAIN_PATH);
|
||||||
|
|
|
@ -1318,10 +1318,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b"
|
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b"
|
||||||
integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==
|
integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==
|
||||||
|
|
||||||
"@types/cytoscape@^3.4.3":
|
"@types/cytoscape@^3.8.0":
|
||||||
version "3.4.3"
|
version "3.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.4.3.tgz#8b9353154dc895231cd344ed1c7eff2d1391c103"
|
resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.8.0.tgz#334006612fc6285dac83ee3665132743d7651f58"
|
||||||
integrity sha512-uADb/vBj/xTeNNRvtYlzPz1rftMR4Jf6ipq4jqKfYibMZ173sAbdFM3Fl2fPbGfP28CWJpqhcpHp4+NUq3Ma4g==
|
integrity sha512-8TJL7HuMEgjQRCcUC3xKenb7Y6Ra3ZJ3LvYDlpxlt5LlAatzRyTBtIuE1JOdRelqAla8r87XJUzTgi92mlUlQQ==
|
||||||
|
|
||||||
"@types/dom4@^2.0.1":
|
"@types/dom4@^2.0.1":
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
|
@ -1376,6 +1376,11 @@
|
||||||
"@types/domutils" "*"
|
"@types/domutils" "*"
|
||||||
"@types/node" "*"
|
"@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":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.4.tgz#f83ec3c3e05b174b7241fadeb6688267fe5b22ca"
|
||||||
integrity sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ==
|
integrity sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ==
|
||||||
|
|
||||||
"@types/node@^12.6.2":
|
"@types/node@^12.6.8":
|
||||||
version "12.6.2"
|
version "12.6.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.2.tgz#a5ccec6abb6060d5f20d256fb03ed743e9774999"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.8.tgz#e469b4bf9d1c9832aee4907ba8a051494357c12c"
|
||||||
integrity sha512-gojym4tX0FWeV2gsW4Xmzo5wxGjXGm550oVUII7f7G5o4BV6c7DBdiG1RRQd+y1bvqRyYtPfMK85UM95vsapqQ==
|
integrity sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==
|
||||||
|
|
||||||
"@types/normalize-package-data@^2.4.0":
|
"@types/normalize-package-data@^2.4.0":
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
|
@ -1490,10 +1495,10 @@
|
||||||
"@types/history" "*"
|
"@types/history" "*"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-virtualized@^9.21.2":
|
"@types/react-virtualized@^9.21.3":
|
||||||
version "9.21.2"
|
version "9.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.2.tgz#c5e4293409593814c35466913e83fb856e2053d0"
|
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.3.tgz#79a44b870a4848cbc7cc04ff4bc06e5a10955262"
|
||||||
integrity sha512-Q6geJaDd8FlBw3ilD4ODferTyVtYAmDE3d7+GacfwN0jPt9rD9XkeuPjcHmyIwTrMXuLv1VIJmRxU9WQoQFBJw==
|
integrity sha512-QhXeiVwXrshVAoq2Cy3SGZEDiFdeFfup2ciQya5RTgr5uycQ2alIKzLfy4X38UCrxonwxe8byk5q8fYV0U87Zg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
"@types/react" "*"
|
"@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"
|
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||||
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
|
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
|
||||||
|
|
||||||
husky@^3.0.0:
|
husky@^3.0.1:
|
||||||
version "3.0.0"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.0.tgz#de63821a7049dc412b1afd753c259e2f6e227562"
|
resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.1.tgz#06152c28e129622b05fa09c494209de8cf2dfb59"
|
||||||
integrity sha512-lKMEn7bRK+7f5eWPNGclDVciYNQt0GIkAQmhKl+uHP1qFzoN0h92kmH9HZ8PCwyVA2EQPD8KHf0FYWqnTxau+Q==
|
integrity sha512-PXBv+iGKw23GHUlgELRlVX9932feFL407/wHFwtsGeArp0dDM4u+/QusSQwPKxmNgjpSL+ustbOdQ2jetgAZbA==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
chalk "^2.4.2"
|
||||||
cosmiconfig "^5.2.1"
|
cosmiconfig "^5.2.1"
|
||||||
execa "^1.0.0"
|
execa "^1.0.0"
|
||||||
get-stdin "^7.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"
|
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
|
||||||
integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
|
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:
|
inflight@^1.0.4:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||||
|
|
||||||
lodash@^4.17.12, lodash@^4.17.14:
|
lodash@^4.17.12:
|
||||||
version "4.17.14"
|
version "4.17.14"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
|
||||||
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
|
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:
|
log-symbols@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
|
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"
|
strip-ansi "5.2.0"
|
||||||
text-table "0.2.0"
|
text-table "0.2.0"
|
||||||
|
|
||||||
react-dom@^16.4.2:
|
react-dom@^16.8.0:
|
||||||
version "16.8.6"
|
version "16.8.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
|
||||||
integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==
|
integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==
|
||||||
|
@ -9055,7 +9071,7 @@ react-virtualized@^9.21.1:
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
react-lifecycles-compat "^3.0.4"
|
react-lifecycles-compat "^3.0.4"
|
||||||
|
|
||||||
react@^16.4.2:
|
react@^16.8.0:
|
||||||
version "16.8.6"
|
version "16.8.6"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
|
||||||
integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==
|
integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==
|
||||||
|
|
Loading…
Reference in a new issue