Skip to content

Commit

Permalink
Merge pull request #2373 from MahtabBukhari/render_dynamic_stats_and_…
Browse files Browse the repository at this point in the history
…icon

[Stats] - render dynamic stats and icon
  • Loading branch information
Rassl authored Oct 22, 2024
2 parents 83e1ee8 + 4f636f4 commit 5a19d26
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 145 deletions.
82 changes: 31 additions & 51 deletions src/components/Stats/__tests__/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* 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, StatsConfig } from '..'
import { Stats } from '..'
import * as network from '../../../network/fetchSourcesData'
import { useDataStore } from '../../../stores/useDataStore'
import { useUserStore } from '../../../stores/useUserStore'
Expand Down Expand Up @@ -35,14 +35,14 @@ const mockedUseDataStore = useDataStore as jest.MockedFunction<typeof useDataSto
const mockedUseUserStore = useUserStore as jest.MockedFunction<typeof useUserStore>

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: '200',
episodes_count: '2,000',
node_sount: '5,000',
twitter_spaceCount: '300',
video_count: '800',
documents_count: '1,483',
}

const mockBudget = 20000
Expand Down Expand Up @@ -73,17 +73,17 @@ 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(<Stats />)

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', () => {
Expand All @@ -97,15 +97,15 @@ describe('Component Test Stats', () => {
})()
})

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(<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()])

Expand All @@ -116,42 +116,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(<Stats />)

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(<Stats />)

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 () => {
Expand Down
98 changes: 29 additions & 69 deletions src/components/Stats/index.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,27 @@
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'
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: <NodesIcon />,
key: 'nodeCount',
dataKey: 'node_count',
mediaType: '',
tooltip: 'All Nodes',
},
{
name: 'Episodes',
icon: <EpisodeIcon />,
key: 'numEpisodes',
dataKey: 'num_episodes',
mediaType: 'episode',
tooltip: 'Episodes',
},
{
name: 'Audio',
icon: <AudioIcon />,
key: 'numAudio',
dataKey: 'num_audio',
mediaType: 'audio',
tooltip: 'Audios',
},
{
name: 'Video',
icon: <VideoIcon />,
key: 'numVideo',
dataKey: 'num_video',
mediaType: 'video',
tooltip: 'Videos',
},
{
name: 'Twitter Spaces',
icon: <TwitterIcon />,
key: 'numTwitterSpace',
dataKey: 'num_tweet',
mediaType: 'twitter',
tooltip: 'Posts',
},
{
name: 'Document',
icon: <DocumentIcon />,
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,
Expand Down Expand Up @@ -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 (
<StatisticsContainer>
<StatisticsWrapper>
{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 ? (
<Stat key={name} data-testid={mediaType} onClick={() => handleStatClick(mediaType)}>
<Tooltip content={tooltip} margin="13px">
<div className="icon">{icon}</div>
<div className="icon">
<Icon />
</div>
<div className="text">{stats[key as keyof TStats]}</div>
</Tooltip>
</Stat>
Expand Down
12 changes: 1 addition & 11 deletions src/network/fetchSourcesData/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
9 changes: 1 addition & 8 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
26 changes: 20 additions & 6 deletions src/utils/splash/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<string, number>)

return top5Object
}

/**
* Formats the splash message based on the statistics response.
Expand Down

0 comments on commit 5a19d26

Please sign in to comment.