add sorting to tabular view
This commit is contained in:
parent
19b3a3806d
commit
ee48bc8d10
|
@ -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 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 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).
|
- Added federation tab that shows federation restrictions (only available for some Pleroma instances).
|
||||||
|
- Add tabular view of instances.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,39 @@ defmodule Backend.Api do
|
||||||
import Backend.Util
|
import Backend.Util
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
@spec get_instances(Integer.t() | nil) :: Scrivener.Page.t()
|
@type instance_sort_field :: :name | :user_count | :status_count | :insularity
|
||||||
def get_instances(page \\ nil) do
|
@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
|
Instance
|
||||||
|> where([i], not is_nil(i.type) and not i.opt_out)
|
|> where([i], not is_nil(i.type) and not i.opt_out)
|
||||||
|
|> maybe_order_by(sort_field, sort_direction)
|
||||||
|> Repo.paginate(page: page)
|
|> Repo.paginate(page: page)
|
||||||
end
|
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
|
@spec get_instance(String.t()) :: Instance.t() | nil
|
||||||
def get_instance(domain) do
|
def get_instance(domain) do
|
||||||
Instance
|
Instance
|
||||||
|
@ -41,7 +67,8 @@ defmodule Backend.Api do
|
||||||
* the user count is > the threshold
|
* the user count is > the threshold
|
||||||
* have x and y coordinates
|
* 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()]
|
@spec list_nodes() :: [Instance.t()]
|
||||||
def list_nodes(domain \\ nil) do
|
def list_nodes(domain \\ nil) do
|
||||||
|
|
|
@ -6,24 +6,54 @@ defmodule BackendWeb.InstanceController do
|
||||||
|
|
||||||
action_fallback(BackendWeb.FallbackController)
|
action_fallback(BackendWeb.FallbackController)
|
||||||
|
|
||||||
|
# sobelow_skip ["DOS.StringToAtom"]
|
||||||
def index(conn, params) do
|
def index(conn, params) do
|
||||||
page = Map.get(params, "page")
|
page = Map.get(params, "page")
|
||||||
|
sort_field = Map.get(params, "sortField")
|
||||||
|
sort_direction = Map.get(params, "sortDirection")
|
||||||
|
|
||||||
%{
|
cond do
|
||||||
entries: instances,
|
not Enum.member?([nil, "domain", "userCount", "statusCount", "insularity"], sort_field) ->
|
||||||
total_pages: total_pages,
|
render(conn, "error.json", error: "Invalid sort field")
|
||||||
page_number: page_number,
|
|
||||||
total_entries: total_entries,
|
|
||||||
page_size: page_size
|
|
||||||
} = Api.get_instances(page)
|
|
||||||
|
|
||||||
render(conn, "index.json",
|
not Enum.member?([nil, "asc", "desc"], sort_direction) ->
|
||||||
instances: instances,
|
render(conn, "error.json", error: "Invalid sort direction")
|
||||||
total_pages: total_pages,
|
|
||||||
page_number: page_number,
|
true ->
|
||||||
total_entries: total_entries,
|
sort_field =
|
||||||
page_size: page_size
|
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
|
end
|
||||||
|
|
||||||
def show(conn, %{"id" => domain}) do
|
def show(conn, %{"id" => domain}) do
|
||||||
|
|
|
@ -20,8 +20,8 @@ defmodule BackendWeb.InstanceView do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Used when rendering the index of all instances (the different from show.json is primarily that it does not
|
Used when rendering the index of all instances (the difference from show.json is primarily that
|
||||||
include peers).
|
it does not include peers).
|
||||||
"""
|
"""
|
||||||
def render("index_instance.json", %{instance: instance}) do
|
def render("index_instance.json", %{instance: instance}) do
|
||||||
%{
|
%{
|
||||||
|
@ -63,6 +63,10 @@ defmodule BackendWeb.InstanceView do
|
||||||
%{name: instance.domain}
|
%{name: instance.domain}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("error.json", %{error: error}) do
|
||||||
|
%{error: error}
|
||||||
|
end
|
||||||
|
|
||||||
defp render_personal_instance(instance) do
|
defp render_personal_instance(instance) do
|
||||||
%{
|
%{
|
||||||
name: instance.domain,
|
name: instance.domain,
|
||||||
|
|
|
@ -50,7 +50,10 @@ defmodule BackendWeb.InstanceControllerTest do
|
||||||
describe "update instance" do
|
describe "update instance" do
|
||||||
setup [:create_instance]
|
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)
|
conn = put(conn, Routes.instance_path(conn, :update, instance), instance: @update_attrs)
|
||||||
assert %{"id" => ^id} = json_response(conn, 200)["data"]
|
assert %{"id" => ^id} = json_response(conn, 200)["data"]
|
||||||
|
|
||||||
|
|
|
@ -11,15 +11,21 @@ const Backdrop = styled.div`
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Container = styled.div`
|
interface IContainerProps {
|
||||||
max-width: 800px;
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
const Container = styled.div<IContainerProps>`
|
||||||
|
max-width: ${props => (props.fullWidth ? "100%" : "800px")};
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Page: React.FC = ({ children }) => (
|
interface IPageProps {
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
const Page: React.FC<IPageProps> = ({ children, fullWidth }) => (
|
||||||
<Backdrop>
|
<Backdrop>
|
||||||
<Container>{children}</Container>
|
<Container fullWidth={fullWidth}>{children}</Container>
|
||||||
</Backdrop>
|
</Backdrop>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Button, ButtonGroup, Code, HTMLTable, Intent, NonIdealState, Spinner } from "@blueprintjs/core";
|
import { Button, ButtonGroup, Code, HTMLTable, Intent, NonIdealState, Spinner } from "@blueprintjs/core";
|
||||||
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
import { push } from "connected-react-router";
|
import { push } from "connected-react-router";
|
||||||
import { range, sortBy, sortedUniq, zip } from "lodash";
|
import { range, sortBy, sortedUniq, zip } from "lodash";
|
||||||
import * as numeral from "numeral";
|
import * as numeral from "numeral";
|
||||||
|
@ -7,7 +8,7 @@ import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { loadInstanceList } from "../../redux/actions";
|
import { loadInstanceList } from "../../redux/actions";
|
||||||
import { IAppState, IInstanceListResponse } from "../../redux/types";
|
import { IAppState, IInstanceListResponse, IInstanceSort, SortField } from "../../redux/types";
|
||||||
import { InstanceType } from "../atoms";
|
import { InstanceType } from "../atoms";
|
||||||
import { ErrorState } from "../molecules";
|
import { ErrorState } from "../molecules";
|
||||||
|
|
||||||
|
@ -21,19 +22,38 @@ const PaginationContainer = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
align-items: center;
|
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 {
|
interface IInstanceTableProps {
|
||||||
loadError: boolean;
|
loadError: boolean;
|
||||||
instancesResponse?: IInstanceListResponse;
|
instancesResponse?: IInstanceListResponse;
|
||||||
|
instanceListSort: IInstanceSort;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
fetchInstances: (page?: number) => void;
|
loadInstanceList: (page?: number, sort?: IInstanceSort) => void;
|
||||||
navigate: (path: string) => void;
|
navigate: (path: string) => void;
|
||||||
}
|
}
|
||||||
class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
const { isLoading, instancesResponse, loadError } = this.props;
|
const { isLoading, instancesResponse, loadError } = this.props;
|
||||||
if (!isLoading && !instancesResponse && !loadError) {
|
if (!isLoading && !instancesResponse && !loadError) {
|
||||||
this.props.fetchInstances();
|
this.props.loadInstanceList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,12 +73,44 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
<StyledTable striped={true} bordered={true} interactive={true}>
|
<StyledTable striped={true} bordered={true} interactive={true}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Instance</th>
|
<InstanceColumn>
|
||||||
<th>Server type</th>
|
Instance
|
||||||
<th>Version</th>
|
<Button
|
||||||
<th>Users</th>
|
minimal={true}
|
||||||
<th>Statuses</th>
|
icon={this.getSortIcon("domain")}
|
||||||
<th>Insularity</th>
|
onClick={this.sortByFactory("domain")}
|
||||||
|
intent={this.getSortIntent("domain")}
|
||||||
|
/>
|
||||||
|
</InstanceColumn>
|
||||||
|
<ServerColumn>Server type</ServerColumn>
|
||||||
|
<VersionColumn>Version</VersionColumn>
|
||||||
|
<UserCountColumn>
|
||||||
|
Users
|
||||||
|
<Button
|
||||||
|
minimal={true}
|
||||||
|
icon={this.getSortIcon("userCount")}
|
||||||
|
onClick={this.sortByFactory("userCount")}
|
||||||
|
intent={this.getSortIntent("userCount")}
|
||||||
|
/>
|
||||||
|
</UserCountColumn>
|
||||||
|
<StatusCountColumn>
|
||||||
|
Statuses
|
||||||
|
<Button
|
||||||
|
minimal={true}
|
||||||
|
icon={this.getSortIcon("statusCount")}
|
||||||
|
onClick={this.sortByFactory("statusCount")}
|
||||||
|
intent={this.getSortIntent("statusCount")}
|
||||||
|
/>
|
||||||
|
</StatusCountColumn>
|
||||||
|
<InsularityColumn>
|
||||||
|
Insularity
|
||||||
|
<Button
|
||||||
|
minimal={true}
|
||||||
|
icon={this.getSortIcon("insularity")}
|
||||||
|
onClick={this.sortByFactory("insularity")}
|
||||||
|
intent={this.getSortIntent("insularity")}
|
||||||
|
/>
|
||||||
|
</InsularityColumn>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -67,8 +119,8 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
<td>{i.name}</td>
|
<td>{i.name}</td>
|
||||||
<td>{i.type && <InstanceType type={i.type} />}</td>
|
<td>{i.type && <InstanceType type={i.type} />}</td>
|
||||||
<td>{i.version && <Code>{i.version}</Code>}</td>
|
<td>{i.version && <Code>{i.version}</Code>}</td>
|
||||||
<td>{i.userCount}</td>
|
<td>{i.userCount && numeral.default(i.userCount).format("0,0")}</td>
|
||||||
<td>{i.statusCount}</td>
|
<td>{i.statusCount && numeral.default(i.statusCount).format("0,0")}</td>
|
||||||
<td>{i.insularity && numeral.default(i.insularity).format("0.0%")}</td>
|
<td>{i.insularity && numeral.default(i.insularity).format("0.0%")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
@ -84,7 +136,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{zip(pagesToDisplay, pagesToDisplay.slice(1)).map(([page, nextPage], idx) => {
|
{zip(pagesToDisplay, pagesToDisplay.slice(1)).map(([page, nextPage], idx) => {
|
||||||
if (page === undefined) {
|
if (page === undefined) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
const isCurrentPage = currentPage === page;
|
const isCurrentPage = currentPage === page;
|
||||||
const isEndOfSection = nextPage !== undefined && page + 1 !== nextPage && page !== totalPages;
|
const isEndOfSection = nextPage !== undefined && page + 1 !== nextPage && page !== totalPages;
|
||||||
|
@ -113,14 +165,44 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sortByFactory = (field: SortField) => () => {
|
||||||
|
const { instancesResponse, instanceListSort } = this.props;
|
||||||
|
|
||||||
|
const page = (instancesResponse && instancesResponse.pageNumber) || 1;
|
||||||
|
const nextSortDirection =
|
||||||
|
instanceListSort.field === field && instanceListSort.direction === "desc" ? "asc" : "desc";
|
||||||
|
|
||||||
|
this.props.loadInstanceList(page, { field, direction: nextSortDirection });
|
||||||
|
};
|
||||||
|
|
||||||
private loadPageFactory = (page: number) => () => {
|
private loadPageFactory = (page: number) => () => {
|
||||||
this.props.fetchInstances(page);
|
this.props.loadInstanceList(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
private goToInstanceFactory = (domain: string) => () => {
|
private goToInstanceFactory = (domain: string) => () => {
|
||||||
this.props.navigate(`/instance/${domain}`);
|
this.props.navigate(`/instance/${domain}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private getSortIcon = (field: SortField) => {
|
||||||
|
const { instanceListSort } = this.props;
|
||||||
|
if (instanceListSort.field !== field) {
|
||||||
|
return IconNames.SORT;
|
||||||
|
} else if (instanceListSort.direction === "asc") {
|
||||||
|
return IconNames.SORT_ASC;
|
||||||
|
} else {
|
||||||
|
return IconNames.SORT_DESC;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getSortIntent = (field: SortField) => {
|
||||||
|
const { instanceListSort } = this.props;
|
||||||
|
if (instanceListSort.field === field) {
|
||||||
|
return Intent.PRIMARY;
|
||||||
|
} else {
|
||||||
|
return Intent.NONE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private getPagesToDisplay = (totalPages: number, currentPage: number) => {
|
private getPagesToDisplay = (totalPages: number, currentPage: number) => {
|
||||||
if (totalPages < 10) {
|
if (totalPages < 10) {
|
||||||
return range(1, totalPages + 1);
|
return range(1, totalPages + 1);
|
||||||
|
@ -138,13 +220,14 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => {
|
const mapStateToProps = (state: IAppState) => {
|
||||||
return {
|
return {
|
||||||
|
instanceListSort: state.data.instanceListSort,
|
||||||
instancesResponse: state.data.instancesResponse,
|
instancesResponse: state.data.instancesResponse,
|
||||||
isLoading: state.data.isLoadingInstanceList,
|
isLoading: state.data.isLoadingInstanceList,
|
||||||
loadError: state.data.instanceListLoadError
|
loadError: state.data.instanceListLoadError
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
fetchInstances: (page?: number) => dispatch(loadInstanceList(page) as any),
|
loadInstanceList: (page?: number, sort?: IInstanceSort) => dispatch(loadInstanceList(page, sort) as any),
|
||||||
navigate: (path: string) => dispatch(push(path))
|
navigate: (path: string) => dispatch(push(path))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { InstanceTable } from "../organisms";
|
||||||
class TableScreen extends React.PureComponent {
|
class TableScreen extends React.PureComponent {
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page fullWidth={true}>
|
||||||
<H1>{"Instances"}</H1>
|
<H1>{"Instances"}</H1>
|
||||||
<InstanceTable />
|
<InstanceTable />
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Dispatch } from "redux";
|
||||||
import { push } from "connected-react-router";
|
import { push } from "connected-react-router";
|
||||||
import { ISearchFilter } from "../searchFilters";
|
import { ISearchFilter } from "../searchFilters";
|
||||||
import { getFromApi } from "../util";
|
import { getFromApi } from "../util";
|
||||||
import { ActionType, IAppState, IGraph, IInstanceDetails, ISearchResponse } from "./types";
|
import { ActionType, IAppState, IGraph, IInstanceDetails, IInstanceSort, ISearchResponse } from "./types";
|
||||||
|
|
||||||
// Instance details
|
// Instance details
|
||||||
const requestInstanceDetails = (instanceName: string) => {
|
const requestInstanceDetails = (instanceName: string) => {
|
||||||
|
@ -49,7 +49,8 @@ const graphLoadFailed = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Instance list
|
// Instance list
|
||||||
const requestInstanceList = () => ({
|
const requestInstanceList = (sort?: IInstanceSort) => ({
|
||||||
|
payload: sort,
|
||||||
type: ActionType.REQUEST_INSTANCES
|
type: ActionType.REQUEST_INSTANCES
|
||||||
});
|
});
|
||||||
const receiveInstanceList = (instances: IInstanceDetails[]) => ({
|
const receiveInstanceList = (instances: IInstanceDetails[]) => ({
|
||||||
|
@ -151,14 +152,19 @@ export const fetchGraph = () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadInstanceList = (page?: number) => {
|
export const loadInstanceList = (page?: number, sort?: IInstanceSort) => {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch, getState: () => IAppState) => {
|
||||||
dispatch(requestInstanceList());
|
sort = sort ? sort : getState().data.instanceListSort;
|
||||||
let params = "";
|
dispatch(requestInstanceList(sort));
|
||||||
|
const params: string[] = [];
|
||||||
if (!!page) {
|
if (!!page) {
|
||||||
params += `page=${page}`;
|
params.push(`page=${page}`);
|
||||||
}
|
}
|
||||||
const path = !!params ? `instances?${params}` : "instances";
|
if (!!sort) {
|
||||||
|
params.push(`sortField=${sort.field}`);
|
||||||
|
params.push(`sortDirection=${sort.direction}`);
|
||||||
|
}
|
||||||
|
const path = !!params ? `instances?${params.join("&")}` : "instances";
|
||||||
return getFromApi(path)
|
return getFromApi(path)
|
||||||
.then(instancesListResponse => dispatch(receiveInstanceList(instancesListResponse)))
|
.then(instancesListResponse => dispatch(receiveInstanceList(instancesListResponse)))
|
||||||
.catch(() => dispatch(instanceListLoadFailed()));
|
.catch(() => dispatch(instanceListLoadFailed()));
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { ActionType, IAction, ICurrentInstanceState, IDataState, ISearchState }
|
||||||
const initialDataState: IDataState = {
|
const initialDataState: IDataState = {
|
||||||
graphLoadError: false,
|
graphLoadError: false,
|
||||||
instanceListLoadError: false,
|
instanceListLoadError: false,
|
||||||
|
instanceListSort: { field: "userCount", direction: "desc" },
|
||||||
isLoadingGraph: false,
|
isLoadingGraph: false,
|
||||||
isLoadingInstanceList: false
|
isLoadingInstanceList: false
|
||||||
};
|
};
|
||||||
|
@ -35,6 +36,7 @@ const data = (state: IDataState = initialDataState, action: IAction): IDataState
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
instanceListLoadError: false,
|
instanceListLoadError: false,
|
||||||
|
instanceListSort: action.payload,
|
||||||
instancesResponse: undefined,
|
instancesResponse: undefined,
|
||||||
isLoadingInstanceList: true
|
isLoadingInstanceList: true
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,6 +30,13 @@ export interface IAction {
|
||||||
payload: any;
|
payload: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SortField = "domain" | "userCount" | "statusCount" | "insularity";
|
||||||
|
export type SortDirection = "asc" | "desc";
|
||||||
|
export interface IInstanceSort {
|
||||||
|
field: SortField;
|
||||||
|
direction: SortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPeer {
|
export interface IPeer {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
@ -129,6 +136,7 @@ export interface ICurrentInstanceState {
|
||||||
export interface IDataState {
|
export interface IDataState {
|
||||||
graphResponse?: IGraphResponse;
|
graphResponse?: IGraphResponse;
|
||||||
instancesResponse?: IInstanceListResponse;
|
instancesResponse?: IInstanceListResponse;
|
||||||
|
instanceListSort: IInstanceSort;
|
||||||
isLoadingGraph: boolean;
|
isLoadingGraph: boolean;
|
||||||
isLoadingInstanceList: boolean;
|
isLoadingInstanceList: boolean;
|
||||||
graphLoadError: boolean;
|
graphLoadError: boolean;
|
||||||
|
|
Loading…
Reference in a new issue