Merge pull request #452 from acelaya-forks/feature/auto-pwa-restart

Feature/auto pwa restart
This commit is contained in:
Alejandro Celaya 2021-07-12 16:39:27 +02:00 committed by GitHub
commit 56d9dcf562
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 154 additions and 78 deletions

View file

@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* [#431](https://github.com/shlinkio/shlink-web-client/pull/431) Added support to filter out visits from potential bots in visits sections, when consuming Shlink >=2.7.
* [#430](https://github.com/shlinkio/shlink-web-client/pull/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7.
* [#450](https://github.com/shlinkio/shlink-web-client/pull/450) Improved landing page design.
* [#449](https://github.com/shlinkio/shlink-web-client/pull/449) Improved PWA update banner, allowing to restart the app directly from it without having to close the tab.
### Changed
* [#442](https://github.com/shlinkio/shlink-web-client/pull/442) Visits filtering now goes through the corresponding reducer.

84
package-lock.json generated
View file

@ -7609,9 +7609,9 @@
"dev": true
},
"arch": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.1.2.tgz",
"integrity": "sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
"integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
"dev": true
},
"arg": {
@ -10792,40 +10792,48 @@
"dev": true
},
"clipboardy": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.3.tgz",
"integrity": "sha512-2WNImOvCRe6r63Gk9pShfkwXsVtKCroMAevIbiae021mS850UkWPbevxsBz3tnvjZIEGvlwaqCPsw+4ulzNgJA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
"integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==",
"dev": true,
"requires": {
"arch": "^2.1.0",
"execa": "^0.8.0"
"arch": "^2.1.1",
"execa": "^1.0.0",
"is-wsl": "^2.1.1"
},
"dependencies": {
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"dev": true,
"requires": {
"lru-cache": "^4.0.1",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"execa": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz",
"integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
"dev": true,
"requires": {
"cross-spawn": "^5.0.1",
"get-stream": "^3.0.0",
"cross-spawn": "^6.0.0",
"get-stream": "^4.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
}
},
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"requires": {
"is-docker": "^2.0.0"
}
}
}
},
@ -14252,12 +14260,6 @@
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"dev": true
},
"fast-glob": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz",
@ -26161,34 +26163,22 @@
}
},
"serve": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/serve/-/serve-11.3.2.tgz",
"integrity": "sha512-yKWQfI3xbj/f7X1lTBg91fXBP0FqjJ4TEi+ilES5yzH0iKJpN5LjNb1YzIfQg9Rqn4ECUS2SOf2+Kmepogoa5w==",
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/serve/-/serve-12.0.0.tgz",
"integrity": "sha512-BkTsETQYynAZ7rXX414kg4X6EvuZQS3UVs1NY0VQYdRHSTYWPYcH38nnDh48D0x6ONuislgjag8uKlU2gTBImA==",
"dev": true,
"requires": {
"@zeit/schemas": "2.6.0",
"ajv": "6.5.3",
"ajv": "6.12.6",
"arg": "2.0.0",
"boxen": "1.3.0",
"chalk": "2.4.1",
"clipboardy": "1.2.3",
"clipboardy": "2.3.0",
"compression": "1.7.3",
"serve-handler": "6.1.3",
"update-check": "1.5.2"
},
"dependencies": {
"ajv": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz",
"integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==",
"dev": true,
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"chalk": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",

View file

@ -130,7 +130,7 @@
"resolve": "^1.19.0",
"sass": "^1.29.0",
"sass-loader": "^10.1.0",
"serve": "^11.3.2",
"serve": "^12.0.0",
"stryker-cli": "^1.0.0",
"style-loader": "^2.0.0",
"stylelint": "^13.7.2",

View file

@ -1,5 +1,4 @@
@import './utils/base';
@import './utils/mixins/horizontal-align';
.app-container {
height: 100%;
@ -25,18 +24,3 @@
padding: 0 15px;
}
}
.app__update-banner.app__update-banner {
@include horizontal-align();
position: fixed;
top: $headerHeight - 25px;
padding: 0 4rem 0 0;
z-index: 1040;
margin: 0;
color: var(--text-color);
text-align: center;
width: 700px;
max-width: calc(100% - 30px);
box-shadow: 0 0 1rem var(--brand-color);
}

View file

@ -1,11 +1,11 @@
import { useEffect, FC } from 'react';
import { Route, Switch } from 'react-router-dom';
import { Alert } from 'reactstrap';
import NotFound from './common/NotFound';
import { ServersMap } from './servers/data';
import { Settings } from './settings/reducers/settings';
import { changeThemeInMarkup } from './utils/theme';
import { SimpleCard } from './utils/SimpleCard';
import { AppUpdateBanner } from './common/AppUpdateBanner';
import { forceUpdate } from './utils/helpers/sw';
import './App.scss';
interface AppProps {
@ -55,16 +55,7 @@ const App = (
</div>
</div>
<Alert
className="app__update-banner"
tag={SimpleCard}
color="secondary"
isOpen={appUpdated}
toggle={resetAppUpdate}
>
<h4 className="mb-4">This app has just been updated!</h4>
<p className="mb-0">Restart it to enjoy the new features.</p>
</Alert>
<AppUpdateBanner isOpen={appUpdated} toggle={resetAppUpdate} forceUpdate={forceUpdate} />
</div>
);
};

