diff --git a/console/src/Endpoints.js b/console/src/Endpoints.js index 90509bd0fd052..9c832ecbf8175 100644 --- a/console/src/Endpoints.js +++ b/console/src/Endpoints.js @@ -12,6 +12,8 @@ const Endpoints = { schemaChange: `${baseUrl}/v1/query`, query: `${baseUrl}/v1/query`, rawSQL: `${baseUrl}/v1/query`, + version: `${baseUrl}/v1/version`, + updateCheck: 'https://releases.hasura.io/graphql-engine', hasuractlMigrate: `${hasuractlUrl}/apis/migrate`, hasuractlMetadata: `${hasuractlUrl}/apis/metadata`, hasuractlMigrateSettings: `${hasuractlUrl}/apis/migrate/settings`, diff --git a/console/src/components/ApiExplorer/Actions.js b/console/src/components/ApiExplorer/Actions.js index 08131de6bdbeb..d04be9e6bf3b4 100644 --- a/console/src/components/ApiExplorer/Actions.js +++ b/console/src/components/ApiExplorer/Actions.js @@ -198,7 +198,6 @@ const graphqlSubscriber = (graphQLParams, url, headers) => { }; return fetcher(graphQLParams); } catch (e) { - console.log(e); return e.json(); } }; diff --git a/console/src/components/Main/Actions.js b/console/src/components/Main/Actions.js index 8fc404940f4fc..352c8e5cf04a6 100644 --- a/console/src/components/Main/Actions.js +++ b/console/src/components/Main/Actions.js @@ -3,6 +3,7 @@ import globals from 'Globals'; import defaultState from './State'; import Endpoints from '../../Endpoints'; import requestAction from '../../utils/requestAction'; +import requestActionPlain from '../../utils/requestActionPlain'; import { globalCookiePolicy } from '../../Endpoints'; import { saveAccessKeyState } from '../AppState'; import { @@ -13,6 +14,11 @@ import { changeRequestHeader } from '../ApiExplorer/Actions'; const SET_MIGRATION_STATUS_SUCCESS = 'Main/SET_MIGRATION_STATUS_SUCCESS'; const SET_MIGRATION_STATUS_ERROR = 'Main/SET_MIGRATION_STATUS_ERROR'; +const SET_SERVER_VERSION_SUCCESS = 'Main/SET_SERVER_VERSION_SUCCESS'; +const SET_SERVER_VERSION_ERROR = 'Main/SET_SERVER_VERSION_ERROR'; +const SET_LATEST_SERVER_VERSION_SUCCESS = + 'Main/SET_LATEST_SERVER_VERSION_SUCCESS'; +const SET_LATEST_SERVER_VERSION_ERROR = 'Main/SET_LATEST_SERVER_VERSION_ERROR'; const UPDATE_MIGRATION_STATUS_SUCCESS = 'Main/UPDATE_MIGRATION_STATUS_SUCCESS'; const UPDATE_MIGRATION_STATUS_ERROR = 'Main/UPDATE_MIGRATION_STATUS_ERROR'; const HASURACTL_URL_ENV = 'Main/HASURACTL_URL_ENV'; @@ -41,6 +47,63 @@ const loadMigrationStatus = () => dispatch => { ); }; +const loadServerVersion = () => dispatch => { + const url = Endpoints.version; + const options = { + method: 'GET', + credentials: globalCookiePolicy, + headers: { 'Content-Type': 'application/json' }, + }; + return dispatch(requestActionPlain(url, options)).then( + data => { + let parsedVersion; + try { + parsedVersion = JSON.parse(data); + dispatch({ + type: SET_SERVER_VERSION_SUCCESS, + data: parsedVersion.version, + }); + } catch (e) { + console.error(e); + } + }, + error => { + console.error(error); + dispatch({ type: SET_SERVER_VERSION_ERROR, data: null }); + } + ); +}; + +const checkServerUpdates = () => (dispatch, getState) => { + const url = + Endpoints.updateCheck + + '?agent=console&version=' + + getState().main.serverVersion; + const options = { + method: 'GET', + credentials: globalCookiePolicy, + headers: { 'Content-Type': 'application/json' }, + }; + return dispatch(requestActionPlain(url, options)).then( + data => { + let parsedVersion; + try { + parsedVersion = JSON.parse(data); + dispatch({ + type: SET_LATEST_SERVER_VERSION_SUCCESS, + data: parsedVersion.latest, + }); + } catch (e) { + console.error(e); + } + }, + error => { + console.error(error); + dispatch({ type: SET_LATEST_SERVER_VERSION_ERROR, data: null }); + } + ); +}; + const validateLogin = isInitialLoad => (dispatch, getState) => { const url = Endpoints.getSchema; const currentSchema = getState().tables.currentSchema; @@ -149,6 +212,27 @@ const mainReducer = (state = defaultState, action) => { ...state, migrationMode: action.data.migration_mode === 'true', }; + case SET_SERVER_VERSION_SUCCESS: + return { + ...state, + serverVersion: action.data, + }; + case SET_SERVER_VERSION_ERROR: + return { + ...state, + serverVersion: null, + }; + + case SET_LATEST_SERVER_VERSION_SUCCESS: + return { + ...state, + latestServerVersion: action.data, + }; + case SET_LATEST_SERVER_VERSION_ERROR: + return { + ...state, + latestServerVersion: null, + }; case UPDATE_MIGRATION_STATUS_SUCCESS: return { ...state, @@ -204,4 +288,6 @@ export { LOGIN_IN_PROGRESS, LOGIN_ERROR, validateLogin, + loadServerVersion, + checkServerUpdates, }; diff --git a/console/src/components/Main/Main.js b/console/src/components/Main/Main.js index 2c3a23dd7a4fd..638d79e1f6104 100644 --- a/console/src/components/Main/Main.js +++ b/console/src/components/Main/Main.js @@ -3,119 +3,211 @@ import { connect } from 'react-redux'; import { Link } from 'react-router'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import globals from '../../Globals'; - import * as tooltip from './Tooltips'; import 'react-toggle/style.css'; import Spinner from '../Common/Spinner/Spinner'; +import { loadServerVersion, checkServerUpdates } from './Actions'; -const Main = ({ children, location, migrationModeProgress, currentSchema }) => { - const styles = require('./Main.scss'); - const appPrefix = ''; - const logo = require('./logo.svg'); - const currentLocation = location.pathname; - const currentActiveBlock = currentLocation.split('/')[1]; - - const sidebarClass = styles.sidebar; +const semver = require('semver'); - let mainContent = null; - if (migrationModeProgress) { - mainContent = ( -
- {' '} - {' '} -
- ); - } else { - mainContent = children && React.cloneElement(children); +class Main extends React.Component { + constructor(props) { + super(props); + this.state = { + showBannerNotification: false, + }; + } + componentDidMount() { + const { dispatch } = this.props; + dispatch(loadServerVersion()).then(() => { + dispatch(checkServerUpdates()).then(() => { + let isUpdateAvailable = false; + try { + isUpdateAvailable = semver.gt( + this.props.latestServerVersion, + this.props.serverVersion + ); + const isClosedBefore = window.localStorage.getItem( + this.props.latestServerVersion + '_BANNER_NOTIFICATION_CLOSED' + ); + if (isClosedBefore === 'true') { + isUpdateAvailable = false; + this.setState({ showBannerNotification: false }); + } else { + this.setState({ showBannerNotification: isUpdateAvailable }); + } + } catch (e) { + console.error(e); + } + }); + }); } - let accessKeyHtml = null; - if (globals.accessKey === '' || globals.accessKey === null) { - accessKeyHtml = ( - - - - - + closeUpdateBanner() { + const { latestServerVersion } = this.props; + window.localStorage.setItem( + latestServerVersion + '_BANNER_NOTIFICATION_CLOSED', + 'true' ); + this.setState({ showBannerNotification: false }); } - return ( -
-
-
-
-
-
+ render() { + const { + children, + location, + migrationModeProgress, + currentSchema, + latestServerVersion, + } = this.props; + const styles = require('./Main.scss'); + const appPrefix = ''; + const logo = require('./logo.svg'); + const currentLocation = location.pathname; + const currentActiveBlock = currentLocation.split('/')[1]; + + const sidebarClass = styles.sidebar; + + let mainContent = null; + if (migrationModeProgress) { + mainContent = ( +
+ {' '} + {' '} +
+ ); + } else { + mainContent = children && React.cloneElement(children); + } + let accessKeyHtml = null; + if (globals.accessKey === '' || globals.accessKey === null) { + accessKeyHtml = ( + + + + + + ); + } + + return ( +
+
+
+
+
+
+ + + +
- +
HASURA
- -
HASURA
-
+
+
    + +
  • + +
    +