Skip to content

Commit

Permalink
feat: add service worker and pwa capabilities (#550)
Browse files Browse the repository at this point in the history
* feat: add & update files, add deps

* fix: test cache-first route for file extension

* fix: add pwa_enabled to env vars to conditionally register SW

* fix: use older syntax

* feat: filter out url patterns from d2.config when caching by default

* fix: parse url array env var

* feat: add manifest.json

* chore: add comments

* fix: use 'browser' as pwa display mode

* feat: add recording started and completed messages

* feat: generate manifest.json programatically

* refactor: generate manifests before build step to precache

* feat: use workbox-cli to precache all static assets

* fix: linting error

* fix: remove 'pwa enabled' condition for SW event listener

* feat: add omitExternalRequests option to d2.config

* refactor: tentative SWR strategy for static assets

* chore: remove chatty console log

* refactor: clean up index.js; crudely compartmentalize SW reg

* feat: add 'getRecordedSections' function in sw

* feat: tentative messaging interface implementation

* refactor: move sw content to new package

* chore: add scripts & update deps

* chore: clean up shell

* chore: fix d2 config entry point

* refactor: move manifest injection to cli package

* chore: format

* docs: add pwa docs

* chore: init new example app for PWA

* chore: export constants from sw package

* chore: rename var

* test: add first draft of cacheable section attempt

* refactor: improve test component cascading fetch

* fix: receive 'children' prop

* fix: add recording states var

* fix: handle error when idb is not set up

* fix: handle required arg errors

* fix: handle more errors

* feat: section wrapper

* chore: add todo

* chore: set up demo config & scripts

* refactor: rename message keys

* refactor: callback interface; linting

* fix: recording timeout delay

* refactor: cacheable section to avoid unnecessary rerenders

* fix: don't record requests during confirmation

* refactor: just use sectionId for cache key

* fix: try cache if network fails for *all* requests

* fix: typo

* fix: remove recording on error

* chore: remove extraneous 'clientId's in payload

* fix: handle recording errors better

* fix: clean up listeners on recording success and failure

* chore: update comment

* fix: convert cachedSections to a map

* chore: script for pwa app demo

* refactor: add 'useCachedSection' for just one section

* chore: rename to initOfflineInterface

* chore: formatting

* chore: comment and var rename

* chore: remove unused 'interface' idea

* feat: add circular loader to screen cover

* chore: comments from meeting feedback

* refactor: init function (and meeting feedback)

* refactor: offline interface into a class

* chore: add JSDoc comments

* refactor: make promptUpdate API more generic

* chore: remove debugging log

* refactor: handle SW registration in offlineInterface

* fix: call 'onUpdate' if new SW is already waiting

* refactor: use arrow function to avoid using '.bind()'

* chore: add comments

* feat: decouple useCacheableSection and <CacheableSection>

* fix: avoid side effects if pwa is not enabled

* refactor: separate components to illustrate uCS decoupling

* chore: remove unused hook

* chore: translate user-facing strings

* chore: comment

* chore: tl strings

* chore: remove comments

* feat: add alerts to cacheable section

* fix: enable unregistration when pwa is not enabled

* feat: translations and alerts in useCacheableSection

* fix: add pwa-app to main build script

* chore: update yarn.lock

* fix: dependency resolutions

* feat: use alert for feedback when removing section

* feat: handle sections update error with alert feedback

* refactor: use async try/catch in removeSection()

* chore: rename to visualizations list

* chore: a newline

* chore: demo recordingState API

* fix: generate manifests in 'start' script too to avoid error

* refactor: make pwa config cleaner and more robust

* chore: update d2 config in pwa-app

* chore: rename to generateManifests now that it generates two

* chore: remove conditional chaining

* chore: remove unnecessary var

* fix: avoid DB-opening side effect when PWA is not enabled

* feat: clear rogue service workers

* feat: tear down caches & db if PWA is not enabled

* chore: update comment

* chore: update comments

* refactor: consolidate to one provider

* chore: update serve

* refactor: use modules from local app-runtime

* chore: update demo script to update local runtime

* chore: remove unused code that has moved to runtime

* chore: add 'verbose' to demo script

* refactor: move OfflineInterface to sw package

* refactor: move offline interface and provider to app adapter

* refactor: add pwaEnabled to app config & offline provider

* chore: update script

* feat(pwa-app): use online status

* chore: move 'serve' package to pwa-app

* refactor: quieter logging

* refactor(pwa-app): provide loading mask in app

* refactor: fix listener clean-up

* feat(pwa-app): show more offline API features

* refactor(pwa-app): show debounceDelay option

* chore: package resolutions

* chore(prettier): add vscode plugin workaround

* chore(pwa-app deps): update cli scripts

* refactor(registration): better custom register/update flow

* chore: use newest cli-style

* chore: update comment

* chore(pwa-app): add comment

* chore(sw): inline comments

* chore(sw): more comments

* chore(sw): more comments

* docs(pwa): update a bit

* chore(sw): dependencies

* chore(deps): add sw to deps

* fix(deps): bump @babel/preset-env to fix build error

* refactor(registration): change update function signature

* feat(sw): use service worker in development mode (#580)

* feat(sw): use sw in dev mode (WIP)

* chore(sections-db): switch to debug

* fix: use correct env vars for dev sw

* chore: use available var

* chore(pwa-app): remove unused tl strings

* feat(adapter): smarter error boundaries (#582)

* feat(adapter): add error boundary around app

* chore(pwa-app): use --debug in build script

* feat(adapter): check for SW update and reload in err boundary

* chore: change build order in `yarn build`

* chore(deps): update yarn.lock

* refactor(offline-interface): improve error protections

* docs: add link to PWA page from sidebar [skip-ci]

* refactor: use requests size instead of list

* chore(sw): use constants for message definitions

* docs(pwa): describe development mode

* fix(cli): change short_name to config.title in manifest.json

* refactor(pwa): prefix pwa_caching env vars

* refactor(cli): delete config.pwa from d2.config.json

* chore: clarify workbox precache comment

* chore(deps): bump react scripts

* chore: rename SW for consistency in file

* refactor(sw): break into multiple files

* refactor(sw): simplify fulfilled and pending requests

* refactor(error-boundary): use button and `onRetry` prop

* chore: remove unused pwaEnabled prop

* feat: show error message in fatal error boundary and allow copying stack trace to clipboard (#592)

* chore: brackets around 'if' blocks

Co-authored-by: Médi-Rémi Hashim <4295266+mediremi@users.noreply.github.com>

* chore(deps): use released app-runtime dependency

* refactor: rename sw package to 'pwa'

* refactor: use workspace resolution for pwa pkg

* chore: comment out './locales' to hopefully fix build on CI

* test: update error boundary test

* chore: clean up after alpha merge

* chore(shell): add to kssw explanation

* chore: update snapshot

* chore: update versions after merge

* chore: remove locales comment

Co-authored-by: Médi-Rémi Hashim <4295266+mediremi@users.noreply.github.com>
  • Loading branch information
KaiVandivier and mediremi authored Jul 23, 2021
1 parent 56c06df commit 225069e
Show file tree
Hide file tree
Showing 55 changed files with 17,106 additions and 683 deletions.
34 changes: 20 additions & 14 deletions adapter/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,44 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2020-10-08T15:51:57.880Z\n"
"PO-Revision-Date: 2020-10-08T15:51:57.880Z\n"
"POT-Creation-Date: 2021-07-16T13:22:12.456Z\n"
"PO-Revision-Date: 2021-07-16T13:22:12.456Z\n"

msgid "An error occurred in the DHIS2 application."
msgstr ""
msgstr "An error occurred in the DHIS2 application."

msgid "Technical details copied to clipboard"
msgstr "Technical details copied to clipboard"

msgid "Something went wrong"
msgstr ""
msgstr "Something went wrong"

msgid "Refresh to try again"
msgstr ""
msgid "Try again"
msgstr "Try again"

msgid "Hide technical details"
msgstr ""
msgstr "Hide technical details"

msgid "Show technical details"
msgstr ""
msgstr "Show technical details"

msgid "The following information may be requested by technical support."
msgstr ""
msgstr "The following information may be requested by technical support."

msgid "Copy technical details to clipboard"
msgstr "Copy technical details to clipboard"

msgid "Please sign in"
msgstr ""
msgstr "Please sign in"

msgid "Server"
msgstr ""
msgstr "Server"

msgid "Username"
msgstr ""
msgstr "Username"

msgid "Password"
msgstr ""
msgstr "Password"

msgid "Sign in"
msgstr ""
msgstr "Sign in"
1 change: 1 addition & 0 deletions adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build"
],
"dependencies": {
"@dhis2/pwa": "7.2.0",
"moment": "^2.24.0"
},
"devDependencies": {
Expand Down
7 changes: 6 additions & 1 deletion adapter/src/components/AppWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import { useCurrentUserLocale } from '../utils/useLocale.js'
import { Alerts } from './Alerts.js'
import { ErrorBoundary } from './ErrorBoundary.js'
import { LoadingMask } from './LoadingMask.js'
import { styles } from './styles/AppWrapper.style.js'

Expand All @@ -17,7 +18,11 @@ export const AppWrapper = ({ appName, children }) => {
<div className="app-shell-adapter">
<style jsx>{styles}</style>
<HeaderBar appName={appName} />
<div className="app-shell-app">{children}</div>
<div className="app-shell-app">
<ErrorBoundary onRetry={() => window.location.reload()}>
{children}
</ErrorBoundary>
</div>
<Alerts />
</div>
)
Expand Down
123 changes: 123 additions & 0 deletions adapter/src/components/ErrorBoundary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import i18n from '@dhis2/d2-i18n'
import cx from 'classnames'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import buttonStyles from './styles/Button.style'
import styles from './styles/ErrorBoundary.style'

// In order to avoid using @dhis2/ui components in the error boundary - as anything
// that breaks within it will not be caught properly - we define a component
// with the same styles as Button
const UIButton = ({ children, onClick }) => (
<>
<style jsx>{buttonStyles}</style>
<button onClick={onClick}>{children}</button>
</>
)

UIButton.propTypes = {
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired,
}

const translatedErrorHeading = i18n.t(
'An error occurred in the DHIS2 application.'
)

export class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = {
error: null,
errorInfo: null,
drawerOpen: false,
}
this.errorDetailsRef = React.createRef()
}

componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo,
})
}

