Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add offline service #874

Merged
merged 50 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
76c86c2
chore: set up workspace
KaiVandivier Jun 7, 2021
9e80f65
chore: switch to JS for now
KaiVandivier Jun 7, 2021
cdfad61
feat: add JS code
KaiVandivier Jun 8, 2021
ed52b4f
chore: update deps
KaiVandivier Jun 8, 2021
a512495
chore: remove offline service reexport until published
KaiVandivier Jun 8, 2021
c244d15
refactor: accept pwaEnabled prop
KaiVandivier Jun 9, 2021
830460d
test: add basic test
KaiVandivier Jun 9, 2021
334879d
refactor: only use pwaEnabled from prop
KaiVandivier Jun 9, 2021
d2f466f
feat: receive startRecording config from app
KaiVandivier Jun 10, 2021
0734add
chore: rename & move offline-provider
KaiVandivier Jun 10, 2021
c179fef
feat: add online status hook
KaiVandivier Jun 10, 2021
bfd7d7c
test: add first test for online status
KaiVandivier Jun 10, 2021
3e1249a
refactor: useCallback for update function
KaiVandivier Jun 10, 2021
3a4d4c8
test: add browser event and throttling tests
KaiVandivier Jun 10, 2021
6d28f08
chore: export online status from package
KaiVandivier Jun 11, 2021
27265f6
refactor: remove alerts
KaiVandivier Jun 11, 2021
d8f06a0
refactor: receive loading mask as a prop
KaiVandivier Jun 15, 2021
0e9dd3e
refactor: new onError function signature
KaiVandivier Jun 15, 2021
a77054d
refactor: use debounce with leading edge instead of throttle
KaiVandivier Jun 15, 2021
f6500b3
refactor: use trailing edge debounce
KaiVandivier Jun 16, 2021
743357f
chore(cacheable-section): explanatory comments
KaiVandivier Jun 29, 2021
0e7fbe3
chore(cached-sections): explanatory comments
KaiVandivier Jun 29, 2021
d6c68ad
chore(offline-interface): comments
KaiVandivier Jun 29, 2021
193e149
chore(offline-provider): comment
KaiVandivier Jun 29, 2021
1292a76
chore(offline-status): comment
KaiVandivier Jun 29, 2021
47160f4
fix(cached-sections): don't update if PWA is not enabled
KaiVandivier Jul 2, 2021
1110178
docs: add docs for offline service
KaiVandivier Jul 5, 2021
a0d13a0
Merge pull request #891 from dhis2/docs-offline-service
KaiVandivier Jul 5, 2021
51e90cb
fix: add default param to startRecording
KaiVandivier Jul 9, 2021
c22879e
refactor: cacheable sections state and add tests (#901)
KaiVandivier Jul 14, 2021
bbb4dc6
feat: expose useCachedSections
KaiVandivier Jul 15, 2021
f43bf2d
refactor: make cached status an object so it's easily extensible
KaiVandivier Jul 15, 2021
ef9cada
chore: bump timeout a little bit
KaiVandivier Jul 15, 2021
9eb42bb
fix: make `offlineInterface` optional in offline provider
KaiVandivier Jul 16, 2021
8fa2ef5
chore: kai as package author
KaiVandivier Jul 21, 2021
79e6cca
docs: fill in a few links ahead of release
KaiVandivier Jul 21, 2021
5ef7055
feat: add offline-service features to main app-runtime package
KaiVandivier Jul 21, 2021
18628d2
Merge branch 'feat-add-offline-service' of https://github.com/dhis2/a…
KaiVandivier Jul 21, 2021
cc75349
chore: bump versions for pwa release
KaiVandivier Jul 21, 2021
71051d7
chore: cut 2.10.0-pwa.1
amcgee Jul 21, 2021
dbf1a7a
docs: add link preemptively
KaiVandivier Jul 22, 2021
8915e1a
chore: add more feedback if context is missing
KaiVandivier Jul 22, 2021
3193145
refactor: offline service to typescript (#921)
KaiVandivier Jul 26, 2021
d31d349
refactor: fix type checks
KaiVandivier Jul 26, 2021
eb30250
fix: allow useCachableSection hook to be used in child of CacheableSe…
KaiVandivier Aug 12, 2021
3ec3e2a
chore: cut 2.10.0-pwa.2
amcgee Aug 12, 2021
7d4b464
fix: update state when id arg changes (#962)
KaiVandivier Aug 16, 2021
16287b7
chore: cut 2.10.0-pwa.3
amcgee Aug 17, 2021
ed5dfe8
Merge branch 'master' into feat-add-offline-service
KaiVandivier Aug 31, 2021
74aa6f6
chore: cut 2.10.0-pwa.4
KaiVandivier Aug 31, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions docs/advanced/offline.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,141 @@
# Offline tools

The app platform provides some support for PWA features, including a `manifest.json` file for installability and service worker which can provide offline caching. In addition to those features, the app runtime provides support for "cacheable sections", which are sections of an app that can be individually cached on-demand. The [`useCacheableSection`](#usecacheablesection-api) hook and the [`CacheableSection`](#cacheablesection-api) component provide the controls for the section and the wrapper for the section, respectively. The [`useCachedSections`](#usecachedsections-api) hook returns a list of sections that are stored in the cache and a function that can delete them.

There is also a [`useOnlineStatus`](#online-status) hook which returns the online or offline status of the client.

To see a good example of these functions' APIs and their usage, see `SectionWrapper.js` in the [PWA example app](https://github.com/dhis2/app-platform/tree/master/examples/pwa-app/src/components/SectionWrapper.js) in the platform repository.

## Cacheable sections

> This feature can only be used when PWA is enabled in `d2.config.js`. See the [App Platform docs](https://platform.dhis2.nu/#/pwa/pwa) for more information.

These features are supported by an `<OfflineProvider>` component which the app platform provides to the app.

### How it works

Cacheable sections enable sections of an app to be individually cached offline on demand. Using the `CacheableSection` wrapper and the `useCacheableSection` hook, when a user requests a section to be cached for offline use, the section's component tree will rerender, and the app's service worker will listen to all the network traffic for the component to cache it offline. To avoid caching that components' data before a user requests to do so, you can use the [URL filters feature](https://platform.dhis2.nu/#/pwa/pwa?id=opting-in) in `d2.config.js`.

Note that, without using these features, an app using offline caching will cache all the data that is requested by user as they use the app without needing to use cacheable sections.

Keep an eye out for this feature in use in the Dashboards app coming soon!

### Usage

Wrap the component to be cached in a `CacheableSection` hook, providing an `id` and `loadingMask` prop. The loading mask should block the screen from interaction, and it will be rendered while the component is in 'recording mode'. Note that the `useCacheableSection` hook _does not need to be used in the same component_ as the `<CacheableSection>`, they just need to use the same `id`. There is an example of this in the file linked below.

Here is an example of the basic usage:

```jsx
import { CacheableSection, useCacheableSection } from '@dhis2/app-runtime'
import { Button, Layer, CenteredContent, CircularLoader } from '@dhis2/ui'
import { Dashboard } from './Dashboard'

/** An example loading mask */
const LoadingMask = () => (
<Layer translucent>
<CenteredContent>
<CircularLoader />
</CenteredContent>
</Layer>
)

export function CacheableSectionWrapper({ id }) {
const { startRecording, isCached, lastUpdated } = useCacheableSection(id)

return (
<div>
<p>{isCached ? `Last updated: ${lastUpdated}` : 'Not cached'}</p>
<Button onClick={startRecording}>
<CacheableSection id={id} loadingMask={<LoadingMask />}>
<Dashboard id={id} />
</CacheableSection>
</div>
)
}
```

#### `CacheableSection` API

| Prop | Type | Required? | Description |
| ------------- | --------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `children` | Component | Yes | Section that will be cached upon recording |
| `id` | String | Yes | ID of the section to be cached. Should match the ID used in the `useCacheableSection` hook. |
| `loadingMask` | Component | Yes | A UI mask that should block the screen from interaction. While the component is rerendering and recording, this mask will be rendered to block user interaction which may interfere with the recorded data. |

#### `useCacheableSection` API

```jsx
import { useCacheableSections } from '@dhis2/app-runtime'

function DemoComponent() {
const {
startRecording,
remove,
lastUpdated,
isCached,
recordingState,
} = useCacheableSection(id)
}
```

`useCacheableSection` takes an `id` parameter (a string) and returns an object with the following properties:

| Property | Type | Description |
| ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startRecording` | Function | Initiates recording of a cacheable section's data for offline use. Causes a `<CacheableSection>` component with a matching ID to rerender with a loading mask based on the recording state to initiate the network requests. See the full API in the [`startRecording`](#startrecording-api) section below. |
| `remove` | Function | Removes this section from offline storage. Returns a promise that resolves to `true` if the section was successfully removed or `false` if that section was not found in offline storage. |
| `lastUpdated` | Date | A timestamp of the last time this section was successfully recorded. |
| `isCached` | Boolean | `true` if this section is in offline storage; Provided for convenience. |
| `recordingState` | String | One of `'default'`, `'pending'`, `'recording'`, or `'error'`. Under the hood, the `CacheableSection` component changes how its children are rendered based on the states. They are returned here in case an app wants to change UI or behavior based on the recording state. |

#### `startRecording` API

The `startRecording` function returned by `useCacheableSection` returns a promise that resolves if 'start recording' signal is sent successfully or rejects if there is an error with the offline interface initiating recording. It accepts an `options` parameter with the following optional properties:

| Property | Type | Default | Description |
| ----------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onStarted` | Function | | A callback to be called once a recording has started and the service worker is listening to network requests. Receives no arguments |
| `onCompleted` | Function | | A callback to be called when the recording has completed successfully. Receives no arguments |
| `onError` | Function | | A callback to be called in the case of an error during recording. Receives an `error` object as an argument |
| `recordingTimeoutDelay` | Number | `1000` | The time (in ms) to wait after all pending network requests have finished before stopping a recording. If a user's device is slow, and there might be long pauses between requests that are necessary for that section to run offline, this number may need to be increased from its default to prevent recording from stopping prematurely. |

Example:

```jsx
import { useCacheableSection } from '@dhis2/app-runtime'
import { Button } from '@dhis2/ui'

function StartRecordingButton({ id }) {
const { startRecording } = useCacheableSection(id)

function handleStartRecording() {
startRecording({
onStarted: () => console.log('Recording started'),
onCompleted: () => console.log('Recording completed'),
onError: err => console.error(err),
recordingTimeoutDelay: 1000, // the default
})
.then(() => console.log('startRecording signal sent successfully'))
.catch(err =>
console.error(`Error when starting recording: ${err}`)
)
}

return <Button onClick={handleStartRecording}>Save offline</Button>
}
```

#### `useCachedSections` API

The `useCachedSections` hook returns a list of all the sections that are cached, which can be useful if an app needs to manage that whole list at once. It takes no arguments and it returns an object with the following properties:

| Property | Type | Description |
| -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cachedSections` | Object | An object of cached sections' statuses, where the keys are the section IDs and the values are objects with a `lastUpdated` property that holds a `Date` object reflecting the time this section was last updated. |
| `removeById` | Function | Receives an `id` parameter and attempts to remove the section with that ID from the offline cache. If successful, updates the cached sections list. Returns a promise that resolves to `true` if that section is successfully removed or `false` if a section with that ID was not found. |
| `syncCachedSections` | Function | Syncs the list of cached sections with the list in IndexedDB. Returns a promise. This is handled by the `removeById` function and is probably not necessary to use in most applications. |

## Online status

The `useOnlineStatus` returns the client's online or offline status. It debounces the values by default to prevent rapid changes of any UI elements that depend on the online status.
Expand Down
33 changes: 20 additions & 13 deletions examples/cra/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1047,24 +1047,26 @@
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==

"@dhis2/app-runtime@file:../../runtime":
version "2.8.0"
version "2.10.0-pwa.4"
dependencies:
"@dhis2/app-service-alerts" "2.8.0"
"@dhis2/app-service-config" "2.8.0"
"@dhis2/app-service-data" "2.8.0"
"@dhis2/app-service-offline" "2.8.0"
"@dhis2/app-service-alerts" "2.10.0-pwa.4"
"@dhis2/app-service-config" "2.10.0-pwa.4"
"@dhis2/app-service-data" "2.10.0-pwa.4"
"@dhis2/app-service-offline" "2.10.0-pwa.4"

"@dhis2/app-service-alerts@2.8.0", "@dhis2/app-service-alerts@file:../../services/alerts":
version "2.8.0"
"@dhis2/app-service-alerts@2.10.0-pwa.4", "@dhis2/app-service-alerts@file:../../services/alerts":
version "2.10.0-pwa.4"

"@dhis2/app-service-config@2.8.0", "@dhis2/app-service-config@file:../../services/config":
version "2.8.0"
"@dhis2/app-service-config@2.10.0-pwa.4", "@dhis2/app-service-config@file:../../services/config":
version "2.10.0-pwa.4"

"@dhis2/app-service-data@2.8.0", "@dhis2/app-service-data@file:../../services/data":
version "2.8.0"
"@dhis2/app-service-data@2.10.0-pwa.4", "@dhis2/app-service-data@file:../../services/data":
version "2.10.0-pwa.4"

"@dhis2/app-service-offline@2.8.0", "@dhis2/app-service-offline@file:../../services/offline":
version "2.8.0"
"@dhis2/app-service-offline@2.10.0-pwa.4", "@dhis2/app-service-offline@file:../../services/offline":
version "2.10.0-pwa.4"
dependencies:
lodash "^4.17.21"

"@hapi/address@2.x.x":
version "2.1.4"
Expand Down Expand Up @@ -6507,6 +6509,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==

lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

loglevel@^1.6.6:
version "1.6.8"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
Expand Down
36 changes: 26 additions & 10 deletions examples/query-playground/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1782,25 +1782,36 @@
dependencies:
moment "^2.24.0"

"@dhis2/app-runtime@*", "@dhis2/app-runtime@^2.2.2", "@dhis2/app-runtime@file:../../runtime":
"@dhis2/app-runtime@*":
version "2.8.0"
resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-2.8.0.tgz#83ca6e96c299686ee72eea3e1825e04aa53cd5d2"
integrity sha512-Ru6x9L61fD7ITzVaxFqx88kV5/ypB9xSr8nHgRj4EE/kHl/aVejXuwnSS2OIWh80J3mtD1dpNRN/GJ8o0x0HYg==
dependencies:
"@dhis2/app-service-alerts" "2.8.0"
"@dhis2/app-service-config" "2.8.0"
"@dhis2/app-service-data" "2.8.0"
"@dhis2/app-service-offline" "2.8.0"

"@dhis2/app-service-alerts@2.8.0", "@dhis2/app-service-alerts@file:../../services/alerts":
version "2.8.0"
"@dhis2/app-runtime@^2.2.2", "@dhis2/app-runtime@file:../../runtime":
version "2.10.0-pwa.4"
dependencies:
"@dhis2/app-service-alerts" "2.10.0-pwa.4"
"@dhis2/app-service-config" "2.10.0-pwa.4"
"@dhis2/app-service-data" "2.10.0-pwa.4"
"@dhis2/app-service-offline" "2.10.0-pwa.4"

"@dhis2/app-service-config@2.8.0", "@dhis2/app-service-config@file:../../services/config":
version "2.8.0"
"@dhis2/app-service-alerts@2.10.0-pwa.4", "@dhis2/app-service-alerts@2.8.0", "@dhis2/app-service-alerts@file:../../services/alerts":
version "2.10.0-pwa.4"

"@dhis2/app-service-data@2.8.0", "@dhis2/app-service-data@file:../../services/data":
version "2.8.0"
"@dhis2/app-service-config@2.10.0-pwa.4", "@dhis2/app-service-config@2.8.0", "@dhis2/app-service-config@file:../../services/config":
version "2.10.0-pwa.4"

"@dhis2/app-service-offline@2.8.0", "@dhis2/app-service-offline@file:../../services/offline":
version "2.8.0"
"@dhis2/app-service-data@2.10.0-pwa.4", "@dhis2/app-service-data@2.8.0", "@dhis2/app-service-data@file:../../services/data":
version "2.10.0-pwa.4"

"@dhis2/app-service-offline@2.10.0-pwa.4", "@dhis2/app-service-offline@file:../../services/offline":
version "2.10.0-pwa.4"
dependencies:
lodash "^4.17.21"

"@dhis2/app-shell@5.2.0":
version "5.2.0"
Expand Down Expand Up @@ -8254,6 +8265,11 @@ lodash@^4.17.19:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==

lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

loglevel@^1.6.6:
version "1.6.7"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56"
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "2.10.0",
"version": "2.10.0-pwa.4",
"description": "A singular runtime dependency for applications on the DHIS2 platform",
"repository": "https://github.com/dhis2/app-runtime.git",
"author": "Austin McGee <austin@dhis2.org>",
Expand Down Expand Up @@ -57,5 +57,8 @@
"name": "DHIS2 Application Runtime",
"description": "A singular runtime dependency for applications on the [DHIS2 platform](https://platform.dhis2.nu)"
}
},
"resolutions": {
"@dhis2/app-runtime": "2.10.0-pwa.4"
}
}
10 changes: 5 additions & 5 deletions runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@dhis2/app-runtime",
"description": "A singular runtime dependency for applications on the DHIS2 platform",
"version": "2.10.0",
"version": "2.10.0-pwa.4",
"main": "./build/cjs/index.js",
"module": "./build/es/index.js",
"types": "./build/types/index.d.ts",
Expand All @@ -23,10 +23,10 @@
"build/**"
],
"dependencies": {
"@dhis2/app-service-alerts": "2.10.0",
"@dhis2/app-service-config": "2.10.0",
"@dhis2/app-service-data": "2.10.0",
"@dhis2/app-service-offline": "2.10.0"
"@dhis2/app-service-alerts": "2.10.0-pwa.4",
"@dhis2/app-service-config": "2.10.0-pwa.4",
"@dhis2/app-service-data": "2.10.0-pwa.4",
"@dhis2/app-service-offline": "2.10.0-pwa.4"
},
"peerDependencies": {
"prop-types": "^15.7.2",
Expand Down
Loading