Dashboard styles, typescript, add useAppDispatch
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
3ecd7c3961
commit
39b819241f
|
@ -1,87 +0,0 @@
|
||||||
import { is } from 'immutable';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { fetchUsers } from 'soapbox/actions/admin';
|
|
||||||
import compareId from 'soapbox/compare_id';
|
|
||||||
import AccountListPanel from 'soapbox/features/ui/components/account_list_panel';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
|
||||||
expand: { id: 'admin.latest_accounts_panel.expand_message', defaultMessage: 'Click to see {count} more {count, plural, one {account} other {accounts}}' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const accountIds = state.getIn(['admin', 'latestUsers']);
|
|
||||||
|
|
||||||
// HACK: AdminAPI only recently started sorting new users at the top.
|
|
||||||
// Try a dirty check to see if the users are sorted properly, or don't show the panel.
|
|
||||||
// Probably works most of the time.
|
|
||||||
const sortedIds = accountIds.sort(compareId).reverse();
|
|
||||||
const hasDates = accountIds.every(id => state.getIn(['accounts', id, 'created_at']));
|
|
||||||
const isSorted = hasDates && is(accountIds, sortedIds);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isSorted,
|
|
||||||
accountIds,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
|
||||||
@injectIntl
|
|
||||||
class LatestAccountsPanel extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
accountIds: ImmutablePropTypes.orderedSet.isRequired,
|
|
||||||
limit: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
limit: 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
total: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { dispatch, limit } = this.props;
|
|
||||||
|
|
||||||
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
|
|
||||||
.then(({ count }) => {
|
|
||||||
this.setState({ total: count });
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { intl, accountIds, limit, isSorted, ...props } = this.props;
|
|
||||||
const { total } = this.state;
|
|
||||||
|
|
||||||
if (!isSorted || !accountIds || accountIds.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandCount = total - accountIds.size;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccountListPanel
|
|
||||||
icon={require('@tabler/icons/icons/users.svg')}
|
|
||||||
title={intl.formatMessage(messages.title)}
|
|
||||||
accountIds={accountIds}
|
|
||||||
limit={limit}
|
|
||||||
total={total}
|
|
||||||
expandMessage={intl.formatMessage(messages.expand, { count: expandCount })}
|
|
||||||
expandRoute='/admin/users'
|
|
||||||
withDate
|
|
||||||
withRelationship={false}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { OrderedSet as ImmutableOrderedSet, is } from 'immutable';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { fetchUsers } from 'soapbox/actions/admin';
|
||||||
|
import compareId from 'soapbox/compare_id';
|
||||||
|
import { Text, Widget } from 'soapbox/components/ui';
|
||||||
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
||||||
|
expand: { id: 'admin.latest_accounts_panel.expand_message', defaultMessage: 'Click to see {count} more {count, plural, one {account} other {accounts}}' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ILatestAccountsPanel {
|
||||||
|
limit?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers'));
|
||||||
|
const hasDates = useAppSelector((state) => accountIds.every(id => !!state.accounts.getIn([id, 'created_at'])));
|
||||||
|
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
|
||||||
|
.then((value) => {
|
||||||
|
setTotal((value as { count: number }).count);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sortedIds = accountIds.sort(compareId).reverse();
|
||||||
|
const isSorted = hasDates && is(accountIds, sortedIds);
|
||||||
|
|
||||||
|
if (!isSorted || !accountIds || accountIds.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandCount = total - accountIds.size;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget title={intl.formatMessage(messages.title)}>
|
||||||
|
{accountIds.take(limit).map((account) => (
|
||||||
|
<AccountContainer key={account} id={account} withRelationship={false} />
|
||||||
|
))}
|
||||||
|
{!!expandCount && (
|
||||||
|
<Link className='wtf-panel__expand-btn' to='/admin/users'>
|
||||||
|
<Text>{intl.formatMessage(messages.expand, { count: expandCount })}</Text>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LatestAccountsPanel;
|
|
@ -7,6 +7,7 @@ import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email_list';
|
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email_list';
|
||||||
|
import { Text } from 'soapbox/components/ui';
|
||||||
import sourceCode from 'soapbox/utils/code';
|
import sourceCode from 'soapbox/utils/code';
|
||||||
import { parseVersion } from 'soapbox/utils/features';
|
import { parseVersion } from 'soapbox/utils/features';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
@ -86,56 +87,46 @@ class Dashboard extends ImmutablePureComponent {
|
||||||
<Column icon='tachometer-alt' label={intl.formatMessage(messages.heading)}>
|
<Column icon='tachometer-alt' label={intl.formatMessage(messages.heading)}>
|
||||||
<div className='dashcounters'>
|
<div className='dashcounters'>
|
||||||
{mau && <div className='dashcounter'>
|
{mau && <div className='dashcounter'>
|
||||||
<div>
|
<Text align='center' size='2xl' weight='medium'>
|
||||||
<div className='dashcounter__num'>
|
|
||||||
<FormattedNumber value={mau} />
|
<FormattedNumber value={mau} />
|
||||||
</div>
|
</Text>
|
||||||
<div className='dashcounter__label'>
|
<Text align='center'>
|
||||||
<FormattedMessage id='admin.dashcounters.mau_label' defaultMessage='monthly active users' />
|
<FormattedMessage id='admin.dashcounters.mau_label' defaultMessage='monthly active users' />
|
||||||
</div>
|
</Text>
|
||||||
</div>
|
|
||||||
</div>}
|
</div>}
|
||||||
<div className='dashcounter'>
|
<Link className='dashcounter' to='/admin/users'>
|
||||||
<Link to='/admin/users'>
|
<Text align='center' size='2xl' weight='medium'>
|
||||||
<div className='dashcounter__num'>
|
|
||||||
<FormattedNumber value={userCount} />
|
<FormattedNumber value={userCount} />
|
||||||
</div>
|
</Text>
|
||||||
<div className='dashcounter__label'>
|
<Text align='center'>
|
||||||
<FormattedMessage id='admin.dashcounters.user_count_label' defaultMessage='total users' />
|
<FormattedMessage id='admin.dashcounters.user_count_label' defaultMessage='total users' />
|
||||||
</div>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
{isNumber(retention) && (
|
{isNumber(retention) && (
|
||||||
<div className='dashcounter'>
|
<div className='dashcounter'>
|
||||||
<div>
|
<Text align='center' size='2xl' weight='medium'>
|
||||||
<div className='dashcounter__num'>
|
|
||||||
{retention}%
|
{retention}%
|
||||||
</div>
|
</Text>
|
||||||
<div className='dashcounter__label'>
|
<Text align='center'>
|
||||||
<FormattedMessage id='admin.dashcounters.retention_label' defaultMessage='user retention' />
|
<FormattedMessage id='admin.dashcounters.retention_label' defaultMessage='user retention' />
|
||||||
</div>
|
</Text>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='dashcounter'>
|
<Link className='dashcounter' to='/timeline/local'>
|
||||||
<Link to='/timeline/local'>
|
<Text align='center' size='2xl' weight='medium'>
|
||||||
<div className='dashcounter__num'>
|
|
||||||
<FormattedNumber value={instance.getIn(['stats', 'status_count'])} />
|
<FormattedNumber value={instance.getIn(['stats', 'status_count'])} />
|
||||||
</div>
|
</Text>
|
||||||
<div className='dashcounter__label'>
|
<Text align='center'>
|
||||||
<FormattedMessage id='admin.dashcounters.status_count_label' defaultMessage='posts' />
|
<FormattedMessage id='admin.dashcounters.status_count_label' defaultMessage='posts' />
|
||||||
</div>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
<div className='dashcounter'>
|
<div className='dashcounter'>
|
||||||
<div>
|
<Text align='center' size='2xl' weight='medium'>
|
||||||
<div className='dashcounter__num'>
|
|
||||||
<FormattedNumber value={instance.getIn(['stats', 'domain_count'])} />
|
<FormattedNumber value={instance.getIn(['stats', 'domain_count'])} />
|
||||||
</div>
|
</Text>
|
||||||
<div className='dashcounter__label'>
|
<Text align='center'>
|
||||||
<FormattedMessage id='admin.dashcounters.domain_count_label' defaultMessage='peers' />
|
<FormattedMessage id='admin.dashcounters.domain_count_label' defaultMessage='peers' />
|
||||||
</div>
|
</Text>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{account.admin && <RegistrationModePicker />}
|
{account.admin && <RegistrationModePicker />}
|
||||||
|
|
|
@ -116,6 +116,7 @@ class UserIndex extends ImmutablePureComponent {
|
||||||
showLoading={showLoading}
|
showLoading={showLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={intl.formatMessage(messages.empty)}
|
emptyMessage={intl.formatMessage(messages.empty)}
|
||||||
|
className='mt-4 space-y-4'
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} withDate />,
|
<AccountContainer key={id} id={id} withDate />,
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { AppDispatch } from 'soapbox/store';
|
||||||
|
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
|
@ -1,65 +1,9 @@
|
||||||
.dashcounters {
|
.dashcounters {
|
||||||
display: grid;
|
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 mb-4;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
|
||||||
margin: 0 -5px 0;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashcounter {
|
.dashcounter {
|
||||||
box-sizing: border-box;
|
@apply bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center space-y-2 hover:-translate-y-1 transition-transform cursor-pointer;
|
||||||
flex: 0 0 33.333%;
|
|
||||||
padding: 0 5px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
> a,
|
|
||||||
> div {
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
display: block;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--accent-color--faint);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: 0.2s;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> a:hover {
|
|
||||||
background: var(--accent-color--med);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__num,
|
|
||||||
&__icon,
|
|
||||||
&__text {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 30px;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.svg-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
stroke-width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
font-size: 14px;
|
|
||||||
color: hsla(var(--primary-text-color_hsl), 0.6);
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashwidgets {
|
.dashwidgets {
|
||||||
|
|
|
@ -139,13 +139,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__expand-btn {
|
&__expand-btn {
|
||||||
|
@apply border-gray-300 dark:border-gray-600;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 46px;
|
max-height: 46px;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-top: 1px solid;
|
border-top: 1px solid;
|
||||||
border-color: var(--brand-color--faint);
|
|
||||||
transition: max-height 150ms ease;
|
transition: max-height 150ms ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
Loading…
Reference in New Issue