Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Stats] - render dynamic stats and icon #2373

Merged
merged 4 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading