diff --git a/packages/protocol-dashboard/src/components/BarChart/BarChart.tsx b/packages/protocol-dashboard/src/components/BarChart/BarChart.tsx index 4a6a3c82c6f..6488f8423a2 100644 --- a/packages/protocol-dashboard/src/components/BarChart/BarChart.tsx +++ b/packages/protocol-dashboard/src/components/BarChart/BarChart.tsx @@ -167,7 +167,7 @@ const BarChart: React.FC = ({
{error ? ( - + ) : data && labels ? ( <>
diff --git a/packages/protocol-dashboard/src/components/Error/Error.tsx b/packages/protocol-dashboard/src/components/Error/Error.tsx index e120fdb35d7..31a56fea634 100644 --- a/packages/protocol-dashboard/src/components/Error/Error.tsx +++ b/packages/protocol-dashboard/src/components/Error/Error.tsx @@ -4,7 +4,9 @@ import { ReactComponent as IconUhOh } from 'assets/img/uhOh.svg' import styles from './Error.module.css' -const Error = ({ text }: { text: string }) => { +const DEFAULT_ERROR_TEXT = 'Incomplete Data' + +const Error = ({ text = DEFAULT_ERROR_TEXT }: { text?: string }) => { return (
diff --git a/packages/protocol-dashboard/src/components/LineChart/LineChart.tsx b/packages/protocol-dashboard/src/components/LineChart/LineChart.tsx index 615f9ad5da3..72cc856861f 100644 --- a/packages/protocol-dashboard/src/components/LineChart/LineChart.tsx +++ b/packages/protocol-dashboard/src/components/LineChart/LineChart.tsx @@ -283,7 +283,7 @@ const LineChart: React.FC = ({
{error ? ( - + ) : data && labels ? ( = ({
{error ? ( - + ) : data && labels ? ( ) : ( diff --git a/packages/protocol-dashboard/src/components/Stat/Stat.tsx b/packages/protocol-dashboard/src/components/Stat/Stat.tsx index a2e75cb24f9..f65c10443c8 100644 --- a/packages/protocol-dashboard/src/components/Stat/Stat.tsx +++ b/packages/protocol-dashboard/src/components/Stat/Stat.tsx @@ -18,7 +18,7 @@ const Stat: React.FC = ({ stat, label, error }) => { {error ? (
- +
) : stat !== null ? (
{stat}
diff --git a/packages/protocol-dashboard/src/components/TopAlbums/TopAlbums.tsx b/packages/protocol-dashboard/src/components/TopAlbums/TopAlbums.tsx index e7baa780199..653d0b80c05 100644 --- a/packages/protocol-dashboard/src/components/TopAlbums/TopAlbums.tsx +++ b/packages/protocol-dashboard/src/components/TopAlbums/TopAlbums.tsx @@ -1,7 +1,9 @@ +import Error from 'components/Error' import Loading from 'components/Loading' import Paper from 'components/Paper' import React, { useCallback } from 'react' import { useTopAlbums } from 'store/cache/music/hooks' +import { MusicError } from 'store/cache/music/slice' import { formatShortNumber } from 'utils/format' import styles from './TopAlbums.module.css' @@ -19,37 +21,37 @@ const TopAlbums: React.FC = () => { const goToUrl = useCallback((url: string) => { window.open(url, '_blank') }, []) + + const renderTopAlbums = () => { + if (topAlbums === MusicError.ERROR) return + return !!topAlbums ? ( + topAlbums.map((p, i) => ( +
goToUrl(p.url)}> +
goToUrl(p.url)} + style={{ + backgroundImage: `url(${p.artwork})` + }} + /> +
+
{p.title}
+
{p.handle}
+
+
+ {`${formatShortNumber(p.plays)} Plays`} +
+
+ )) + ) : ( + + ) + } + return (
{messages.title}
-
- {!!topAlbums ? ( - topAlbums.map((p, i) => ( -
goToUrl(p.url)} - > -
goToUrl(p.url)} - style={{ - backgroundImage: `url(${p.artwork})` - }} - /> -
-
{p.title}
-
{p.handle}
-
-
- {`${formatShortNumber(p.plays)} Plays`} -
-
- )) - ) : ( - - )} -
+
{renderTopAlbums()}
) } diff --git a/packages/protocol-dashboard/src/components/TopPlaylists/TopPlaylists.tsx b/packages/protocol-dashboard/src/components/TopPlaylists/TopPlaylists.tsx index 66b1348168d..0cc2fd1d66a 100644 --- a/packages/protocol-dashboard/src/components/TopPlaylists/TopPlaylists.tsx +++ b/packages/protocol-dashboard/src/components/TopPlaylists/TopPlaylists.tsx @@ -1,7 +1,9 @@ +import Error from 'components/Error' import Loading from 'components/Loading' import Paper from 'components/Paper' import React, { useCallback } from 'react' import { useTopPlaylists } from 'store/cache/music/hooks' +import { MusicError } from 'store/cache/music/slice' import { formatShortNumber } from 'utils/format' import styles from './TopPlaylists.module.css' @@ -19,36 +21,35 @@ const TopPlaylists: React.FC = () => { const goToUrl = useCallback((url: string) => { window.open(url, '_blank') }, []) + + const renderTopPlaylists = () => { + if (topPlaylists === MusicError.ERROR) return + return !!topPlaylists ? ( + topPlaylists!.map((p, i) => ( +
goToUrl(p.url)}> +
+
+
{p.title}
+
{p.handle}
+
+
+ {`${formatShortNumber(p.plays)} Plays`} +
+
+ )) + ) : ( + + ) + } return (
{messages.title}
-
- {!!topPlaylists ? ( - topPlaylists!.map((p, i) => ( -
goToUrl(p.url)} - > -
-
-
{p.title}
-
{p.handle}
-
-
- {`${formatShortNumber(p.plays)} Plays`} -
-
- )) - ) : ( - - )} -
+
{renderTopPlaylists()}
) } diff --git a/packages/protocol-dashboard/src/components/TopTracks/TopTracks.tsx b/packages/protocol-dashboard/src/components/TopTracks/TopTracks.tsx index 48fce1fdac1..ddbf23b75cd 100644 --- a/packages/protocol-dashboard/src/components/TopTracks/TopTracks.tsx +++ b/packages/protocol-dashboard/src/components/TopTracks/TopTracks.tsx @@ -1,7 +1,9 @@ +import Error from 'components/Error' import Loading from 'components/Loading' import Paper from 'components/Paper' import React, { useCallback } from 'react' import { useTopTracks } from 'store/cache/music/hooks' +import { MusicError } from 'store/cache/music/slice' import { createStyles } from 'utils/mobile' import desktopStyles from './TopTracks.module.css' @@ -22,32 +24,35 @@ const TopTracks: React.FC = () => { const goToUrl = useCallback((url: string) => { window.open(url, '_blank') }, []) + + const renderTopTracks = () => { + if (topTracks === MusicError.ERROR) return + return !!topTracks ? ( + topTracks.map((t, i) => ( +
+
goToUrl(t.url)} + style={{ + backgroundImage: `url(${t.artwork})` + }} + /> +
goToUrl(t.url)}> + {t.title} +
+
goToUrl(t.userUrl)}> + {t.handle} +
+
+ )) + ) : ( + + ) + } return (
{messages.title}
-
- {!!topTracks ? ( - topTracks.map((t, i) => ( -
-
goToUrl(t.url)} - style={{ - backgroundImage: `url(${t.artwork})` - }} - /> -
goToUrl(t.url)}> - {t.title} -
-
goToUrl(t.userUrl)}> - {t.handle} -
-
- )) - ) : ( - - )} -
+
{renderTopTracks()}
) } diff --git a/packages/protocol-dashboard/src/services/Audius/helpers.ts b/packages/protocol-dashboard/src/services/Audius/helpers.ts index 72f135240b3..f0a29a63719 100644 --- a/packages/protocol-dashboard/src/services/Audius/helpers.ts +++ b/packages/protocol-dashboard/src/services/Audius/helpers.ts @@ -2,6 +2,7 @@ import { Utils } from '@audius/libs' import { formatNumber, formatAudString } from 'utils/format' import AudiusClient from './AudiusClient' import { Permission, BigNumber } from 'types' +import { fetchWithTimeout } from 'utils/fetch' // Helpers export async function hasPermissions( @@ -113,9 +114,9 @@ export function getWei(amount: BigNumber) { export async function getNodeVersion(endpoint: string): Promise { try { - const version = await fetch(`${endpoint}/health_check`) - .then(res => res.json()) - .then(r => r.data.version) + const version = await fetchWithTimeout(`${endpoint}/health_check`).then( + r => r.data.version + ) return version } catch (e) { console.error(e) diff --git a/packages/protocol-dashboard/src/store/cache/analytics/hooks.ts b/packages/protocol-dashboard/src/store/cache/analytics/hooks.ts index 05f59a95fa9..753cefb3b82 100644 --- a/packages/protocol-dashboard/src/store/cache/analytics/hooks.ts +++ b/packages/protocol-dashboard/src/store/cache/analytics/hooks.ts @@ -25,7 +25,7 @@ import { useAverageBlockTime, useEthBlockNumber } from '../protocol/hooks' import { weiAudToAud } from 'utils/numeric' import { ELECTRONIC_SUB_GENRES } from './genres' import { performWithFallback } from 'utils/performWithFallback' - +import { fetchWithTimeout } from '../../../utils/fetch' dayjs.extend(duration) const MONTH_IN_MS = dayjs.duration({ months: 1 }).asMilliseconds() @@ -187,7 +187,7 @@ async function fetchTimeSeries( try { const bucket_size = BUCKET_GRANULARITY_MAP[bucket] const url = `${node.endpoint}/v1/metrics/${route}?bucket_size=${bucket_size}&start_time=${startTime}` - const res = await (await fetch(url)).json() + const res = await fetchWithTimeout(url) return res.data } catch (e) { console.error(e) @@ -291,9 +291,7 @@ export function fetchTotalStaked( const getTrailingAPI = (endpoint: string) => async () => { const url = `${endpoint}/v1/metrics/routes/trailing/month` - const res = await fetch(url) - if (!res.ok) throw new Error(res.statusText) - const json = await res.json() + const json = await fetchWithTimeout(url) return { count: json?.data?.count ?? 0, unique_count: json?.data?.unique_count ?? 0 @@ -305,11 +303,7 @@ const getTrailingAPILegacy = ( startTime: number ) => async () => { const url = `${endpoint}/v1/metrics/routes?bucket_size=century&start_time=${startTime}` - const res = await fetch(url) - if (!res.ok) { - throw new Error(res.statusText) - } - const json = await res.json() + const json = await fetchWithTimeout(url) return { count: json?.data?.[0]?.count ?? 0, unique_count: json?.data?.[0]?.unique_count ?? 0 @@ -363,11 +357,7 @@ const getTrailingTopApps = ( const bucketPath = bucketPaths[bucket] if (!bucketPath) throw new Error('Invalid bucket') const url = `${endpoint}/v1/metrics/app_name/trailing/${bucketPath}?limit=${limit}` - const res = await fetch(url) - if (!res.ok) { - throw new Error(res.statusText) - } - const json = await res.json() + const json = await fetchWithTimeout(url) if (!json.data) return {} return json } @@ -378,11 +368,7 @@ const getTopAppsLegacy = ( limit: number ) => async () => { const url = `${endpoint}/v1/metrics/app_name?start_time=${startTime}&limit=${limit}&include_unknown=true` - const res = await fetch(url) - if (!res.ok) { - throw new Error(res.statusText) - } - const json = await res.json() + const json = await fetchWithTimeout(url) if (!json.data) return {} return json } @@ -452,7 +438,7 @@ export function fetchTrailingTopGenres( try { const startTime = getStartTime(bucket) const url = `${node.endpoint}/v1/metrics/genres?start_time=${startTime}` - const res = await (await fetch(url)).json() + const res = await fetchWithTimeout(url) const agg: CountRecord = { Electronic: 0 diff --git a/packages/protocol-dashboard/src/store/cache/music/hooks.ts b/packages/protocol-dashboard/src/store/cache/music/hooks.ts index 9d811bb76c0..8b97823cecc 100644 --- a/packages/protocol-dashboard/src/store/cache/music/hooks.ts +++ b/packages/protocol-dashboard/src/store/cache/music/hooks.ts @@ -6,7 +6,13 @@ import AppState from 'store/types' import { DiscoveryProvider, Playlist, Track } from 'types' import { useDiscoveryProviders } from '../discoveryProvider/hooks' import { useEffect, useState } from 'react' -import { setTopAlbums, setTopPlaylists, setTopTracks } from './slice' +import { + MusicError, + setTopAlbums, + setTopPlaylists, + setTopTracks +} from './slice' +import { fetchWithTimeout } from '../../../utils/fetch' const AUDIUS_URL = process.env.REACT_APP_AUDIUS_URL @@ -25,7 +31,7 @@ export function fetchTopTracks( return async (dispatch, getState, aud) => { try { const url = `${node.endpoint}/v1/tracks/trending?limit=4` - const res = await (await fetch(url)).json() + const res = await fetchWithTimeout(url) const tracks: Track[] = res.data.slice(0, 4).map((d: any) => ({ title: d.title, handle: d.user.handle, @@ -35,6 +41,7 @@ export function fetchTopTracks( })) dispatch(setTopTracks({ tracks })) } catch (e) { + dispatch(setTopTracks({ tracks: MusicError.ERROR })) console.error(e) } } @@ -46,7 +53,7 @@ export function fetchTopPlaylists( return async (dispatch, getState, aud) => { try { const url = `${node.endpoint}/v1/playlists/top?type=playlist&limit=5` - const res = await (await fetch(url)).json() + const res = await fetchWithTimeout(url) const playlists: Playlist[] = res.data.map((d: any) => ({ title: d.playlist_name, handle: d.user.handle, @@ -57,6 +64,7 @@ export function fetchTopPlaylists( dispatch(setTopPlaylists({ playlists })) } catch (e) { console.error(e) + dispatch(setTopPlaylists({ playlists: MusicError.ERROR })) } } } @@ -67,7 +75,7 @@ export function fetchTopAlbums( return async (dispatch, getState, aud) => { try { const url = `${node.endpoint}/v1/playlists/top?type=album&limit=5` - const res = await (await fetch(url)).json() + const res = await fetchWithTimeout(url) const albums: Playlist[] = res.data.map((d: any) => ({ title: d.playlist_name, handle: d.user.handle, @@ -78,6 +86,7 @@ export function fetchTopAlbums( dispatch(setTopAlbums({ albums })) } catch (e) { console.error(e) + dispatch(setTopAlbums({ albums: MusicError.ERROR })) } } } diff --git a/packages/protocol-dashboard/src/store/cache/music/slice.ts b/packages/protocol-dashboard/src/store/cache/music/slice.ts index 8a44c09989e..ffc3e8072f3 100644 --- a/packages/protocol-dashboard/src/store/cache/music/slice.ts +++ b/packages/protocol-dashboard/src/store/cache/music/slice.ts @@ -2,9 +2,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { Album, Playlist, Track } from 'types' export type State = { - topTracks: Track[] | null - topPlaylists: Playlist[] | null - topAlbums: Album[] | null + topTracks: Track[] | null | MusicError + topPlaylists: Playlist[] | null | MusicError + topAlbums: Album[] | null | MusicError } export const initialState: State = { @@ -13,9 +13,13 @@ export const initialState: State = { topAlbums: null } -type SetTopTracks = { tracks: Track[] } -type SetTopPlaylists = { playlists: Playlist[] } -type SetTopAlbums = { albums: Album[] } +export enum MusicError { + ERROR = 'error' +} + +type SetTopTracks = { tracks: Track[] | MusicError } +type SetTopPlaylists = { playlists: Playlist[] | MusicError } +type SetTopAlbums = { albums: Album[] | MusicError } const slice = createSlice({ name: 'music', diff --git a/packages/protocol-dashboard/src/utils/fetch.ts b/packages/protocol-dashboard/src/utils/fetch.ts new file mode 100644 index 00000000000..5a461a884e7 --- /dev/null +++ b/packages/protocol-dashboard/src/utils/fetch.ts @@ -0,0 +1,17 @@ +const DEFAULT_TIMEOUT_MS = 7500 +const TIMED_OUT_ERROR = 'Request Timed Out' + +export const fetchWithTimeout = async ( + url: string, + timeout: number = DEFAULT_TIMEOUT_MS +) => { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`${TIMED_OUT_ERROR}:${url}`)), timeout) + }) + + const res = (await Promise.race([fetch(url), timeoutPromise])) as Response + if (!res.ok) { + throw new Error(res.statusText) + } + return res.json() +}