View file

@ -0,0 +1,17 @@
@import '../utils/base';
@import '../utils/mixins/horizontal-align';
.app-update-banner.app-update-banner {
@include horizontal-align();
position: fixed;
top: $headerHeight - 25px;
padding: 0 4rem 0 0;
z-index: 1040;
margin: 0;
color: var(--text-color);
text-align: center;
width: 700px;
max-width: calc(100% - 30px);
box-shadow: 0 0 1rem var(--brand-color);
}

View file

@ -0,0 +1,34 @@
import { FC, MouseEventHandler } from 'react';
import { Alert, Button } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSyncAlt as reloadIcon } from '@fortawesome/free-solid-svg-icons';
import { SimpleCard } from '../utils/SimpleCard';
import { useToggle } from '../utils/helpers/hooks';
import './AppUpdateBanner.scss';
interface AppUpdateBannerProps {
isOpen: boolean;
toggle: MouseEventHandler<any>;
forceUpdate: Function;
}
export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, toggle, forceUpdate }) => {
const [ isUpdating,, setUpdating ] = useToggle();
const update = () => {
setUpdating();
forceUpdate();
};
return (
<Alert className="app-update-banner" isOpen={isOpen} toggle={toggle} tag={SimpleCard} color="secondary">
<h4 className="mb-4">This app has just been updated!</h4>
<p className="mb-0">
Restart it to enjoy the new features.
<Button disabled={isUpdating} className="ml-2" color="secondary" size="sm" onClick={update}>
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} className="ml-1" /></>}
{isUpdating && <>Restarting...</>}
</Button>
</p>
</Alert>
);
};

16
src/utils/helpers/sw.ts Normal file
View file

@ -0,0 +1,16 @@
export const forceUpdate = async () => {
const registrations = await navigator.serviceWorker?.getRegistrations() ?? [];
for (const registration of registrations) {
const { waiting } = registration;
waiting?.addEventListener('statechange', (event) => {
if ((event.target as any)?.state === 'activated') {
window.location.reload();
}
});
// The logic that makes skipWaiting to be called when this message is posted is in service-worker.ts
waiting?.postMessage({ type: 'SKIP_WAITING' });
}
};

View file

@ -1,9 +1,9 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Route } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import { Alert } from 'reactstrap';
import { Settings } from '../src/settings/reducers/settings';
import appFactory from '../src/App';
import { AppUpdateBanner } from '../src/common/AppUpdateBanner';
describe('<App />', () => {
let wrapper: ShallowWrapper;
@ -29,7 +29,7 @@ describe('<App />', () => {
it('renders versions', () => expect(wrapper.find(ShlinkVersions)).toHaveLength(1));
it('renders an Alert', () => expect(wrapper.find(Alert)).toHaveLength(1));
it('renders an update banner', () => expect(wrapper.find(AppUpdateBanner)).toHaveLength(1));
it('renders app main routes', () => {
const routes = wrapper.find(Route);

View file

@ -0,0 +1,43 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Button } from 'reactstrap';
import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
import { SimpleCard } from '../../src/utils/SimpleCard';
describe('<AppUpdateBanner />', () => {
const toggle = jest.fn();
const forceUpdate = jest.fn();
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(<AppUpdateBanner isOpen={true} toggle={toggle} forceUpdate={forceUpdate} />);
});
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders an alert with expected props', () => {
expect(wrapper.prop('className')).toEqual('app-update-banner');
expect(wrapper.prop('isOpen')).toEqual(true);
expect(wrapper.prop('toggle')).toEqual(toggle);
expect(wrapper.prop('tag')).toEqual(SimpleCard);
expect(wrapper.prop('color')).toEqual('secondary');
});
it('invokes toggle when alert is toggled', () => {
(wrapper.prop('toggle') as Function)(); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
expect(toggle).toHaveBeenCalled();
});
it('triggers the update when clicking the button', () => {
expect(wrapper.find(Button).html()).toContain('Restart now');
expect(wrapper.find(Button).prop('disabled')).toEqual(false);
expect(forceUpdate).not.toHaveBeenCalled();
wrapper.find(Button).simulate('click');
expect(wrapper.find(Button).html()).toContain('Restarting...');
expect(wrapper.find(Button).prop('disabled')).toEqual(true);
expect(forceUpdate).toHaveBeenCalled();
});
});