diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx index 4280bacbcc..c9b8a70542 100644 --- a/apps/studio/components/layouts/DefaultLayout.tsx +++ b/apps/studio/components/layouts/DefaultLayout.tsx @@ -1,10 +1,11 @@ +import { useRouter } from 'next/router' import { PropsWithChildren } from 'react' import { useParams } from 'common' import { AppBannerWrapper } from 'components/interfaces/App' import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext' import { Sidebar } from 'components/interfaces/Sidebar' -import { useRouter } from 'next/router' +import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy' import { SidebarProvider } from 'ui' import { LayoutHeader } from './ProjectLayout/LayoutHeader' import MobileNavigationBar from './ProjectLayout/NavigationBar/MobileNavigationBar' @@ -29,6 +30,8 @@ const DefaultLayout = ({ children, headerTitle }: PropsWithChildren diff --git a/apps/studio/data/utils/deployment-commit-query.ts b/apps/studio/data/utils/deployment-commit-query.ts new file mode 100644 index 0000000000..67403cf23f --- /dev/null +++ b/apps/studio/data/utils/deployment-commit-query.ts @@ -0,0 +1,23 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { fetchHandler } from 'data/fetchers' +import { BASE_PATH } from 'lib/constants' +import { ResponseError } from 'types' + +export async function getDeploymentCommit(signal?: AbortSignal) { + const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`) + return (await response.json()) as { commitSha: string; commitTime: string } +} + +export type DeploymentCommitData = Awaited> + +export const useDeploymentCommitQuery = ({ + enabled = true, + ...options +}: UseQueryOptions = {}) => + useQuery( + ['deployment-commit'], + ({ signal }) => getDeploymentCommit(signal), + { + ...options, + } + ) diff --git a/apps/studio/hooks/use-check-latest-deploy.tsx b/apps/studio/hooks/use-check-latest-deploy.tsx new file mode 100644 index 0000000000..cad2e77b00 --- /dev/null +++ b/apps/studio/hooks/use-check-latest-deploy.tsx @@ -0,0 +1,83 @@ +import dayjs from 'dayjs' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' + +import { IS_PLATFORM } from 'common' +import { useDeploymentCommitQuery } from 'data/utils/deployment-commit-query' +import { Button, StatusIcon } from 'ui' +import { useFlag } from './ui/useFlag' + +const DeployCheckToast = ({ id }: { id: string | number }) => { + const router = useRouter() + + return ( +
+
+ +
+

A new version of this page is available

+

Refresh to see the latest changes.

+
+
+ +
+ + +
+
+ ) +} + +// This hook checks if the user is using old Studio pages and shows a toast to refresh the page. It's only triggered if +// 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. +// [Joshen] K-Dog has a suggestion here to bring down the time period here by checking commits +export function useCheckLatestDeploy() { + const showRefreshToast = useFlag('showRefreshToast') + + const [currentCommitTime, setCurrentCommitTime] = useState('') + const [isToastShown, setIsToastShown] = useState(false) + + const { data: commit } = useDeploymentCommitQuery({ + enabled: IS_PLATFORM && showRefreshToast, + staleTime: 10000, // 10 seconds + }) + + useEffect(() => { + // if the fetched commit is undefined is undefined + if (!commit || commit.commitTime === 'unknown') { + return + } + + // set the current commit on first load + if (!currentCommitTime) { + setCurrentCommitTime(commit.commitTime) + return + } + + // if the current commit is the same as the fetched commit, do nothing + if (currentCommitTime === commit.commitTime) { + return + } + + // prevent showing the toast again if user has already seen and dismissed it + if (isToastShown) { + return + } + + // check if the time difference between commits is more than 24 hours + const hourDiff = dayjs(commit.commitTime).diff(dayjs(currentCommitTime), 'hour') + if (hourDiff < 24) { + return + } + + // show the toast + toast.custom((id) => , { + duration: Infinity, + position: 'bottom-right', + }) + setIsToastShown(true) + }, [commit, isToastShown, currentCommitTime]) +} diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts index 912c8ccb14..17896820bd 100644 --- a/apps/studio/middleware.ts +++ b/apps/studio/middleware.ts @@ -16,6 +16,7 @@ const HOSTED_SUPPORTED_API_URLS = [ '/ai/feedback/classify', '/get-ip-address', '/get-utc-time', + '/get-deployment-commit', '/check-cname', '/edge-functions/test', '/edge-functions/body', diff --git a/apps/studio/pages/api/get-deployment-commit.ts b/apps/studio/pages/api/get-deployment-commit.ts new file mode 100644 index 0000000000..5b7cde2c50 --- /dev/null +++ b/apps/studio/pages/api/get-deployment-commit.ts @@ -0,0 +1,40 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +async function getCommitTime(commitSha: string) { + try { + const response = await fetch(`https://github.com/supabase/supabase/commit/${commitSha}.json`, { + headers: { + Accept: 'application/json', + }, + }) + + if (!response.ok) { + throw new Error('Failed to fetch commit details') + } + + const data = await response.json() + return new Date(data.payload.commit.committedDate).toISOString() + } catch (error) { + console.error('Error fetching commit time:', error) + return 'unknown' + } +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<{ commitSha: string; commitTime: string }> +) { + // Set cache control headers for 10 minutes so that we don't get banned by Github API + res.setHeader('Cache-Control', 's-maxage=600') + + // Get the build commit SHA from Vercel environment variable + const commitSha = process.env.VERCEL_GIT_COMMIT_SHA || 'development' + + // Only fetch commit time if we have a valid SHA + const commitTime = commitSha !== 'development' ? await getCommitTime(commitSha) : 'unknown' + + res.status(200).json({ + commitSha, + commitTime, + }) +} diff --git a/turbo.json b/turbo.json index 0e9c09686f..c1a8a6dc53 100644 --- a/turbo.json +++ b/turbo.json @@ -102,7 +102,7 @@ "AI_PRO_MODEL", "AI_NORMAL_MODEL" ], - "passThroughEnv": ["CURRENT_CLI_VERSION"], + "passThroughEnv": ["CURRENT_CLI_VERSION", "VERCEL_GIT_COMMIT_SHA"], "outputs": [".next/**", "!.next/cache/**"] }, "www#build": {