diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js deleted file mode 100644 index 93a38c53f..000000000 --- a/app/soapbox/features/admin/index.js +++ /dev/null @@ -1,154 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; - -import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email_list'; -import { Text } from 'soapbox/components/ui'; -import sourceCode from 'soapbox/utils/code'; -import { parseVersion } from 'soapbox/utils/features'; -import { getFeatures } from 'soapbox/utils/features'; -import { isNumber } from 'soapbox/utils/numbers'; - -import Column from '../ui/components/column'; - -import RegistrationModePicker from './components/registration_mode_picker'; - -// https://stackoverflow.com/a/53230807 -const download = (response, filename) => { - const url = URL.createObjectURL(new Blob([response.data])); - const link = document.createElement('a'); - link.href = url; - link.setAttribute('download', filename); - document.body.appendChild(link); - link.click(); - link.remove(); -}; - -const messages = defineMessages({ - heading: { id: 'column.admin.dashboard', defaultMessage: 'Dashboard' }, -}); - -const mapStateToProps = (state, props) => { - const me = state.get('me'); - - return { - instance: state.get('instance'), - supportsEmailList: getFeatures(state.get('instance')).emailList, - account: state.getIn(['accounts', me]), - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class Dashboard extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - instance: ImmutablePropTypes.map.isRequired, - supportsEmailList: PropTypes.bool, - account: ImmutablePropTypes.record, - }; - - handleSubscribersClick = e => { - this.props.dispatch(getSubscribersCsv()).then((response) => { - download(response, 'subscribers.csv'); - }).catch(() => {}); - e.preventDefault(); - } - - handleUnsubscribersClick = e => { - this.props.dispatch(getUnsubscribersCsv()).then((response) => { - download(response, 'unsubscribers.csv'); - }).catch(() => {}); - e.preventDefault(); - } - - handleCombinedClick = e => { - this.props.dispatch(getCombinedCsv()).then((response) => { - download(response, 'combined.csv'); - }).catch(() => {}); - e.preventDefault(); - } - - render() { - const { intl, instance, supportsEmailList, account } = this.props; - const v = parseVersion(instance.get('version')); - const userCount = instance.getIn(['stats', 'user_count']); - const mau = instance.getIn(['pleroma', 'stats', 'mau']); - const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : null; - - if (!account) return null; - - return ( - -
- {mau &&
- - - - - - -
} - - - - - - - - - {isNumber(retention) && ( -
- - {retention}% - - - - -
- )} - - - - - - - - -
- - - - - - -
-
- {account.admin && } -
-
-

-
    -
  • {sourceCode.displayName} {sourceCode.version}
  • -
  • {v.software} {v.version}
  • -
-
- {supportsEmailList && account.admin &&
-

- -
} -
-
- ); - } - -} diff --git a/app/soapbox/features/admin/index.tsx b/app/soapbox/features/admin/index.tsx new file mode 100644 index 000000000..948c9b5c9 --- /dev/null +++ b/app/soapbox/features/admin/index.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { defineMessages, useIntl, FormattedMessage, FormattedNumber } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email_list'; +import { Text } from 'soapbox/components/ui'; +import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks'; +import sourceCode from 'soapbox/utils/code'; +import { parseVersion } from 'soapbox/utils/features'; +import { isNumber } from 'soapbox/utils/numbers'; + +import Column from '../ui/components/column'; + +import RegistrationModePicker from './components/registration_mode_picker'; + +import type { AxiosResponse } from 'axios'; + +/** Download the file from the response instead of opening it in a tab. */ +// https://stackoverflow.com/a/53230807 +const download = (response: AxiosResponse, filename: string) => { + const url = URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + link.remove(); +}; + +const messages = defineMessages({ + heading: { id: 'column.admin.dashboard', defaultMessage: 'Dashboard' }, +}); + +const Dashboard: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const instance = useAppSelector(state => state.instance); + const features = useFeatures(); + const account = useOwnAccount(); + + const handleSubscribersClick: React.MouseEventHandler = e => { + dispatch(getSubscribersCsv()).then((response) => { + download(response, 'subscribers.csv'); + }).catch(() => {}); + e.preventDefault(); + }; + + const handleUnsubscribersClick: React.MouseEventHandler = e => { + dispatch(getUnsubscribersCsv()).then((response) => { + download(response, 'unsubscribers.csv'); + }).catch(() => {}); + e.preventDefault(); + }; + + const handleCombinedClick: React.MouseEventHandler = e => { + dispatch(getCombinedCsv()).then((response) => { + download(response, 'combined.csv'); + }).catch(() => {}); + e.preventDefault(); + }; + + const v = parseVersion(instance.version); + + const userCount = instance.stats.get('user_count'); + const statusCount = instance.stats.get('status_count'); + const domainCount = instance.stats.get('domain_count'); + + const mau = instance.pleroma.getIn(['stats', 'mau']) as number | undefined; + const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : null; + + if (!account) return null; + + return ( + +
+ {isNumber(mau) && ( +
+ + + + + + +
+ )} + {isNumber(userCount) && ( + + + + + + + + + )} + {isNumber(retention) && ( +
+ + {retention}% + + + + +
+ )} + {isNumber(statusCount) && ( + + + + + + + + + )} + {isNumber(domainCount) && ( +
+ + + + + + +
+ )} +
+ + {account.admin && } + +
+
+

+
    +
  • {sourceCode.displayName} {sourceCode.version}
  • +
  • {v.software} {v.version}
  • +
+
+ {features.emailList && account.admin &&
+

+ +
} +
+
+ ); +}; + +export default Dashboard; diff --git a/app/soapbox/utils/numbers.tsx b/app/soapbox/utils/numbers.tsx index ba5e3cbe4..db04b30d2 100644 --- a/app/soapbox/utils/numbers.tsx +++ b/app/soapbox/utils/numbers.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormattedNumber } from 'react-intl'; /** Check if a value is REALLY a number. */ -export const isNumber = (number: unknown): boolean => typeof number === 'number' && !isNaN(number); +export const isNumber = (value: unknown): value is number => typeof value === 'number' && !isNaN(value); /** Display a number nicely for the UI, eg 1000 becomes 1K. */ export const shortNumberFormat = (number: any): React.ReactNode => {