Skip to content

Commit

Permalink
Merge pull request #632 from systemli/Improve-background-loading-for-…
Browse files Browse the repository at this point in the history
…Tickers

⚡️ Improve background loading for Tickers
  • Loading branch information
0x46616c6b authored May 12, 2024
2 parents 8643d72 + a760e7f commit 7dde41c
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 208 deletions.
9 changes: 2 additions & 7 deletions src/api/Ticker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ export interface GetTickersQueryParams {
active?: boolean
domain?: string
title?: string
order_by: string
sort: SortDirection
order_by?: string
sort?: SortDirection
}

export function useTickerApi(token: string) {
Expand Down Expand Up @@ -134,10 +134,6 @@ export function useTickerApi(token: string) {
return fetch(`${ApiUrl}/admin/tickers?${query}`, { headers: headers }).then(response => response.json())
}

const getTicker = (id: number): Promise<Response<TickerResponseData>> => {
return fetch(`${ApiUrl}/admin/tickers/${id}`, { headers: headers }).then(response => response.json())
}

const postTicker = (data: Ticker): Promise<Response<TickerResponseData>> => {
return fetch(`${ApiUrl}/admin/tickers`, {
headers: headers,
Expand Down Expand Up @@ -218,7 +214,6 @@ export function useTickerApi(token: string) {
deleteTicker,
deleteTickerUser,
getTickers,
getTicker,
getTickerUsers,
postTicker,
putTicker,
Expand Down
77 changes: 48 additions & 29 deletions src/components/ticker/Ticker.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,67 @@
import { faGear } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Alert, Box, Button, Card, CardContent, Grid, Stack, Typography } from '@mui/material'
import { FC, useState } from 'react'
import { Ticker as Model } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import Loader from '../Loader'
import MessageForm from '../message/MessageForm'
import TickerCard from './TickerCard'
import MessageList from '../message/MessageList'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faGear } from '@fortawesome/free-solid-svg-icons'
import { Alert, Box, Button, Card, CardContent, Grid, Stack, Typography } from '@mui/material'
import TickerModalForm from './TickerModalForm'
import TickerCard from './TickerCard'
import TickerDangerZoneCard from './TickerDangerZoneCard'
import TickerModalForm from './TickerModalForm'
import TickerUsersCard from './TickerUsersCard'
import useAuth from '../../contexts/useAuth'

interface Props {
ticker: Model
ticker?: Model
isLoading: boolean
}

const Ticker: FC<Props> = ({ ticker }) => {
const Ticker: FC<Props> = ({ ticker, isLoading }) => {
const { user } = useAuth()
const [formModalOpen, setFormModalOpen] = useState<boolean>(false)

const headline = () => (
<Stack alignItems="center" direction="row" justifyContent="space-between" mb={2}>
<Typography component="h2" gutterBottom variant="h3">
Ticker
</Typography>
<Button
onClick={() => {
setFormModalOpen(true)
}}
startIcon={<FontAwesomeIcon icon={faGear} />}
variant="contained"
>
Configure
</Button>
<TickerModalForm
onClose={() => {
setFormModalOpen(false)
}}
open={formModalOpen}
ticker={ticker}
/>
</Stack>
)

if (ticker === undefined || isLoading) {
return (
<Grid container spacing={2}>
<Grid item xs={12}>
{headline()}
</Grid>
<Grid item xs={12}>
<Loader />
</Grid>
</Grid>
)
}

return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack alignItems="center" direction="row" justifyContent="space-between" mb={2}>
<Typography component="h2" gutterBottom variant="h3">
Ticker
</Typography>
<Button
onClick={() => {
setFormModalOpen(true)
}}
startIcon={<FontAwesomeIcon icon={faGear} />}
variant="contained"
>
Configure
</Button>
<TickerModalForm
onClose={() => {
setFormModalOpen(false)
}}
open={formModalOpen}
ticker={ticker}
/>
</Stack>
{headline()}
</Grid>
{!ticker.active ? (
<Grid item xs={12}>
Expand Down
4 changes: 2 additions & 2 deletions src/components/ticker/TickerList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('TickerList', function () {
<QueryClientProvider client={client}>
<MemoryRouter>
<AuthProvider>
<TickerList />
<TickerList token="1" />
</AuthProvider>
</MemoryRouter>
</QueryClientProvider>
Expand All @@ -44,6 +44,6 @@ describe('TickerList', function () {

setup()

expect(TickerListItems).toHaveBeenCalledTimes(4)
expect(TickerListItems).toHaveBeenCalledTimes(3)
})
})
19 changes: 11 additions & 8 deletions src/components/ticker/TickerList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { FC, useEffect, useState } from 'react'
import TickerListItems from './TickerListItems'
import { Table, TableCell, TableContainer, TableHead, TableRow, TableSortLabel } from '@mui/material'
import useDebounce from '../../hooks/useDebounce'
import { FC, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { GetTickersQueryParams } from '../../api/Ticker'
import useDebounce from '../../hooks/useDebounce'
import TickerListFilter from './TickerListFilter'
import TickerListItems from './TickerListItems'

interface Props {
token: string
}

const TickerList: FC = () => {
const initialState = { order_by: 'id', sort: 'asc' } as GetTickersQueryParams
const [params, setParams] = useState<GetTickersQueryParams>(initialState)
const debouncedValue = useDebounce<GetTickersQueryParams>(params, 200, initialState)
const TickerList: FC<Props> = ({ token }) => {
const [params, setParams] = useState<GetTickersQueryParams>({})
const debouncedValue = useDebounce<GetTickersQueryParams>(params, 200, {})
const [, setSearchParams] = useSearchParams()

useEffect(() => {
Expand Down Expand Up @@ -88,7 +91,7 @@ const TickerList: FC = () => {
<TableCell />
</TableRow>
</TableHead>
<TickerListItems params={debouncedValue} />
<TickerListItems token={token} params={debouncedValue} />
</Table>
</TableContainer>
)
Expand Down
10 changes: 5 additions & 5 deletions src/components/ticker/TickerListItems.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react'
import { GetTickersQueryParams } from '../../api/Ticker'
import TickerListItems from './TickerListItems'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import sign from 'jwt-encode'
import { MemoryRouter } from 'react-router'
import { GetTickersQueryParams } from '../../api/Ticker'
import { AuthProvider } from '../../contexts/AuthContext'
import sign from 'jwt-encode'
import TickerListItems from './TickerListItems'

describe('TickerListItems', function () {
beforeEach(() => {
Expand Down Expand Up @@ -35,7 +35,7 @@ describe('TickerListItems', function () {
<QueryClientProvider client={client}>
<MemoryRouter>
<AuthProvider>
<TickerListItems params={params} />
<TickerListItems params={params} token={jwt('admin')} />
</AuthProvider>
</MemoryRouter>
</QueryClientProvider>
Expand Down
30 changes: 9 additions & 21 deletions src/components/ticker/TickerListItems.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import { CircularProgress, Stack, TableBody, TableCell, TableRow, Typography } from '@mui/material'
import { FC } from 'react'
import { GetTickersQueryParams, useTickerApi } from '../../api/Ticker'
import { GetTickersQueryParams } from '../../api/Ticker'
import useTickersQuery from '../../queries/tickers'
import TickerListItem from './TickerListItem'
import useAuth from '../../contexts/useAuth'
import { useQuery } from '@tanstack/react-query'
import { Navigate } from 'react-router'

interface Props {
token: string
params: GetTickersQueryParams
}

const TickerListItems: FC<Props> = ({ params }: Props) => {
const { token, user } = useAuth()
const { getTickers } = useTickerApi(token)
const { isFetching, error, data } = useQuery({
queryKey: ['tickers', params],
queryFn: () => getTickers(params),
placeholderData: previousData => previousData,
})
const TickerListItems: FC<Props> = ({ token, params }) => {
const { data, isLoading, error } = useTickersQuery({ token, params: params })
const tickers = data?.data.tickers || []

if (data === undefined && isFetching) {
if (isLoading) {
return (
<TableBody>
<TableRow>
Expand All @@ -36,7 +30,7 @@ const TickerListItems: FC<Props> = ({ params }: Props) => {
)
}

if (error || data === undefined || data.status === 'error') {
if (error) {
return (
<TableBody>
<TableRow>
Expand All @@ -48,7 +42,7 @@ const TickerListItems: FC<Props> = ({ params }: Props) => {
)
}

if (data.status === 'success' && data.data.tickers.length === 0) {
if (tickers.length === 0) {
return (
<TableBody>
<TableRow>
Expand All @@ -62,12 +56,6 @@ const TickerListItems: FC<Props> = ({ params }: Props) => {
)
}

const tickers = data.data.tickers

if (tickers.length === 1 && !user?.roles.includes('admin')) {
return <Navigate replace to={`/ticker/${tickers[0].id}`} />
}

return (
<TableBody>
{tickers.map(ticker => (
Expand Down
19 changes: 19 additions & 0 deletions src/queries/tickers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useQuery } from '@tanstack/react-query'
import { GetTickersQueryParams, useTickerApi } from '../api/Ticker'

interface Props {
token: string
params: GetTickersQueryParams
}

const useTickersQuery = ({ token, params }: Props) => {
const { getTickers } = useTickerApi(token)

return useQuery({
queryKey: ['tickers', params],
queryFn: () => getTickers(params),
placeholderData: previousData => previousData,
})
}

export default useTickersQuery
93 changes: 93 additions & 0 deletions src/views/HomeView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import sign from 'jwt-encode'
import { MemoryRouter } from 'react-router'
import { AuthProvider } from '../contexts/AuthContext'
import HomeView from './HomeView'

describe('HomeView', () => {
function setup() {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})

return render(
<QueryClientProvider client={client}>
<MemoryRouter>
<AuthProvider>
<HomeView />
</AuthProvider>
</MemoryRouter>
</QueryClientProvider>
)
}

it('should render error when query fails', async () => {
fetchMock.mockReject(new Error('Failed to fetch'))

const token = sign(
{
id: 1,
email: '[email protected]',
roles: ['user', 'admin'],
exp: new Date().getTime() / 1000 + 600,
},
'secret'
)
vi.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue(token)

setup()

expect(fetchMock).toHaveBeenCalledTimes(1)

expect(screen.getByText(/loading/i)).toBeInTheDocument()
expect(await screen.findByText('Unable to fetch tickers from server.')).toBeInTheDocument()
})

it('should render tickers list', async () => {
fetchMock.mockResponseOnce(JSON.stringify({ data: { tickers: [] } }))

const token = sign(
{
id: 1,
email: '[email protected]',
roles: ['user', 'admin'],
exp: new Date().getTime() / 1000 + 600,
},
'secret'
)

vi.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue(token)

setup()

expect(fetchMock).toHaveBeenCalledTimes(2)

expect(screen.getByText(/loading/i)).toBeInTheDocument()
expect(await screen.findByText('Tickers')).toBeInTheDocument()
})

it('should redirect to ticker when user has only one ticker', async () => {
fetchMock.mockResponseOnce(JSON.stringify({ data: { tickers: [{ id: 1 }] } }))

const token = sign(
{
id: 1,
email: '[email protected]',
roles: ['user'],
exp: new Date().getTime() / 1000 + 600,
},
'secret'
)

vi.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue(token)

setup()

expect(screen.getByText(/loading/i)).toBeInTheDocument()
})
})
Loading

0 comments on commit 7dde41c

Please sign in to comment.