Add useAdminAccount hook, refactor admin accounts in dashboard, refactor admin actions

environments/review-rm-pl-admi-mja3h5/deployments/4925
Alex Gleason 2024-10-19 13:23:08 -05:00
rodzic dde5e9154b
commit c0f8c9d5e7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
6 zmienionych plików z 114 dodań i 116 usunięć

Wyświetl plik

@ -173,16 +173,12 @@ function fetchUsers(filters: string[] = [], page = 1, query?: string | null, pag
try { try {
const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params }); const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params });
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next'); const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next')?.uri;
const count = next
? page * pageSize + 1
: (page - 1) * pageSize + accounts.length;
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account))); dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id))); dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id)));
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false }); dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, accounts, pageSize, filters, page, next });
return { users: accounts, count, pageSize, next: next?.uri || false }; return { accounts, next };
} catch (error) { } catch (error) {
return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }); return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
} }
@ -401,13 +397,13 @@ const fetchUserIndex = () =>
dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST }); dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST });
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize)) dispatch(fetchUsers([...filters], page + 1, query, pageSize))
.then((data: any) => { .then((data: any) => {
if (data.error) { if (data.error) {
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL }); dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
} else { } else {
const { users, count, next } = (data); const { accounts, next } = data;
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next }); dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, accounts, next });
} }
}).catch(() => { }).catch(() => {
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL }); dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
@ -422,13 +418,13 @@ const expandUserIndex = () =>
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST }); dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next)) dispatch(fetchUsers([...filters], page + 1, query, pageSize, next))
.then((data: any) => { .then((data) => {
if (data.error) { if ('error' in data) {
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL }); dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
} else { } else {
const { users, count, next } = (data); const { accounts, next } = data;
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next }); dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, accounts, next });
} }
}).catch(() => { }).catch(() => {
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL }); dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });

Wyświetl plik

@ -1,7 +1,7 @@
import { HTTPError } from './HTTPError'; import { HTTPError } from './HTTPError';
interface Opts { interface Opts {
searchParams?: Record<string, string | number | boolean>; searchParams?: URLSearchParams | Record<string, string | number | boolean>;
headers?: Record<string, string>; headers?: Record<string, string>;
signal?: AbortSignal; signal?: AbortSignal;
} }
@ -51,7 +51,9 @@ export class MastodonClient {
const url = new URL(path, this.baseUrl); const url = new URL(path, this.baseUrl);
if (opts.searchParams) { if (opts.searchParams) {
const params = Object const params = opts.searchParams instanceof URLSearchParams
? opts.searchParams
: Object
.entries(opts.searchParams) .entries(opts.searchParams)
.map(([key, value]) => ([key, String(value)])); .map(([key, value]) => ([key, String(value)]));

Wyświetl plik

@ -0,0 +1,29 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useEntities } from 'soapbox/entity-store/hooks';
import { useApi } from 'soapbox/hooks';
import { adminAccountSchema } from 'soapbox/schemas/admin-account';
type Filter = 'local' | 'remote' | 'active' | 'pending' | 'disabled' | 'silenced' | 'suspended' | 'sensitized';
/** https://docs.joinmastodon.org/methods/admin/accounts/#v1 */
export function useAdminAccounts(filters: Filter[] = [], limit?: number) {
const api = useApi();
const searchParams = new URLSearchParams();
for (const filter of filters) {
searchParams.append(filter, 'true');
}
if (typeof limit === 'number') {
searchParams.append('limit', limit.toString());
}
const { entities, ...rest } = useEntities(
[Entities.ACCOUNTS, searchParams.toString()],
() => api.get('/api/v1/admin/accounts', { searchParams }),
{ schema: adminAccountSchema.transform(({ account }) => account) },
);
return { accounts: entities, ...rest };
}

Wyświetl plik

@ -1,16 +1,13 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable'; import React from 'react';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { fetchUsers } from 'soapbox/actions/admin'; import { useAdminAccounts } from 'soapbox/api/hooks/admin/useAdminAccounts';
import Account from 'soapbox/components/account';
import { Widget } from 'soapbox/components/ui'; import { Widget } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' }, title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
expand: { id: 'admin.latest_accounts_panel.more', defaultMessage: 'Click to see {count, plural, one {# account} other {# accounts}}' },
}); });
interface ILatestAccountsPanel { interface ILatestAccountsPanel {
@ -20,18 +17,8 @@ interface ILatestAccountsPanel {
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => { const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
const intl = useIntl(); const intl = useIntl();
const history = useHistory(); const history = useHistory();
const dispatch = useAppDispatch();
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
const [total, setTotal] = useState(accountIds.size); const { accounts } = useAdminAccounts(['local', 'active'], limit);
useEffect(() => {
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
.then((value) => {
setTotal((value as { count: number }).count);
})
.catch(() => {});
}, []);
const handleAction = () => { const handleAction = () => {
history.push('/soapbox/admin/users'); history.push('/soapbox/admin/users');
@ -41,10 +28,9 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
<Widget <Widget
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
onActionClick={handleAction} onActionClick={handleAction}
actionTitle={intl.formatMessage(messages.expand, { count: total })}
> >
{accountIds.take(limit).map((account) => ( {accounts.slice(0, limit).map(account => (
<AccountContainer key={account} id={account} withRelationship={false} withDate /> <Account key={account.id} account={account} withRelationship={false} withDate />
))} ))}
</Widget> </Widget>
); );

Wyświetl plik

@ -1,12 +1,10 @@
import debounce from 'lodash/debounce'; import React from 'react';
import React, { useCallback, useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { expandUserIndex, fetchUserIndex, setUserIndexQuery } from 'soapbox/actions/admin'; import { useAdminAccounts } from 'soapbox/api/hooks/admin/useAdminAccounts';
import Account from 'soapbox/components/account';
import ScrollableList from 'soapbox/components/scrollable-list'; import ScrollableList from 'soapbox/components/scrollable-list';
import { Column, Input } from 'soapbox/components/ui'; import { Column } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.admin.users', defaultMessage: 'Users' }, heading: { id: 'column.admin.users', defaultMessage: 'Users' },
@ -15,51 +13,30 @@ const messages = defineMessages({
}); });
const UserIndex: React.FC = () => { const UserIndex: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const { isLoading, items, total, query, next } = useAppSelector((state) => state.admin_user_index); const { accounts, isLoading, hasNextPage, fetchNextPage } = useAdminAccounts(['local']);
const handleLoadMore = () => { const handleLoadMore = () => {
if (!isLoading) dispatch(expandUserIndex()); if (!isLoading) {
fetchNextPage();
}
}; };
const updateQuery = useCallback(debounce(() => {
dispatch(fetchUserIndex());
}, 900, { leading: true }), []);
const handleQueryChange: React.ChangeEventHandler<HTMLInputElement> = e => {
dispatch(setUserIndexQuery(e.target.value));
updateQuery();
};
useEffect(() => {
updateQuery();
}, []);
const hasMore = items.count() < total && !!next;
const showLoading = isLoading && items.isEmpty();
return ( return (
<Column label={intl.formatMessage(messages.heading)}> <Column label={intl.formatMessage(messages.heading)}>
<Input
value={query}
onChange={handleQueryChange}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
/>
<ScrollableList <ScrollableList
scrollKey='user-index' scrollKey='user-index'
hasMore={hasMore} hasMore={hasNextPage}
isLoading={isLoading} isLoading={isLoading}
showLoading={showLoading} showLoading={isLoading}
onLoadMore={handleLoadMore} onLoadMore={handleLoadMore}
emptyMessage={intl.formatMessage(messages.empty)} emptyMessage={intl.formatMessage(messages.empty)}
className='mt-4' className='mt-4'
itemClassName='pb-4' itemClassName='pb-4'
> >
{items.map(id => {accounts.map((account) =>
<AccountContainer key={id} id={id} withDate />, <Account key={account.id} account={account} withDate />,
)} )}
</ScrollableList> </ScrollableList>
</Column> </Column>

Wyświetl plik

@ -1,5 +1,3 @@
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
import { import {
ADMIN_USER_INDEX_EXPAND_FAIL, ADMIN_USER_INDEX_EXPAND_FAIL,
ADMIN_USER_INDEX_EXPAND_REQUEST, ADMIN_USER_INDEX_EXPAND_REQUEST,
@ -11,57 +9,67 @@ import {
} from 'soapbox/actions/admin'; } from 'soapbox/actions/admin';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
import type { APIEntity } from 'soapbox/types/entities';
const ReducerRecord = ImmutableRecord({ interface State {
isLoading: boolean;
loaded: boolean;
items: Set<string>;
filters: Set<string>;
pageSize: number;
page: number;
query: string;
next: string | null;
}
function createState(): State {
return {
isLoading: false, isLoading: false,
loaded: false, loaded: false,
items: ImmutableOrderedSet<string>(), items: new Set(),
filters: ImmutableSet(['local', 'active']), filters: new Set(['local', 'active']),
total: Infinity,
pageSize: 50, pageSize: 50,
page: -1, page: -1,
query: '', query: '',
next: null as string | null, next: null,
}); };
}
type State = ReturnType<typeof ReducerRecord>; export default function admin_user_index(state: State = createState(), action: AnyAction): State {
export default function admin_user_index(state: State = ReducerRecord(), action: AnyAction): State {
switch (action.type) { switch (action.type) {
case ADMIN_USER_INDEX_QUERY_SET: case ADMIN_USER_INDEX_QUERY_SET:
return state.set('query', action.query); return { ...state, query: action.query };
case ADMIN_USER_INDEX_FETCH_REQUEST: case ADMIN_USER_INDEX_FETCH_REQUEST:
return state return {
.set('isLoading', true) ...state,
.set('loaded', true) isLoading: true,
.set('items', ImmutableOrderedSet()) loaded: true,
.set('total', action.count) items: new Set(),
.set('page', 0) page: 0,
.set('next', null); next: null,
};
case ADMIN_USER_INDEX_FETCH_SUCCESS: case ADMIN_USER_INDEX_FETCH_SUCCESS:
return state return {
.set('isLoading', false) ...state,
.set('loaded', true) isLoading: false,
.set('items', ImmutableOrderedSet(action.users.map((user: APIEntity) => user.id))) loaded: true,
.set('total', action.count) items: new Set(action.accounts.map((account: { id: string }) => account.id)),
.set('page', 1) page: 1,
.set('next', action.next); next: action.next,
};
case ADMIN_USER_INDEX_FETCH_FAIL: case ADMIN_USER_INDEX_FETCH_FAIL:
case ADMIN_USER_INDEX_EXPAND_FAIL: case ADMIN_USER_INDEX_EXPAND_FAIL:
return state return { ...state, isLoading: false };
.set('isLoading', false);
case ADMIN_USER_INDEX_EXPAND_REQUEST: case ADMIN_USER_INDEX_EXPAND_REQUEST:
return state return { ...state, isLoading: true };
.set('isLoading', true);
case ADMIN_USER_INDEX_EXPAND_SUCCESS: case ADMIN_USER_INDEX_EXPAND_SUCCESS:
return state return {
.set('isLoading', false) ...state,
.set('loaded', true) isLoading: false,
.set('items', state.items.union(action.users.map((user: APIEntity) => user.id))) loaded: true,
.set('total', action.count) items: new Set([...state.items, ...action.accounts.map((account: { id: string }) => account.id)]),
.set('page', 1) page: 1,
.set('next', action.next); next: action.next,
};
default: default:
return state; return state;
} }