Merge: * /control/version.json: add "recheck_now" parameter

Close #815

* commit 'd2258cb66de32092f145f2803a7be3d7869970f2':
  * openapi.yaml: update /version.json
  + client: add button for check updates
  * /control/version.json: add "recheck_now" parameter
This commit is contained in:
Simon Zolin 2019-06-27 11:23:29 +03:00
commit b4b11406cf
15 changed files with 133 additions and 49 deletions

View File

@ -257,7 +257,11 @@ Server can only auto-update if the current version is equal or higher than `self
Request: Request:
GET /control/version.json POST /control/version.json
{
"recheck_now": true | false // if false, server will check for a new version data only once in several hours
}
Response: Response:

View File

@ -312,5 +312,7 @@
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.", "access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
"access_blocked_title": "Blocked domains", "access_blocked_title": "Blocked domains",
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.", "access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
"access_settings_saved": "Access settings successfully saved" "access_settings_saved": "Access settings successfully saved",
"updates_checked": "Updates successfully checked",
"check_updates_now": "Check updates now"
} }

View File

@ -145,11 +145,14 @@ export const getVersionRequest = createAction('GET_VERSION_REQUEST');
export const getVersionFailure = createAction('GET_VERSION_FAILURE'); export const getVersionFailure = createAction('GET_VERSION_FAILURE');
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS'); export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
export const getVersion = () => async (dispatch) => { export const getVersion = (recheck = false) => async (dispatch) => {
dispatch(getVersionRequest()); dispatch(getVersionRequest());
try { try {
const newVersion = await apiClient.getGlobalVersion(); const newVersion = await apiClient.getGlobalVersion({ recheck_now: recheck });
dispatch(getVersionSuccess(newVersion)); dispatch(getVersionSuccess(newVersion));
if (recheck) {
dispatch(addSuccessToast('updates_checked'));
}
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(getVersionFailure()); dispatch(getVersionFailure());

View File

@ -36,7 +36,7 @@ export default class Api {
GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' }; GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' }; GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' }; GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'GET' }; GLOBAL_VERSION = { path: 'version.json', method: 'POST' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' }; GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' }; GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
GLOBAL_UPDATE = { path: 'update', method: 'POST' }; GLOBAL_UPDATE = { path: 'update', method: 'POST' };
@ -125,9 +125,13 @@ export default class Api {
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
getGlobalVersion() { getGlobalVersion(data) {
const { path, method } = this.GLOBAL_VERSION; const { path, method } = this.GLOBAL_VERSION;
return this.makeRequest(path, method); const config = {
data,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
} }
enableGlobalProtection() { enableGlobalProtection() {

View File

@ -65,8 +65,7 @@ class App extends Component {
render() { render() {
const { dashboard, encryption } = this.props; const { dashboard, encryption } = this.props;
const updateAvailable = const updateAvailable = dashboard.isCoreRunning && dashboard.isUpdateAvailable;
!dashboard.processingVersions && dashboard.isCoreRunning && dashboard.isUpdateAvailable;
return ( return (
<HashRouter hashType="noslash"> <HashRouter hashType="noslash">

View File

@ -50,8 +50,26 @@ class Dashboard extends Component {
dashboard.processingClients || dashboard.processingClients ||
dashboard.processingTopStats; dashboard.processingTopStats;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.getAllStats()}><Trans>refresh_statics</Trans></button>; const refreshFullButton = (
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.getAllStats()} />; <button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={() => this.getAllStats()}
>
<Trans>refresh_statics</Trans>
</button>
);
const refreshButton = (
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm"
onClick={() => this.getAllStats()}
>
<svg className="icons">
<use xlinkHref="#refresh" />
</svg>
</button>
);
return ( return (
<Fragment> <Fragment>

View File

@ -75,7 +75,11 @@
} }
.nav-version__value { .nav-version__value {
max-width: 110px;
font-weight: 600; font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
.nav-version__link { .nav-version__link {
@ -85,6 +89,11 @@
cursor: pointer; cursor: pointer;
} }
.nav-version__text {
display: flex;
justify-content: flex-end;
}
.header-brand-img { .header-brand-img {
height: 32px; height: 32px;
} }

View File

@ -4,12 +4,26 @@ import { Trans, withNamespaces } from 'react-i18next';
import { getDnsAddress } from '../../helpers/helpers'; import { getDnsAddress } from '../../helpers/helpers';
function Version(props) { const Version = (props) => {
const { dnsVersion, dnsAddresses, dnsPort } = props; const {
dnsVersion, dnsAddresses, dnsPort, processingVersion, t,
} = props;
return ( return (
<div className="nav-version"> <div className="nav-version">
<div className="nav-version__text"> <div className="nav-version__text">
<Trans>version</Trans>: <span className="nav-version__value">{dnsVersion}</span> <Trans>version</Trans>:&nbsp;<span className="nav-version__value" title={dnsVersion}>{dnsVersion}</span>
<button
type="button"
className="btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2"
onClick={() => props.getVersion(true)}
disabled={processingVersion}
title={t('check_updates_now')}
>
<svg className="icons">
<use xlinkHref="#refresh" />
</svg>
</button>
</div> </div>
<div className="nav-version__link"> <div className="nav-version__link">
<div className="popover__trigger popover__trigger--address"> <div className="popover__trigger popover__trigger--address">
@ -17,20 +31,23 @@ function Version(props) {
</div> </div>
<div className="popover__body popover__body--address"> <div className="popover__body popover__body--address">
<div className="popover__list"> <div className="popover__list">
{dnsAddresses {dnsAddresses.map(ip => (
.map(ip => <li key={ip}>{getDnsAddress(ip, dnsPort)}</li>) <li key={ip}>{getDnsAddress(ip, dnsPort)}</li>
} ))}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} };
Version.propTypes = { Version.propTypes = {
dnsVersion: PropTypes.string.isRequired, dnsVersion: PropTypes.string.isRequired,
dnsAddresses: PropTypes.array.isRequired, dnsAddresses: PropTypes.array.isRequired,
dnsPort: PropTypes.number.isRequired, dnsPort: PropTypes.number.isRequired,
getVersion: PropTypes.func.isRequired,
processingVersion: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
}; };
export default withNamespaces()(Version); export default withNamespaces()(Version);

View File

@ -23,7 +23,7 @@ class Header extends Component {
}; };
render() { render() {
const { dashboard } = this.props; const { dashboard, getVersion, location } = this.props;
const { isMenuOpen } = this.state; const { isMenuOpen } = this.state;
const badgeClass = classnames({ const badgeClass = classnames({
'badge dns-status': true, 'badge dns-status': true,
@ -51,7 +51,7 @@ class Header extends Component {
</div> </div>
</div> </div>
<Menu <Menu
location={this.props.location} location={location}
isMenuOpen={isMenuOpen} isMenuOpen={isMenuOpen}
toggleMenuOpen={this.toggleMenuOpen} toggleMenuOpen={this.toggleMenuOpen}
closeMenu={this.closeMenu} closeMenu={this.closeMenu}
@ -59,7 +59,8 @@ class Header extends Component {
{!dashboard.processing && {!dashboard.processing &&
<div className="col col-sm-6 col-lg-3"> <div className="col col-sm-6 col-lg-3">
<Version <Version
{ ...this.props.dashboard } { ...dashboard }
getVersion={getVersion}
/> />
</div> </div>
} }
@ -71,8 +72,9 @@ class Header extends Component {
} }
Header.propTypes = { Header.propTypes = {
dashboard: PropTypes.object, dashboard: PropTypes.object.isRequired,
location: PropTypes.object, location: PropTypes.object.isRequired,
getVersion: PropTypes.func.isRequired,
}; };
export default withNamespaces()(Header); export default withNamespaces()(Header);

View File

@ -88,3 +88,10 @@
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
.btn-icon-sm {
width: 23px;
height: 23px;
min-width: 23px;
padding: 5px;
}

View File

@ -33,21 +33,6 @@
text-align: center; text-align: center;
} }
.card-refresh {
height: 26px;
width: 26px;
background-size: 14px;
background-position: center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiM0NjdmY2YiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==");
}
.card-refresh:hover,
.card-refresh:not(:disabled):not(.disabled):active,
.card-refresh:focus:active {
background-image: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==");
}
.card-title-stats { .card-title-stats {
font-size: 13px; font-size: 13px;
color: #9aa0ac; color: #9aa0ac;

View File

@ -55,6 +55,10 @@ const Icons = () => (
<symbol id="settings" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"> <symbol id="settings" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/> <circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/>
</symbol> </symbol>
<symbol id="refresh" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</symbol>
</svg> </svg>
); );

View File

@ -137,6 +137,7 @@ const dashboard = handleActions({
newVersion, newVersion,
canAutoUpdate, canAutoUpdate,
isUpdateAvailable: true, isUpdateAvailable: true,
processingVersion: false,
}; };
return newState; return newState;
} }

View File

@ -51,6 +51,10 @@ func getVersionResp(data []byte) []byte {
return d return d
} }
type getVersionJSONRequest struct {
RecheckNow bool `json:"recheck_now"`
}
// Get the latest available version from the Internet // Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL) log.Tracef("%s %v", r.Method, r.URL)
@ -60,19 +64,29 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
return return
} }
req := getVersionJSONRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
now := time.Now() now := time.Now()
if !req.RecheckNow {
controlLock.Lock() controlLock.Lock()
cached := now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0 cached := now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0
data := versionCheckJSON data := versionCheckJSON
controlLock.Unlock() controlLock.Unlock()
if cached { if cached {
// return cached copy log.Tracef("Returning cached data")
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Write(getVersionResp(data)) w.Write(getVersionResp(data))
return return
} }
}
log.Tracef("Downloading data from %s", versionCheckURL)
resp, err := client.Get(versionCheckURL) resp, err := client.Get(versionCheckURL)
if err != nil { if err != nil {
httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err) httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err)

View File

@ -135,11 +135,19 @@ paths:
"192.168.1.104:53535": "Couldn't communicate with DNS server" "192.168.1.104:53535": "Couldn't communicate with DNS server"
/version.json: /version.json:
get: post:
tags: tags:
- global - global
operationId: getVersionJson operationId: getVersionJson
summary: 'Gets information about the latest available version of AdGuard' summary: 'Gets information about the latest available version of AdGuard'
consumes:
- application/json
parameters:
- in: "body"
name: "body"
required: true
schema:
$ref: "#/definitions/GetVersionRequest"
produces: produces:
- 'application/json' - 'application/json'
responses: responses:
@ -994,6 +1002,13 @@ definitions:
example: example:
- '||example.org^' - '||example.org^'
- '||example.com^' - '||example.com^'
GetVersionRequest:
type: "object"
description: "/version.json request data"
properties:
recheck_now:
description: "If false, server will check for a new version data only once in several hours"
type: "boolean"
VersionInfo: VersionInfo:
type: "object" type: "object"
description: "Information about the latest available version of AdGuard Home" description: "Information about the latest available version of AdGuard Home"