From cb3f7f2834a429b3a9f49fe12d8a4132ae1d99c0 Mon Sep 17 00:00:00 2001
From: Ildar Kamalov <i.kamalov@adguard.com>
Date: Thu, 16 May 2019 17:24:30 +0300
Subject: [PATCH] + client: handle update

---
 client/src/__locales/en.json              |  4 ++-
 client/src/actions/index.js               | 25 +++++++++++++-
 client/src/api/Api.js                     |  7 ++++
 client/src/components/App/index.js        | 20 +++++++++---
 client/src/components/ui/Overlay.css      | 40 +++++++++++++++++++++++
 client/src/components/ui/UpdateOverlay.js | 26 +++++++++++++++
 client/src/components/ui/UpdateTopline.js | 39 +++++++++++++++-------
 client/src/reducers/index.js              | 12 +++++++
 8 files changed, 155 insertions(+), 18 deletions(-)
 create mode 100644 client/src/components/ui/Overlay.css
 create mode 100644 client/src/components/ui/UpdateOverlay.js

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index e0029945..eaf9dc63 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -260,5 +260,7 @@
     "dns_addresses": "DNS addresses",
     "down": "Down",
     "fix": "Fix",
-    "dns_providers": "Here is a <0>list of known DNS providers</0> to choose from."
+    "dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
+    "update_now": "Update now",
+    "processing_update": "Please wait, AdGuard Home is being updated"
 }
\ No newline at end of file
diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index 94830ada..02d327cf 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -4,7 +4,7 @@ import { t } from 'i18next';
 import { showLoading, hideLoading } from 'react-redux-loading-bar';
 
 import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
-import { SETTINGS_NAMES } from '../helpers/constants';
+import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
 import Api from '../api/Api';
 
 const apiClient = new Api();
@@ -154,6 +154,29 @@ export const getVersion = () => async (dispatch) => {
     }
 };
 
