Skip to content

Commit 1bc28c7

Browse files
ivasilovjoshenlim
andauthored
feat: Add a toast to refresh the page if there's a new version of Studio available (supabase#36323)
* Initial work. * Add missing import. * Minor fixes. * Minor fixes. * Add a feature flag for the refresh toast. * Add a commit for testing. Revert before merging. * Remove header caching for testing. * Tiny lint * Fix * One more --------- Co-authored-by: Joshen Lim <[email protected]>
1 parent dfa0052 commit 1bc28c7

File tree

6 files changed

+152
-2
lines changed

6 files changed

+152
-2
lines changed

apps/studio/components/layouts/DefaultLayout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { useRouter } from 'next/router'
12
import { PropsWithChildren } from 'react'
23

34
import { useParams } from 'common'
45
import { AppBannerWrapper } from 'components/interfaces/App'
56
import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext'
67
import { Sidebar } from 'components/interfaces/Sidebar'
7-
import { useRouter } from 'next/router'
8+
import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy'
89
import { SidebarProvider } from 'ui'
910
import { LayoutHeader } from './ProjectLayout/LayoutHeader'
1011
import MobileNavigationBar from './ProjectLayout/NavigationBar/MobileNavigationBar'
@@ -29,6 +30,8 @@ const DefaultLayout = ({ children, headerTitle }: PropsWithChildren<DefaultLayou
2930
const router = useRouter()
3031
const showProductMenu = !!ref && router.pathname !== '/project/[ref]'
3132

33+
useCheckLatestDeploy()
34+
3235
return (
3336
<SidebarProvider defaultOpen={false}>
3437
<ProjectContextProvider projectRef={ref}>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
2+
import { fetchHandler } from 'data/fetchers'
3+
import { BASE_PATH } from 'lib/constants'
4+
import { ResponseError } from 'types'
5+
6+
export async function getDeploymentCommit(signal?: AbortSignal) {
7+
const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`)
8+
return (await response.json()) as { commitSha: string; commitTime: string }
9+
}
10+
11+
export type DeploymentCommitData = Awaited<ReturnType<typeof getDeploymentCommit>>
12+
13+
export const useDeploymentCommitQuery = <TData = DeploymentCommitData>({
14+
enabled = true,
15+
...options
16+
}: UseQueryOptions<DeploymentCommitData, ResponseError, TData> = {}) =>
17+
useQuery<DeploymentCommitData, ResponseError, TData>(
18+
['deployment-commit'],
19+
({ signal }) => getDeploymentCommit(signal),
20+
{
21+
...options,
22+
}
23+
)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import dayjs from 'dayjs'
2+
import { useRouter } from 'next/router'
3+
import { useEffect, useState } from 'react'
4+
import { toast } from 'sonner'
5+
6+
import { IS_PLATFORM } from 'common'
7+
import { useDeploymentCommitQuery } from 'data/utils/deployment-commit-query'
8+
import { Button, StatusIcon } from 'ui'
9+
import { useFlag } from './ui/useFlag'
10+
11+
const DeployCheckToast = ({ id }: { id: string | number }) => {
12+
const router = useRouter()
13+
14+
return (
15+
<div className="flex gap-3 flex-col w-full">
16+
<div className="flex gap-3 flex-row">
17+
<StatusIcon variant="default" className="mt-0.5" />
18+
<div className="flex w-full justify-between flex-col text-sm">
19+
<p className="text-foreground">A new version of this page is available</p>
20+
<p className="text-foreground-light">Refresh to see the latest changes.</p>
21+
</div>
22+
</div>
23+
24+
<div className="flex gap-5 justify-end">
25+
<Button type="outline" onClick={() => toast.dismiss(id)}>
26+
Not now
27+
</Button>
28+
<Button onClick={() => router.reload()}>Refresh</Button>
29+
</div>
30+
</div>
31+
)
32+
}
33+
34+
// This hook checks if the user is using old Studio pages and shows a toast to refresh the page. It's only triggered if
35+
// there's a new version of Studio is available, and the user has been on the old dashboard (based on commit) for more than 24 hours.
36+
// [Joshen] K-Dog has a suggestion here to bring down the time period here by checking commits
37+
export function useCheckLatestDeploy() {
38+
const showRefreshToast = useFlag('showRefreshToast')
39+
40+
const [currentCommitTime, setCurrentCommitTime] = useState('')
41+
const [isToastShown, setIsToastShown] = useState(false)
42+
43+
const { data: commit } = useDeploymentCommitQuery({
44+
enabled: IS_PLATFORM && showRefreshToast,
45+
staleTime: 10000, // 10 seconds
46+
})
47+
48+
useEffect(() => {
49+
// if the fetched commit is undefined is undefined
50+
if (!commit || commit.commitTime === 'unknown') {
51+
return
52+
}
53+
54+
// set the current commit on first load
55+
if (!currentCommitTime) {
56+
setCurrentCommitTime(commit.commitTime)
57+
return
58+
}
59+
60+
// if the current commit is the same as the fetched commit, do nothing
61+
if (currentCommitTime === commit.commitTime) {
62+
return
63+
}
64+
65+
// prevent showing the toast again if user has already seen and dismissed it
66+
if (isToastShown) {
67+
return
68+
}
69+
70+
// check if the time difference between commits is more than 24 hours
71+
const hourDiff = dayjs(commit.commitTime).diff(dayjs(currentCommitTime), 'hour')
72+
if (hourDiff < 24) {
73+
return
74+
}
75+
76+
// show the toast
77+
toast.custom((id) => <DeployCheckToast id={id} />, {
78+
duration: Infinity,
79+
position: 'bottom-right',
80+
})
81+
setIsToastShown(true)
82+
}, [commit, isToastShown, currentCommitTime])
83+
}

apps/studio/middleware.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const HOSTED_SUPPORTED_API_URLS = [
1616
'/ai/feedback/classify',
1717
'/get-ip-address',
1818
'/get-utc-time',
19+
'/get-deployment-commit',
1920
'/check-cname',
2021
'/edge-functions/test',
2122
'/edge-functions/body',
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextApiRequest, NextApiResponse } from 'next'
2+
3+
async function getCommitTime(commitSha: string) {
4+
try {
5+
const response = await fetch(`https://github.com/supabase/supabase/commit/${commitSha}.json`, {
6+
headers: {
7+
Accept: 'application/json',
8+
},
9+
})
10+
11+
if (!response.ok) {
12+
throw new Error('Failed to fetch commit details')
13+
}
14+
15+
const data = await response.json()
16+
return new Date(data.payload.commit.committedDate).toISOString()
17+
} catch (error) {
18+
console.error('Error fetching commit time:', error)
19+
return 'unknown'
20+
}
21+
}
22+
23+
export default async function handler(
24+
req: NextApiRequest,
25+
res: NextApiResponse<{ commitSha: string; commitTime: string }>
26+
) {
27+
// Set cache control headers for 10 minutes so that we don't get banned by Github API
28+
res.setHeader('Cache-Control', 's-maxage=600')
29+
30+
// Get the build commit SHA from Vercel environment variable
31+
const commitSha = process.env.VERCEL_GIT_COMMIT_SHA || 'development'
32+
33+
// Only fetch commit time if we have a valid SHA
34+
const commitTime = commitSha !== 'development' ? await getCommitTime(commitSha) : 'unknown'
35+
36+
res.status(200).json({
37+
commitSha,
38+
commitTime,
39+
})
40+
}

turbo.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"AI_PRO_MODEL",
103103
"AI_NORMAL_MODEL"
104104
],
105-
"passThroughEnv": ["CURRENT_CLI_VERSION"],
105+
"passThroughEnv": ["CURRENT_CLI_VERSION", "VERCEL_GIT_COMMIT_SHA"],
106106
"outputs": [".next/**", "!.next/cache/**"]
107107
},
108108
"www#build": {

0 commit comments

Comments
 (0)