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 to choose from." + "dns_providers": "Here is a <0>list of known DNS providers 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 { {updateAvailable && - + + + + } {!encryption.processing && @@ -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 ( +
+
+ processing_update +
+ ); +}; + +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 => ( - - Click here - , - ]} - > - update_announcement - + + + Click here + , + ]} + > + update_announcement + + {props.canAutoUpdate && + + } + ); 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,