toggleTechInfoDrawer = () => {
this.setState({
drawerOpen: !this.state.drawerOpen,
})
}

handleCopyErrorDetails = () => {
const errorDetails = this.errorDetailsRef.current.textContent
navigator.clipboard.writeText(errorDetails).then(() => {
alert(i18n.t('Technical details copied to clipboard'))
})
}

render() {
const { children, fullscreen, onRetry } = this.props
if (this.state.error) {
return (
<div className={cx('mask', { fullscreen })}>
<style jsx>{styles}</style>
<div className="container">
<h1 className="message">
{i18n.t('Something went wrong')}
</h1>
{onRetry && (
<div className="retry">
<UIButton onClick={onRetry}>
{i18n.t('Try again')}
</UIButton>
</div>
)}
<button
className="drawerToggle"
onClick={this.toggleTechInfoDrawer}
>
{this.state.drawerOpen
? i18n.t('Hide technical details')
: i18n.t('Show technical details')}
</button>
<div
className={cx('drawer', {
hidden: !this.state.drawerOpen,
})}
>
<div className="errorIntro">
<p>{translatedErrorHeading}</p>
<p>
{i18n.t(
'The following information may be requested by technical support.'
)}
</p>
<UIButton onClick={this.handleCopyErrorDetails}>
{i18n.t(
'Copy technical details to clipboard'
)}
</UIButton>
</div>
<pre
className="errorDetails"
ref={this.errorDetailsRef}
>
{`${this.state.error}\n`}
{this.state.error.stack}
{this.state.errorInfo.componentStack}
</pre>
</div>
</div>
</div>
)
}

return children
}
}

