-
Notifications
You must be signed in to change notification settings - Fork 447
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: notify AUS studio users of new studio versions (#6893)
* WIP * feat: notify AUS users of new packages * fix: return early if not auto-update studio * feat: show toast on every poll * test: add e2e tests * fix: use interval for more reliable checks * fix: put some guards in place to make test less flaky * refactor: update toast duration and logic * refactor: streamline Promise.all to modules endpoint Co-authored-by: Rico Kahler <ricokahler@gmail.com> * feat: add staging URL * fix: update durations and add comment * test: skip versionStatus tests because of flakiness for now * fix: typo in current packages check Co-authored-by: Rico Kahler <ricokahler@gmail.com> * test: restore tests * fix: update toast language Co-authored-by: Rune Botten <rbotten@gmail.com> * fix: update toast button Co-authored-by: Rune Botten <rbotten@gmail.com> * fix update toast title Co-authored-by: Rune Botten <rbotten@gmail.com> * chore: update tests for new language * format: update i18n formatting --------- Co-authored-by: Rico Kahler <ricokahler@gmail.com> Co-authored-by: Rune Botten <rbotten@gmail.com>
- Loading branch information
1 parent
6bbf609
commit e9b16c8
Showing
6 changed files
with
209 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
packages/sanity/src/core/studio/packageVersionStatus/PackageVersionStatusProvider.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import {Box, Button, Stack, useToast} from '@sanity/ui' | ||
import {type ReactNode, useCallback, useEffect, useRef} from 'react' | ||
import {SANITY_VERSION} from 'sanity' | ||
import semver from 'semver' | ||
|
||
import {hasSanityPackageInImportMap} from '../../environment/hasSanityPackageInImportMap' | ||
import {useTranslation} from '../../i18n' | ||
import {checkForLatestVersions} from './checkForLatestVersions' | ||
|
||
// How often to to check last timestamp. at 30 min, should fetch new version | ||
const REFRESH_INTERVAL = 1000 * 30 // every 30 seconds | ||
const SHOW_TOAST_FREQUENCY = 1000 * 60 * 30 //half hour | ||
|
||
const currentPackageVersions: Record<string, string> = { | ||
sanity: SANITY_VERSION, | ||
} | ||
|
||
export function PackageVersionStatusProvider({children}: {children: ReactNode}) { | ||
const toast = useToast() | ||
const {t} = useTranslation() | ||
const lastCheckedTimeRef = useRef<number | null>(null) | ||
|
||
const autoUpdatingPackages = hasSanityPackageInImportMap() | ||
|
||
const showNewPackageAvailableToast = useCallback(() => { | ||
const onClick = () => { | ||
window.location.reload() | ||
} | ||
|
||
toast.push({ | ||
id: 'new-package-available', | ||
title: t('package-version.new-package-available.title'), | ||
description: ( | ||
<Stack space={2} paddingBottom={2}> | ||
<Box>{t('package-version.new-package-available.description')}</Box> | ||
<Box> | ||
<Button | ||
aria-label={t('package-version.new-package-available.reload-button')} | ||
onClick={onClick} | ||
text={t('package-version.new-package-available.reload-button')} | ||
tone={'primary'} | ||
/> | ||
</Box> | ||
</Stack> | ||
), | ||
closable: true, | ||
status: 'info', | ||
/* | ||
* We want to show the toast until the user closes it. | ||
* Because of the toast ID, we should never see it twice. | ||
* https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value | ||
*/ | ||
duration: 1000 * 60 * 60 * 24 * 24, | ||
}) | ||
}, [toast, t]) | ||
|
||
useEffect(() => { | ||
if (!autoUpdatingPackages) return undefined | ||
|
||
const fetchLatestVersions = () => { | ||
if ( | ||
lastCheckedTimeRef.current && | ||
lastCheckedTimeRef.current + SHOW_TOAST_FREQUENCY > Date.now() | ||
) { | ||
return | ||
} | ||
|
||
checkForLatestVersions(currentPackageVersions).then((latestPackageVersions) => { | ||
lastCheckedTimeRef.current = Date.now() | ||
|
||
if (!latestPackageVersions) return | ||
|
||
const foundNewVersion = Object.entries(latestPackageVersions).some(([pkg, version]) => { | ||
if (!version || !currentPackageVersions[pkg]) return false | ||
return semver.gt(version, currentPackageVersions[pkg]) | ||
}) | ||
|
||
if (foundNewVersion) { | ||
showNewPackageAvailableToast() | ||
} | ||
}) | ||
} | ||
|
||
// Run on first render | ||
fetchLatestVersions() | ||
|
||
// Set interval for subsequent runs | ||
const intervalId = setInterval(fetchLatestVersions, REFRESH_INTERVAL) | ||
|
||
return () => clearInterval(intervalId) | ||
}, [autoUpdatingPackages, showNewPackageAvailableToast]) | ||
|
||
return <>{children}</> | ||
} |
44 changes: 44 additions & 0 deletions
44
packages/sanity/src/core/studio/packageVersionStatus/checkForLatestVersions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
//object like {sanity: '3.40.1'} | ||
interface VersionMap { | ||
[key: string]: string | undefined | ||
} | ||
|
||
//e2e tests also check for this URL pattern -- please update if it changes! | ||
const MODULES_URL_VERSION = 'v1' | ||
const MODULES_HOST = | ||
process.env.SANITY_INTERNAL_ENV === 'staging' | ||
? 'https://sanity-cdn.work' | ||
: 'https://sanity-cdn.com' | ||
const MODULES_URL = `${MODULES_HOST}/${MODULES_URL_VERSION}/modules/` | ||
|
||
const fetchLatestVersionForPackage = async (pkg: string, version: string) => { | ||
try { | ||
const res = await fetch(`${MODULES_URL}${pkg}/default/^${version}`, { | ||
headers: { | ||
accept: 'application/json', | ||
}, | ||
}) | ||
return res.json().then((data) => data.packageVersion) | ||
} catch (err) { | ||
console.error('Failed to fetch latest version for package', pkg, 'Error:', err) | ||
return undefined | ||
} | ||
} | ||
|
||
/* | ||
* | ||
*/ | ||
export const checkForLatestVersions = async ( | ||
packages: Record<string, string>, | ||
): Promise<VersionMap | undefined> => { | ||
const packageNames = Object.keys(packages) | ||
|
||
const results = await Promise.all( | ||
Object.entries(packages).map(async ([pkg, version]) => [ | ||
pkg, | ||
await fetchLatestVersionForPackage(pkg, version), | ||
]), | ||
) | ||
const packageVersions: VersionMap = Object.fromEntries(results) | ||
return packageVersions | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import {expect} from '@playwright/test' | ||
import {test} from '@sanity/test' | ||
|
||
//non-updating studio case | ||
test('should not show package version toast if not in auto-updating studio', async ({ | ||
page, | ||
baseURL, | ||
}) => { | ||
await page.goto(baseURL ?? '') | ||
|
||
await expect(page.getByText('Sanity Studio was updated')).not.toBeVisible() | ||
}) | ||
|
||
test.describe('auto-updating studio behavior', () => { | ||
test.beforeEach(async ({page, baseURL}) => { | ||
await page.goto(baseURL ?? '', {waitUntil: 'domcontentloaded'}) | ||
// Inject a script tag with importmap into the page | ||
await page.evaluate(() => { | ||
const importMap = { | ||
imports: { | ||
sanity: 'https://sanity-cdn.com/v1/modules/sanity/default/example.js', | ||
}, | ||
} | ||
const script = document.createElement('script') | ||
script.type = 'importmap' | ||
script.textContent = JSON.stringify(importMap) | ||
document.head.appendChild(script) | ||
}) | ||
}) | ||
|
||
test('should facilitate reload if in auto-updating studio, and version is higher', async ({ | ||
page, | ||
}) => { | ||
// Intercept the API request and provide a mock response | ||
await page.route('https://sanity-cdn.**/v1/modules/sanity/default/**', (route) => { | ||
route.fulfill({ | ||
status: 200, | ||
contentType: 'application/json', | ||
body: JSON.stringify({ | ||
packageVersion: '3.1000.0', | ||
}), | ||
}) | ||
}) | ||
await expect(page.getByText('Sanity Studio was updated')).toBeVisible() | ||
}) | ||
|
||
test('should show nothing if in auto-updating studio, and version is lower', async ({page}) => { | ||
// Intercept the API request and provide a mock response | ||
await page.route('https://sanity-cdn.**/v1/modules/sanity/default/**', (route) => { | ||
route.fulfill({ | ||
status: 200, | ||
contentType: 'application/json', | ||
body: JSON.stringify({ | ||
packageVersion: '3.0.0', | ||
}), | ||
}) | ||
}) | ||
|
||
await expect(page.getByText('Sanity Studio was updated')).not.toBeVisible() | ||
}) | ||
}) |