-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9059 from marmelab/code-has-changed
Introduce CheckForApplicationUpdate
- Loading branch information
Showing
15 changed files
with
475 additions
and
2 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
--- | ||
layout: default | ||
title: "The CheckForApplicationUpdate component" | ||
--- | ||
|
||
# `CheckForApplicationUpdate` | ||
|
||
When your admin application is a Single Page Application, users who keep a browser tab open at all times might not use the most recent version of the application unless you tell them to refresh the page. | ||
|
||
This component regularly checks whether the application source code has changed and prompts users to reload the page when an update is available. To detect updates, it fetches the current URL at regular intervals and compares the hash of the response content (usually the HTML source). This should be enough in most cases as bundlers usually update the links to the application bundles after an update. | ||
|
||
![CheckForApplicationUpdate](./img/CheckForApplicationUpdate.png) | ||
|
||
## Usage | ||
|
||
Include this component in a custom layout: | ||
|
||
```tsx | ||
// in src/MyLayout.tsx | ||
import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; | ||
|
||
export const MyLayout = ({ children, ...props }: LayoutProps) => ( | ||
<Layout {...props}> | ||
{children} | ||
<CheckForApplicationUpdate /> | ||
</Layout> | ||
); | ||
|
||
// in src/App.tsx | ||
import { Admin, ListGuesser, Resource } from 'react-admin'; | ||
import { MyLayout } from './MyLayout'; | ||
|
||
export const App = () => ( | ||
<Admin layout={MyLayout}> | ||
<Resource name="posts" list={ListGuesser} /> | ||
</Admin> | ||
); | ||
``` | ||
|
||
## Props | ||
|
||
`<CheckForApplicationUpdate>` accepts the following props: | ||
|
||
| Prop | Required | Type | Default | Description | | ||
| --------------- | -------- | -------- | ------------------ |-------------------------------------------------------------------- | | ||
| `interval` | Optional | number | `3600000` (1 hour) | The interval in milliseconds between two checks | | ||
| `disabled` | Optional | boolean | `false` in `production` mode | Whether the automatic check is disabled | | ||
| `notification` | Optional | ReactElement | | The notification to display to the user when an update is available | | ||
| `url` | Optional | string | current URL | The URL to download to check for code update | | ||
|
||
## `interval` | ||
|
||
You can customize the interval between each check by providing the `interval` prop. It accepts a number of milliseconds and is set to `3600000` (1 hour) by default. | ||
|
||
```tsx | ||
// in src/MyLayout.tsx | ||
import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; | ||
|
||
const HALF_HOUR = 30 * 60 * 1000; | ||
|
||
export const MyLayout = ({ children, ...props }: LayoutProps) => ( | ||
<Layout {...props}> | ||
{children} | ||
<CheckForApplicationUpdate interval={HALF_HOUR} /> | ||
</Layout> | ||
); | ||
``` | ||
|
||
## `disabled` | ||
|
||
You can dynamically disable the automatic application update detection by providing the `disabled` prop. By default, it's only enabled in `production` mode. | ||
|
||
```tsx | ||
// in src/MyLayout.tsx | ||
import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; | ||
|
||
export const MyLayout = ({ children, ...props }: LayoutProps) => ( | ||
<Layout {...props}> | ||
{children} | ||
<CheckForApplicationUpdate disabled={process.env.NODE_ENV !== 'production'} /> | ||
</Layout> | ||
); | ||
``` | ||
|
||
## `notification` | ||
|
||
You can customize the notification shown to users when an update is available by passing your own element to the `notification` prop. | ||
Note that you must wrap your component with `forwardRef`. | ||
|
||
```tsx | ||
// in src/MyLayout.tsx | ||
import { forwardRef } from 'react'; | ||
import { Layout, CheckForApplicationUpdate } from 'react-admin'; | ||
|
||
const CustomAppUpdatedNotification = forwardRef((props, ref) => ( | ||
<Alert | ||
ref={ref} | ||
severity="info" | ||
action={ | ||
<Button | ||
color="inherit" | ||
size="small" | ||
onClick={() => window.location.reload()} | ||
> | ||
Update | ||
</Button> | ||
} | ||
> | ||
A new version of the application is available. Please update. | ||
</Alert> | ||
)); | ||
|
||
const MyLayout = ({ children, ...props }) => ( | ||
<Layout {...props}> | ||
{children} | ||
<CheckForApplicationUpdate notification={<CustomAppUpdatedNotification />}/> | ||
</Layout> | ||
); | ||
``` | ||
|
||
If you just want to customize the notification texts, including the button, check out the [Internationalization section](#internationalization). | ||
|
||
## `url` | ||
|
||
You can customize the URL fetched to detect updates by providing the `url` prop. By default it's the current URL. | ||
|
||
```tsx | ||
// in src/MyLayout.tsx | ||
import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; | ||
|
||
const MY_APP_ROOT_URL = 'http://admin.mycompany.com'; | ||
|
||
export const MyLayout = ({ children, ...props }: LayoutProps) => ( | ||
<Layout {...props}> | ||
{children} | ||
<CheckForApplicationUpdate url={MY_APP_ROOT_URL} /> | ||
</Layout> | ||
); | ||
``` | ||
|
||
## Internationalization | ||
|
||
You can customize the texts of the default notification by overriding the following keys: | ||
|
||
* `ra.notification.application_update_available`: the notification text | ||
* `ra.action.update_application`: the reload button text |
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
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
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,94 @@ | ||
import { useEffect, useRef } from 'react'; | ||
import { useEvent } from './useEvent'; | ||
|
||
/** | ||
* Checks if the application code has changed and calls the provided onNewVersionAvailable function when needed. | ||
* | ||
* It checks for code update by downloading the provided URL (default to the HTML page) and | ||
* comparing the hash of the response with the hash of the current page. | ||
* | ||
* @param {UseCheckForApplicationUpdateOptions} options The options | ||
* @param {Function} options.onNewVersionAvailable The function to call when a new version of the application is available. | ||
* @param {string} options.url Optional. The URL to download to check for code update. Defaults to the current URL. | ||
* @param {number} options.interval Optional. The interval in milliseconds between two checks. Defaults to 3600000 (1 hour). | ||
* @param {boolean} options.disabled Optional. Whether the check should be disabled. Defaults to false. | ||
*/ | ||
export const useCheckForApplicationUpdate = ( | ||
options: UseCheckForApplicationUpdateOptions | ||
) => { | ||
const { | ||
url = window.location.href, | ||
interval: delay = ONE_HOUR, | ||
onNewVersionAvailable: onNewVersionAvailableProp, | ||
disabled = process.env.NODE_ENV !== 'production', | ||
} = options; | ||
const currentHash = useRef<number>(); | ||
const onNewVersionAvailable = useEvent(onNewVersionAvailableProp); | ||
|
||
useEffect(() => { | ||
if (disabled) return; | ||
|
||
getHashForUrl(url).then(hash => { | ||
if (hash != null) { | ||
currentHash.current = hash; | ||
} | ||
}); | ||
}, [disabled, url]); | ||
|
||
useEffect(() => { | ||
if (disabled) return; | ||
|
||
const interval = setInterval(() => { | ||
getHashForUrl(url) | ||
.then(hash => { | ||
if (hash != null && currentHash.current !== hash) { | ||
// Store the latest hash to avoid calling the onNewVersionAvailable function multiple times | ||
// or when users have closed the notification | ||
currentHash.current = hash; | ||
onNewVersionAvailable(); | ||
} | ||
}) | ||
.catch(() => { | ||
// Ignore errors to avoid issues when connectivity is lost | ||
}); | ||
}, delay); | ||
return () => clearInterval(interval); | ||
}, [delay, onNewVersionAvailable, disabled, url]); | ||
}; | ||
|
||
const getHashForUrl = async (url: string) => { | ||
try { | ||
const response = await fetch(url); | ||
if (!response.ok) return null; | ||
const text = await response.text(); | ||
return hash(text); | ||
} catch (e) { | ||
return null; | ||
} | ||
}; | ||
|
||
// Simple hash function, taken from https://stackoverflow.com/a/52171480/3723993, suggested by Copilot | ||
const hash = (value: string, seed = 0) => { | ||
let h1 = 0xdeadbeef ^ seed, | ||
h2 = 0x41c6ce57 ^ seed; | ||
for (let i = 0, ch; i < value.length; i++) { | ||
ch = value.charCodeAt(i); | ||
h1 = Math.imul(h1 ^ ch, 2654435761); | ||
h2 = Math.imul(h2 ^ ch, 1597334677); | ||
} | ||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); | ||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); | ||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); | ||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); | ||
|
||
return 4294967296 * (2097151 & h2) + (h1 >>> 0); | ||
}; | ||
|
||
const ONE_HOUR = 1000 * 60 * 60; | ||
|
||
export interface UseCheckForApplicationUpdateOptions { | ||
onNewVersionAvailable: () => void; | ||
interval?: number; | ||
url?: string; | ||
disabled?: boolean; | ||
} |
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
Oops, something went wrong.