From 607513ce75b7ea774307fcf8caca4ad96a080f9b Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Tue, 22 Oct 2024 13:37:29 +0500 Subject: [PATCH 1/4] fix(stats): render dynamic stats and icon --- src/components/Stats/__tests__/index.tsx | 108 +++++++++-------------- src/components/Stats/index.tsx | 98 ++++++-------------- src/network/fetchSourcesData/index.ts | 12 +-- src/types/index.ts | 9 +- src/utils/splash/index.ts | 26 ++++-- 5 files changed, 91 insertions(+), 162 deletions(-) diff --git a/src/components/Stats/__tests__/index.tsx b/src/components/Stats/__tests__/index.tsx index d6cdd7d3a..eb2aecdab 100644 --- a/src/components/Stats/__tests__/index.tsx +++ b/src/components/Stats/__tests__/index.tsx @@ -3,7 +3,7 @@ import '@testing-library/jest-dom' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import React from 'react' import { ProcessingResponse, getTotalProcessing } from '~/network/fetchSourcesData' -import { Stats, StatsConfig } from '..' +import { Stats } from '..' import * as network from '../../../network/fetchSourcesData' import { useDataStore } from '../../../stores/useDataStore' import { useUserStore } from '../../../stores/useUserStore' @@ -35,14 +35,14 @@ const mockedUseDataStore = useDataStore as jest.MockedFunction const mockStats = { - numAudio: '1,000', - numContributors: '500', - numDaily: '100', - numEpisodes: '2,000', - nodeCount: '5,000', - numTwitterSpace: '300', - numVideo: '800', - numDocuments: '1483', + audio_count: '1,000', + contributors_count: '500', + daily_count: '100', + episodes_count: '2,000', + node_sount: '5,000', + twitter_spaceCount: '300', + video_count: '800', + documents_count: '1,483', } const mockBudget = 20000 @@ -54,15 +54,14 @@ describe('Component Test Stats', () => { mockedUseUserStore.mockImplementation(() => [mockBudget]) }) - it('verify that the component triggers the fetching of stats on mount.', () => { + it('verifies that the component triggers the fetching of stats on mount.', async () => { const mockedGetStats = jest.spyOn(network, 'getStats') render() - ;(async () => { - await waitFor(() => { - expect(mockedGetStats).toHaveBeenCalled() - }) - })() + + await waitFor(() => { + expect(mockedGetStats).toHaveBeenCalled() + }) }) it('should display null if no stats are available', () => { @@ -73,31 +72,30 @@ describe('Component Test Stats', () => { expect(container.innerHTML).toBe('') }) - it('correctly displayed upon successful fetching.', () => { + it('correctly displays stats upon successful fetching.', () => { mockedUseDataStore.mockReturnValue([mockStats, jest.fn()]) const { getByText } = render() - expect(getByText(mockStats.nodeCount)).toBeInTheDocument() - expect(getByText(mockStats.numAudio)).toBeInTheDocument() - expect(getByText(mockStats.numEpisodes)).toBeInTheDocument() - expect(getByText(mockStats.numVideo)).toBeInTheDocument() - expect(getByText(mockStats.numTwitterSpace)).toBeInTheDocument() - expect(getByText(mockStats.numDocuments)).toBeInTheDocument() + expect(getByText(mockStats.audio_count)).toBeInTheDocument() + expect(getByText(mockStats.contributors_count)).toBeInTheDocument() + expect(getByText(mockStats.daily_count)).toBeInTheDocument() + expect(getByText(mockStats.documents_count)).toBeInTheDocument() + expect(getByText(mockStats.episodes_count)).toBeInTheDocument() + expect(getByText(mockStats.video_count)).toBeInTheDocument() }) - it('test formatting of numbers', () => { + it('tests formatting of numbers', async () => { const mockedFormatStats = jest.spyOn(formatStats, 'formatNumberWithCommas') render() - ;(async () => { - await waitFor(() => { - expect(mockedFormatStats).toHaveBeenCalledTimes(8) - }) - })() + + await waitFor(() => { + expect(mockedFormatStats).toHaveBeenCalledTimes(8) + }) }) - it('tests that document stat pill is not displayed when document is returned in the response', () => { + it('tests that document stat pill is not displayed when the document count is zero', () => { mockedUseDataStore.mockReturnValue([{ ...mockStats, numDocuments: '0' }, jest.fn()]) const { queryByTestId } = render() @@ -105,7 +103,7 @@ describe('Component Test Stats', () => { expect(queryByTestId('DocumentIcon')).toBeNull() }) - it('test the formatting of the budget', () => { + it('tests the formatting of the budget', () => { mockedUseUserStore.mockReturnValue([mockBudget]) mockedUseDataStore.mockReturnValue([mockStats, jest.fn()]) @@ -116,42 +114,22 @@ describe('Component Test Stats', () => { expect(mockFormatBudget).toHaveBeenCalledWith(mockBudget) }) - it('ensure that each stat is accompanied by its corresponding icon and label', () => { + it('ensures that each stat is accompanied by its corresponding icon and label', () => { mockedUseDataStore.mockReturnValue([mockStats, jest.fn()]) const { getByText, getByTestId } = render() - expect(getByText(mockStats.nodeCount)).toBeInTheDocument() - expect(getByText(mockStats.numAudio)).toBeInTheDocument() - expect(getByText(mockStats.numEpisodes)).toBeInTheDocument() - expect(getByText(mockStats.numVideo)).toBeInTheDocument() - expect(getByText(mockStats.numTwitterSpace)).toBeInTheDocument() - - expect(getByTestId('AudioIcon')).toBeInTheDocument() - expect(getByTestId('BudgetIcon')).toBeInTheDocument() - expect(getByTestId('NodesIcon')).toBeInTheDocument() - expect(getByTestId('TwitterIcon')).toBeInTheDocument() - expect(getByTestId('VideoIcon')).toBeInTheDocument() - expect(getByTestId('DocumentIcon')).toBeInTheDocument() - }) - - it('asserts that OnClick, prediction/content/latest endpoint is called with media type query', () => { - const mockedSetBudget = jest.fn() - const fetchDataMock = jest.fn() - const setSelectedNode = jest.fn() - mockedUseUserStore.mockReturnValue([mockBudget, mockedSetBudget]) - mockedUseDataStore.mockReturnValue([mockStats, setSelectedNode, jest.fn(), fetchDataMock]) - - const { getByText } = render() - - StatsConfig.forEach(async ({ key, mediaType }) => { - expect(getByText(mockStats[key])).toBeInTheDocument() - fireEvent.click(getByText(mockStats[key])) - - await waitFor(() => { - expect(fetchDataMock).toHaveBeenCalledWith(mockedSetBudget, { ...(mediaType ? { media_type: mediaType } : {}) }) - }) - }) + expect(getByText(mockStats.node_sount)).toBeInTheDocument() + expect(getByText(mockStats.audio_count)).toBeInTheDocument() + expect(getByText(mockStats.episodes_count)).toBeInTheDocument() + expect(getByText(mockStats.video_count)).toBeInTheDocument() + expect(getByText(mockStats.twitter_spaceCount)).toBeInTheDocument() + + expect(getByTestId('Audio')).toBeInTheDocument() + expect(getByTestId('Episodes')).toBeInTheDocument() + expect(getByTestId('Node')).toBeInTheDocument() + expect(getByTestId('Twitter')).toBeInTheDocument() + expect(getByTestId('Video')).toBeInTheDocument() }) it('should render the button only if totalProcessing is present and greater than 0', async () => { @@ -163,13 +141,10 @@ describe('Component Test Stats', () => { totalProcessing: 0, } - // Now, simulate a response where totalProcessing is not present or is 0 mockedGetTotalProcessing.mockResolvedValueOnce(mockResponse) - // Re-render the component to reflect the new mock response render() - // The button should not be visible since totalProcessing is equal to 0 const viewContent = screen.queryByTestId('view-content') expect(viewContent).toBeNull() @@ -179,15 +154,12 @@ describe('Component Test Stats', () => { totalProcessing: 100, } - // Mocking a response where totalProcessing is present and greater than 0 mockedGetTotalProcessing.mockResolvedValueOnce(mockResponse2) render() - // Wait for the component to finish loading await screen.findByTestId('view-content') - // The button should be visible since totalProcessing is present and greater than 0 const button = screen.getByText('100') expect(button).toBeInTheDocument() }) diff --git a/src/components/Stats/index.tsx b/src/components/Stats/index.tsx index dadc1255c..2d06344e2 100644 --- a/src/components/Stats/index.tsx +++ b/src/components/Stats/index.tsx @@ -1,13 +1,10 @@ import { noop } from 'lodash' import { useEffect, useState } from 'react' import styled from 'styled-components' -import AudioIcon from '~/components/Icons/AudioIcon' import BudgetIcon from '~/components/Icons/BudgetIcon' import NodesIcon from '~/components/Icons/NodesIcon' -import TwitterIcon from '~/components/Icons/TwitterIcon' -import VideoIcon from '~/components/Icons/VideoIcon' import { Tooltip } from '~/components/common/ToolTip' -import { TStatParams, getStats, getTotalProcessing } from '~/network/fetchSourcesData' +import { getStats, getTotalProcessing } from '~/network/fetchSourcesData' import { useDataStore } from '~/stores/useDataStore' import { useUpdateSelectedNode } from '~/stores/useGraphStore' import { useModal } from '~/stores/useModalStore' @@ -15,75 +12,16 @@ import { useUserStore } from '~/stores/useUserStore' import { TStats } from '~/types' import { formatBudget, formatStatsResponse } from '~/utils' import { colors } from '~/utils/colors' -import DocumentIcon from '../Icons/DocumentIcon' -import EpisodeIcon from '../Icons/EpisodeIcon' import { Flex } from '../common/Flex' import { Animation } from './Animation' - -interface StatConfigItem { - name: string - icon: JSX.Element - key: keyof TStats - dataKey: keyof TStatParams - mediaType: string - tooltip: string -} - -export const StatsConfig: StatConfigItem[] = [ - { - name: 'Nodes', - icon: , - key: 'nodeCount', - dataKey: 'node_count', - mediaType: '', - tooltip: 'All Nodes', - }, - { - name: 'Episodes', - icon: , - key: 'numEpisodes', - dataKey: 'num_episodes', - mediaType: 'episode', - tooltip: 'Episodes', - }, - { - name: 'Audio', - icon: , - key: 'numAudio', - dataKey: 'num_audio', - mediaType: 'audio', - tooltip: 'Audios', - }, - { - name: 'Video', - icon: , - key: 'numVideo', - dataKey: 'num_video', - mediaType: 'video', - tooltip: 'Videos', - }, - { - name: 'Twitter Spaces', - icon: , - key: 'numTwitterSpace', - dataKey: 'num_tweet', - mediaType: 'twitter', - tooltip: 'Posts', - }, - { - name: 'Document', - icon: , - key: 'numDocuments', - dataKey: 'num_documents', - mediaType: 'document', - tooltip: 'Documents', - }, -] +import { Icons } from '~/components/Icons' +import { useSchemaStore } from '~/stores/useSchemaStore' export const Stats = () => { const [isTotalProcessing, setIsTotalProcessing] = useState(false) const [totalProcessing, setTotalProcessing] = useState(0) const [budget, setBudget] = useUserStore((s) => [s.budget, s.setBudget]) + const { normalizedSchemasByType } = useSchemaStore((s) => s) const [stats, setStats, fetchData, setAbortRequests] = useDataStore((s) => [ s.stats, @@ -151,14 +89,36 @@ export const Stats = () => { return null } + const convertToTitleCase = (key: string) => key.replace(/\b\w/g, (char) => char.toUpperCase()) + + const generateStatConfigItem = (key: string) => { + const name = convertToTitleCase(key.split('_')[0]) + const tooltip = name + const primaryIcon = normalizedSchemasByType[name]?.icon + const Icon = Icons[primaryIcon as string] || NodesIcon + + return { + name, + Icon, + key, + dataKey: key, + mediaType: name, + tooltip, + } + } + + const StatsConfig = Object.keys(stats).map((key) => generateStatConfigItem(key)) + return ( - {StatsConfig.map(({ name, icon, key, mediaType, tooltip }) => - stats[key as keyof TStats] !== '0' ? ( + {StatsConfig.map(({ name, Icon, key, mediaType, tooltip }) => + stats[key as keyof TStats] !== 0 ? ( handleStatClick(mediaType)}> -
{icon}
+
+ +
{stats[key as keyof TStats]}
diff --git a/src/network/fetchSourcesData/index.ts b/src/network/fetchSourcesData/index.ts index 90731fba6..cdc17a04e 100644 --- a/src/network/fetchSourcesData/index.ts +++ b/src/network/fetchSourcesData/index.ts @@ -27,17 +27,7 @@ export type TAboutParams = { } export type TStatParams = { - num_audio: number - num_contributors: number - num_daily: number - num_episodes: number - num_nodes: number - num_people: number - num_tweet: number - num_twitter_space: number - num_video: number - num_documents: number - [key: string]: number + [type: string]: number } export type TPriceParams = { diff --git a/src/types/index.ts b/src/types/index.ts index 55cd48c9e..bab9fe772 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -262,14 +262,7 @@ export type BalanceResponse = { } export type TStats = { - numAudio?: string - numContributors?: string - numDaily?: string - numEpisodes?: string - nodeCount?: string - numTwitterSpace?: string - numVideo?: string - numDocuments?: string + [key: string]: number } export type RelayUser = { diff --git a/src/utils/splash/index.ts b/src/utils/splash/index.ts index e72398cc5..2793a46ad 100644 --- a/src/utils/splash/index.ts +++ b/src/utils/splash/index.ts @@ -1,5 +1,4 @@ import { initialMessageData } from '~/components/App/Splash/constants' -import { StatsConfig } from '~/components/Stats' import { TStatParams } from '~/network/fetchSourcesData' import { TStats } from '~/types' import { formatNumberWithCommas } from '../formatStats' @@ -10,12 +9,27 @@ import { formatNumberWithCommas } from '../formatStats' * @returns {TStats} The formatted statistics object. */ -export const formatStatsResponse = (statsResponse: TStatParams): TStats => - StatsConfig.reduce((updatedStats: TStats, { key, dataKey }) => { - const formattedValue = formatNumberWithCommas(statsResponse[dataKey] ?? 0) +export const formatStatsResponse = (statsResponse: TStatParams): TStats => { + // Filter out keys that start with 'num_' + const filteredData = Object.keys(statsResponse) + .filter((key) => !key.startsWith('num_')) + .map((key) => ({ + key, + value: statsResponse[key], + })) - return { ...updatedStats, [key]: formattedValue } - }, {}) + // Sort the stats by their values and take the top 5 + const top5 = filteredData.sort((a, b) => b.value - a.value).slice(0, 5) + + // Convert the array back into an object format + const top5Object = top5.reduce((acc, { key, value }) => { + acc[key] = value + + return acc + }, {} as Record) + + return top5Object +} /** * Formats the splash message based on the statistics response. From 9df84844e2245e7858cf5ed46197b766686234f6 Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Tue, 22 Oct 2024 13:42:33 +0500 Subject: [PATCH 2/4] fix(stats): stats changes --- src/components/Stats/__tests__/index.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Stats/__tests__/index.tsx b/src/components/Stats/__tests__/index.tsx index eb2aecdab..2402fa456 100644 --- a/src/components/Stats/__tests__/index.tsx +++ b/src/components/Stats/__tests__/index.tsx @@ -54,14 +54,15 @@ describe('Component Test Stats', () => { mockedUseUserStore.mockImplementation(() => [mockBudget]) }) - it('verifies that the component triggers the fetching of stats on mount.', async () => { + it('verify that the component triggers the fetching of stats on mount.', () => { const mockedGetStats = jest.spyOn(network, 'getStats') render() - - await waitFor(() => { - expect(mockedGetStats).toHaveBeenCalled() - }) + ;(async () => { + await waitFor(() => { + expect(mockedGetStats).toHaveBeenCalled() + }) + })() }) it('should display null if no stats are available', () => { From 143159405b30eb00e5cefc529bfc40a4d2013a4e Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Tue, 22 Oct 2024 13:45:17 +0500 Subject: [PATCH 3/4] fix(stats): stats changes push again --- src/components/Stats/__tests__/index.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Stats/__tests__/index.tsx b/src/components/Stats/__tests__/index.tsx index 2402fa456..192c89836 100644 --- a/src/components/Stats/__tests__/index.tsx +++ b/src/components/Stats/__tests__/index.tsx @@ -86,14 +86,15 @@ describe('Component Test Stats', () => { expect(getByText(mockStats.video_count)).toBeInTheDocument() }) - it('tests formatting of numbers', async () => { + it('test formatting of numbers', () => { const mockedFormatStats = jest.spyOn(formatStats, 'formatNumberWithCommas') render() - - await waitFor(() => { - expect(mockedFormatStats).toHaveBeenCalledTimes(8) - }) + ;(async () => { + await waitFor(() => { + expect(mockedFormatStats).toHaveBeenCalledTimes(8) + }) + })() }) it('tests that document stat pill is not displayed when the document count is zero', () => { From 4f636f4170ef1437b5caf7a29983fadb3a067dd1 Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Tue, 22 Oct 2024 13:57:49 +0500 Subject: [PATCH 4/4] fix(stats): fixed eslint and unit tests --- src/components/Stats/__tests__/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Stats/__tests__/index.tsx b/src/components/Stats/__tests__/index.tsx index 192c89836..270d928dd 100644 --- a/src/components/Stats/__tests__/index.tsx +++ b/src/components/Stats/__tests__/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable padding-line-between-statements */ import '@testing-library/jest-dom' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import React from 'react' import { ProcessingResponse, getTotalProcessing } from '~/network/fetchSourcesData' import { Stats } from '..' @@ -37,7 +37,7 @@ const mockedUseUserStore = useUserStore as jest.MockedFunction { totalProcessing: 0, } + // Now, simulate a response where totalProcessing is not present or is 0 mockedGetTotalProcessing.mockResolvedValueOnce(mockResponse) + // Re-render the component to reflect the new mock response render() + // The button should not be visible since totalProcessing is equal to 0 const viewContent = screen.queryByTestId('view-content') expect(viewContent).toBeNull() @@ -156,12 +159,15 @@ describe('Component Test Stats', () => { totalProcessing: 100, } + // Mocking a response where totalProcessing is present and greater than 0 mockedGetTotalProcessing.mockResolvedValueOnce(mockResponse2) render() + // Wait for the component to finish loading await screen.findByTestId('view-content') + // The button should be visible since totalProcessing is present and greater than 0 const button = screen.getByText('100') expect(button).toBeInTheDocument() })