pl-fe: steal more dashboard code from mastodon
Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -4411,7 +4411,7 @@ class PlApiClient {
|
|||||||
getRetention: async (start_at: string, end_at: string, frequency: 'day' | 'month') => {
|
getRetention: async (start_at: string, end_at: string, frequency: 'day' | 'month') => {
|
||||||
const response = await this.request('/api/v1/admin/retention', { method: 'POST', params: { start_at, end_at, frequency } });
|
const response = await this.request('/api/v1/admin/retention', { method: 'POST', params: { start_at, end_at, frequency } });
|
||||||
|
|
||||||
return v.parse(adminCohortSchema, response.json);
|
return v.parse(filteredArray(adminCohortSchema), response.json);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const adminCohortSchema = v.object({
|
|||||||
data: v.array(v.object({
|
data: v.array(v.object({
|
||||||
date: datetimeSchema,
|
date: datetimeSchema,
|
||||||
rate: v.number(),
|
rate: v.number(),
|
||||||
value: v.pipe(v.number(), v.integer()),
|
value: v.pipe(v.unknown(), v.transform(Number)),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import * as v from 'valibot';
|
|||||||
*/
|
*/
|
||||||
const adminDimensionSchema = v.object({
|
const adminDimensionSchema = v.object({
|
||||||
key: v.string(),
|
key: v.string(),
|
||||||
data: v.object({
|
data: v.array(v.object({
|
||||||
key: v.string(),
|
key: v.string(),
|
||||||
human_key: v.string(),
|
human_key: v.string(),
|
||||||
value: v.string(),
|
value: v.string(),
|
||||||
unit: v.fallback(v.optional(v.string()), undefined),
|
unit: v.fallback(v.optional(v.string()), undefined),
|
||||||
human_value: v.fallback(v.optional(v.string()), undefined),
|
human_value: v.fallback(v.optional(v.string()), undefined),
|
||||||
}),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pl-api",
|
"name": "pl-api",
|
||||||
"version": "1.0.0-rc.57",
|
"version": "1.0.0-rc.58",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api",
|
"homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@ -104,7 +104,7 @@
|
|||||||
"multiselect-react-dropdown": "^2.0.25",
|
"multiselect-react-dropdown": "^2.0.25",
|
||||||
"mutative": "^1.1.0",
|
"mutative": "^1.1.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pl-api": "^1.0.0-rc.57",
|
"pl-api": "^1.0.0-rc.58",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"punycode": "^2.1.1",
|
"punycode": "^2.1.1",
|
||||||
|
|||||||
@ -79,7 +79,7 @@ const Counter: React.FC<ICounter> = ({
|
|||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div>
|
<div className='mt-auto'>
|
||||||
<Sparklines width={259} height={55} data={data?.[0].data.map(x => x.value * 1) || []}>
|
<Sparklines width={259} height={55} data={data?.[0].data.map(x => x.value * 1) || []}>
|
||||||
<SparklinesCurve />
|
<SparklinesCurve />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
@ -87,15 +87,17 @@ const Counter: React.FC<ICounter> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const className = 'relative flex flex-col rounded bg-gray-200 font-medium dark:bg-gray-800';
|
||||||
|
|
||||||
if (to) {
|
if (to) {
|
||||||
return (
|
return (
|
||||||
<Link to={to} className='relative rounded bg-gray-200 font-medium transition-transform hover:-translate-y-1 dark:bg-gray-800' target={target}>
|
<Link to={to} className={clsx(className, 'transition-transform hover:-translate-y-1')} target={target}>
|
||||||
{inner}
|
{inner}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className='relative rounded bg-gray-200 font-medium dark:bg-gray-800'>
|
<div className={className}>
|
||||||
{inner}
|
{inner}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -45,7 +45,7 @@ interface IDashCounters {
|
|||||||
|
|
||||||
/** Wrapper container for dash counters. */
|
/** Wrapper container for dash counters. */
|
||||||
const DashCounters: React.FC<IDashCounters> = ({ children }) => (
|
const DashCounters: React.FC<IDashCounters> = ({ children }) => (
|
||||||
<div className='grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3'>
|
<div className='grid grid-cols-1 gap-2 sm:grid-cols-2'>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
83
packages/pl-fe/src/features/admin/components/dimension.tsx
Normal file
83
packages/pl-fe/src/features/admin/components/dimension.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
import Text from 'pl-fe/components/ui/text';
|
||||||
|
import { useDimensions } from 'pl-fe/queries/admin/use-metrics';
|
||||||
|
|
||||||
|
import type { AdminDimensionKey, AdminGetDimensionsParams } from 'pl-api';
|
||||||
|
|
||||||
|
interface IDimension {
|
||||||
|
dimension: AdminDimensionKey;
|
||||||
|
startAt: string;
|
||||||
|
endAt: string;
|
||||||
|
label: JSX.Element;
|
||||||
|
params: AdminGetDimensionsParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dimension: React.FC<IDimension> = ({
|
||||||
|
dimension,
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
label,
|
||||||
|
params,
|
||||||
|
}) => {
|
||||||
|
const { data } = useDimensions([dimension], { ...params, start_at: startAt, end_at: endAt });
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
content = (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{Array.from(Array(params.limit)).map((_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
{/* <Skeleton width={100} /> */}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{/* <Skeleton width={60} /> */}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const sum = data[0].data.reduce((sum, cur) => sum + (+cur.value * 1), 0);
|
||||||
|
|
||||||
|
content = (
|
||||||
|
<table className='w-full'>
|
||||||
|
<tbody>
|
||||||
|
{data[0].data.map(item => (
|
||||||
|
<tr className='border-b border-primary-200 last:border-none dark:border-gray-800' key={item.key}>
|
||||||
|
<td className='p-2.5'>
|
||||||
|
<span
|
||||||
|
className='mr-2 inline-block size-2 rounded-full bg-green-500 shadow-sm'
|
||||||
|
style={{ opacity: +item.value / sum }}
|
||||||
|
/>
|
||||||
|
<Text title={item.key} weight='medium' size='xs' tag='span'>{item.human_key}</Text>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='p-2.5 text-end'>
|
||||||
|
<Text size='xs'>
|
||||||
|
{typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={+item.value} />}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text className='border-b border-primary-200 pb-1 dark:border-gray-800' weight='medium' size='sm'>{label}</Text>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Dimension };
|
||||||
132
packages/pl-fe/src/features/admin/components/retention.tsx
Normal file
132
packages/pl-fe/src/features/admin/components/retention.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedDate, FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
import Text from 'pl-fe/components/ui/text';
|
||||||
|
import { useRetention } from 'pl-fe/queries/admin/use-metrics';
|
||||||
|
|
||||||
|
import type { AdminCohort } from 'pl-api';
|
||||||
|
|
||||||
|
const dateForCohort = (cohort: AdminCohort) => {
|
||||||
|
const timeZone = 'UTC';
|
||||||
|
switch (cohort.frequency) {
|
||||||
|
case 'day':
|
||||||
|
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
|
||||||
|
default:
|
||||||
|
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IRetention {
|
||||||
|
startAt: string;
|
||||||
|
endAt: string;
|
||||||
|
frequency: 'day' | 'month';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Retention: React.FC<IRetention> = ({ startAt, endAt, frequency }) => {
|
||||||
|
const { data } = useRetention(startAt, endAt, frequency);
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading…' />;
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<table className='text-xs'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<div className='p-2.5 pl-0 font-bold'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th>
|
||||||
|
<div className='p-2.5 font-bold'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
{data[0].data.slice(1).map((retention, i) => (
|
||||||
|
<th key={retention.date} className='w-14'>
|
||||||
|
<div className='p-2.5 font-bold'>
|
||||||
|
{i + 1}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div className='p-2.5 pl-0 font-bold'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div className='p-2.5 text-center'>
|
||||||
|
<FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{data[0].data.slice(1).map((retention, i) => {
|
||||||
|
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum) / (k + 1) : sum, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td key={retention.date}>
|
||||||
|
<div className='bg-primary-200 p-2.5 font-medium dark:bg-gray-800' style={{ ['--tw-bg-opacity' as any]: 0.5 + average / 2 }}>
|
||||||
|
<FormattedNumber value={average} style='percent' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{data.slice(0, -1).map(cohort => (
|
||||||
|
<tr key={cohort.period}>
|
||||||
|
<td>
|
||||||
|
<div className='p-2.5 pl-0'>
|
||||||
|
{dateForCohort(cohort)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div className='p-2.5 text-center'>
|
||||||
|
<FormattedNumber value={cohort.data[0].value} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{cohort.data.slice(1).map(retention => (
|
||||||
|
<td key={retention.date}>
|
||||||
|
<div className='bg-primary-200 p-2.5 font-medium dark:bg-gray-800' style={{ ['--tw-bg-opacity' as any]: 0.5 + retention.rate / 2 }}>
|
||||||
|
<FormattedNumber value={retention.rate} style='percent' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = null;
|
||||||
|
switch (frequency) {
|
||||||
|
case 'day':
|
||||||
|
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='col-span-2'>
|
||||||
|
<Text className='border-b border-primary-200 pb-1 dark:border-gray-800' weight='medium' size='sm'>{title}</Text>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Retention };
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
import List, { ListItem } from 'pl-fe/components/list';
|
import List, { ListItem } from 'pl-fe/components/list';
|
||||||
import { CardTitle } from 'pl-fe/components/ui/card';
|
import { CardTitle } from 'pl-fe/components/ui/card';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
|
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||||
import { useInstance } from 'pl-fe/hooks/use-instance';
|
import { useInstance } from 'pl-fe/hooks/use-instance';
|
||||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||||
@ -12,13 +13,20 @@ import sourceCode from 'pl-fe/utils/code';
|
|||||||
|
|
||||||
import { Counter } from '../components/counter';
|
import { Counter } from '../components/counter';
|
||||||
import { DashCounter, DashCounters } from '../components/dashcounter';
|
import { DashCounter, DashCounters } from '../components/dashcounter';
|
||||||
|
import { Dimension } from '../components/dimension';
|
||||||
import RegistrationModePicker from '../components/registration-mode-picker';
|
import RegistrationModePicker from '../components/registration-mode-picker';
|
||||||
|
import { Retention } from '../components/retention';
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
|
|
||||||
|
const { pendingReports, pendingUsers } = useAppSelector((state) => ({
|
||||||
|
pendingReports: state.admin.openReports.length,
|
||||||
|
pendingUsers: state.admin.awaitingApproval.length,
|
||||||
|
}));
|
||||||
|
|
||||||
const v = features.version;
|
const v = features.version;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -30,8 +38,9 @@ const Dashboard: React.FC = () => {
|
|||||||
const mau = instance.usage.users.active_month ?? instance.pleroma.stats.mau;
|
const mau = instance.usage.users.active_month ?? instance.pleroma.stats.mau;
|
||||||
const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : undefined;
|
const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : undefined;
|
||||||
|
|
||||||
const [endDay] = useState<string>(new Date().toISOString().slice(0, 10));
|
const [today] = useState<string>(new Date().toISOString().slice(0, 10));
|
||||||
const [startDay] = useState<string>(new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10));
|
const [monthAgo] = useState<string>(new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10));
|
||||||
|
const [sixMonthsAgo] = useState<string>(new Date(new Date().getTime() - 30 * 6 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10));
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
@ -41,8 +50,8 @@ const Dashboard: React.FC = () => {
|
|||||||
{features.mastodonAdminMetrics ? (
|
{features.mastodonAdminMetrics ? (
|
||||||
<Counter
|
<Counter
|
||||||
measure='new_users'
|
measure='new_users'
|
||||||
startAt={startDay}
|
startAt={monthAgo}
|
||||||
endAt={endDay}
|
endAt={today}
|
||||||
to='/pl-fe/admin/users'
|
to='/pl-fe/admin/users'
|
||||||
label={<FormattedMessage id='admin.counters.new_users' defaultMessage='new users' />}
|
label={<FormattedMessage id='admin.counters.new_users' defaultMessage='new users' />}
|
||||||
/>
|
/>
|
||||||
@ -56,8 +65,8 @@ const Dashboard: React.FC = () => {
|
|||||||
{features.mastodonAdminMetrics ? (
|
{features.mastodonAdminMetrics ? (
|
||||||
<Counter
|
<Counter
|
||||||
measure='active_users'
|
measure='active_users'
|
||||||
startAt={startDay}
|
startAt={monthAgo}
|
||||||
endAt={endDay}
|
endAt={today}
|
||||||
label={<FormattedMessage id='admin.counters.active_users' defaultMessage='active users' />}
|
label={<FormattedMessage id='admin.counters.active_users' defaultMessage='active users' />}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -77,21 +86,21 @@ const Dashboard: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Counter
|
<Counter
|
||||||
measure='interactions'
|
measure='interactions'
|
||||||
startAt={startDay}
|
startAt={monthAgo}
|
||||||
endAt={endDay}
|
endAt={today}
|
||||||
label={<FormattedMessage id='admin.counters.interactions' defaultMessage='interactions' />}
|
label={<FormattedMessage id='admin.counters.interactions' defaultMessage='interactions' />}
|
||||||
/>
|
/>
|
||||||
<Counter
|
<Counter
|
||||||
measure='opened_reports'
|
measure='opened_reports'
|
||||||
startAt={startDay}
|
startAt={monthAgo}
|
||||||
endAt={endDay}
|
endAt={today}
|
||||||
to='/pl-fe/admin/reports'
|
to='/pl-fe/admin/reports'
|
||||||
label={<FormattedMessage id='admin.counters.opened_reports' defaultMessage='reports opened' />}
|
label={<FormattedMessage id='admin.counters.opened_reports' defaultMessage='reports opened' />}
|
||||||
/>
|
/>
|
||||||
<Counter
|
<Counter
|
||||||
measure='resolved_reports'
|
measure='resolved_reports'
|
||||||
startAt={startDay}
|
startAt={monthAgo}
|
||||||
endAt={endDay}
|
endAt={today}
|
||||||
to='/pl-fe/admin/reports'
|
to='/pl-fe/admin/reports'
|
||||||
label={<FormattedMessage id='admin.counters.resolved_reports' defaultMessage='reports resolved' />}
|
label={<FormattedMessage id='admin.counters.resolved_reports' defaultMessage='reports resolved' />}
|
||||||
/>
|
/>
|
||||||
@ -106,6 +115,52 @@ const Dashboard: React.FC = () => {
|
|||||||
count={domainCount}
|
count={domainCount}
|
||||||
label={<FormattedMessage id='admin.dashcounters.domain_count_label' defaultMessage='peers' />}
|
label={<FormattedMessage id='admin.dashcounters.domain_count_label' defaultMessage='peers' />}
|
||||||
/>
|
/>
|
||||||
|
<List>
|
||||||
|
<ListItem size='sm' to='/pl-fe/admin/reports' label={<FormattedMessage id='admin.links.pending_reports' defaultMessage='{count, plural, one {{formattedCount} pending report} other {{formattedCount} pending reports}}' values={{ count: pendingReports, formattedCount: <strong><FormattedNumber value={pendingReports} /></strong> }} />} />
|
||||||
|
<ListItem size='sm' to='/pl-fe/admin/users' label={<FormattedMessage id='admin.links.pending_users' defaultMessage='{count, plural, one {{formattedCount} pending user} other {{formattedCount} pending users}}' values={{ count: pendingUsers, formattedCount: <strong><FormattedNumber value={pendingUsers} /></strong> }} />} />
|
||||||
|
{/* <ListItem size='sm' to='/pl-fe/admin' label={<FormattedMessage id='admin.links.pending_tags' defaultMessage='{count} pending tags' values={{ count: <strong>0</strong> }} />} />
|
||||||
|
<ListItem size='sm' to='/pl-fe/admin' label={<FormattedMessage id='admin.links.pending_appeals' defaultMessage='{count} pending appeals' values={{ count: <strong>0</strong> }} />} /> */}
|
||||||
|
</List>
|
||||||
|
{features.mastodonAdminMetrics && (
|
||||||
|
<>
|
||||||
|
<Dimension
|
||||||
|
dimension='sources'
|
||||||
|
startAt={monthAgo}
|
||||||
|
endAt={today}
|
||||||
|
params={{ limit: 8 }}
|
||||||
|
label={<FormattedMessage id='admin.dimensions.sources' defaultMessage='Sign-up sources' />}
|
||||||
|
/>
|
||||||
|
<Dimension
|
||||||
|
dimension='languages'
|
||||||
|
startAt={monthAgo}
|
||||||
|
endAt={today}
|
||||||
|
params={{ limit: 8 }}
|
||||||
|
label={<FormattedMessage id='admin.dimensions.top_languages' defaultMessage='Top active languages' />}
|
||||||
|
/>
|
||||||
|
<Dimension
|
||||||
|
dimension='servers'
|
||||||
|
startAt={monthAgo}
|
||||||
|
endAt={today}
|
||||||
|
params={{ limit: 8 }}
|
||||||
|
label={<FormattedMessage id='admin.dimensions.top_servers' defaultMessage='Top active servers' />}
|
||||||
|
/>
|
||||||
|
<Retention startAt={sixMonthsAgo} endAt={today} frequency='month' />
|
||||||
|
<Dimension
|
||||||
|
dimension='software_versions'
|
||||||
|
startAt={monthAgo}
|
||||||
|
endAt={today}
|
||||||
|
params={{ limit: 4 }}
|
||||||
|
label={<FormattedMessage id='admin.dimensions.software' defaultMessage='Software' />}
|
||||||
|
/>
|
||||||
|
<Dimension
|
||||||
|
dimension='space_usage'
|
||||||
|
startAt={monthAgo}
|
||||||
|
endAt={today}
|
||||||
|
params={{ limit: 3 }}
|
||||||
|
label={<FormattedMessage id='admin.dimensions.media_storage' defaultMessage='Media storage' />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DashCounters>
|
</DashCounters>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
@ -175,9 +230,11 @@ const Dashboard: React.FC = () => {
|
|||||||
</a>
|
</a>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem label={<FormattedMessage id='admin.software.backend' defaultMessage='Backend' />}>
|
{!features.mastodonAdminMetrics && (
|
||||||
<span>{v.software + (v.build ? `+${v.build}` : '')} {v.version}</span>
|
<ListItem label={<FormattedMessage id='admin.software.backend' defaultMessage='Backend' />}>
|
||||||
</ListItem>
|
<span>{v.software + (v.build ? `+${v.build}` : '')} {v.version}</span>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -107,6 +107,8 @@
|
|||||||
"admin.counters.new_users": "new users",
|
"admin.counters.new_users": "new users",
|
||||||
"admin.counters.opened_reports": "reports opened",
|
"admin.counters.opened_reports": "reports opened",
|
||||||
"admin.counters.resolved_reports": "reports resolved",
|
"admin.counters.resolved_reports": "reports resolved",
|
||||||
|
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
||||||
|
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
||||||
"admin.dashboard.registration_mode.approval_hint": "Users can sign up, but their account only gets activated when an admin approves it.",
|
"admin.dashboard.registration_mode.approval_hint": "Users can sign up, but their account only gets activated when an admin approves it.",
|
||||||
"admin.dashboard.registration_mode.approval_label": "Approval Required",
|
"admin.dashboard.registration_mode.approval_label": "Approval Required",
|
||||||
"admin.dashboard.registration_mode.closed_hint": "Nobody can sign up. You can still invite people.",
|
"admin.dashboard.registration_mode.closed_hint": "Nobody can sign up. You can still invite people.",
|
||||||
@ -114,6 +116,9 @@
|
|||||||
"admin.dashboard.registration_mode.open_hint": "Anyone can join.",
|
"admin.dashboard.registration_mode.open_hint": "Anyone can join.",
|
||||||
"admin.dashboard.registration_mode.open_label": "Open",
|
"admin.dashboard.registration_mode.open_label": "Open",
|
||||||
"admin.dashboard.registration_mode_label": "Registrations",
|
"admin.dashboard.registration_mode_label": "Registrations",
|
||||||
|
"admin.dashboard.retention.average": "Average",
|
||||||
|
"admin.dashboard.retention.cohort": "Sign-up month",
|
||||||
|
"admin.dashboard.retention.cohort_size": "New users",
|
||||||
"admin.dashboard.settings_saved": "Settings saved!",
|
"admin.dashboard.settings_saved": "Settings saved!",
|
||||||
"admin.dashcounters.domain_count_label": "peers",
|
"admin.dashcounters.domain_count_label": "peers",
|
||||||
"admin.dashcounters.mau_label": "monthly active users",
|
"admin.dashcounters.mau_label": "monthly active users",
|
||||||
@ -121,6 +126,11 @@
|
|||||||
"admin.dashcounters.status_count_label": "posts",
|
"admin.dashcounters.status_count_label": "posts",
|
||||||
"admin.dashcounters.user_count_label": "total users",
|
"admin.dashcounters.user_count_label": "total users",
|
||||||
"admin.dashwidgets.software_header": "Software",
|
"admin.dashwidgets.software_header": "Software",
|
||||||
|
"admin.dimensions.media_storage": "Media storage",
|
||||||
|
"admin.dimensions.software": "Software",
|
||||||
|
"admin.dimensions.sources": "Sign-up sources",
|
||||||
|
"admin.dimensions.top_languages": "Top active languages",
|
||||||
|
"admin.dimensions.top_servers": "Top active servers",
|
||||||
"admin.domains.action": "Create domain",
|
"admin.domains.action": "Create domain",
|
||||||
"admin.domains.delete": "Delete",
|
"admin.domains.delete": "Delete",
|
||||||
"admin.domains.edit": "Edit",
|
"admin.domains.edit": "Edit",
|
||||||
@ -161,6 +171,8 @@
|
|||||||
"admin.edit_rule.updated": "Rule edited",
|
"admin.edit_rule.updated": "Rule edited",
|
||||||
"admin.latest_accounts_panel.more": "Click to see {count, plural, one {# account} other {# accounts}}",
|
"admin.latest_accounts_panel.more": "Click to see {count, plural, one {# account} other {# accounts}}",
|
||||||
"admin.latest_accounts_panel.title": "Latest Accounts",
|
"admin.latest_accounts_panel.title": "Latest Accounts",
|
||||||
|
"admin.links.pending_reports": "{count, plural, one {{formattedCount} pending report} other {{formattedCount} pending reports}}",
|
||||||
|
"admin.links.pending_users": "{count, plural, one {{formattedCount} pending user} other {{formattedCount} pending users}}",
|
||||||
"admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.",
|
"admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.",
|
||||||
"admin.relays.add.fail": "Failed to follow the instance relay",
|
"admin.relays.add.fail": "Failed to follow the instance relay",
|
||||||
"admin.relays.add.success": "Instance relay followed",
|
"admin.relays.add.success": "Instance relay followed",
|
||||||
|
|||||||
@ -49,9 +49,12 @@ const shortNumberFormat = (number: any, max?: number): React.ReactNode => {
|
|||||||
/** Check if an entity ID is an integer (eg not a FlakeId). */
|
/** Check if an entity ID is an integer (eg not a FlakeId). */
|
||||||
const isIntegerId = (id: string): boolean => new RegExp(/^-?[0-9]+$/g).test(id);
|
const isIntegerId = (id: string): boolean => new RegExp(/^-?[0-9]+$/g).test(id);
|
||||||
|
|
||||||
|
const roundTo10 = (num: number) => Math.round(num / 10) * 10;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isNumber,
|
isNumber,
|
||||||
roundDown,
|
roundDown,
|
||||||
shortNumberFormat,
|
shortNumberFormat,
|
||||||
isIntegerId,
|
isIntegerId,
|
||||||
|
roundTo10,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6858,10 +6858,10 @@ pkg-dir@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
find-up "^4.0.0"
|
||||||
|
|
||||||
pl-api@^1.0.0-rc.57:
|
pl-api@^1.0.0-rc.58:
|
||||||
version "1.0.0-rc.57"
|
version "1.0.0-rc.58"
|
||||||
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.57.tgz#848259e4a38e3c44dc3048a86578d877b36e50e7"
|
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.58.tgz#3605128a53029ef4abd8316fdabe3ab4e0c2552a"
|
||||||
integrity sha512-ndT9fnL0LJt5EMRMHWRovAd/YE8w6Ya/Ow1TUbiwM760IqQUAIyxBNt5gLB89OlkbJQiVC+RD/vVUxBdBMisLg==
|
integrity sha512-GR/gXEQGwo1SueSwPzPsRUeuTpulRx5Wpd3y6/bOC7r8BGJyKK0FI34/y3PYLEzBveYo5W2JP+tCI4dUKk+akQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
blurhash "^2.0.5"
|
blurhash "^2.0.5"
|
||||||
http-link-header "^1.1.3"
|
http-link-header "^1.1.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user