+export const getUpdateRequest = createAction('GET_UPDATE_REQUEST');
+export const getUpdateFailure = createAction('GET_UPDATE_FAILURE');
+export const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS');
+
+export const getUpdate = () => async (dispatch) => {
+    dispatch(getUpdateRequest());
+    try {
+        await apiClient.getUpdate();
+
+        const timer = setInterval(async () => {
+            const dnsStatus = await apiClient.getGlobalStatus();
+            if (dnsStatus) {
+                clearInterval(timer);
+                dispatch(getUpdateSuccess());
+                window.location.reload(true);
+            }
+        }, CHECK_TIMEOUT);
+    } catch (error) {
+        dispatch(addErrorToast({ error }));
+        dispatch(getUpdateFailure());
+    }
+};
+
 export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
 export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
 export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index 6d8a2f52..1743cc06 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -40,6 +40,8 @@ export default class Api {
     GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
     GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
     GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
+    GLOBAL_CLIENTS = { path: 'clients', method: 'GET' };
+    GLOBAL_UPDATE = { path: 'update', method: 'POST' };
 
     restartGlobalFiltering() {
         const { path, method } = this.GLOBAL_RESTART;
@@ -145,6 +147,11 @@ export default class Api {
         return this.makeRequest(path, method);
     }
 
+    getUpdate() {
+        const { path, method } = this.GLOBAL_UPDATE;
+        return this.makeRequest(path, method);
+    }
+
     // Filtering
     FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
     FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js
index 545d5007..157a55e6 100644
--- a/client/src/components/App/index.js
+++ b/client/src/components/App/index.js
@@ -19,6 +19,7 @@ import Toasts from '../Toasts';
 import Footer from '../ui/Footer';
 import Status from '../ui/Status';
 import UpdateTopline from '../ui/UpdateTopline';
+import UpdateOverlay from '../ui/UpdateOverlay';
 import EncryptionTopline from '../ui/EncryptionTopline';
 import i18n from '../../i18n';
 
@@ -37,6 +38,10 @@ class App extends Component {
         this.props.enableDns();
     };
 
+    handleUpdate = () => {
+        this.props.getUpdate();
+    }
+
     setLanguage = () => {
         const { processing, language } = this.props.dashboard;
 
@@ -62,10 +67,16 @@ class App extends Component {
             <HashRouter hashType='noslash'>
                 <Fragment>
                     {updateAvailable &&
-                        <UpdateTopline
-                            url={dashboard.announcementUrl}
-                            version={dashboard.version}
-                        />
+                        <Fragment>
+                            <UpdateTopline
+                                url={dashboard.announcementUrl}
+                                version={dashboard.newVersion}
+                                canAutoUpdate={dashboard.canAutoUpdate}
+                                getUpdate={this.handleUpdate}
+                                processingUpdate={dashboard.processingUpdate}
+                            />
+                            <UpdateOverlay processingUpdate={dashboard.processingUpdate} />
+                        </Fragment>
                     }
                     {!encryption.processing &&
                         <EncryptionTopline notAfter={encryption.not_after} />
@@ -100,6 +111,7 @@ class App extends Component {
 
 App.propTypes = {
     getDnsStatus: PropTypes.func,
+    getUpdate: PropTypes.func,
     enableDns: PropTypes.func,
     dashboard: PropTypes.object,
     isCoreRunning: PropTypes.bool,
diff --git a/client/src/components/ui/Overlay.css b/client/src/components/ui/Overlay.css
new file mode 100644
index 00000000..d12a55b7
--- /dev/null
+++ b/client/src/components/ui/Overlay.css
@@ -0,0 +1,40 @@
+.overlay {
+    display: none;
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 110;
+    width: 100%;
+    height: 100%;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 20px;
+    font-size: 28px;
+    font-weight: 600;
+    text-align: center;
+    background-color: rgba(255, 255, 255, 0.8);
+}
+
+.overlay--visible {
+    display: flex;
+}
+
+.overlay__loading {
+    width: 40px;
+    height: 40px;
+    margin-bottom: 20px;
+    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E");
+    will-change: transform;
+    animation: clockwise 2s linear infinite;
+}
+
+@keyframes clockwise {
+    0% {
+        transform: rotate(0deg);
+    }
+
+    100% {
+        transform: rotate(360deg);
+    }
+}
diff --git a/client/src/components/ui/UpdateOverlay.js b/client/src/components/ui/UpdateOverlay.js
new file mode 100644
index 00000000..7a35264a
--- /dev/null
+++ b/client/src/components/ui/UpdateOverlay.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Trans, withNamespaces } from 'react-i18next';
+import classnames from 'classnames';
+
+import './Overlay.css';
+
+const UpdateOverlay = (props) => {
+    const overlayClass = classnames({
+        overlay: true,
+        'overlay--visible': props.processingUpdate,
+    });
+
+    return (
+        <div className={overlayClass}>
+            <div className="overlay__loading"></div>
+            <Trans>processing_update</Trans>
+        </div>
+    );
+};
+
+UpdateOverlay.propTypes = {
+    processingUpdate: PropTypes.bool,
+};
+
+export default withNamespaces()(UpdateOverlay);
diff --git a/client/src/components/ui/UpdateTopline.js b/client/src/components/ui/UpdateTopline.js
index a9124666..833a833d 100644
--- a/client/src/components/ui/UpdateTopline.js
+++ b/client/src/components/ui/UpdateTopline.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { Trans, withNamespaces } from 'react-i18next';
 
@@ -6,22 +6,37 @@ import Topline from './Topline';
 
 const UpdateTopline = props => (
     <Topline type="info">
-        <Trans
-            values={{ version: props.version }}
-            components={[
-                <a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
-                    Click here
-                </a>,
-            ]}
-        >
-            update_announcement
-        </Trans>
+        <Fragment>
+            <Trans
+                values={{ version: props.version }}
+                components={[
+                    <a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
+                        Click here
+                    </a>,
+                ]}
+            >
+                update_announcement
+            </Trans>
+            {props.canAutoUpdate &&
+                <button
+                    type="button"
+                    className="btn btn-sm btn-primary ml-3"
+                    onClick={props.getUpdate}
+                    disabled={props.processingUpdate}
+                >
+                    <Trans>update_now</Trans>
+                </button>
+            }
+        </Fragment>
     </Topline>
 );
 
 UpdateTopline.propTypes = {
-    version: PropTypes.string.isRequired,
+    version: PropTypes.string,
     url: PropTypes.string.isRequired,
+    canAutoUpdate: PropTypes.bool,
+    getUpdate: PropTypes.func,
+    processingUpdate: PropTypes.bool,
 };
 
 export default withNamespaces()(UpdateTopline);
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 25156688..58b16e94 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -126,12 +126,16 @@ const dashboard = handleActions({
             const {
                 version,
                 announcement_url: announcementUrl,
+                new_version: newVersion,
+                can_autoupdate: canAutoUpdate,
             } = payload;
 
             const newState = {
                 ...state,
                 version,
                 announcementUrl,
+                newVersion,
+                canAutoUpdate,
                 isUpdateAvailable: true,
             };
             return newState;
@@ -140,6 +144,13 @@ const dashboard = handleActions({
         return state;
     },
 
+    [actions.getUpdateRequest]: state => ({ ...state, processingUpdate: true }),
+    [actions.getUpdateFailure]: state => ({ ...state, processingUpdate: false }),
+    [actions.getUpdateSuccess]: (state) => {
+        const newState = { ...state, processingUpdate: false };
+        return newState;
+    },
+
     [actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
     [actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
     [actions.getFilteringSuccess]: (state, { payload }) => {
@@ -187,6 +198,7 @@ const dashboard = handleActions({
     processingVersion: true,
     processingFiltering: true,
     processingClients: true,
+    processingUpdate: false,
     upstreamDns: '',
     bootstrapDns: '',
     allServers: false,