ErrorBoundary.propTypes = {
children: PropTypes.node.isRequired,
fullscreen: PropTypes.bool,
onRetry: PropTypes.func,
}
98 changes: 0 additions & 98 deletions adapter/src/components/FatalErrorBoundary.js

This file was deleted.

9 changes: 8 additions & 1 deletion adapter/src/components/ServerVersionProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { parseServerVersion } from '../utils/parseServerVersion.js'
import { LoadingMask } from './LoadingMask.js'
import { LoginModal } from './LoginModal.js'

export const ServerVersionProvider = ({ url, apiVersion, children }) => {
export const ServerVersionProvider = ({
url,
apiVersion,
offlineInterface,
children,
}) => {
const [{ loading, error, systemInfo }, setState] = useState({
loading: true,
})
Expand Down Expand Up @@ -51,6 +56,7 @@ export const ServerVersionProvider = ({ url, apiVersion, children }) => {
serverVersion,
systemInfo,
}}
offlineInterface={offlineInterface}
>
{children}
</Provider>
Expand All @@ -60,5 +66,6 @@ export const ServerVersionProvider = ({ url, apiVersion, children }) => {
ServerVersionProvider.propTypes = {
apiVersion: PropTypes.number,
children: PropTypes.element,
offlineInterface: PropTypes.shape({}),
url: PropTypes.string,
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import { shallow } from 'enzyme'
import React from 'react'
import { FatalErrorBoundary } from '../FatalErrorBoundary'
import { ErrorBoundary } from '../ErrorBoundary'

const Something = () => {
// Placeholder
return null
}

describe('FatalErrorBoundary', () => {
describe('ErrorBoundary', () => {
it('Should render the normal tree when no error occurs', () => {
const wrapper = shallow(
<FatalErrorBoundary classes={{}}>
<ErrorBoundary classes={{}}>
<div>
<span id="testme">Hello there!</span>
</div>
</FatalErrorBoundary>
</ErrorBoundary>
)

expect(wrapper.find('span#testme').length).toBe(1)
})

it('Should render the error mask when an error is thrown', () => {
const wrapper = shallow(
<FatalErrorBoundary classes={{}}>
<ErrorBoundary classes={{}}>
<Something />
</FatalErrorBoundary>
</ErrorBoundary>
)

expect(wrapper.find(Something).length).toBe(1)
Expand All @@ -37,9 +37,9 @@ describe('FatalErrorBoundary', () => {

it('Should match the snapshot tree when error is manually invoked', () => {
const wrapper = shallow(
<FatalErrorBoundary classes={{}}>
<ErrorBoundary classes={{}}>
<Something />
</FatalErrorBoundary>
</ErrorBoundary>
)

const error = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ErrorBoundary Should match the snapshot tree when error is manually invoked 1`] = `ShallowWrapper {}`;

This file was deleted.

Loading

0 comments on commit 225069e

Please sign in to comment.