diff --git a/libs/model/src/aggregates/super-admin/GetStats.query.ts b/libs/model/src/aggregates/super-admin/GetStats.query.ts new file mode 100644 index 00000000000..f8d74aa2728 --- /dev/null +++ b/libs/model/src/aggregates/super-admin/GetStats.query.ts @@ -0,0 +1,148 @@ +import { Query } from '@hicommonwealth/core'; +import * as schemas from '@hicommonwealth/schemas'; +import { ALL_COMMUNITIES } from '@hicommonwealth/shared'; +import { BindOrReplacements, QueryTypes } from 'sequelize'; +import { z } from 'zod'; +import { models } from '../../database'; +import { isSuperAdmin } from '../../middleware'; + +export function GetStats(): Query { + return { + ...schemas.GetStats, + auth: [isSuperAdmin], + secure: true, + body: async ({ payload }) => { + const { community_id } = payload; + + const oneMonthAgo = new Date(); + oneMonthAgo.setDate(oneMonthAgo.getDate() - 30); + + const replacements: BindOrReplacements = { + oneMonthAgo, + ...(community_id ? { community_id } : {}), + }; + const community_filter = + community_id && community_id !== ALL_COMMUNITIES + ? `AND X.community_id = :community_id` + : ''; + + const [ + lastMonthNewCommunities, + [{ monthlySummary }], + [{ result: averageAddressesPerCommunity }], + [{ result: populatedCommunities }], + ] = await Promise.all([ + models.sequelize.query<{ id: string; created_at: Date }>( + `SELECT id, created_at FROM "Communities" + WHERE created_at >= NOW() - INTERVAL '30 days' + ORDER BY created_at desc`, + { type: QueryTypes.SELECT }, + ), + models.sequelize.query<{ + monthlySummary: z.infer; + }>( + ` + WITH MonthlyStats AS ( + SELECT + 'numCommentsLastMonth' as label, + COUNT(C.*) as count + FROM "Comments" C JOIN "Threads" X ON C.thread_id = X.id + WHERE C."created_at" >= :oneMonthAgo + ${community_filter} + UNION ALL + SELECT + 'numThreadsLastMonth' as label, + COUNT(*) + FROM "Threads" X + WHERE "created_at" >= :oneMonthAgo + ${community_filter} + UNION ALL + SELECT + 'numReactionsLastMonth' as label, + COUNT(R.*) + FROM "Reactions" R + LEFT JOIN "Threads" T ON R.thread_id = T.id + LEFT JOIN "Comments" C ON R.comment_id = C.id + LEFT JOIN "Threads" TC ON C.thread_id = TC.id + WHERE R."created_at" >= :oneMonthAgo + ${community_filter ? `AND (T.community_id = :community_id OR TC.community_id = :community_id)` : ''} + UNION ALL + SELECT + 'numProposalVotesLastMonth' as label, + COUNT(*) + FROM "Votes" X + WHERE "created_at" >= :oneMonthAgo + ${community_filter} + UNION ALL + SELECT + 'numPollsLastMonth' as label, + COUNT(*) + FROM "Polls" X + WHERE "created_at" >= :oneMonthAgo + ${community_filter} + UNION ALL + SELECT + 'numMembersLastMonth' as label, + COUNT(*) + FROM "Addresses" X + WHERE "created_at" >= :oneMonthAgo + ${community_filter} + UNION ALL + SELECT + 'numGroupsLastMonth' as label, + COUNT(*) + FROM "Groups" X + WHERE "created_at" >= :oneMonthAgo + ${community_filter} + ) + SELECT json_build_object( + 'numCommentsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numCommentsLastMonth'), + 'numThreadsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numThreadsLastMonth'), + 'numReactionsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numReactionsLastMonth'), + 'numProposalVotesLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numProposalVotesLastMonth'), + 'numPollsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numPollsLastMonth'), + 'numMembersLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numMembersLastMonth'), + 'numGroupsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numGroupsLastMonth') + ) AS "monthlySummary"; + `, + { replacements, type: QueryTypes.SELECT }, + ), + models.sequelize.query<{ result: string }>( + ` + SELECT AVG(profile_count) as result + FROM ( + SELECT "Communities".id, COUNT("Addresses".id) as profile_count + FROM "Communities" + JOIN "Addresses" ON "Addresses".community_id = "Communities".id + GROUP BY "Communities".id + ) as _; + `, + { type: QueryTypes.SELECT }, + ), + models.sequelize.query<{ result: string }>( + ` + SELECT COUNT(communities_count) as result FROM ( + SELECT "Communities".id + FROM "Communities" + JOIN "Addresses" ON "Addresses".community_id = "Communities".id + GROUP BY "Communities".id + HAVING COUNT("Addresses".id) > 2 + ) as communities_count; + `, + { type: QueryTypes.SELECT }, + ), + ]); + + return { + lastMonthNewCommunities: lastMonthNewCommunities.map( + ({ id, created_at }) => ({ id, created_at }), + ), + totalStats: { + ...monthlySummary, + averageAddressesPerCommunity: +averageAddressesPerCommunity, + populatedCommunities: +populatedCommunities, + }, + }; + }, + }; +} diff --git a/packages/commonwealth/server/controllers/server_admin_methods/get_top_users.ts b/libs/model/src/aggregates/super-admin/GetTopUsers.query.ts similarity index 66% rename from packages/commonwealth/server/controllers/server_admin_methods/get_top_users.ts rename to libs/model/src/aggregates/super-admin/GetTopUsers.query.ts index 2e1dfc3c180..294a87a3a1a 100644 --- a/packages/commonwealth/server/controllers/server_admin_methods/get_top_users.ts +++ b/libs/model/src/aggregates/super-admin/GetTopUsers.query.ts @@ -1,31 +1,20 @@ -import { AppError } from '@hicommonwealth/core'; -import { UserInstance } from '@hicommonwealth/model'; +import { Query } from '@hicommonwealth/core'; +import * as schemas from '@hicommonwealth/schemas'; import { QueryTypes } from 'sequelize'; -import { ServerAdminController } from '../server_admin_controller'; +import { models } from '../../database'; +import { isSuperAdmin } from '../../middleware'; -export const Errors = { - NotAdmin: 'Must be a site admin', -}; - -export type GetTopUsersOptions = { - user: UserInstance; -}; - -export type GetTopUsersResult = any[]; - -export async function __getTopUsers( - this: ServerAdminController, - { user }: GetTopUsersOptions, -): Promise { - if (!user.isAdmin) { - throw new AppError(Errors.NotAdmin); - } - - const sql = ` +export function GetTopUsers(): Query { + return { + ...schemas.GetTopUsers, + auth: [isSuperAdmin], + secure: true, + body: async () => { + const sql = ` WITH Stats as ( SELECT u.profile->>'name' AS profile_name, - p.user_id as user_id, + u.id as user_id, COUNT(DISTINCT t.id) AS thread_count, COUNT(DISTINCT c.id) AS comment_count, COUNT(DISTINCT c.id) + COUNT(DISTINCT t.id) AS total_activity, @@ -40,7 +29,7 @@ export async function __getTopUsers( LEFT JOIN "Threads" AS t ON a.id = t.address_id LEFT JOIN "Comments" AS c ON a.id = c.address_id WHERE u."isAdmin" = FALSE - GROUP BY p.id + GROUP BY u.id ORDER BY total_activity DESC LIMIT 150 ) @@ -57,9 +46,9 @@ export async function __getTopUsers( FROM Stats `; - const result = await this.models.sequelize.query(sql, { - type: QueryTypes.SELECT, - }); - - return result; + return await models.sequelize.query(sql, { + type: QueryTypes.SELECT, + }); + }, + }; } diff --git a/libs/model/src/aggregates/super-admin/index.ts b/libs/model/src/aggregates/super-admin/index.ts index 8d8072be82e..9ceebae03d4 100644 --- a/libs/model/src/aggregates/super-admin/index.ts +++ b/libs/model/src/aggregates/super-admin/index.ts @@ -1,4 +1,6 @@ export * from './EnableDigestEmail'; +export * from './GetStats.query'; +export * from './GetTopUsers.query'; export * from './SetCommunityTier.command'; export * from './SetUserTier.command'; export * from './TriggerNotificationsWorkflow.command'; diff --git a/libs/schemas/src/queries/index.ts b/libs/schemas/src/queries/index.ts index 6f8cba1958b..91606aa3de2 100644 --- a/libs/schemas/src/queries/index.ts +++ b/libs/schemas/src/queries/index.ts @@ -7,6 +7,7 @@ export * from './pagination'; export * from './poll.schemas'; export * from './quest.schemas'; export * from './subscription.schemas'; +export * from './super-admin.schemas'; export * from './tag.schemas'; export * from './thread.schemas'; export * from './token.schemas'; diff --git a/libs/schemas/src/queries/super-admin.schemas.ts b/libs/schemas/src/queries/super-admin.schemas.ts new file mode 100644 index 00000000000..81c88f7c597 --- /dev/null +++ b/libs/schemas/src/queries/super-admin.schemas.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +export const TotalStats = z.object({ + numCommentsLastMonth: z.number(), + numThreadsLastMonth: z.number(), + numPollsLastMonth: z.number(), + numReactionsLastMonth: z.number(), + numProposalVotesLastMonth: z.number(), + numMembersLastMonth: z.number(), + numGroupsLastMonth: z.number(), +}); + +export const GetStats = { + input: z.object({ + community_id: z.string().optional(), + }), + output: z.object({ + lastMonthNewCommunities: z.array( + z.object({ + id: z.string(), + created_at: z.date(), + }), + ), + totalStats: TotalStats.extend({ + averageAddressesPerCommunity: z.number(), + populatedCommunities: z.number(), + }), + }), +}; + +export const GetTopUsers = { + input: z.object({}), + output: z.array(z.any()), +}; diff --git a/packages/commonwealth/client/scripts/state/api/superAdmin/getStats.ts b/packages/commonwealth/client/scripts/state/api/superAdmin/getStats.ts new file mode 100644 index 00000000000..deb6cee591a --- /dev/null +++ b/packages/commonwealth/client/scripts/state/api/superAdmin/getStats.ts @@ -0,0 +1,21 @@ +import { notifyError } from 'client/scripts/controllers/app/notifications'; +import { trpc } from 'utils/trpcClient'; + +const STATS_STALE_TIME = 60 * 60 * 1000; + +const useGetStatsQuery = (community_id?: string) => { + return trpc.superAdmin.getStats.useQuery( + { + community_id, + }, + { + staleTime: STATS_STALE_TIME, + enabled: !!community_id, + onError: (error) => { + notifyError(`Error fetching stats: ${error.message}`); + }, + }, + ); +}; + +export default useGetStatsQuery; diff --git a/packages/commonwealth/client/scripts/state/api/superAdmin/getTopUsers.ts b/packages/commonwealth/client/scripts/state/api/superAdmin/getTopUsers.ts new file mode 100644 index 00000000000..82c5d549c68 --- /dev/null +++ b/packages/commonwealth/client/scripts/state/api/superAdmin/getTopUsers.ts @@ -0,0 +1,15 @@ +import { trpc } from 'utils/trpcClient'; + +const TOP_USERS_STALE_TIME = 6 * 60 * 60 * 1000; + +const useGetTopUsersQuery = (enabled: boolean) => { + return trpc.superAdmin.getTopUsers.useQuery( + {}, + { + staleTime: TOP_USERS_STALE_TIME, + enabled, + }, + ); +}; + +export default useGetTopUsersQuery; diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/AdminPanel.scss b/packages/commonwealth/client/scripts/views/pages/AdminPanel/AdminPanel.scss index 15ba0d04b6b..bbab42c1fc6 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/AdminPanel.scss +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/AdminPanel.scss @@ -13,8 +13,8 @@ .TaskRow { display: flex; flex-direction: row; - gap: 44px; - align-items: flex-end; + gap: 15px; + align-items: center; .Downloading { display: flex; diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/Analytics.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/Analytics.tsx index f30df9f0fa1..a43fce31407 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/Analytics.tsx +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/Analytics.tsx @@ -1,86 +1,25 @@ -import axios from 'axios'; -import { notifyError } from 'controllers/app/notifications'; -import useNecessaryEffect from 'hooks/useNecessaryEffect'; +import { ALL_COMMUNITIES } from '@hicommonwealth/shared'; +import useGetStatsQuery from 'client/scripts/state/api/superAdmin/getStats'; import React, { useState } from 'react'; -import { SERVER_URL } from 'state/api/config'; -import useUserStore from 'state/ui/user'; import { CWText } from '../../components/component_kit/cw_text'; import CWCircleMultiplySpinner from '../../components/component_kit/new_designs/CWCircleMultiplySpinner'; import './AdminPanel.scss'; import CommunityFinder from './CommunityFinder'; -type Stats = { - numCommentsLastMonth: number; - numThreadsLastMonth: number; - numPollsLastMonth: number; - numReactionsLastMonth: number; - numProposalVotesLastMonth: number; - numMembersLastMonth: number; - numGroupsLastMonth: number; - averageAddressesPerCommunity: number; - populatedCommunities: number; -}; - const Analytics = () => { - const [initialized, setInitialized] = useState(false); - const [lastMonthNewCommunties, setLastMonthNewCommunities] = useState< - { id: string; created_at: string }[] - >([]); - const [globalStats, setGlobalStats] = useState(); - const [communityLookupCompleted, setCommunityLookupCompleted] = - useState(false); - const [communityAnalytics, setCommunityAnalytics] = useState(); - const user = useUserStore(); - - const getCommunityAnalytics = (communityId: string) => { - axios - .get(`${SERVER_URL}/admin/analytics?community_id=${communityId}`, { - params: { - auth: true, - jwt: user.jwt, - }, - }) - .then((response) => { - setCommunityLookupCompleted(true); - setCommunityAnalytics(response.data.result.totalStats); - }) - .catch((error) => { - console.log(error); - notifyError('Error fetching community analytics'); - }); - }; - - useNecessaryEffect(() => { - // Fetch global analytics on load - const fetchAnalytics = async () => { - axios - .get(`${SERVER_URL}/admin/analytics`, { - params: { - auth: true, - jwt: user.jwt, - }, - }) - .then((response) => { - setLastMonthNewCommunities( - response.data.result.lastMonthNewCommunities, - ); - setGlobalStats(response.data.result.totalStats); - }) - .catch((error) => { - console.error(error); - }); - }; + const { data: globalStats, isLoading: globalStatsLoading } = + useGetStatsQuery(ALL_COMMUNITIES); + const [selectedCommunityId, setSelectedCommunityId] = useState(''); + const { data: communityAnalytics, isLoading: communityAnalyticsLoading } = + useGetStatsQuery(selectedCommunityId); - if (!initialized) { - fetchAnalytics().then(() => { - setInitialized(true); - }); - } - }, [initialized]); + function getCommunityAnalytics(community_id: string) { + setSelectedCommunityId(community_id); + } return (
- {!initialized ? ( + {globalStatsLoading ? ( ) : ( <> @@ -94,43 +33,43 @@ const Analytics = () => {
Total Threads - {globalStats?.numThreadsLastMonth} + {globalStats?.totalStats.numThreadsLastMonth}
Total Comments - {globalStats?.numCommentsLastMonth} + {globalStats?.totalStats.numCommentsLastMonth}
Total Reactions - {globalStats?.numReactionsLastMonth} + {globalStats?.totalStats.numReactionsLastMonth}
Total Polls - {globalStats?.numPollsLastMonth} + {globalStats?.totalStats.numPollsLastMonth}
Total Votes - {globalStats?.numProposalVotesLastMonth} + {globalStats?.totalStats.numProposalVotesLastMonth}
Total New Addresses - {globalStats?.numMembersLastMonth} + {globalStats?.totalStats.numMembersLastMonth}
Total New Groups - {globalStats?.numGroupsLastMonth} + {globalStats?.totalStats.numGroupsLastMonth}
@@ -138,8 +77,9 @@ const Analytics = () => { Average Addresses Per Community - {/*@ts-expect-error StrictNullChecks*/} - {Math.round(globalStats?.averageAddressesPerCommunity)} + {Math.round( + globalStats?.totalStats?.averageAddressesPerCommunity || 0, + )}
@@ -147,8 +87,9 @@ const Analytics = () => { {'Total Communities with > 2 addresses'} - {/* @ts-expect-error StrictNullChecks*/} - {Math.round(globalStats?.populatedCommunities)} + {Math.round( + globalStats?.totalStats?.populatedCommunities || 0, + )}
@@ -162,48 +103,48 @@ const Analytics = () => { ctaLabel="Search" onAction={getCommunityAnalytics} /> - {communityAnalytics && communityLookupCompleted && ( + {communityAnalytics && !communityAnalyticsLoading && (
Total Threads - {communityAnalytics?.numThreadsLastMonth} + {communityAnalytics?.totalStats.numThreadsLastMonth}
Total Comments - {communityAnalytics?.numCommentsLastMonth} + {communityAnalytics?.totalStats.numCommentsLastMonth}
Total Reactions - {communityAnalytics?.numReactionsLastMonth} + {communityAnalytics?.totalStats.numReactionsLastMonth}
Total Polls - {communityAnalytics?.numPollsLastMonth} + {communityAnalytics?.totalStats.numPollsLastMonth}
Total Votes - {communityAnalytics?.numProposalVotesLastMonth} + {communityAnalytics?.totalStats.numProposalVotesLastMonth}
Total New Addresses - {communityAnalytics?.numMembersLastMonth} + {communityAnalytics?.totalStats.numMembersLastMonth}
Total New Groups - {communityAnalytics?.numGroupsLastMonth} + {communityAnalytics?.totalStats.numGroupsLastMonth}
@@ -219,8 +160,8 @@ const Analytics = () => { Name Created At - {lastMonthNewCommunties && - lastMonthNewCommunties?.map((community) => { + {globalStats?.lastMonthNewCommunities && + globalStats?.lastMonthNewCommunities?.map((community) => { return ( { diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/TopUsers.tsx b/packages/commonwealth/client/scripts/views/pages/AdminPanel/TopUsers.tsx index fcce5cce348..3027552f0ed 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/TopUsers.tsx +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/TopUsers.tsx @@ -1,14 +1,21 @@ +import useGetTopUsersQuery from 'client/scripts/state/api/superAdmin/getTopUsers'; import React from 'react'; import { CWText } from '../../components/component_kit/cw_text'; import { CWButton } from '../../components/component_kit/new_designs/CWButton'; +import CWCircleRingSpinner from '../../components/component_kit/new_designs/CWCircleRingSpinner'; import './AdminPanel.scss'; -import { downloadCSV as downloadAsCSV, getTopUsersList } from './utils'; +import { downloadCSV as downloadAsCSV } from './utils'; const TopUsers = () => { - const generateAndDownload = async () => { - const result = await getTopUsersList(); - downloadAsCSV(result, 'top_users.csv'); - }; + const { + data: topUsers, + isFetching: isFetchingTopUsers, + refetch: refetchTopUsers, + } = useGetTopUsersQuery(false); + + React.useEffect(() => { + !isFetchingTopUsers && topUsers && downloadAsCSV(topUsers, 'top_users.csv'); + }, [topUsers, isFetchingTopUsers]); return (
@@ -18,11 +25,18 @@ const TopUsers = () => { activity (threads and comments created).
- + {isFetchingTopUsers ? ( + <> + + Fetching top users, please wait... + + ) : ( + refetchTopUsers()} + /> + )}
); diff --git a/packages/commonwealth/client/scripts/views/pages/AdminPanel/utils.ts b/packages/commonwealth/client/scripts/views/pages/AdminPanel/utils.ts index 4099a328896..1076ff06cbc 100644 --- a/packages/commonwealth/client/scripts/views/pages/AdminPanel/utils.ts +++ b/packages/commonwealth/client/scripts/views/pages/AdminPanel/utils.ts @@ -98,15 +98,6 @@ export const getCSVContent = async ({ id }: { id: string }) => { return res.data.result.data[0]; }; -export const getTopUsersList = async () => { - const res = await axios.get(`${SERVER_URL}/admin/top-users`, { - params: { - jwt: userStore.getState().jwt, - }, - }); - return res.data.result; -}; - type CSVRow = Record; export function downloadCSV(rows: CSVRow[], filename: string) { diff --git a/packages/commonwealth/server/api/super-admin.ts b/packages/commonwealth/server/api/super-admin.ts index cde7fd68661..6a83ebbf901 100644 --- a/packages/commonwealth/server/api/super-admin.ts +++ b/packages/commonwealth/server/api/super-admin.ts @@ -19,4 +19,6 @@ export const trpcRouter = trpc.router({ trpc.Tag.SuperAdmin, ), setUserTier: trpc.command(SuperAdmin.SetUserTier, trpc.Tag.SuperAdmin), + getStats: trpc.query(SuperAdmin.GetStats, trpc.Tag.SuperAdmin), + getTopUsers: trpc.query(SuperAdmin.GetTopUsers, trpc.Tag.SuperAdmin), }); diff --git a/packages/commonwealth/server/controllers/server_admin_controller.ts b/packages/commonwealth/server/controllers/server_admin_controller.ts deleted file mode 100644 index 1c63fb9304e..00000000000 --- a/packages/commonwealth/server/controllers/server_admin_controller.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DB } from '@hicommonwealth/model'; -import { - GetStatsOptions, - GetStatsResult, - __getStats, -} from './server_admin_methods/get_stats'; -import { - GetTopUsersOptions, - GetTopUsersResult, - __getTopUsers, -} from './server_admin_methods/get_top_users'; - -export class ServerAdminController { - constructor(public models: DB) {} - - async getStats(options: GetStatsOptions): Promise { - return __getStats.call(this, options); - } - - async getTopUsers(options: GetTopUsersOptions): Promise { - return __getTopUsers.call(this, options); - } -} diff --git a/packages/commonwealth/server/controllers/server_admin_methods/get_stats.ts b/packages/commonwealth/server/controllers/server_admin_methods/get_stats.ts deleted file mode 100644 index 4525d50ebf5..00000000000 --- a/packages/commonwealth/server/controllers/server_admin_methods/get_stats.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { AppError } from '@hicommonwealth/core'; -import { CommunityAttributes, UserInstance } from '@hicommonwealth/model'; -import { BindOrReplacements, QueryTypes } from 'sequelize'; -import { ServerAdminController } from '../server_admin_controller'; - -export const Errors = { - NotAdmin: 'Must be a site admin', - CommunityNotFound: 'Community not found', -}; - -export type GetStatsOptions = { - user: UserInstance; - communityId?: string; -}; - -type TableCounts = { - numCommentsLastMonth: number; - numThreadsLastMonth: number; - numPollsLastMonth: number; - numReactionsLastMonth: number; - numProposalVotesLastMonth: number; - numMembersLastMonth: number; - numGroupsLastMonth: number; -}; - -export type GetStatsResult = { - lastMonthNewCommunities: Array<{ id: string; created_at: string }>; - totalStats: TableCounts & { - averageAddressesPerCommunity: number; - populatedCommunities: number; - }; -}; - -export async function __getStats( - this: ServerAdminController, - { user, communityId }: GetStatsOptions, -): Promise { - if (!user.isAdmin) { - throw new AppError(Errors.NotAdmin); - } - - // community is optional - let community: CommunityAttributes | undefined = undefined; - if (communityId) { - // @ts-expect-error StrictNullChecks - community = await this.models.Community.findByPk(communityId); - if (!community) { - throw new AppError(Errors.CommunityNotFound); - } - } - - const oneMonthAgo = new Date(); - oneMonthAgo.setDate(oneMonthAgo.getDate() - 30); - - const monthlyStatsReplacements: BindOrReplacements = { - oneMonthAgo, - ...(community ? { communityId: community.id } : {}), - }; - - const [ - lastMonthNewCommunities, - [{ monthlySummary }], - [{ result: averageAddressesPerCommunity }], - [{ result: populatedCommunities }], - ] = await Promise.all([ - this.models.sequelize.query<{ id: string; created_at: string }>( - `SELECT id, created_at FROM "Communities" - WHERE created_at >= NOW() - INTERVAL '30 days' - ORDER BY created_at desc`, - { type: QueryTypes.SELECT }, - ), - this.models.sequelize.query<{ monthlySummary: TableCounts }>( - ` - WITH MonthlyStats AS ( - SELECT - 'numCommentsLastMonth' as label, - COUNT(C.*) as count - FROM "Comments" C JOIN "Threads" T ON C.thread_id = T.id - WHERE C."created_at" >= :oneMonthAgo - ${community ? `AND T.community_id = :communityId` : ''} - UNION ALL - SELECT - 'numThreadsLastMonth' as label, - COUNT(*) - FROM "Threads" - WHERE "created_at" >= :oneMonthAgo - ${community ? `AND community_id = :communityId` : ''} - UNION ALL - SELECT - 'numReactionsLastMonth' as label, - COUNT(R.*) - FROM "Reactions" R - LEFT JOIN "Threads" T ON R.thread_id = T.id ${community ? `AND T.community_id = :communityId` : ''} - LEFT JOIN "Comments" C ON R.comment_id = C.id - LEFT JOIN "Threads" TC ON C.thread_id = TC.id ${community ? `AND TC.community_id = :communityId` : ''} - WHERE R."created_at" >= :oneMonthAgo - UNION ALL - SELECT - 'numProposalVotesLastMonth' as label, - COUNT(*) - FROM "Votes" - WHERE "created_at" >= :oneMonthAgo - ${community ? `AND community_id = :communityId` : ''} - UNION ALL - SELECT - 'numPollsLastMonth' as label, - COUNT(*) - FROM "Polls" - WHERE "created_at" >= :oneMonthAgo - ${community ? `AND community_id = :communityId` : ''} - UNION ALL - SELECT - 'numMembersLastMonth' as label, - COUNT(*) - FROM "Addresses" - WHERE "created_at" >= :oneMonthAgo - ${community ? `AND community_id = :communityId` : ''} - UNION ALL - SELECT - 'numGroupsLastMonth' as label, - COUNT(*) - FROM "Groups" - WHERE "created_at" >= :oneMonthAgo - ${community ? `AND community_id = :communityId` : ''} - ) - SELECT json_build_object( - 'numCommentsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numCommentsLastMonth'), - 'numThreadsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numThreadsLastMonth'), - 'numReactionsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numReactionsLastMonth'), - 'numProposalVotesLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numProposalVotesLastMonth'), - 'numPollsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numPollsLastMonth'), - 'numMembersLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numMembersLastMonth'), - 'numGroupsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numGroupsLastMonth') - ) AS "monthlySummary"; - `, - { - replacements: monthlyStatsReplacements, - type: QueryTypes.SELECT, - }, - ), - this.models.sequelize.query<{ result: number }>( - ` - SELECT AVG(profile_count) as result - FROM ( - SELECT "Communities".id, COUNT("Addresses".id) as profile_count - FROM "Communities" - JOIN "Addresses" ON "Addresses".community_id = "Communities".id - GROUP BY "Communities".id - ) as _; - `, - { type: QueryTypes.SELECT }, - ), - this.models.sequelize.query<{ result: number }>( - ` - SELECT COUNT(communities_count) as result FROM ( - SELECT "Communities".id - FROM "Communities" - JOIN "Addresses" ON "Addresses".community_id = "Communities".id - GROUP BY "Communities".id - HAVING COUNT("Addresses".id) > 2 - ) as communities_count; - `, - { type: QueryTypes.SELECT }, - ), - ]); - - return { - lastMonthNewCommunities: lastMonthNewCommunities.map( - ({ id, created_at }) => ({ id, created_at }), - ), - totalStats: { - ...monthlySummary, - averageAddressesPerCommunity, - populatedCommunities, - }, - }; -} diff --git a/packages/commonwealth/server/routes/admin/get_stats_handler.ts b/packages/commonwealth/server/routes/admin/get_stats_handler.ts deleted file mode 100644 index 348aaa46f42..00000000000 --- a/packages/commonwealth/server/routes/admin/get_stats_handler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { GetStatsResult } from '../../controllers/server_admin_methods/get_stats'; -import { ServerControllers } from '../../routing/router'; -import { TypedRequestQuery, TypedResponse, success } from '../../types'; - -type GetStatsRequestQuery = { - community_id: string; -}; -type GetStatsResponse = GetStatsResult; - -export const getStatsHandler = async ( - controllers: ServerControllers, - req: TypedRequestQuery, - res: TypedResponse, -) => { - const stats = await controllers.admin.getStats({ - // @ts-expect-error StrictNullChecks - user: req.user, - communityId: req.query.community_id, - }); - return success(res, stats); -}; diff --git a/packages/commonwealth/server/routes/admin/get_top_users_handler.ts b/packages/commonwealth/server/routes/admin/get_top_users_handler.ts deleted file mode 100644 index b50f587ee19..00000000000 --- a/packages/commonwealth/server/routes/admin/get_top_users_handler.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { GetTopUsersResult } from 'server/controllers/server_admin_methods/get_top_users'; -import { ServerControllers } from '../../routing/router'; -import { TypedRequest, TypedResponse, success } from '../../types'; - -type GetTopUsersResponse = GetTopUsersResult; - -export const getTopUsersHandler = async ( - controllers: ServerControllers, - req: TypedRequest, - res: TypedResponse, -) => { - const result = await controllers.admin.getTopUsers({ - // @ts-expect-error StrictNullChecks - user: req.user, - }); - return success(res, result); -}; diff --git a/packages/commonwealth/server/routing/router.ts b/packages/commonwealth/server/routing/router.ts index 7ee10344b15..5bba8a215ff 100644 --- a/packages/commonwealth/server/routing/router.ts +++ b/packages/commonwealth/server/routing/router.ts @@ -47,15 +47,12 @@ import * as controllers from '../controller'; import deleteThreadLinks from '../routes/linking/deleteThreadLinks'; import getLinks from '../routes/linking/getLinks'; -import { ServerAdminController } from '../controllers/server_admin_controller'; import { ServerCommunitiesController } from '../controllers/server_communities_controller'; import { CacheDecorator } from '@hicommonwealth/adapters'; import { rateLimiterMiddleware } from 'server/middleware/rateLimiter'; -import { getTopUsersHandler } from 'server/routes/admin/get_top_users_handler'; import { getNamespaceMetadata } from 'server/routes/communities/get_namespace_metadata'; import { config } from '../config'; -import { getStatsHandler } from '../routes/admin/get_stats_handler'; import { aiCompletionHandler } from '../routes/ai'; import { getCanvasClockHandler } from '../routes/canvas/get_canvas_clock_handler'; import { createChainNodeHandler } from '../routes/communities/create_chain_node_handler'; @@ -72,7 +69,6 @@ import setupUniswapProxy from '../util/uniswapProxy'; export type ServerControllers = { communities: ServerCommunitiesController; - admin: ServerAdminController; }; function setupRouter( @@ -82,14 +78,10 @@ function setupRouter( databaseValidationService: DatabaseValidationService, cacheDecorator: CacheDecorator, ) { - // controllers const serverControllers: ServerControllers = { communities: new ServerCommunitiesController(models), - admin: new ServerAdminController(models), }; - // --- - const router = express.Router(); router.use(useragent.express()); @@ -191,22 +183,6 @@ function setupRouter( starCommunity.bind(this, models), ); - registerRoute( - router, - 'get', - '/admin/analytics', - passport.authenticate('jwt', { session: false }), - getStatsHandler.bind(this, serverControllers), - ); - - registerRoute( - router, - 'get', - '/admin/top-users', - passport.authenticate('jwt', { session: false }), - getTopUsersHandler.bind(this, serverControllers), - ); - registerRoute( router, 'get',