diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js deleted file mode 100644 index 93a38c53fa..0000000000 Binary files a/app/soapbox/features/admin/index.js and /dev/null differ diff --git a/app/soapbox/features/admin/index.tsx b/app/soapbox/features/admin/index.tsx new file mode 100644 index 0000000000..948c9b5c92 --- /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 ba5e3cbe40..db04b30d23 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 => {