diff --git a/CHANGELOG.md b/CHANGELOG.md index a4005b0..06533ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added option to hide edges between instances if there are only mentions in one direction (off by default). - Added note to neighbors tab to make it explicit that blocked instances may appear. - Added federation tab that shows federation restrictions (only available for some Pleroma instances). +- Add tabular view of instances. ### Changed diff --git a/backend/lib/backend/api.ex b/backend/lib/backend/api.ex index b5c8164..cabd619 100644 --- a/backend/lib/backend/api.ex +++ b/backend/lib/backend/api.ex @@ -6,13 +6,39 @@ defmodule Backend.Api do import Backend.Util import Ecto.Query - @spec get_instances(Integer.t() | nil) :: Scrivener.Page.t() - def get_instances(page \\ nil) do + @type instance_sort_field :: :name | :user_count | :status_count | :insularity + @type sort_direction :: :asc | :desc + @spec get_instances(Integer.t() | nil, instance_sort_field | nil, sort_direction | nil) :: + Scrivener.Page.t() + def get_instances(page \\ nil, sort_field \\ nil, sort_direction \\ nil) do Instance |> where([i], not is_nil(i.type) and not i.opt_out) + |> maybe_order_by(sort_field, sort_direction) |> Repo.paginate(page: page) end + defp maybe_order_by(query, sort_field, sort_direction) do + cond do + sort_field == nil and sort_direction != nil -> + query + + sort_field != nil and sort_direction == nil -> + query + |> order_by(desc: ^sort_field) + + sort_direction == :asc -> + query + |> order_by(asc_nulls_last: ^sort_field) + + sort_direction == :desc -> + query + |> order_by(desc_nulls_last: ^sort_field) + + true -> + query + end + end + @spec get_instance(String.t()) :: Instance.t() | nil def get_instance(domain) do Instance @@ -41,7 +67,8 @@ defmodule Backend.Api do * the user count is > the threshold * have x and y coordinates - If `domain` is passed, then this function only returns nodes that are neighbors of that instance. + If `domain` is passed, then this function only returns nodes that are neighbors of that + instance. """ @spec list_nodes() :: [Instance.t()] def list_nodes(domain \\ nil) do diff --git a/backend/lib/backend_web/controllers/instance_controller.ex b/backend/lib/backend_web/controllers/instance_controller.ex index 7ad2690..5a66390 100644 --- a/backend/lib/backend_web/controllers/instance_controller.ex +++ b/backend/lib/backend_web/controllers/instance_controller.ex @@ -6,24 +6,54 @@ defmodule BackendWeb.InstanceController do action_fallback(BackendWeb.FallbackController) + # sobelow_skip ["DOS.StringToAtom"] def index(conn, params) do page = Map.get(params, "page") + sort_field = Map.get(params, "sortField") + sort_direction = Map.get(params, "sortDirection") - %{ - entries: instances, - total_pages: total_pages, - page_number: page_number, - total_entries: total_entries, - page_size: page_size - } = Api.get_instances(page) + cond do + not Enum.member?([nil, "domain", "userCount", "statusCount", "insularity"], sort_field) -> + render(conn, "error.json", error: "Invalid sort field") - render(conn, "index.json", - instances: instances, - total_pages: total_pages, - page_number: page_number, - total_entries: total_entries, - page_size: page_size - ) + not Enum.member?([nil, "asc", "desc"], sort_direction) -> + render(conn, "error.json", error: "Invalid sort direction") + + true -> + sort_field = + if sort_field != nil do + sort_field + |> Recase.to_snake() + |> String.to_atom() + else + nil + end + + sort_direction = + if sort_direction != nil do + sort_direction + |> Recase.to_snake() + |> String.to_atom() + else + nil + end + + %{ + entries: instances, + total_pages: total_pages, + page_number: page_number, + total_entries: total_entries, + page_size: page_size + } = Api.get_instances(page, sort_field, sort_direction) + + render(conn, "index.json", + instances: instances, + total_pages: total_pages, + page_number: page_number, + total_entries: total_entries, + page_size: page_size + ) + end end def show(conn, %{"id" => domain}) do diff --git a/backend/lib/backend_web/views/instance_view.ex b/backend/lib/backend_web/views/instance_view.ex index e79d51c..b42fdb1 100644 --- a/backend/lib/backend_web/views/instance_view.ex +++ b/backend/lib/backend_web/views/instance_view.ex @@ -20,8 +20,8 @@ defmodule BackendWeb.InstanceView do end @doc """ - Used when rendering the index of all instances (the different from show.json is primarily that it does not - include peers). + Used when rendering the index of all instances (the difference from show.json is primarily that + it does not include peers). """ def render("index_instance.json", %{instance: instance}) do %{ @@ -63,6 +63,10 @@ defmodule BackendWeb.InstanceView do %{name: instance.domain} end + def render("error.json", %{error: error}) do + %{error: error} + end + defp render_personal_instance(instance) do %{ name: instance.domain, diff --git a/backend/test/backend_web/controllers/instance_controller_test.exs b/backend/test/backend_web/controllers/instance_controller_test.exs index 3225376..a86f7a3 100644 --- a/backend/test/backend_web/controllers/instance_controller_test.exs +++ b/backend/test/backend_web/controllers/instance_controller_test.exs @@ -50,7 +50,10 @@ defmodule BackendWeb.InstanceControllerTest do describe "update instance" do setup [:create_instance] - test "renders instance when data is valid", %{conn: conn, instance: %Instance{id: id} = instance} do + test "renders instance when data is valid", %{ + conn: conn, + instance: %Instance{id: id} = instance + } do conn = put(conn, Routes.instance_path(conn, :update, instance), instance: @update_attrs) assert %{"id" => ^id} = json_response(conn, 200)["data"] diff --git a/frontend/src/components/atoms/Page.tsx b/frontend/src/components/atoms/Page.tsx index 59c7cbb..e54ec4a 100644 --- a/frontend/src/components/atoms/Page.tsx +++ b/frontend/src/components/atoms/Page.tsx @@ -11,15 +11,21 @@ const Backdrop = styled.div` z-index: 100; `; -const Container = styled.div` - max-width: 800px; +interface IContainerProps { + fullWidth?: boolean; +} +const Container = styled.div` + max-width: ${props => (props.fullWidth ? "100%" : "800px")}; margin: auto; padding: 2em; `; -const Page: React.FC = ({ children }) => ( +interface IPageProps { + fullWidth?: boolean; +} +const Page: React.FC = ({ children, fullWidth }) => ( - {children} + {children} ); diff --git a/frontend/src/components/organisms/InstanceTable.tsx b/frontend/src/components/organisms/InstanceTable.tsx index c0a8e7f..fe5d277 100644 --- a/frontend/src/components/organisms/InstanceTable.tsx +++ b/frontend/src/components/organisms/InstanceTable.tsx @@ -1,4 +1,5 @@ import { Button, ButtonGroup, Code, HTMLTable, Intent, NonIdealState, Spinner } from "@blueprintjs/core"; +import { IconNames } from "@blueprintjs/icons"; import { push } from "connected-react-router"; import { range, sortBy, sortedUniq, zip } from "lodash"; import * as numeral from "numeral"; @@ -7,7 +8,7 @@ import { connect } from "react-redux"; import { Dispatch } from "redux"; import styled from "styled-components"; import { loadInstanceList } from "../../redux/actions"; -import { IAppState, IInstanceListResponse } from "../../redux/types"; +import { IAppState, IInstanceListResponse, IInstanceSort, SortField } from "../../redux/types"; import { InstanceType } from "../atoms"; import { ErrorState } from "../molecules"; @@ -21,19 +22,38 @@ const PaginationContainer = styled.div` flex: 1; align-items: center; `; +const InstanceColumn = styled.th` + width: 15%; +`; +const ServerColumn = styled.th` + width: 20%; +`; +const VersionColumn = styled.th` + width: 20%; +`; +const UserCountColumn = styled.th` + width: 15%; +`; +const StatusCountColumn = styled.th` + width: 15%; +`; +const InsularityColumn = styled.th` + width: 15%; +`; interface IInstanceTableProps { loadError: boolean; instancesResponse?: IInstanceListResponse; + instanceListSort: IInstanceSort; isLoading: boolean; - fetchInstances: (page?: number) => void; + loadInstanceList: (page?: number, sort?: IInstanceSort) => void; navigate: (path: string) => void; } class InstanceTable extends React.PureComponent { public componentDidMount() { const { isLoading, instancesResponse, loadError } = this.props; if (!isLoading && !instancesResponse && !loadError) { - this.props.fetchInstances(); + this.props.loadInstanceList(); } } @@ -53,12 +73,44 @@ class InstanceTable extends React.PureComponent { - Instance - Server type - Version - Users - Statuses - Insularity + + Instance +