diff --git a/.eslintignore b/.eslintignore index 49fefb80d..fee7d9f6b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -42,7 +42,6 @@ src/Contracts/ViewModels.ts src/Controls/Heatmap/Heatmap.test.ts src/Controls/Heatmap/Heatmap.ts src/Controls/Heatmap/HeatmapDatatypes.ts -src/Definitions/adal.d.ts src/Definitions/datatables.d.ts src/Definitions/gif.d.ts src/Definitions/globals.d.ts @@ -242,9 +241,6 @@ src/Platform/Hosted/Authorization.ts src/Platform/Hosted/DataAccessUtility.ts src/Platform/Hosted/ExplorerFactory.ts src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts -src/Platform/Hosted/Helpers/ConnectionStringParser.ts -src/Platform/Hosted/HostedUtils.test.ts -src/Platform/Hosted/HostedUtils.ts src/Platform/Hosted/Main.ts src/Platform/Hosted/Maint.test.ts src/Platform/Hosted/NotificationsClient.ts diff --git a/.eslintrc.js b/.eslintrc.js index 8695e29f2..ae05c511a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,10 +20,7 @@ module.exports = { overrides: [ { files: ["**/*.tsx"], - env: { - jest: true - }, - extends: ["plugin:react/recommended"], + extends: ["plugin:react/recommended"], // TODO: Add react-hooks plugins: ["react"] }, { @@ -36,6 +33,7 @@ module.exports = { } ], rules: { + "no-console": ["error", { allow: ["error", "warn", "dir"] }], curly: "error", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-extraneous-class": "error", @@ -43,6 +41,7 @@ module.exports = { "@typescript-eslint/no-explicit-any": "error", "prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }], eqeqeq: "error", + "react/display-name": "off", "no-restricted-syntax": [ "error", { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 440c65185..f911378d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,7 @@ jobs: npm ci npm start & npm run wait-for-server - npx jest -c ./jest.config.e2e.js --detectOpenHandles sql + npx jest -c ./jest.config.e2e.js --detectOpenHandles test/sql/container.spec.ts shell: bash env: DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator" diff --git a/externals/adal.js b/externals/adal.js deleted file mode 100644 index e0cae17cb..000000000 --- a/externals/adal.js +++ /dev/null @@ -1,1963 +0,0 @@ -//---------------------------------------------------------------------- -// AdalJS v1.0.17.1 -// @preserve Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// Apache License 2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//---------------------------------------------------------------------- - -/** - * This is a customized version of adal on top of version 1.0.17 which does not support multi tenant - * Customized version add tenantId to stored tokens so when tenant change, adal will refetch instead of read from sessionStorage - */ -var AuthenticationContext = (function () { - - 'use strict'; - - /** - * Configuration options for Authentication Context. - * @class config - * @property {string} tenant - Your target tenant. - * @property {string} clientId - Client ID assigned to your app by Azure Active Directory. - * @property {string} redirectUri - Endpoint at which you expect to receive tokens.Defaults to `window.location.href`. - * @property {string} instance - Azure Active Directory Instance.Defaults to `https://login.microsoftonline.com/`. - * @property {Array} endpoints - Collection of {Endpoint-ResourceId} used for automatically attaching tokens in webApi calls. - * @property {Boolean} popUp - Set this to true to enable login in a popup winodow instead of a full redirect.Defaults to `false`. - * @property {string} localLoginUrl - Set this to redirect the user to a custom login page. - * @property {function} displayCall - User defined function of handling the navigation to Azure AD authorization endpoint in case of login. Defaults to 'null'. - * @property {string} postLogoutRedirectUri - Redirects the user to postLogoutRedirectUri after logout. Defaults is 'redirectUri'. - * @property {string} cacheLocation - Sets browser storage to either 'localStorage' or sessionStorage'. Defaults to 'sessionStorage'. - * @property {Array.} anonymousEndpoints Array of keywords or URI's. Adal will not attach a token to outgoing requests that have these keywords or uri. Defaults to 'null'. - * @property {number} expireOffsetSeconds If the cached token is about to be expired in the expireOffsetSeconds (in seconds), Adal will renew the token instead of using the cached token. Defaults to 300 seconds. - * @property {string} correlationId Unique identifier used to map the request with the response. Defaults to RFC4122 version 4 guid (128 bits). - * @property {number} loadFrameTimeout The number of milliseconds of inactivity before a token renewal response from AAD should be considered timed out. - */ - - /** - * Creates a new AuthenticationContext object. - * @constructor - * @param {config} config Configuration options for AuthenticationContext - */ - - AuthenticationContext = function (config) { - /** - * Enum for request type - * @enum {string} - */ - this.REQUEST_TYPE = { - LOGIN: 'LOGIN', - RENEW_TOKEN: 'RENEW_TOKEN', - UNKNOWN: 'UNKNOWN' - }; - - this.RESPONSE_TYPE = { - ID_TOKEN_TOKEN: 'id_token token', - TOKEN: 'token' - }; - - /** - * Enum for storage constants - * @enum {string} - */ - this.CONSTANTS = { - ACCESS_TOKEN: 'access_token', - EXPIRES_IN: 'expires_in', - ID_TOKEN: 'id_token', - ERROR_DESCRIPTION: 'error_description', - SESSION_STATE: 'session_state', - ERROR: 'error', - STORAGE: { - TOKEN_KEYS: 'adal.token.keys', - ACCESS_TOKEN_KEY: 'adal.access.token.key', - EXPIRATION_KEY: 'adal.expiration.key', - STATE_LOGIN: 'adal.state.login', - STATE_RENEW: 'adal.state.renew', - NONCE_IDTOKEN: 'adal.nonce.idtoken', - SESSION_STATE: 'adal.session.state', - USERNAME: 'adal.username', - IDTOKEN: 'adal.idtoken', - ERROR: 'adal.error', - ERROR_DESCRIPTION: 'adal.error.description', - LOGIN_REQUEST: 'adal.login.request', - LOGIN_ERROR: 'adal.login.error', - RENEW_STATUS: 'adal.token.renew.status', - ANGULAR_LOGIN_REQUEST: 'adal.angular.login.request' - }, - RESOURCE_DELIMETER: '|', - CACHE_DELIMETER: '||', - LOADFRAME_TIMEOUT: 6000, - TOKEN_RENEW_STATUS_CANCELED: 'Canceled', - TOKEN_RENEW_STATUS_COMPLETED: 'Completed', - TOKEN_RENEW_STATUS_IN_PROGRESS: 'In Progress', - LOGGING_LEVEL: { - ERROR: 0, - WARN: 1, - INFO: 2, - VERBOSE: 3 - }, - LEVEL_STRING_MAP: { - 0: 'ERROR:', - 1: 'WARNING:', - 2: 'INFO:', - 3: 'VERBOSE:' - }, - POPUP_WIDTH: 483, - POPUP_HEIGHT: 600 - }; - - if (AuthenticationContext.prototype._singletonInstance) { - return AuthenticationContext.prototype._singletonInstance; - } - AuthenticationContext.prototype._singletonInstance = this; - - // public - this.instance = 'https://login.microsoftonline.com/'; - this.config = {}; - this.callback = null; - this.popUp = false; - this.isAngular = false; - - // private - this._user = null; - this._activeRenewals = {}; - this._loginInProgress = false; - this._acquireTokenInProgress = false; - this._renewStates = []; - this._callBackMappedToRenewStates = {}; - this._callBacksMappedToRenewStates = {}; - this._openedWindows = []; - this._requestType = this.REQUEST_TYPE.LOGIN; - window._adalInstance = this; - - // validate before constructor assignments - if (config.displayCall && typeof config.displayCall !== 'function') { - throw new Error('displayCall is not a function'); - } - - if (!config.clientId) { - throw new Error('clientId is required'); - } - - this.config = this._cloneConfig(config); - - if (this.config.navigateToLoginRequestUrl === undefined) - this.config.navigateToLoginRequestUrl = true; - - if (this.config.popUp) - this.popUp = true; - - if (this.config.callback && typeof this.config.callback === 'function') - this.callback = this.config.callback; - - if (this.config.instance) { - this.instance = this.config.instance; - } - - // App can request idtoken for itself using clientid as resource - if (!this.config.loginResource) { - this.config.loginResource = this.config.clientId; - } - - // redirect and logout_redirect are set to current location by default - if (!this.config.redirectUri) { - // strip off query parameters or hashes from the redirect uri as AAD does not allow those. - this.config.redirectUri = window.location.href.split("?")[0].split("#")[0]; - } - - if (!this.config.postLogoutRedirectUri) { - // strip off query parameters or hashes from the post logout redirect uri as AAD does not allow those. - this.config.postLogoutRedirectUri = window.location.href.split("?")[0].split("#")[0]; - } - - if (!this.config.anonymousEndpoints) { - this.config.anonymousEndpoints = []; - } - - if (this.config.isAngular) { - this.isAngular = this.config.isAngular; - } - - if (this.config.loadFrameTimeout) { - this.CONSTANTS.LOADFRAME_TIMEOUT = this.config.loadFrameTimeout; - } - }; - - if (typeof window !== 'undefined') { - window.Logging = { - piiLoggingEnabled: false, - level: 0, - log: function (message) { } - }; - } - - /** - * Initiates the login process by redirecting the user to Azure AD authorization endpoint. - */ - AuthenticationContext.prototype.login = function () { - if (this._loginInProgress) { - this.info("Login in progress"); - return; - } - - this._loginInProgress = true; - - // Token is not present and user needs to login - var expectedState = this._guid(); - this.config.state = expectedState; - this._idTokenNonce = this._guid(); - var loginStartPage = this._getItem(this.CONSTANTS.STORAGE.ANGULAR_LOGIN_REQUEST); - - if (!loginStartPage || loginStartPage === "") { - loginStartPage = window.location.href; - } - else { - this._saveItem(this.CONSTANTS.STORAGE.ANGULAR_LOGIN_REQUEST, "") - } - - this.verbose('Expected state: ' + expectedState + ' startPage:' + loginStartPage); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, loginStartPage); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, expectedState, true); - this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce, true); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); - var urlNavigate = this._getNavigateUrl('id_token', null, null) + '&nonce=' + encodeURIComponent(this._idTokenNonce); - - if (this.config.displayCall) { - // User defined way of handling the navigation - this.config.displayCall(urlNavigate); - } - else if (this.popUp) { - this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, '');// so requestInfo does not match redirect case - this._renewStates.push(expectedState); - this.registerCallback(expectedState, this.config.clientId, this.callback, ''); - this._loginPopup(urlNavigate); - } - else { - this.promptUser(urlNavigate); - } - }; - - /** - * Configures popup window for login. - * @ignore - */ - AuthenticationContext.prototype._openPopup = function (urlNavigate, title, popUpWidth, popUpHeight) { - try { - /** - * adding winLeft and winTop to account for dual monitor - * using screenLeft and screenTop for IE8 and earlier - */ - var winLeft = window.screenLeft ? window.screenLeft : window.screenX; - var winTop = window.screenTop ? window.screenTop : window.screenY; - /** - * window.innerWidth displays browser window's height and width excluding toolbars - * using document.documentElement.clientWidth for IE8 and earlier - */ - var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; - var height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; - var left = ((width / 2) - (popUpWidth / 2)) + winLeft; - var top = ((height / 2) - (popUpHeight / 2)) + winTop; - - var popupWindow = window.open(urlNavigate, title, 'width=' + popUpWidth + ', height=' + popUpHeight + ', top=' + top + ', left=' + left); - - if (popupWindow.focus) { - popupWindow.focus(); - } - - return popupWindow; - } catch (e) { - this.warn('Error opening popup, ' + e.message); - this._loginInProgress = false; - this._acquireTokenInProgress = false; - return null; - } - } - - AuthenticationContext.prototype._handlePopupError = function (loginCallback, resource, error, errorDesc, loginError) { - this.warn(errorDesc); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, error); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, errorDesc); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, loginError); - - if (resource && this._activeRenewals[resource]) { - this._activeRenewals[resource] = null; - } - - this._loginInProgress = false; - this._acquireTokenInProgress = false; - - if (loginCallback) { - loginCallback(errorDesc, null, error); - } - } - - /** - * After authorization, the user will be sent to your specified redirect_uri with the user's bearer token - * attached to the URI fragment as an id_token field. It closes popup window after redirection. - * @ignore - */ - AuthenticationContext.prototype._loginPopup = function (urlNavigate, resource, callback) { - var popupWindow = this._openPopup(urlNavigate, "login", this.CONSTANTS.POPUP_WIDTH, this.CONSTANTS.POPUP_HEIGHT); - var loginCallback = callback || this.callback; - - if (popupWindow == null) { - var error = 'Error opening popup'; - var errorDesc = 'Popup Window is null. This can happen if you are using IE'; - this._handlePopupError(loginCallback, resource, error, errorDesc, errorDesc); - return; - } - - this._openedWindows.push(popupWindow); - - if (this.config.redirectUri.indexOf('#') != -1) { - var registeredRedirectUri = this.config.redirectUri.split("#")[0]; - } - - else { - var registeredRedirectUri = this.config.redirectUri; - } - - var that = this; - - var pollTimer = window.setInterval(function () { - if (!popupWindow || popupWindow.closed || popupWindow.closed === undefined) { - var error = 'Popup Window closed'; - var errorDesc = 'Popup Window closed by UI action/ Popup Window handle destroyed due to cross zone navigation in IE/Edge' - - if (that.isAngular) { - that._broadcast('adal:popUpClosed', errorDesc + that.CONSTANTS.RESOURCE_DELIMETER + error); - } - - that._handlePopupError(loginCallback, resource, error, errorDesc, errorDesc); - window.clearInterval(pollTimer); - return; - } - try { - var popUpWindowLocation = popupWindow.location; - if (encodeURI(popUpWindowLocation.href).indexOf(encodeURI(registeredRedirectUri)) != -1) { - if (that.isAngular) { - that._broadcast('adal:popUpHashChanged', popUpWindowLocation.hash); - } - else { - that.handleWindowCallback(popUpWindowLocation.hash); - } - - window.clearInterval(pollTimer); - that._loginInProgress = false; - that._acquireTokenInProgress = false; - that.info("Closing popup window"); - that._openedWindows = []; - popupWindow.close(); - return; - } - } catch (e) { - } - }, 1); - }; - - AuthenticationContext.prototype._broadcast = function (eventName, data) { - // Custom Event is not supported in IE, below IIFE will polyfill the CustomEvent() constructor functionality in Internet Explorer 9 and higher - (function () { - - if (typeof window.CustomEvent === "function") { - return false; - } - - function CustomEvent(event, params) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; - var evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; - } - - CustomEvent.prototype = window.Event.prototype; - window.CustomEvent = CustomEvent; - })(); - - var evt = new CustomEvent(eventName, { detail: data }); - window.dispatchEvent(evt); - }; - - AuthenticationContext.prototype.loginInProgress = function () { - return this._loginInProgress; - }; - - /** - * Checks for the resource in the cache. By default, cache location is Session Storage - * @ignore - * @returns {Boolean} 'true' if login is in progress, else returns 'false'. - */ - AuthenticationContext.prototype._hasResource = function (key) { - var keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); - return keys && !this._isEmpty(keys) && (keys.indexOf(key + this.CONSTANTS.RESOURCE_DELIMETER) > -1); - }; - - /** - * Gets token for the specified resource from the cache. - * @param {string} resource A URI that identifies the resource for which the token is requested. - * @returns {string} token if if it exists and not expired, otherwise null. - */ - AuthenticationContext.prototype.getCachedToken = function (resource, tenantId) { - if (!this._hasResource(resource)) { - return null; - } - - var token = this._getItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource + tenantId); - var expiry = this._getItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource + tenantId); - - // If expiration is within offset, it will force renew - var offset = this.config.expireOffsetSeconds || 300; - - if (expiry && (expiry > this._now() + offset)) { - return token; - } else { - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource + tenantId, ''); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource + tenantId, 0); - return null; - } - }; - - /** - * User information from idtoken. - * @class User - * @property {string} userName - username assigned from upn or email. - * @property {object} profile - properties parsed from idtoken. - */ - - /** - * If user object exists, returns it. Else creates a new user object by decoding id_token from the cache. - * @returns {User} user object - */ - AuthenticationContext.prototype.getCachedUser = function () { - if (this._user) { - return this._user; - } - - var idtoken = this._getItem(this.CONSTANTS.STORAGE.IDTOKEN); - this._user = this._createUser(idtoken); - return this._user; - }; - - /** - * Adds the passed callback to the array of callbacks for the specified resource and puts the array on the window object. - * @param {string} resource A URI that identifies the resource for which the token is requested. - * @param {string} expectedState A unique identifier (guid). - * @param {tokenCallback} callback - The callback provided by the caller. It will be called with token or error. - */ - AuthenticationContext.prototype.registerCallback = function (expectedState, resource, callback, tenantId) { - this._activeRenewals[resource + tenantId] = expectedState; - - if (!this._callBacksMappedToRenewStates[expectedState]) { - this._callBacksMappedToRenewStates[expectedState] = []; - } - - var self = this; - this._callBacksMappedToRenewStates[expectedState].push(callback); - - if (!this._callBackMappedToRenewStates[expectedState]) { - this._callBackMappedToRenewStates[expectedState] = function (errorDesc, token, error, tokenType) { - self._activeRenewals[resource + tenantId] = null; - - for (var i = 0; i < self._callBacksMappedToRenewStates[expectedState].length; ++i) { - try { - self._callBacksMappedToRenewStates[expectedState][i](errorDesc, token, error, tokenType); - } - catch (error) { - self.warn(error); - } - } - - self._callBacksMappedToRenewStates[expectedState] = null; - self._callBackMappedToRenewStates[expectedState] = null; - }; - } - }; - - // var errorResponse = {error:'', error_description:''}; - // var token = 'string token'; - // callback(errorResponse, token) - // with callback - /** - * Acquires access token with hidden iframe - * @ignore - */ - AuthenticationContext.prototype._renewToken = function (resource, tenantId, callback, responseType) { - // use iframe to try to renew token - // use given resource to create new authz url - this.info('renewToken is called for resource:' + resource); - var frameHandle = this._addAdalFrame('adalRenewFrame' + resource + tenantId); - var expectedState = this._guid() + '|' + resource + '|' + tenantId; - this.config.state = expectedState; - // renew happens in iframe, so it keeps javascript context - this._renewStates.push(expectedState); - this.verbose('Renew token Expected state: ' + expectedState); - // remove the existing prompt=... query parameter and add prompt=none - responseType = responseType || 'token'; - var urlNavigate = this._urlRemoveQueryStringParameter(this._getNavigateUrl(responseType, resource, tenantId), 'prompt'); - - if (responseType === this.RESPONSE_TYPE.ID_TOKEN_TOKEN) { - this._idTokenNonce = this._guid(); - this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce, true); - urlNavigate += '&nonce=' + encodeURIComponent(this._idTokenNonce); - } - - urlNavigate = urlNavigate + '&prompt=none'; - urlNavigate = this._addHintParameters(urlNavigate, !tenantId); - this.registerCallback(expectedState, resource, callback, tenantId); - this.verbosePii('Navigate to:' + urlNavigate); - frameHandle.src = 'about:blank'; - this._loadFrameTimeout(urlNavigate, 'adalRenewFrame' + resource + tenantId, resource, tenantId); - - }; - - /** - * Renews idtoken for app's own backend when resource is clientId and calls the callback with token/error - * @ignore - */ - AuthenticationContext.prototype._renewIdToken = function (callback, responseType) { - // use iframe to try to renew token - this.info('renewIdToken is called'); - var frameHandle = this._addAdalFrame('adalIdTokenFrame'); - var expectedState = this._guid() + '|' + this.config.clientId; - this._idTokenNonce = this._guid(); - this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce, true); - this.config.state = expectedState; - // renew happens in iframe, so it keeps javascript context - this._renewStates.push(expectedState); - this.verbose('Renew Idtoken Expected state: ' + expectedState); - // remove the existing prompt=... query parameter and add prompt=none - var resource = responseType === null || typeof (responseType) === "undefined" ? null : this.config.clientId; - var responseType = responseType || 'id_token'; - var urlNavigate = this._urlRemoveQueryStringParameter(this._getNavigateUrl(responseType, resource, null), 'prompt'); - urlNavigate = urlNavigate + '&prompt=none'; - urlNavigate = this._addHintParameters(urlNavigate, true); - urlNavigate += '&nonce=' + encodeURIComponent(this._idTokenNonce); - this.registerCallback(expectedState, this.config.clientId, callback, undefined); - this.verbosePii('Navigate to:' + urlNavigate); - frameHandle.src = 'about:blank'; - this._loadFrameTimeout(urlNavigate, 'adalIdTokenFrame', this.config.clientId, ''); - }; - - /** - * Checks if the authorization endpoint URL contains query string parameters - * @ignore - */ - AuthenticationContext.prototype._urlContainsQueryStringParameter = function (name, url) { - // regex to detect pattern of a ? or & followed by the name parameter and an equals character - var regex = new RegExp("[\\?&]" + name + "="); - return regex.test(url); - } - - /** - * Removes the query string parameter from the authorization endpoint URL if it exists - * @ignore - */ - AuthenticationContext.prototype._urlRemoveQueryStringParameter = function (url, name) { - // we remove &name=value, name=value& and name=value - // &name=value - var regex = new RegExp('(\\&' + name + '=)[^\&]+'); - url = url.replace(regex, ''); - // name=value& - regex = new RegExp('(' + name + '=)[^\&]+&'); - url = url.replace(regex, ''); - // name=value - regex = new RegExp('(' + name + '=)[^\&]+'); - url = url.replace(regex, ''); - return url; - } - - // Calling _loadFrame but with a timeout to signal failure in loadframeStatus. Callbacks are left - // registered when network errors occur and subsequent token requests for same resource are registered to the pending request - /** - * @ignore - */ - AuthenticationContext.prototype._loadFrameTimeout = function (urlNavigation, frameName, resource, tenantId) { - //set iframe session to pending - this.verbose('Set loading state to pending for: ' + resource + tenantId); - this._saveItem(this.CONSTANTS.STORAGE.RENEW_STATUS + resource + tenantId, this.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS); - this._loadFrame(urlNavigation, frameName); - var self = this; - - setTimeout(function () { - if (self._getItem(self.CONSTANTS.STORAGE.RENEW_STATUS + resource + tenantId) === self.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS) { - // fail the iframe session if it's in pending state - self.verbose('Loading frame has timed out after: ' + (self.CONSTANTS.LOADFRAME_TIMEOUT / 1000) + ' seconds for resource ' + resource); - var expectedState = self._activeRenewals[resource + tenantId]; - - if (expectedState && self._callBackMappedToRenewStates[expectedState]) { - self._callBackMappedToRenewStates[expectedState]('Token renewal operation failed due to timeout', null, 'Token Renewal Failed'); - } - - self._saveItem(self.CONSTANTS.STORAGE.RENEW_STATUS + resource + tenantId, self.CONSTANTS.TOKEN_RENEW_STATUS_CANCELED); - } - }, self.CONSTANTS.LOADFRAME_TIMEOUT); - } - - /** - * Loads iframe with authorization endpoint URL - * @ignore - */ - AuthenticationContext.prototype._loadFrame = function (urlNavigate, frameName) { - // This trick overcomes iframe navigation in IE - // IE does not load the page consistently in iframe - var self = this; - self.info('LoadFrame: ' + frameName); - var frameCheck = frameName; - setTimeout(function () { - var frameHandle = self._addAdalFrame(frameCheck); - - if (frameHandle.src === '' || frameHandle.src === 'about:blank') { - frameHandle.src = urlNavigate; - self._loadFrame(urlNavigate, frameCheck); - } - - }, 500); - }; - - /** - * @callback tokenCallback - * @param {string} error_description error description returned from AAD if token request fails. - * @param {string} token token returned from AAD if token request is successful. - * @param {string} error error message returned from AAD if token request fails. - */ - - /** - * Acquires token from the cache if it is not expired. Otherwise sends request to AAD to obtain a new token. - * @param {string} resource ResourceUri identifying the target resource - * @param {tokenCallback} callback - The callback provided by the caller. It will be called with token or error. - */ - AuthenticationContext.prototype.acquireToken = function (resource, tenantId, callback) { - if (this._isEmpty(resource)) { - this.warn('resource is required'); - callback('resource is required', null, 'resource is required'); - return; - } - - var token = this.getCachedToken(resource, tenantId); - - if (token) { - this.info('Token is already in cache for resource:' + resource); - callback(null, token, null); - return; - } - - if (!this._user && !(this.config.extraQueryParameter && this.config.extraQueryParameter.indexOf('login_hint') !== -1)) { - this.warn('User login is required'); - callback('User login is required', null, 'login required'); - return; - } - - // renew attempt with iframe - // Already renewing for this resource, callback when we get the token. - if (this._activeRenewals[resource + tenantId]) { - // Active renewals contains the state for each renewal. - this.registerCallback(this._activeRenewals[resource + tenantId], resource, callback, tenantId); - } - else { - this._requestType = this.REQUEST_TYPE.RENEW_TOKEN; - if (resource === this.config.clientId && !tenantId) { - // App uses idtoken to send to api endpoints - // Default resource is tracked as clientid to store this token - if (this._user) { - this.verbose('renewing idtoken'); - this._renewIdToken(callback); - } - else { - this.verbose('renewing idtoken and access_token'); - this._renewIdToken(callback, this.RESPONSE_TYPE.ID_TOKEN_TOKEN); - } - } else { - if (this._user) { - this.verbose('renewing access_token'); - this._renewToken(resource, tenantId, callback); - } - else { - this.verbose('renewing idtoken and access_token'); - this._renewToken(resource, tenantId, callback, this.RESPONSE_TYPE.ID_TOKEN_TOKEN); - } - } - } - }; - - /** - * Acquires token (interactive flow using a popUp window) by sending request to AAD to obtain a new token. - * @param {string} resource ResourceUri identifying the target resource - * @param {string} extraQueryParameters extraQueryParameters to add to the authentication request - * @param {tokenCallback} callback - The callback provided by the caller. It will be called with token or error. - */ - AuthenticationContext.prototype.acquireTokenPopup = function (resource, tenantId, extraQueryParameters, claims, callback) { - if (this._isEmpty(resource)) { - this.warn('resource is required'); - callback('resource is required', null, 'resource is required'); - return; - } - - if (!this._user) { - this.warn('User login is required'); - callback('User login is required', null, 'login required'); - return; - } - - if (this._acquireTokenInProgress) { - this.warn("Acquire token interactive is already in progress") - callback("Acquire token interactive is already in progress", null, "Acquire token interactive is already in progress"); - return; - } - - var expectedState = this._guid() + '|' + resource + '|' + tenantId; - this.config.state = expectedState; - this._renewStates.push(expectedState); - this._requestType = this.REQUEST_TYPE.RENEW_TOKEN; - this.verbose('Renew token Expected state: ' + expectedState); - // remove the existing prompt=... query parameter and add prompt=select_account - var urlNavigate = this._urlRemoveQueryStringParameter(this._getNavigateUrl('token', resource, tenantId), 'prompt'); - urlNavigate = urlNavigate + '&prompt=select_account'; - - if (extraQueryParameters) { - urlNavigate += extraQueryParameters; - } - - if (claims && (urlNavigate.indexOf("&claims") === -1)) { - urlNavigate += '&claims=' + encodeURIComponent(claims); - } - else if (claims && (urlNavigate.indexOf("&claims") !== -1)) { - throw new Error('Claims cannot be passed as an extraQueryParameter'); - } - - urlNavigate = this._addHintParameters(urlNavigate, !tenantId); - this._acquireTokenInProgress = true; - this.info('acquireToken interactive is called for the resource ' + resource); - this.registerCallback(expectedState, resource, callback, tenantId); - this._loginPopup(urlNavigate, resource, callback); - - }; - - /** - * Acquires token (interactive flow using a redirect) by sending request to AAD to obtain a new token. In this case the callback passed in the Authentication - * request constructor will be called. - * @param {string} resource ResourceUri identifying the target resource - * @param {string} extraQueryParameters extraQueryParameters to add to the authentication request - */ - AuthenticationContext.prototype.acquireTokenRedirect = function (resource, tenantId, extraQueryParameters, claims) { - if (this._isEmpty(resource)) { - this.warn('resource is required'); - callback('resource is required', null, 'resource is required'); - return; - } - - var callback = this.callback; - - if (!this._user) { - this.warn('User login is required'); - callback('User login is required', null, 'login required'); - return; - } - - if (this._acquireTokenInProgress) { - this.warn("Acquire token interactive is already in progress") - callback("Acquire token interactive is already in progress", null, "Acquire token interactive is already in progress"); - return; - } - - var expectedState = this._guid() + '|' + resource + '|' + tenantId; - this.config.state = expectedState; - this.verbose('Renew token Expected state: ' + expectedState); - - // remove the existing prompt=... query parameter and add prompt=select_account - var urlNavigate = this._urlRemoveQueryStringParameter(this._getNavigateUrl('token', resource, tenantId), 'prompt'); - urlNavigate = urlNavigate + '&prompt=select_account'; - if (extraQueryParameters) { - urlNavigate += extraQueryParameters; - } - - if (claims && (urlNavigate.indexOf("&claims") === -1)) { - urlNavigate += '&claims=' + encodeURIComponent(claims); - } - else if (claims && (urlNavigate.indexOf("&claims") !== -1)) { - throw new Error('Claims cannot be passed as an extraQueryParameter'); - } - - urlNavigate = this._addHintParameters(urlNavigate, !tenantId); - this._acquireTokenInProgress = true; - this.info('acquireToken interactive is called for the resource ' + resource); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, window.location.href); - this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, expectedState, true); - this.promptUser(urlNavigate); - }; - /** - * Redirects the browser to Azure AD authorization endpoint. - * @param {string} urlNavigate Url of the authorization endpoint. - */ - AuthenticationContext.prototype.promptUser = function (urlNavigate) { - if (urlNavigate) { - this.infoPii('Navigate to:' + urlNavigate); - window.location.replace(urlNavigate); - } else { - this.info('Navigate url is empty'); - } - }; - - /** - * Clears cache items. - */ - AuthenticationContext.prototype.clearCache = function () { - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, ''); - this._saveItem(this.CONSTANTS.STORAGE.ANGULAR_LOGIN_REQUEST, ''); - this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, ''); - this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, ''); - this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, ''); - this._renewStates = []; - this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, ''); - this._saveItem(this.CONSTANTS.STORAGE.IDTOKEN, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, ''); - var keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); - - if (!this._isEmpty(keys)) { - keys = keys.split(this.CONSTANTS.RESOURCE_DELIMETER); - for (var i = 0; i < keys.length && keys[i] !== ""; i++) { - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + keys[i], ''); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + keys[i], 0); - } - } - - this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, ''); - }; - - /** - * Clears cache items for a given resource. - * @param {string} resource a URI that identifies the resource. - */ - AuthenticationContext.prototype.clearCacheForResource = function (resource) { - this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); - - if (this._hasResource(resource)) { - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, ''); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, 0); - } - }; - - /** - * Redirects user to logout endpoint. - * After logout, it will redirect to postLogoutRedirectUri if added as a property on the config object. - */ - AuthenticationContext.prototype.logOut = function () { - this.clearCache(); - this._user = null; - var urlNavigate; - - if (this.config.logOutUri) { - urlNavigate = this.config.logOutUri; - } else { - var tenant = 'common'; - var logout = ''; - - if (this.config.tenant) { - tenant = this.config.tenant; - } - - if (this.config.postLogoutRedirectUri) { - logout = 'post_logout_redirect_uri=' + encodeURIComponent(this.config.postLogoutRedirectUri); - } - - urlNavigate = this.instance + tenant + '/oauth2/logout?' + logout; - } - - this.infoPii('Logout navigate to: ' + urlNavigate); - this.promptUser(urlNavigate); - }; - - AuthenticationContext.prototype._isEmpty = function (str) { - return (typeof str === 'undefined' || !str || 0 === str.length); - }; - - /** - * @callback userCallback - * @param {string} error error message if user info is not available. - * @param {User} user user object retrieved from the cache. - */ - - /** - * Calls the passed in callback with the user object or error message related to the user. - * @param {userCallback} callback - The callback provided by the caller. It will be called with user or error. - */ - AuthenticationContext.prototype.getUser = function (callback) { - // IDToken is first call - if (typeof callback !== 'function') { - throw new Error('callback is not a function'); - } - - // user in memory - if (this._user) { - callback(null, this._user); - return; - } - - // frame is used to get idtoken - var idtoken = this._getItem(this.CONSTANTS.STORAGE.IDTOKEN); - - if (!this._isEmpty(idtoken)) { - this.info('User exists in cache: '); - this._user = this._createUser(idtoken); - callback(null, this._user); - } else { - this.warn('User information is not available'); - callback('User information is not available', null); - } - }; - - /** - * Adds login_hint to authorization URL which is used to pre-fill the username field of sign in page for the user if known ahead of time. - * domain_hint can be one of users/organisations which when added skips the email based discovery process of the user. - * @ignore - */ - AuthenticationContext.prototype._addHintParameters = function (urlNavigate, addDomainHint) { - //If you don�t use prompt=none, then if the session does not exist, there will be a failure. - //If sid is sent alongside domain or login hints, there will be a failure since request is ambiguous. - //If sid is sent with a prompt value other than none or attempt_none, there will be a failure since the request is ambiguous. - - if (this._user && this._user.profile) { - if (this._user.profile.sid && urlNavigate.indexOf('&prompt=none') !== -1) { - // don't add sid twice if user provided it in the extraQueryParameter value - if (!this._urlContainsQueryStringParameter("sid", urlNavigate)) { - // add sid - urlNavigate += '&sid=' + encodeURIComponent(this._user.profile.sid); - } - } - else if (this._user.profile.upn) { - // don't add login_hint twice if user provided it in the extraQueryParameter value - if (!this._urlContainsQueryStringParameter("login_hint", urlNavigate)) { - // add login_hint - urlNavigate += '&login_hint=' + encodeURIComponent(this._user.profile.upn); - } - // don't add domain_hint twice if user provided it in the extraQueryParameter value - if (addDomainHint && !this._urlContainsQueryStringParameter("domain_hint", urlNavigate) && this._user.profile.upn.indexOf('@') > -1) { - var parts = this._user.profile.upn.split('@'); - // local part can include @ in quotes. Sending last part handles that. - urlNavigate += '&domain_hint=' + encodeURIComponent(parts[parts.length - 1]); - } - } - - } - - return urlNavigate; - } - - /** - * Creates a user object by decoding the id_token - * @ignore - */ - AuthenticationContext.prototype._createUser = function (idToken) { - var user = null; - var parsedJson = this._extractIdToken(idToken); - if (parsedJson && parsedJson.hasOwnProperty('aud')) { - if (parsedJson.aud.toLowerCase() === this.config.clientId.toLowerCase()) { - - user = { - userName: '', - profile: parsedJson - }; - - if (parsedJson.hasOwnProperty('upn')) { - user.userName = parsedJson.upn; - } else if (parsedJson.hasOwnProperty('email')) { - user.userName = parsedJson.email; - } - } else { - this.warn('IdToken has invalid aud field'); - } - - } - - return user; - }; - - /** - * Returns the anchor part(#) of the URL - * @ignore - */ - AuthenticationContext.prototype._getHash = function (hash) { - if (hash.indexOf('#/') > -1) { - hash = hash.substring(hash.indexOf('#/') + 2); - } else if (hash.indexOf('#') > -1) { - hash = hash.substring(1); - } - - return hash; - }; - - /** - * Checks if the URL fragment contains access token, id token or error_description. - * @param {string} hash - Hash passed from redirect page - * @returns {Boolean} true if response contains id_token, access_token or error, false otherwise. - */ - AuthenticationContext.prototype.isCallback = function (hash) { - hash = this._getHash(hash); - var parameters = this._deserialize(hash); - return ( - parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION) || - parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN) || - parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN) - ); - }; - - /** - * Gets login error - * @returns {string} error message related to login. - */ - AuthenticationContext.prototype.getLoginError = function () { - return this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR); - }; - - /** - * Request info object created from the response received from AAD. - * @class RequestInfo - * @property {object} parameters - object comprising of fields such as id_token/error, session_state, state, e.t.c. - * @property {REQUEST_TYPE} requestType - either LOGIN, RENEW_TOKEN or UNKNOWN. - * @property {boolean} stateMatch - true if state is valid, false otherwise. - * @property {string} stateResponse - unique guid used to match the response with the request. - * @property {boolean} valid - true if requestType contains id_token, access_token or error, false otherwise. - */ - - /** - * Creates a requestInfo object from the URL fragment and returns it. - * @returns {RequestInfo} an object created from the redirect response from AAD comprising of the keys - parameters, requestType, stateMatch, stateResponse and valid. - */ - AuthenticationContext.prototype.getRequestInfo = function (hash) { - hash = this._getHash(hash); - var parameters = this._deserialize(hash); - var requestInfo = { - valid: false, - parameters: {}, - stateMatch: false, - stateResponse: '', - requestType: this.REQUEST_TYPE.UNKNOWN, - }; - - if (parameters) { - requestInfo.parameters = parameters; - if (parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION) || - parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN) || - parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)) { - - requestInfo.valid = true; - - // which call - var stateResponse = ''; - if (parameters.hasOwnProperty('state')) { - this.verbose('State: ' + parameters.state); - stateResponse = parameters.state; - } else { - this.warn('No state returned'); - return requestInfo; - } - - requestInfo.stateResponse = stateResponse; - - // async calls can fire iframe and login request at the same time if developer does not use the API as expected - // incoming callback needs to be looked up to find the request type - if (this._matchState(requestInfo)) { // loginRedirect or acquireTokenRedirect - return requestInfo; - } - - // external api requests may have many renewtoken requests for different resource - if (!requestInfo.stateMatch && window.parent) { - requestInfo.requestType = this._requestType; - var statesInParentContext = this._renewStates; - for (var i = 0; i < statesInParentContext.length; i++) { - if (statesInParentContext[i] === requestInfo.stateResponse) { - requestInfo.stateMatch = true; - break; - } - } - } - } - } - return requestInfo; - }; - - /** - * Matches nonce from the request with the response. - * @ignore - */ - AuthenticationContext.prototype._matchNonce = function (user) { - var requestNonce = this._getItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN); - - if (requestNonce) { - requestNonce = requestNonce.split(this.CONSTANTS.CACHE_DELIMETER); - for (var i = 0; i < requestNonce.length; i++) { - if (requestNonce[i] === user.profile.nonce) { - return true; - } - } - } - - return false; - }; - - /** - * Matches state from the request with the response. - * @ignore - */ - AuthenticationContext.prototype._matchState = function (requestInfo) { - var loginStates = this._getItem(this.CONSTANTS.STORAGE.STATE_LOGIN); - - if (loginStates) { - loginStates = loginStates.split(this.CONSTANTS.CACHE_DELIMETER); - for (var i = 0; i < loginStates.length; i++) { - if (loginStates[i] === requestInfo.stateResponse) { - requestInfo.requestType = this.REQUEST_TYPE.LOGIN; - requestInfo.stateMatch = true; - return true; - } - } - } - - var acquireTokenStates = this._getItem(this.CONSTANTS.STORAGE.STATE_RENEW); - - if (acquireTokenStates) { - acquireTokenStates = acquireTokenStates.split(this.CONSTANTS.CACHE_DELIMETER); - for (var i = 0; i < acquireTokenStates.length; i++) { - if (acquireTokenStates[i] === requestInfo.stateResponse) { - requestInfo.requestType = this.REQUEST_TYPE.RENEW_TOKEN; - requestInfo.stateMatch = true; - return true; - } - } - } - - return false; - - }; - - /** - * Extracts resource value from state. - * @ignore - */ - AuthenticationContext.prototype._getResourceFromState = function (state) { - if (state) { - var splitIndex = state.indexOf('|'); - - if (splitIndex > -1 && splitIndex + 1 < state.length) { - let resourceWithTenant = state.substring(splitIndex + 1); - splitIndex = resourceWithTenant.indexOf('|'); - if (splitIndex > -1) { - let resource = resourceWithTenant.substring(0, splitIndex); - return resource; - } - return resourceWithTenant; - } - } - - return ''; - }; - - AuthenticationContext.prototype._getTenantFromState = function (state) { - if (state) { - var splits = state.split('|'); - if (splits.length === 3) { - return splits[splits.length - 1]; - } - } - - return ''; - }; - - /** - * Saves token or error received in the response from AAD in the cache. In case of id_token, it also creates the user object. - */ - AuthenticationContext.prototype.saveTokenFromHash = function (requestInfo) { - this.info('State status:' + requestInfo.stateMatch + '; Request type:' + requestInfo.requestType); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); - - var resource = this._getResourceFromState(requestInfo.stateResponse); - var tenant = this._getTenantFromState(requestInfo.stateResponse); - - // Record error - if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION)) { - this.infoPii('Error :' + requestInfo.parameters.error + '; Error description:' + requestInfo.parameters[this.CONSTANTS.ERROR_DESCRIPTION]); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, requestInfo.parameters.error); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, requestInfo.parameters[this.CONSTANTS.ERROR_DESCRIPTION]); - - if (requestInfo.requestType === this.REQUEST_TYPE.LOGIN) { - this._loginInProgress = false; - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, requestInfo.parameters.error_description); - } - } else { - // It must verify the state from redirect - if (requestInfo.stateMatch) { - // record tokens to storage if exists - this.info('State is right'); - if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.SESSION_STATE)) { - this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, requestInfo.parameters[this.CONSTANTS.SESSION_STATE]); - } - - var keys; - - if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN)) { - this.info('Fragment has access token'); - - if (!this._hasResource(resource)) { - keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; - this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, keys + resource + this.CONSTANTS.RESOURCE_DELIMETER); - } - - // save token with related resource - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource + tenant, requestInfo.parameters[this.CONSTANTS.ACCESS_TOKEN]); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource + tenant, this._expiresIn(requestInfo.parameters[this.CONSTANTS.EXPIRES_IN])); - } - - if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)) { - this.info('Fragment has id token'); - this._loginInProgress = false; - this._user = this._createUser(requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); - if (this._user && this._user.profile) { - if (!this._matchNonce(this._user)) { - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'Nonce received: ' + this._user.profile.nonce + ' is not same as requested: ' + - this._getItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN)); - this._user = null; - } else { - this._saveItem(this.CONSTANTS.STORAGE.IDTOKEN, requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); - - // Save idtoken as access token for app itself - resource = this.config.loginResource ? this.config.loginResource : this.config.clientId; - - if (!this._hasResource(resource)) { - keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; - this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, keys + resource + this.CONSTANTS.RESOURCE_DELIMETER); - } - - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource + tenant, requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource + tenant, this._user.profile.exp); - } - } - else { - requestInfo.parameters['error'] = 'invalid id_token'; - requestInfo.parameters['error_description'] = 'Invalid id_token. id_token: ' + requestInfo.parameters[this.CONSTANTS.ID_TOKEN]; - this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'invalid id_token'); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Invalid id_token. id_token: ' + requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); - } - } - } else { - requestInfo.parameters['error'] = 'Invalid_state'; - requestInfo.parameters['error_description'] = 'Invalid_state. state: ' + requestInfo.stateResponse; - this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'Invalid_state'); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Invalid_state. state: ' + requestInfo.stateResponse); - } - } - - this._saveItem(this.CONSTANTS.STORAGE.RENEW_STATUS + resource + tenant, this.CONSTANTS.TOKEN_RENEW_STATUS_COMPLETED); - }; - - /** - * Gets resource for given endpoint if mapping is provided with config. - * @param {string} endpoint - The URI for which the resource Id is requested. - * @returns {string} resource for this API endpoint. - */ - AuthenticationContext.prototype.getResourceForEndpoint = function (endpoint) { - - // if user specified list of anonymous endpoints, no need to send token to these endpoints, return null. - if (this.config && this.config.anonymousEndpoints) { - for (var i = 0; i < this.config.anonymousEndpoints.length; i++) { - if (endpoint.indexOf(this.config.anonymousEndpoints[i]) > -1) { - return null; - } - } - } - - if (this.config && this.config.endpoints) { - for (var configEndpoint in this.config.endpoints) { - // configEndpoint is like /api/Todo requested endpoint can be /api/Todo/1 - if (endpoint.indexOf(configEndpoint) > -1) { - return this.config.endpoints[configEndpoint]; - } - } - } - - // default resource will be clientid if nothing specified - // App will use idtoken for calls to itself - // check if it's staring from http or https, needs to match with app host - if (endpoint.indexOf('http://') > -1 || endpoint.indexOf('https://') > -1) { - if (this._getHostFromUri(endpoint) === this._getHostFromUri(this.config.redirectUri)) { - return this.config.loginResource; - } - } - else { - // in angular level, the url for $http interceptor call could be relative url, - // if it's relative call, we'll treat it as app backend call. - return this.config.loginResource; - } - - // if not the app's own backend or not a domain listed in the endpoints structure - return null; - }; - - /** - * Strips the protocol part of the URL and returns it. - * @ignore - */ - AuthenticationContext.prototype._getHostFromUri = function (uri) { - // remove http:// or https:// from uri - var extractedUri = String(uri).replace(/^(https?:)\/\//, ''); - extractedUri = extractedUri.split('/')[0]; - return extractedUri; - }; - - /** - * This method must be called for processing the response received from AAD. It extracts the hash, processes the token or error, saves it in the cache and calls the registered callbacks with the result. - * @param {string} [hash=window.location.hash] - Hash fragment of Url. - */ - AuthenticationContext.prototype.handleWindowCallback = function (hash) { - // This is for regular javascript usage for redirect handling - // need to make sure this is for callback - if (hash == null) { - hash = window.location.hash; - } - - if (this.isCallback(hash)) { - var self = null; - var isPopup = false; - - if (this._openedWindows.length > 0 && this._openedWindows[this._openedWindows.length - 1].opener - && this._openedWindows[this._openedWindows.length - 1].opener._adalInstance) { - self = this._openedWindows[this._openedWindows.length - 1].opener._adalInstance; - isPopup = true; - } - else if (window.parent && window.parent._adalInstance) { - self = window.parent._adalInstance; - } - - var requestInfo = self.getRequestInfo(hash); - var token, tokenReceivedCallback, tokenType = null; - - if (isPopup || window.parent !== window) { - tokenReceivedCallback = self._callBackMappedToRenewStates[requestInfo.stateResponse]; - } - else { - tokenReceivedCallback = self.callback; - } - - self.info("Returned from redirect url"); - self.saveTokenFromHash(requestInfo); - - if ((requestInfo.requestType === this.REQUEST_TYPE.RENEW_TOKEN) && window.parent) { - if (window.parent !== window) { - self.verbose("Window is in iframe, acquiring token silently"); - } else { - self.verbose("acquiring token interactive in progress"); - } - - token = requestInfo.parameters[self.CONSTANTS.ACCESS_TOKEN] || requestInfo.parameters[self.CONSTANTS.ID_TOKEN]; - tokenType = self.CONSTANTS.ACCESS_TOKEN; - } else if (requestInfo.requestType === this.REQUEST_TYPE.LOGIN) { - token = requestInfo.parameters[self.CONSTANTS.ID_TOKEN]; - tokenType = self.CONSTANTS.ID_TOKEN; - } - - var errorDesc = requestInfo.parameters[self.CONSTANTS.ERROR_DESCRIPTION]; - var error = requestInfo.parameters[self.CONSTANTS.ERROR]; - try { - if (tokenReceivedCallback) { - tokenReceivedCallback(errorDesc, token, error, tokenType); - } - - } catch (err) { - self.error("Error occurred in user defined callback function: " + err); - } - - if (window.parent === window && !isPopup) { - if (self.config.navigateToLoginRequestUrl) { - window.location.href = self._getItem(self.CONSTANTS.STORAGE.LOGIN_REQUEST); - } else window.location.hash = ''; - } - } - }; - - /** - * Constructs the authorization endpoint URL and returns it. - * @ignore - */ - AuthenticationContext.prototype._getNavigateUrl = function (responseType, resource, overrideTenant) { - var tenant = 'common'; - if (overrideTenant || this.config.tenant) { - tenant = overrideTenant || this.config.tenant; - } - - var urlNavigate = this.instance + tenant + '/oauth2/authorize' + this._serialize(responseType, this.config, resource) + this._addLibMetadata(); - this.info('Navigate url:' + urlNavigate); - return urlNavigate; - }; - - /** - * Returns the decoded id_token. - * @ignore - */ - AuthenticationContext.prototype._extractIdToken = function (encodedIdToken) { - // id token will be decoded to get the username - var decodedToken = this._decodeJwt(encodedIdToken); - - if (!decodedToken) { - return null; - } - - try { - var base64IdToken = decodedToken.JWSPayload; - var base64Decoded = this._base64DecodeStringUrlSafe(base64IdToken); - - if (!base64Decoded) { - this.info('The returned id_token could not be base64 url safe decoded.'); - return null; - } - - // ECMA script has JSON built-in support - return JSON.parse(base64Decoded); - } catch (err) { - this.error('The returned id_token could not be decoded', err); - } - - return null; - }; - - /** - * Decodes a string of data which has been encoded using base-64 encoding. - * @ignore - */ - AuthenticationContext.prototype._base64DecodeStringUrlSafe = function (base64IdToken) { - // html5 should support atob function for decoding - base64IdToken = base64IdToken.replace(/-/g, '+').replace(/_/g, '/'); - - if (window.atob) { - return decodeURIComponent(escape(window.atob(base64IdToken))); // jshint ignore:line - } - else { - return decodeURIComponent(escape(this._decode(base64IdToken))); - } - }; - - //Take https://cdnjs.cloudflare.com/ajax/libs/Base64/0.3.0/base64.js and https://en.wikipedia.org/wiki/Base64 as reference. - AuthenticationContext.prototype._decode = function (base64IdToken) { - var codes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - base64IdToken = String(base64IdToken).replace(/=+$/, ''); - - var length = base64IdToken.length; - - if (length % 4 === 1) { - throw new Error('The token to be decoded is not correctly encoded.'); - } - - var h1, h2, h3, h4, bits, c1, c2, c3, decoded = ''; - - for (var i = 0; i < length; i += 4) { - //Every 4 base64 encoded character will be converted to 3 byte string, which is 24 bits - // then 6 bits per base64 encoded character - h1 = codes.indexOf(base64IdToken.charAt(i)); - h2 = codes.indexOf(base64IdToken.charAt(i + 1)); - h3 = codes.indexOf(base64IdToken.charAt(i + 2)); - h4 = codes.indexOf(base64IdToken.charAt(i + 3)); - - // For padding, if last two are '=' - if (i + 2 === length - 1) { - bits = h1 << 18 | h2 << 12 | h3 << 6; - c1 = bits >> 16 & 255; - c2 = bits >> 8 & 255; - decoded += String.fromCharCode(c1, c2); - break; - } - // if last one is '=' - else if (i + 1 === length - 1) { - bits = h1 << 18 | h2 << 12 - c1 = bits >> 16 & 255; - decoded += String.fromCharCode(c1); - break; - } - - bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; - - // then convert to 3 byte chars - c1 = bits >> 16 & 255; - c2 = bits >> 8 & 255; - c3 = bits & 255; - - decoded += String.fromCharCode(c1, c2, c3); - } - - return decoded; - }; - - /** - * Decodes an id token into an object with header, payload and signature fields. - * @ignore - */ - // Adal.node js crack function - AuthenticationContext.prototype._decodeJwt = function (jwtToken) { - if (this._isEmpty(jwtToken)) { - return null; - }; - - var idTokenPartsRegex = /^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/; - - var matches = idTokenPartsRegex.exec(jwtToken); - - if (!matches || matches.length < 4) { - this.warn('The returned id_token is not parseable.'); - return null; - } - - var crackedToken = { - header: matches[1], - JWSPayload: matches[2], - JWSSig: matches[3] - }; - - return crackedToken; - }; - - /** - * Converts string to represent binary data in ASCII string format by translating it into a radix-64 representation and returns it - * @ignore - */ - AuthenticationContext.prototype._convertUrlSafeToRegularBase64EncodedString = function (str) { - return str.replace('-', '+').replace('_', '/'); - }; - - /** - * Serializes the parameters for the authorization endpoint URL and returns the serialized uri string. - * @ignore - */ - AuthenticationContext.prototype._serialize = function (responseType, obj, resource) { - var str = []; - - if (obj !== null) { - str.push('?response_type=' + responseType); - str.push('client_id=' + encodeURIComponent(obj.clientId)); - if (resource) { - str.push('resource=' + encodeURIComponent(resource)); - } - - str.push('redirect_uri=' + encodeURIComponent(obj.redirectUri)); - str.push('state=' + encodeURIComponent(obj.state)); - - if (obj.hasOwnProperty('slice')) { - str.push('slice=' + encodeURIComponent(obj.slice)); - } - - if (obj.hasOwnProperty('extraQueryParameter')) { - str.push(obj.extraQueryParameter); - } - - var correlationId = obj.correlationId ? obj.correlationId : this._guid(); - str.push('client-request-id=' + encodeURIComponent(correlationId)); - } - - return str.join('&'); - }; - - /** - * Parses the query string parameters into a key-value pair object. - * @ignore - */ - AuthenticationContext.prototype._deserialize = function (query) { - var match, - pl = /\+/g, // Regex for replacing addition symbol with a space - search = /([^&=]+)=([^&]*)/g, - decode = function (s) { - return decodeURIComponent(s.replace(pl, ' ')); - }, - obj = {}; - match = search.exec(query); - - while (match) { - obj[decode(match[1])] = decode(match[2]); - match = search.exec(query); - } - - return obj; - }; - - /** - * Converts decimal value to hex equivalent - * @ignore - */ - AuthenticationContext.prototype._decimalToHex = function (number) { - var hex = number.toString(16); - - while (hex.length < 2) { - hex = '0' + hex; - } - return hex; - } - - /** - * Generates RFC4122 version 4 guid (128 bits) - * @ignore - */ - /* jshint ignore:start */ - AuthenticationContext.prototype._guid = function () { - // RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or - // pseudo-random numbers. - // The algorithm is as follows: - // Set the two most significant bits (bits 6 and 7) of the - // clock_seq_hi_and_reserved to zero and one, respectively. - // Set the four most significant bits (bits 12 through 15) of the - // time_hi_and_version field to the 4-bit version number from - // Section 4.1.3. Version4 - // Set all the other bits to randomly (or pseudo-randomly) chosen - // values. - // UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node - // time-low = 4hexOctet - // time-mid = 2hexOctet - // time-high-and-version = 2hexOctet - // clock-seq-and-reserved = hexOctet: - // clock-seq-low = hexOctet - // node = 6hexOctet - // Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - // y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10 - // y values are 8, 9, A, B - var cryptoObj = window.crypto || window.msCrypto; // for IE 11 - if (cryptoObj && cryptoObj.getRandomValues) { - var buffer = new Uint8Array(16); - cryptoObj.getRandomValues(buffer); - //buffer[6] and buffer[7] represents the time_hi_and_version field. We will set the four most significant bits (4 through 7) of buffer[6] to represent decimal number 4 (UUID version number). - buffer[6] |= 0x40; //buffer[6] | 01000000 will set the 6 bit to 1. - buffer[6] &= 0x4f; //buffer[6] & 01001111 will set the 4, 5, and 7 bit to 0 such that bits 4-7 == 0100 = "4". - //buffer[8] represents the clock_seq_hi_and_reserved field. We will set the two most significant bits (6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively. - buffer[8] |= 0x80; //buffer[8] | 10000000 will set the 7 bit to 1. - buffer[8] &= 0xbf; //buffer[8] & 10111111 will set the 6 bit to 0. - return this._decimalToHex(buffer[0]) + this._decimalToHex(buffer[1]) + this._decimalToHex(buffer[2]) + this._decimalToHex(buffer[3]) + '-' + this._decimalToHex(buffer[4]) + this._decimalToHex(buffer[5]) + '-' + this._decimalToHex(buffer[6]) + this._decimalToHex(buffer[7]) + '-' + - this._decimalToHex(buffer[8]) + this._decimalToHex(buffer[9]) + '-' + this._decimalToHex(buffer[10]) + this._decimalToHex(buffer[11]) + this._decimalToHex(buffer[12]) + this._decimalToHex(buffer[13]) + this._decimalToHex(buffer[14]) + this._decimalToHex(buffer[15]); - } - else { - var guidHolder = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; - var hex = '0123456789abcdef'; - var r = 0; - var guidResponse = ""; - for (var i = 0; i < 36; i++) { - if (guidHolder[i] !== '-' && guidHolder[i] !== '4') { - // each x and y needs to be random - r = Math.random() * 16 | 0; - } - if (guidHolder[i] === 'x') { - guidResponse += hex[r]; - } else if (guidHolder[i] === 'y') { - // clock-seq-and-reserved first hex is filtered and remaining hex values are random - r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0?? - r |= 0x8; // set pos 3 to 1 as 1??? - guidResponse += hex[r]; - } else { - guidResponse += guidHolder[i]; - } - } - return guidResponse; - } - }; - /* jshint ignore:end */ - - /** - * Calculates the expires in value in milliseconds for the acquired token - * @ignore - */ - AuthenticationContext.prototype._expiresIn = function (expires) { - // if AAD did not send "expires_in" property, use default expiration of 3599 seconds, for some reason AAD sends 3599 as "expires_in" value instead of 3600 - if (!expires) expires = 3599; - return this._now() + parseInt(expires, 10); - }; - - /** - * Return the number of milliseconds since 1970/01/01 - * @ignore - */ - AuthenticationContext.prototype._now = function () { - return Math.round(new Date().getTime() / 1000.0); - }; - - /** - * Adds the hidden iframe for silent token renewal - * @ignore - */ - AuthenticationContext.prototype._addAdalFrame = function (iframeId) { - if (typeof iframeId === 'undefined') { - return; - } - - this.info('Add adal frame to document:' + iframeId); - var adalFrame = document.getElementById(iframeId); - - if (!adalFrame) { - if (document.createElement && document.documentElement && - (window.opera || window.navigator.userAgent.indexOf('MSIE 5.0') === -1)) { - var ifr = document.createElement('iframe'); - ifr.setAttribute('id', iframeId); - ifr.setAttribute('aria-hidden', 'true'); - ifr.style.visibility = 'hidden'; - ifr.style.position = 'absolute'; - ifr.style.width = ifr.style.height = ifr.borderWidth = '0px'; - - adalFrame = document.getElementsByTagName('body')[0].appendChild(ifr); - } - else if (document.body && document.body.insertAdjacentHTML) { - document.body.insertAdjacentHTML('beforeEnd', ''); - } - if (window.frames && window.frames[iframeId]) { - adalFrame = window.frames[iframeId]; - } - } - - return adalFrame; - }; - - /** - * Saves the key-value pair in the cache - * @ignore - */ - AuthenticationContext.prototype._saveItem = function (key, obj, preserve) { - - if (this.config && this.config.cacheLocation && this.config.cacheLocation === 'localStorage') { - - if (!this._supportsLocalStorage()) { - this.info('Local storage is not supported'); - return false; - } - - if (preserve) { - var value = this._getItem(key) || ''; - localStorage.setItem(key, value + obj + this.CONSTANTS.CACHE_DELIMETER); - } - else { - localStorage.setItem(key, obj); - } - - return true; - } - - // Default as session storage - if (!this._supportsSessionStorage()) { - this.info('Session storage is not supported'); - return false; - } - - sessionStorage.setItem(key, obj); - return true; - }; - - /** - * Searches the value for the given key in the cache - * @ignore - */ - AuthenticationContext.prototype._getItem = function (key) { - - if (this.config && this.config.cacheLocation && this.config.cacheLocation === 'localStorage') { - - if (!this._supportsLocalStorage()) { - this.info('Local storage is not supported'); - return null; - } - - return localStorage.getItem(key); - } - - // Default as session storage - if (!this._supportsSessionStorage()) { - this.info('Session storage is not supported'); - return null; - } - - return sessionStorage.getItem(key); - }; - - /** - * Returns true if browser supports localStorage, false otherwise. - * @ignore - */ - AuthenticationContext.prototype._supportsLocalStorage = function () { - try { - if (!window.localStorage) return false; // Test availability - window.localStorage.setItem('storageTest', 'A'); // Try write - if (window.localStorage.getItem('storageTest') != 'A') return false; // Test read/write - window.localStorage.removeItem('storageTest'); // Try delete - if (window.localStorage.getItem('storageTest')) return false; // Test delete - return true; // Success - } catch (e) { - return false; - } - }; - - /** - * Returns true if browser supports sessionStorage, false otherwise. - * @ignore - */ - AuthenticationContext.prototype._supportsSessionStorage = function () { - try { - if (!window.sessionStorage) return false; // Test availability - window.sessionStorage.setItem('storageTest', 'A'); // Try write - if (window.sessionStorage.getItem('storageTest') != 'A') return false; // Test read/write - window.sessionStorage.removeItem('storageTest'); // Try delete - if (window.sessionStorage.getItem('storageTest')) return false; // Test delete - return true; // Success - } catch (e) { - return false; - } - }; - - /** - * Returns a cloned copy of the passed object. - * @ignore - */ - AuthenticationContext.prototype._cloneConfig = function (obj) { - if (null === obj || 'object' !== typeof obj) { - return obj; - } - - var copy = {}; - for (var attr in obj) { - if (obj.hasOwnProperty(attr)) { - copy[attr] = obj[attr]; - } - } - return copy; - }; - - /** - * Adds the library version and returns it. - * @ignore - */ - AuthenticationContext.prototype._addLibMetadata = function () { - // x-client-SKU - // x-client-Ver - return '&x-client-SKU=Js&x-client-Ver=' + this._libVersion(); - }; - - /** - * Checks the Logging Level, constructs the Log message and logs it. Users need to implement/override this method to turn on Logging. - * @param {number} level - Level can be set 0,1,2 and 3 which turns on 'error', 'warning', 'info' or 'verbose' level logging respectively. - * @param {string} message - Message to log. - * @param {string} error - Error to log. - */ - AuthenticationContext.prototype.log = function (level, message, error, containsPii) { - - if (level <= Logging.level) { - - if (!Logging.piiLoggingEnabled && containsPii) - return; - - var timestamp = new Date().toUTCString(); - var formattedMessage = ''; - - if (this.config.correlationId) - formattedMessage = timestamp + ':' + this.config.correlationId + '-' + this._libVersion() + '-' + this.CONSTANTS.LEVEL_STRING_MAP[level] + ' ' + message; - else - formattedMessage = timestamp + ':' + this._libVersion() + '-' + this.CONSTANTS.LEVEL_STRING_MAP[level] + ' ' + message; - - if (error) { - formattedMessage += '\nstack:\n' + error.stack; - } - - Logging.log(formattedMessage); - } - }; - - /** - * Logs messages when Logging Level is set to 0. - * @param {string} message - Message to log. - * @param {string} error - Error to log. - */ - AuthenticationContext.prototype.error = function (message, error) { - this.log(this.CONSTANTS.LOGGING_LEVEL.ERROR, message, error); - }; - - /** - * Logs messages when Logging Level is set to 1. - * @param {string} message - Message to log. - */ - AuthenticationContext.prototype.warn = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.WARN, message, null); - }; - - /** - * Logs messages when Logging Level is set to 2. - * @param {string} message - Message to log. - */ - AuthenticationContext.prototype.info = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.INFO, message, null); - }; - - /** - * Logs messages when Logging Level is set to 3. - * @param {string} message - Message to log. - */ - AuthenticationContext.prototype.verbose = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.VERBOSE, message, null); - }; - - /** - * Logs Pii messages when Logging Level is set to 0 and window.piiLoggingEnabled is set to true. - * @param {string} message - Message to log. - * @param {string} error - Error to log. - */ - AuthenticationContext.prototype.errorPii = function (message, error) { - this.log(this.CONSTANTS.LOGGING_LEVEL.ERROR, message, error, true); - }; - - /** - * Logs Pii messages when Logging Level is set to 1 and window.piiLoggingEnabled is set to true. - * @param {string} message - Message to log. - */ - AuthenticationContext.prototype.warnPii = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.WARN, message, null, true); - }; - - /** - * Logs messages when Logging Level is set to 2 and window.piiLoggingEnabled is set to true. - * @param {string} message - Message to log. - */ - AuthenticationContext.prototype.infoPii = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.INFO, message, null, true); - }; - - /** - * Logs messages when Logging Level is set to 3 and window.piiLoggingEnabled is set to true. - * @param {string} message - Message to log. - */ - AuthenticationContext.prototype.verbosePii = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.VERBOSE, message, null, true); - }; - /** - * Returns the library version. - * @ignore - */ - AuthenticationContext.prototype._libVersion = function () { - return '1.0.17.1'; - }; - - /** - * Returns a reference of Authentication Context as a result of a require call. - * @ignore - */ - if (typeof module !== 'undefined' && module.exports) { - module.exports = AuthenticationContext; - module.exports.inject = function (conf) { - return new AuthenticationContext(conf); - }; - // workaround jest didn't recognize AuthenticationContext issue. Not ideal - window.AuthenticationContext = AuthenticationContext; - } - - return AuthenticationContext; - -}()); \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 2cc332539..3cceb40a3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -39,10 +39,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 20, - functions: 24, - lines: 30, - statements: 29.0 + branches: 22, + functions: 28, + lines: 33, + statements: 31 } }, diff --git a/less/documentDB.less b/less/documentDB.less index 587670560..76d1e1abd 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -1527,21 +1527,6 @@ p { color: @AccentHigh; } -.inputTooltip { - .inputTooltip(); -} - -.inputTooltip .inputTooltipText { - top: -68px; - .inputTooltipText(); -} - -.inputTooltip .inputTooltipText::after { - border-width: @MediumSpace @MediumSpace 0 @MediumSpace; - top: 55px; - .inputTooltipTextAfter(); -} - .nowrap { white-space: nowrap; } @@ -3042,45 +3027,3 @@ settings-pane { .collapsibleSection :hover { cursor: pointer; } - -.messageBarInfoIcon { - color: #0072c6; -} - -.messageBarWarningIcon { - color: #db7500; -} - -.freeTierInfoBanner { - background-color: @BaseLow; - display: inline-flex; - padding: @DefaultSpace; - width: 100%; - - .freeTierInfoIcon img { - height: 28px; - width: 28px; - margin-left: 4px; - } - - .freeTierInfoMessage { - margin: auto 0; - padding-left: @MediumSpace; - } -} - -.freeTierInlineWarning { - display: inline-flex; - padding: 8px 8px 8px 0; - width: 100%; - - .freeTierWarningIcon img { - height: 20px; - width: 20px; - } - - .freeTierWarningMessage { - margin: auto 0; - padding-left: @SmallSpace; - } -} diff --git a/package-lock.json b/package-lock.json index 5929d906b..c9ba8dadd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,18 +5,11 @@ "requires": true, "dependencies": { "@azure/abort-controller": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.1.tgz", - "integrity": "sha512-wP2Jw6uPp8DEDy0n4KNidvwzDjyVV2xnycEIq7nPzj1rHyb/r+t3OPeNT1INZePP2wy5ZqlwyuyOMTi0ePyY1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.2.tgz", + "integrity": "sha512-XUyTo+bcyxHEf+jlN2MXA7YU9nxVehaubngHV1MIZZaqYmZqykkoeAz/JMMEeR7t3TcyDwbFa3Zw8BZywmIx4g==", "requires": { - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } + "tslib": "^2.0.0" } }, "@azure/arm-cosmosdb": { @@ -37,47 +30,18 @@ } }, "@azure/core-auth": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.1.3.tgz", - "integrity": "sha512-A4xigW0YZZpkj1zK7dKuzbBpGwnhEcRk6WWuIshdHC32raR3EQ1j6VA9XZqE+RFsUgH6OAmIK5BWIz+mZjnd6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.1.4.tgz", + "integrity": "sha512-+j1embyH1jqf04AIfJPdLafd5SC1y6z1Jz4i+USR1XkTp6KM8P5u4/AjmWMVoEQdM/M29PJcRDZcCEWjK9S1bw==", "requires": { "@azure/abort-controller": "^1.0.0", - "@azure/core-tracing": "1.0.0-preview.8", - "@opentelemetry/api": "^0.6.1", "tslib": "^2.0.0" - }, - "dependencies": { - "@azure/core-tracing": { - "version": "1.0.0-preview.8", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.8.tgz", - "integrity": "sha512-ZKUpCd7Dlyfn7bdc+/zC/sf0aRIaNQMDuSj2RhYRFe3p70hVAnYGp3TX4cnG2yoEALp/LTj/XnZGQ8Xzf6Ja/Q==", - "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "^0.6.1", - "tslib": "^1.10.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@opentelemetry/api": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.6.1.tgz", - "integrity": "sha512-wpufGZa7tTxw7eAsjXJtiyIQ42IWQdX9iUQp7ACJcKo1hCtuhLU+K2Nv1U6oRwT1oAlZTE6m4CgWKZBhOiau3Q==", - "requires": { - "@opentelemetry/context-base": "^0.6.1" - } - } } }, "@azure/core-http": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.1.9.tgz", - "integrity": "sha512-wM0HMRNQaE2NtTHb+9FXF7uxUqaAHFTMVu6OzlEll6gUGybcDqM7+9Oklp33BhEfq+ZumpCoqxq3njNbMHuf/w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.2.tgz", + "integrity": "sha512-9eu2OcbR7e44gqBy4U1Uv8NTWgLIMwKXMEGgO2MahsJy5rdTiAhs5fJHQffPq8uX2MFh21iBODwO9R/Xlov88A==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.1.3", @@ -92,7 +56,7 @@ "tough-cookie": "^4.0.0", "tslib": "^2.0.0", "tunnel": "^0.0.6", - "uuid": "^8.1.0", + "uuid": "^8.3.0", "xml2js": "^0.4.19" }, "dependencies": { @@ -171,21 +135,24 @@ } }, "@azure/identity": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.1.0.tgz", - "integrity": "sha512-S4jYqegLWXIwVnkiArFlcTA7KOZmv+LMhQeQJhnmYy/CxrJHyIAEQyJ7qsrSt58bSyDZI2NkmKUBKaYGZU3/5g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.2.1.tgz", + "integrity": "sha512-vCzV4Xg5hWJ2e4Et0waOmIEgYHsqtGF06kklnqblZg0hKDLKxTAX5FzKYuDMk1CctY2UdEmWFcA2li2uOXOLXQ==", "requires": { - "@azure/core-http": "^1.1.6", + "@azure/core-http": "^1.2.0", "@azure/core-tracing": "1.0.0-preview.9", "@azure/logger": "^1.0.0", + "@azure/msal-node": "1.0.0-beta.1", "@opentelemetry/api": "^0.10.2", + "axios": "^0.21.1", "events": "^3.0.0", "jws": "^4.0.0", "keytar": "^5.4.0", "msal": "^1.0.2", + "open": "^7.0.0", "qs": "^6.7.0", "tslib": "^2.0.0", - "uuid": "^8.1.0" + "uuid": "^8.3.0" }, "dependencies": { "qs": { @@ -196,18 +163,11 @@ } }, "@azure/logger": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.0.tgz", - "integrity": "sha512-g2qLDgvmhyIxR3JVS8N67CyIOeFRKQlX/llxYJQr1OSGQqM3HTpVP8MjmjcEKbL/OIt2N9C9UFaNQuKOw1laOA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.1.tgz", + "integrity": "sha512-QYQeaJ+A5x6aMNu8BG5qdsVBnYBop9UMwgUvGihSjf1PdZZXB+c/oMdM2ajKwzobLBh9e9QuMQkN9iL+IxLBLA==", "requires": { - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } + "tslib": "^2.0.0" } }, "@azure/ms-rest-azure-js": { @@ -275,6 +235,56 @@ } } }, + "@azure/msal-common": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-1.7.2.tgz", + "integrity": "sha512-3/voCdFKONENX+5tMrNOBSrVJb6NbE7YB8vc4FZ/4ZbjpK7GVtq9Bu1MW+HZhrmsUzSF/joHx0ZIJDYIequ/jg==", + "requires": { + "debug": "^4.1.1" + } + }, + "@azure/msal-node": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.1.tgz", + "integrity": "sha512-dO/bgVScpl5loZfsfhHXmFLTNoDxGvUiZIsJCe1+HpHyFWXwGsBZ71P5ixbxRhhf/bPpZS3X+/rm1Fq2uUucJw==", + "requires": { + "@azure/msal-common": "^1.7.2", + "axios": "^0.19.2", + "jsonwebtoken": "^8.5.1", + "uuid": "^8.3.0" + }, + "dependencies": { + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", @@ -3064,6 +3074,16 @@ "react-dom": "^16.13.0" }, "dependencies": { + "react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, "react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", @@ -3689,19 +3709,12 @@ "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", "requires": { "@opentelemetry/context-base": "^0.10.2" - }, - "dependencies": { - "@opentelemetry/context-base": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", - "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==" - } } }, "@opentelemetry/context-base": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.6.1.tgz", - "integrity": "sha512-5bHhlTBBq82ti3qPT15TRxkYTFPPQWbnkkQkmHPtqiS1XcTB69cEKd3Jm7Cfi/vkPoyxapmePE9tyA7EzLt8SQ==" + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", + "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==" }, "@peculiar/asn1-schema": { "version": "1.1.2", @@ -3870,6 +3883,198 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@testing-library/dom": { + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.29.4.tgz", + "integrity": "sha512-CtrJRiSYEfbtNGtEsd78mk1n1v2TUbeABlNIcOCJdDfkN5/JTOwQEbbQpoSRxGqzcWPgStMvJ4mNolSuBRv1NA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^4.2.2", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.4", + "lz-string": "^1.4.4", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/yargs": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", + "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/jest-dom": { + "version": "5.11.9", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz", + "integrity": "sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ==", + "requires": { + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^4.2.2", + "chalk": "^3.0.0", + "css": "^3.0.0", + "css.escape": "^1.5.1", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/react": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.3.tgz", + "integrity": "sha512-BirBUGPkTW28ULuCwIbYo0y2+0aavHczBT6N9r3LrsswEW3pg25l1wgoE7I8QBIy1upXWkwKpYdWY7NYYP0Bxw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^7.28.1" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -3888,6 +4093,12 @@ "integrity": "sha512-oIqnhcvhz0NRJ+vSN56leWsHyUmz7bySe5f88ukwPhXlNzfTB3JyZ6/bw4+B8zjSktBoYxR9Pzf0t18XwRiwHw==", "dev": true }, + "@types/aria-query": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.1.tgz", + "integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==", + "dev": true + }, "@types/asap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/asap/-/asap-2.0.0.tgz", @@ -4353,10 +4564,128 @@ } }, "@types/jest": { - "version": "23.3.10", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.3.10.tgz", - "integrity": "sha512-DC8xTuW/6TYgvEg3HEXS7cu9OijFqprVDXXiOcdOKZCU/5PJNLZU37VVvmZHdtMiGOa8wAA/We+JzbdxFzQTRQ==", - "dev": true + "version": "26.0.20", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz", + "integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==", + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/yargs": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", + "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==" + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } }, "@types/jest-environment-puppeteer": { "version": "4.3.2", @@ -4497,12 +4826,11 @@ } }, "@types/react-dom": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.0.7.tgz", - "integrity": "sha512-vaq4vMaJOaNgFff1t3LnHYr6vRa09vRspMkmLdXtFZmO1fwDI2snP+dpOkwrtlU8UC8qsqemCu4RmVM2OLq/fA==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", + "integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==", "dev": true, "requires": { - "@types/node": "*", "@types/react": "*" } }, @@ -4616,6 +4944,14 @@ "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==", "dev": true }, + "@types/testing-library__jest-dom": { + "version": "5.9.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz", + "integrity": "sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ==", + "requires": { + "@types/jest": "*" + } + }, "@types/text-encoding": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/text-encoding/-/text-encoding-0.0.33.tgz", @@ -5443,12 +5779,6 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" }, - "adal-angular": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/adal-angular/-/adal-angular-1.0.15.tgz", - "integrity": "sha1-8qnvgvNYxEToMUKs5l0yJ6RBBDs=", - "dev": true - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -5651,6 +5981,15 @@ "sprintf-js": "~1.0.2" } }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -5982,6 +6321,14 @@ "axe-core": "^3.5.3" } }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -7722,6 +8069,27 @@ "randomfill": "^1.0.3" } }, + "css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + }, + "dependencies": { + "source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + } + } + }, "css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -7873,6 +8241,11 @@ "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", "dev": true }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=" + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -8658,6 +9031,11 @@ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, + "dequal": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz", + "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==" + }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -8796,6 +9174,12 @@ "esutils": "^2.0.2" } }, + "dom-accessibility-api": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz", + "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==", + "dev": true + }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -9582,6 +9966,12 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", + "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -10560,8 +10950,7 @@ "follow-redirects": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", - "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", - "dev": true + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" }, "for-in": { "version": "1.0.2", @@ -12228,9 +12617,7 @@ "is-docker": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", - "dev": true, - "optional": true + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==" }, "is-extendable": { "version": "0.1.1", @@ -14621,6 +15008,34 @@ "graceful-fs": "^4.1.6" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -14648,9 +15063,9 @@ "dev": true }, "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "requires": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -14664,6 +15079,18 @@ "requires": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + } } }, "keytar": { @@ -15125,12 +15552,42 @@ "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", "dev": true }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, "lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", @@ -15148,6 +15605,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.pick": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", @@ -15427,9 +15889,9 @@ } }, "merge-deep": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz", - "integrity": "sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", "dev": true, "requires": { "arr-union": "^3.1.0", @@ -15539,6 +16001,11 @@ "dom-walk": "^0.1.0" } }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" + }, "mini-css-extract-plugin": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.3.tgz", @@ -15791,9 +16258,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "msal": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.3.tgz", - "integrity": "sha512-C90MhgzcBuTSR2BOQ/LQryY1CZVESQLJDdmRDWSsaVde+zwZ2iXD0fWw7zeBd5TzfUCiJEXZVs4lFJ8d/IGbiQ==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.4.tgz", + "integrity": "sha512-aOBD/L6jAsizDFzKxxvXxH0FEDjp6Inr3Ufi/Y2o7KCFKN+akoE2sLeszEb/0Y3VxHxK0F0ea7xQ/HHTomKivw==", "requires": { "tslib": "^1.9.3" }, @@ -15931,9 +16398,9 @@ } }, "node-abi": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.1.tgz", - "integrity": "sha512-HbtmIuByq44yhAzK7b9j/FelKlHYISKQn0mtvcBrU5QBkhoCMp5bu8Hv5AI34DcKfOAcJBcOEMwLlwO62FFu9A==", + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz", + "integrity": "sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==", "optional": true, "requires": { "semver": "^5.4.1" @@ -16442,6 +16909,25 @@ "mimic-fn": "^2.1.0" } }, + "open": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/open/-/open-7.3.1.tgz", + "integrity": "sha512-f2wt9DCBKKjlFbjzGb8MOAW8LH8F0mrs1zc7KTjAJ9PZNQbfenzWbNP1VZJvw6ICMG9r14Ah6yfwPn7T7i646A==", + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + } + } + }, "opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -17650,20 +18136,20 @@ } }, "react-dom": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz", - "integrity": "sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==", + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", + "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.15.0" + "scheduler": "^0.19.1" }, "dependencies": { "scheduler": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz", - "integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -17945,6 +18431,15 @@ "resolve": "^1.1.6" } }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, "redux": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", @@ -19655,6 +20150,14 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -19958,6 +20461,14 @@ "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.3.0.tgz", "integrity": "sha512-N/4UDu3Y2ICik0daMmFW1tplw0XPs1nVIEVYkTiQfj9/JQZeEtAKaSYwheCwje1I4pQ5r22fGpoaNIvGgsyJyg==" }, + "swr": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/swr/-/swr-0.4.0.tgz", + "integrity": "sha512-70qd1FHYHwIdYXW0jTpm5ktitzvPBCtyKz8ZzynWlY/rMqe4drYPgcl/H9Ipuh+Xv6ZW5viNx13ro8EKIWZcoQ==", + "requires": { + "dequal": "2.0.2" + } + }, "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", diff --git a/package.json b/package.json index ee52bd28b..d62c1d083 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "dependencies": { "@azure/arm-cosmosdb": "9.1.0", "@azure/cosmos": "3.9.0", - "@azure/identity": "1.1.0", "@azure/cosmos-language-service": "0.0.5", + "@azure/identity": "1.2.1", "@jupyterlab/services": "6.0.0-rc.2", "@jupyterlab/terminal": "3.0.0-rc.2", "@microsoft/applicationinsights-web": "2.5.9", @@ -36,6 +36,7 @@ "@nteract/transform-vega": "7.0.6", "@octokit/rest": "17.9.2", "@phosphor/widgets": "1.9.3", + "@testing-library/jest-dom": "5.11.9", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", "@uifabric/react-cards": "0.109.110", @@ -69,6 +70,7 @@ "knockout": "3.5.1", "mkdirp": "1.0.4", "monaco-editor": "0.18.1", + "msal": "1.4.4", "object.entries": "1.1.0", "office-ui-fabric-react": "7.134.1", "p-retry": "4.2.0", @@ -80,7 +82,7 @@ "react-animate-height": "2.0.8", "react-dnd": "9.4.0", "react-dnd-html5-backend": "9.4.0", - "react-dom": "16.9.0", + "react-dom": "16.13.1", "react-hotkeys": "2.0.0", "react-notification-system": "0.2.17", "react-redux": "7.1.3", @@ -88,6 +90,7 @@ "rx-jupyter": "5.5.12", "rxjs": "6.6.3", "styled-components": "4.3.2", + "swr": "0.4.0", "text-encoding": "0.7.0", "underscore": "1.9.1", "url-polyfill": "1.1.7", @@ -101,6 +104,7 @@ "@babel/preset-env": "7.9.0", "@babel/preset-react": "7.9.4", "@babel/preset-typescript": "7.9.0", + "@testing-library/react": "11.2.3", "@types/applicationinsights-js": "1.0.7", "@types/codemirror": "0.0.56", "@types/crossroads": "0.0.30", @@ -109,7 +113,7 @@ "@types/enzyme-adapter-react-16": "1.0.6", "@types/expect-puppeteer": "4.4.3", "@types/hasher": "0.0.31", - "@types/jest": "23.3.10", + "@types/jest": "26.0.20", "@types/jest-environment-puppeteer": "4.3.2", "@types/memoize-one": "4.1.1", "@types/node": "12.11.1", @@ -118,7 +122,7 @@ "@types/puppeteer": "3.0.1", "@types/q": "1.5.1", "@types/react": "16.9.56", - "@types/react-dom": "16.0.7", + "@types/react-dom": "17.0.0", "@types/react-notification-system": "0.2.39", "@types/react-redux": "7.1.7", "@types/sinon": "2.3.3", @@ -128,7 +132,6 @@ "@types/webfontloader": "1.6.29", "@typescript-eslint/eslint-plugin": "4.0.1", "@typescript-eslint/parser": "4.0.1", - "adal-angular": "1.0.15", "axe-puppeteer": "1.1.0", "babel-jest": "24.9.0", "babel-loader": "8.1.0", @@ -143,6 +146,7 @@ "eslint-cli": "1.1.1", "eslint-plugin-no-null": "1.0.2", "eslint-plugin-prefer-arrow": "1.2.2", + "eslint-plugin-react-hooks": "4.2.0", "expose-loader": "0.7.5", "file-loader": "2.0.0", "fs-extra": "7.0.0", diff --git a/src/AuthType.ts b/src/AuthType.ts index 4d5428ede..9cc33f789 100644 --- a/src/AuthType.ts +++ b/src/AuthType.ts @@ -2,5 +2,6 @@ export enum AuthType { AAD = "aad", EncryptedToken = "encryptedtoken", MasterKey = "masterkey", - ResourceToken = "resourcetoken" + ResourceToken = "resourcetoken", + ConnectionString = "connectionstring" } diff --git a/src/Bindings/BindingHandlersRegisterer.ts b/src/Bindings/BindingHandlersRegisterer.ts index f2254757b..55f329bec 100644 --- a/src/Bindings/BindingHandlersRegisterer.ts +++ b/src/Bindings/BindingHandlersRegisterer.ts @@ -1,5 +1,6 @@ import * as ko from "knockout"; import * as ReactBindingHandler from "./ReactBindingHandler"; +import "../Explorer/Tables/DataTable/DataTableBindingManager"; export class BindingHandlersRegisterer { public static registerBindingHandlers() { diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 25b1c2b2d..2c4c49675 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -1,10 +1,3 @@ -import { HashMap } from "./HashMap"; - -export class AuthorizationEndpoints { - public static arm: string = "https://management.core.windows.net/"; - public static common: string = "https://login.windows.net/"; -} - export class CodeOfConductEndpoints { public static privacyStatement: string = "https://aka.ms/ms-privacy-policy"; public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct"; @@ -132,8 +125,8 @@ export class Features { export class Flights { public static readonly SettingsV2 = "settingsv2"; public static readonly MongoIndexEditor = "mongoindexeditor"; - public static readonly AutoscaleTest = "autoscaletest"; public static readonly MongoIndexing = "mongoindexing"; + public static readonly AutoscaleTest = "autoscaletest"; } export class AfecFeatures { @@ -142,19 +135,6 @@ export class AfecFeatures { public static readonly StorageAnalytics = "storageanalytics-public-preview"; } -export class Spark { - public static readonly MaxWorkerCount = 10; - public static readonly SKUs: HashMap = new HashMap({ - "Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM", - "Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM", - "Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM", - "Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM", - "Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM", - "Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM", - "Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM" - }); -} - export class TagNames { public static defaultExperience: string = "defaultExperience"; } diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index f54e8a073..95eaf552d 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -77,7 +77,7 @@ export async function getTokenFromAuthService(verb: string, resourceType: string export function client(): Cosmos.CosmosClient { const options: Cosmos.CosmosClientOptions = { - endpoint: endpoint() || " ", // CosmosClient gets upset if we pass a falsy value here + endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called key: userContext.masterKey, tokenProvider, connectionPolicy: { diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index db244e3f2..21f759969 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -104,7 +104,7 @@ export async function initializeConfiguration(): Promise { const platform = params.get("platform"); switch (platform) { default: - console.log("Invalid platform query parameter given, ignoring"); + console.error(`Invalid platform query parameter: ${platform}`); break; case Platform.Portal: case Platform.Hosted: @@ -113,7 +113,7 @@ export async function initializeConfiguration(): Promise { } } } catch (error) { - console.log("No configuration file found using defaults"); + console.error("No configuration file found using defaults"); } return configContext; } diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 1ee61c489..6c1be4093 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -587,11 +587,3 @@ export interface MemoryUsageInfo { freeKB: number; totalKB: number; } - -export interface resourceTokenConnectionStringProperties { - accountEndpoint: string; - collectionId: string; - databaseId: string; - partitionKey?: string; - resourceToken: string; -} diff --git a/src/Definitions/adal.d.ts b/src/Definitions/adal.d.ts deleted file mode 100644 index ce773ff1c..000000000 --- a/src/Definitions/adal.d.ts +++ /dev/null @@ -1,383 +0,0 @@ -// Type definitions for adal-angular 1.0.1.1 -// Project: https://github.com/AzureAD/azure-activedirectory-library-for-js#readme -// Definitions by: Daniel Perez Alvarez -// Anthony Ciccarello -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -//This is a customized version of adal on top of version 1.0.1 which does not support multi tenant -// Customized version add tenantId to stored tokens so when tenant change, adal will refetch instead of read from sessionStorage - -// In module contexts the class constructor function is the exported object -// export = AuthenticationContext; - -// This class is defined globally in not in a module context -declare class AuthenticationContext { - instance: string; - config: AuthenticationContext.Options; - callback: AuthenticationContext.TokenCallback; - popUp: boolean; - isAngular: boolean; - - /** - * Enum for request type - */ - REQUEST_TYPE: AuthenticationContext.RequestType; - RESPONSE_TYPE: AuthenticationContext.ResponseType; - CONSTANTS: AuthenticationContext.Constants; - - constructor(options: AuthenticationContext.Options); - /** - * Initiates the login process by redirecting the user to Azure AD authorization endpoint. - */ - login(): void; - /** - * Returns whether a login is in progress. - */ - loginInProgress(): boolean; - /** - * Gets token for the specified resource from the cache. - * @param resource A URI that identifies the resource for which the token is requested. - * @param tenantId tenant Id. - */ - getCachedToken(resource: string, tenantId: string): string; - /** - * If user object exists, returns it. Else creates a new user object by decoding `id_token` from the cache. - */ - getCachedUser(): AuthenticationContext.UserInfo; - /** - * Adds the passed callback to the array of callbacks for the specified resource. - * @param resource A URI that identifies the resource for which the token is requested. - * @param expectedState A unique identifier (guid). - * @param callback The callback provided by the caller. It will be called with token or error. - */ - registerCallback( - expectedState: string, - resource: string, - callback: AuthenticationContext.TokenCallback, - tenantId: string - ): void; - /** - * Acquires token from the cache if it is not expired. Otherwise sends request to AAD to obtain a new token. - * @param resource Resource URI identifying the target resource. - * @param callback The callback provided by the caller. It will be called with token or error. - */ - acquireToken(resource: string, tenantId: string, callback: AuthenticationContext.TokenCallback): void; - /** - * Acquires token (interactive flow using a popup window) by sending request to AAD to obtain a new token. - * @param resource Resource URI identifying the target resource. - * @param extraQueryParameters Query parameters to add to the authentication request. - * @param claims Claims to add to the authentication request. - * @param callback The callback provided by the caller. It will be called with token or error. - */ - acquireTokenPopup( - resource: string, - tenantId: string, - extraQueryParameters: string | null | undefined, - claims: string | null | undefined, - callback: AuthenticationContext.TokenCallback - ): void; - /** - * Acquires token (interactive flow using a redirect) by sending request to AAD to obtain a new token. In this case the callback passed in the authentication request constructor will be called. - * @param resource Resource URI identifying the target resource. - * @param extraQueryParameters Query parameters to add to the authentication request. - * @param claims Claims to add to the authentication request. - */ - acquireTokenRedirect( - resource: string, - tenantId: string, - extraQueryParameters?: string | null, - claims?: string | null - ): void; - /** - * Redirects the browser to Azure AD authorization endpoint. - * @param urlNavigate URL of the authorization endpoint. - */ - promptUser(urlNavigate: string): void; - /** - * Clears cache items. - */ - clearCache(): void; - /** - * Clears cache items for a given resource. - * @param resource Resource URI identifying the target resource. - */ - clearCacheForResource(resource: string): void; - /** - * Redirects user to logout endpoint. After logout, it will redirect to `postLogoutRedirectUri` if added as a property on the config object. - */ - logOut(): void; - /** - * Calls the passed in callback with the user object or error message related to the user. - * @param callback The callback provided by the caller. It will be called with user or error. - */ - getUser(callback: AuthenticationContext.UserCallback): void; - /** - * Checks if the URL fragment contains access token, id token or error description. - * @param hash Hash passed from redirect page. - */ - isCallback(hash: string): boolean; - /** - * Gets login error. - */ - getLoginError(): string; - /** - * Creates a request info object from the URL fragment and returns it. - */ - getRequestInfo(hash: string): AuthenticationContext.RequestInfo; - /** - * Saves token or error received in the response from AAD in the cache. In case of `id_token`, it also creates the user object. - */ - saveTokenFromHash(requestInfo: AuthenticationContext.RequestInfo): void; - /** - * Gets resource for given endpoint if mapping is provided with config. - * @param endpoint Resource URI identifying the target resource. - */ - getResourceForEndpoint(resource: string): string; - /** - * This method must be called for processing the response received from AAD. It extracts the hash, processes the token or error, saves it in the cache and calls the callbacks with the result. - * @param hash Hash fragment of URL. Defaults to `window.location.hash`. - */ - handleWindowCallback(hash?: string): void; - - /** - * Checks the logging Level, constructs the log message and logs it. Users need to implement/override this method to turn on logging. - * @param level Level can be set 0, 1, 2 and 3 which turns on 'error', 'warning', 'info' or 'verbose' level logging respectively. - * @param message Message to log. - * @param error Error to log. - */ - log(level: AuthenticationContext.LoggingLevel, message: string, error: any): void; - /** - * Logs messages when logging level is set to 0. - * @param message Message to log. - * @param error Error to log. - */ - error(message: string, error: any): void; - /** - * Logs messages when logging level is set to 1. - * @param message Message to log. - */ - warn(message: string): void; - /** - * Logs messages when logging level is set to 2. - * @param message Message to log. - */ - info(message: string): void; - /** - * Logs messages when logging level is set to 3. - * @param message Message to log. - */ - verbose(message: string): void; - - /** - * Logs Pii messages when Logging Level is set to 0 and window.piiLoggingEnabled is set to true. - * @param message Message to log. - * @param error Error to log. - */ - errorPii(message: string, error: any): void; - - /** - * Logs Pii messages when Logging Level is set to 1 and window.piiLoggingEnabled is set to true. - * @param message Message to log. - */ - warnPii(message: string): void; - - /** - * Logs messages when Logging Level is set to 2 and window.piiLoggingEnabled is set to true. - * @param message Message to log. - */ - infoPii(message: string): void; - - /** - * Logs messages when Logging Level is set to 3 and window.piiLoggingEnabled is set to true. - * @param message Message to log. - */ - verbosePii(message: string): void; -} - -declare namespace AuthenticationContext { - function inject(config: Options): AuthenticationContext; - - type LoggingLevel = 0 | 1 | 2 | 3; - - type RequestType = "LOGIN" | "RENEW_TOKEN" | "UNKNOWN"; - - type ResponseType = "id_token token" | "token"; - - interface RequestInfo { - /** - * Object comprising of fields such as id_token/error, session_state, state, e.t.c. - */ - parameters: any; - /** - * Request type. - */ - requestType: RequestType; - /** - * Whether state is valid. - */ - stateMatch: boolean; - /** - * Unique guid used to match the response with the request. - */ - stateResponse: string; - /** - * Whether `requestType` contains `id_token`, `access_token` or error. - */ - valid: boolean; - } - - interface UserInfo { - /** - * Username assigned from UPN or email. - */ - userName: string; - /** - * Properties parsed from `id_token`. - */ - profile: any; - } - - type TokenCallback = (errorDesc: string | null, token: string | null, error: any) => void; - - type UserCallback = (errorDesc: string | null, user: UserInfo | null) => void; - - /** - * Configuration options for Authentication Context - */ - interface Options { - /** - * Client ID assigned to your app by Azure Active Directory. - */ - clientId: string; - /** - * Endpoint at which you expect to receive tokens.Defaults to `window.location.href`. - */ - redirectUri?: string; - /** - * Azure Active Directory instance. Defaults to `https://login.microsoftonline.com/`. - */ - instance?: string; - /** - * Your target tenant. Defaults to `common`. - */ - tenant?: string; - /** - * Query parameters to add to the authentication request. - */ - extraQueryParameter?: string; - /** - * Unique identifier used to map the request with the response. Defaults to RFC4122 version 4 guid (128 bits). - */ - correlationId?: string; - /** - * User defined function of handling the navigation to Azure AD authorization endpoint in case of login. - */ - displayCall?: (url: string) => void; - /** - * Set this to true to enable login in a popup winodow instead of a full redirect. Defaults to `false`. - */ - popUp?: boolean; - /** - * Set this to the resource to request on login. Defaults to `clientId`. - */ - loginResource?: string; - /** - * Set this to redirect the user to a custom login page. - */ - localLoginUrl?: string; - /** - * Redirects to start page after login. Defaults to `true`. - */ - navigateToLoginRequestUrl?: boolean; - /** - * Set this to redirect the user to a custom logout page. - */ - logOutUri?: string; - /** - * Redirects the user to postLogoutRedirectUri after logout. Defaults to `redirectUri`. - */ - postLogoutRedirectUri?: string; - /** - * Sets browser storage to either 'localStorage' or sessionStorage'. Defaults to `sessionStorage`. - */ - cacheLocation?: "localStorage" | "sessionStorage"; - /** - * Array of keywords or URIs. Adal will attach a token to outgoing requests that have these keywords or URIs. - */ - endpoints?: { [resource: string]: string }; - /** - * Array of keywords or URIs. Adal will not attach a token to outgoing requests that have these keywords or URIs. - */ - anonymousEndpoints?: string[]; - /** - * If the cached token is about to be expired in the expireOffsetSeconds (in seconds), Adal will renew the token instead of using the cached token. Defaults to 300 seconds. - */ - expireOffsetSeconds?: number; - /** - * The number of milliseconds of inactivity before a token renewal response from AAD should be considered timed out. Defaults to 6 seconds. - */ - loadFrameTimeout?: number; - /** - * Callback to be invoked when a token is acquired. - */ - callback?: TokenCallback; - } - - interface LoggingConfig { - level: LoggingLevel; - log: (message: string) => void; - piiLoggingEnabled: boolean; - } - - /** - * Enum for storage constants - */ - interface Constants { - ACCESS_TOKEN: "access_token"; - EXPIRES_IN: "expires_in"; - ID_TOKEN: "id_token"; - ERROR_DESCRIPTION: "error_description"; - SESSION_STATE: "session_state"; - STORAGE: { - TOKEN_KEYS: "adal.token.keys"; - ACCESS_TOKEN_KEY: "adal.access.token.key"; - EXPIRATION_KEY: "adal.expiration.key"; - STATE_LOGIN: "adal.state.login"; - STATE_RENEW: "adal.state.renew"; - NONCE_IDTOKEN: "adal.nonce.idtoken"; - SESSION_STATE: "adal.session.state"; - USERNAME: "adal.username"; - IDTOKEN: "adal.idtoken"; - ERROR: "adal.error"; - ERROR_DESCRIPTION: "adal.error.description"; - LOGIN_REQUEST: "adal.login.request"; - LOGIN_ERROR: "adal.login.error"; - RENEW_STATUS: "adal.token.renew.status"; - }; - RESOURCE_DELIMETER: "|"; - LOADFRAME_TIMEOUT: "6000"; - TOKEN_RENEW_STATUS_CANCELED: "Canceled"; - TOKEN_RENEW_STATUS_COMPLETED: "Completed"; - TOKEN_RENEW_STATUS_IN_PROGRESS: "In Progress"; - LOGGING_LEVEL: { - ERROR: 0; - WARN: 1; - INFO: 2; - VERBOSE: 3; - }; - LEVEL_STRING_MAP: { - 0: "ERROR:"; - 1: "WARNING:"; - 2: "INFO:"; - 3: "VERBOSE:"; - }; - POPUP_WIDTH: 483; - POPUP_HEIGHT: 600; - } -} - -// declare global { -// interface Window { -// Logging: AuthenticationContext.LoggingConfig; -// } -// } diff --git a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx b/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx deleted file mode 100644 index cb512ad85..000000000 --- a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React from "react"; -import { shallow, mount } from "enzyme"; -import { AccountSwitchComponent, AccountSwitchComponentProps } from "./AccountSwitchComponent"; -import { AuthType } from "../../../AuthType"; -import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels"; -import { AccountKind } from "../../../Common/Constants"; - -const createBlankProps = (): AccountSwitchComponentProps => { - return { - authType: null, - displayText: "", - accounts: [], - selectedAccountName: null, - isLoadingAccounts: false, - onAccountChange: jest.fn(), - subscriptions: [], - selectedSubscriptionId: null, - isLoadingSubscriptions: false, - onSubscriptionChange: jest.fn() - }; -}; - -const createBlankAccount = (): DatabaseAccount => { - return { - id: "", - kind: AccountKind.Default, - name: "", - properties: null, - location: "", - tags: null, - type: "" - }; -}; - -const createBlankSubscription = (): Subscription => { - return { - subscriptionId: "", - displayName: "", - authorizationSource: "", - state: "", - subscriptionPolicies: null, - tenantId: "", - uniqueDisplayName: "" - }; -}; - -const createFullProps = (): AccountSwitchComponentProps => { - const props = createBlankProps(); - props.authType = AuthType.AAD; - const account1 = createBlankAccount(); - account1.name = "account1"; - const account2 = createBlankAccount(); - account2.name = "account2"; - const account3 = createBlankAccount(); - account3.name = "superlongaccountnamestringtest"; - props.accounts = [account1, account2, account3]; - props.selectedAccountName = "account2"; - - const sub1 = createBlankSubscription(); - sub1.displayName = "sub1"; - sub1.subscriptionId = "a6062a74-5d53-4b20-9545-000b95f22297"; - const sub2 = createBlankSubscription(); - sub2.displayName = "subsubsubsubsubsubsub2"; - sub2.subscriptionId = "b20b3e93-0185-4326-8a9c-d44bac276b6b"; - props.subscriptions = [sub1, sub2]; - props.selectedSubscriptionId = "a6062a74-5d53-4b20-9545-000b95f22297"; - - return props; -}; - -describe("test render", () => { - it("renders no auth type -> handle error in code", () => { - const props = createBlankProps(); - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - // Encrypted Token - it("renders auth security token, with selected account name", () => { - const props = createBlankProps(); - props.authType = AuthType.EncryptedToken; - props.selectedAccountName = "testaccount"; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - // AAD - it("renders auth aad, with all information", () => { - const props = createFullProps(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders auth aad all dropdown menus", () => { - const props = createFullProps(); - const wrapper = mount(); - - expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(false); - wrapper.find("button.accountSwitchButton").simulate("click"); - expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(true); - - expect(wrapper.exists("div.accountSwitchSubscriptionDropdown")).toBe(true); - wrapper.find("DropdownBase.accountSwitchSubscriptionDropdown").simulate("click"); - // Click will dismiss the first contextual menu in enzyme. Need to dig deeper to achieve below test - - // expect(wrapper.exists("div.accountSwitchSubscriptionDropdownMenu")).toBe(true); - // expect(wrapper.find("button.ms-Dropdown-item").length).toBe(2); - // wrapper.find("div.accountSwitchSubscriptionDropdown").simulate("click"); - // expect(wrapper.exists("div.accountSwitchSubscriptionDropdownMenu")).toBe(false); - - // expect(wrapper.exists("div.accountSwitchAccountDropdown")).toBe(true); - // wrapper.find("div.accountSwitchAccountDropdown").simulate("click"); - // expect(wrapper.exists("div.accountSwitchAccountDropdownMenu")).toBe(true); - // expect(wrapper.find("button.ms-Dropdown-item").length).toBe(3); - // wrapper.find("div.accountSwitchAccountDropdown").simulate("click"); - // expect(wrapper.exists("div.accountSwitchAccountDropdownMenu")).toBe(false); - - // wrapper.find("button.accountSwitchButton").simulate("click"); - // expect(wrapper.exists("div.accountSwitchContextualMenu")).toBe(false); - - wrapper.unmount(); - }); -}); - -// describe("test function", () => { -// it("switch subscription function", () => { -// const props = createFullProps(); -// const wrapper = mount(); - -// wrapper.find("button.accountSwitchButton").simulate("click"); -// wrapper.find("div.accountSwitchSubscriptionDropdown").simulate("click"); -// wrapper -// .find("button.ms-Dropdown-item") -// .at(1) -// .simulate("click"); -// expect(props.onSubscriptionChange).toBeCalled(); -// expect(props.onSubscriptionChange).toHaveBeenCalled(); - -// wrapper.unmount(); -// }); - -// it("switch account", () => { -// const props = createFullProps(); -// const wrapper = mount(); - -// wrapper.find("button.accountSwitchButton").simulate("click"); -// wrapper.find("div.accountSwitchAccountDropdown").simulate("click"); -// wrapper -// .find("button.ms-Dropdown-item") -// .at(0) -// .simulate("click"); -// expect(props.onAccountChange).toBeCalled(); -// expect(props.onAccountChange).toHaveBeenCalled(); - -// wrapper.unmount(); -// }); -// }); diff --git a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx b/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx deleted file mode 100644 index 6527b6efc..000000000 --- a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { AuthType } from "../../../AuthType"; -import { StyleConstants } from "../../../Common/Constants"; -import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels"; - -import * as React from "react"; -import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button"; -import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu"; -import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown"; - -export interface AccountSwitchComponentProps { - authType: AuthType; - selectedAccountName: string; - accounts: DatabaseAccount[]; - isLoadingAccounts: boolean; - onAccountChange: (newAccount: DatabaseAccount) => void; - selectedSubscriptionId: string; - subscriptions: Subscription[]; - isLoadingSubscriptions: boolean; - onSubscriptionChange: (newSubscription: Subscription) => void; - displayText?: string; -} - -export class AccountSwitchComponent extends React.Component { - public render(): JSX.Element { - return this.props.authType === AuthType.AAD ? this._renderSwitchDropDown() : this._renderAccountName(); - } - - private _renderSwitchDropDown(): JSX.Element { - const { displayText, selectedAccountName } = this.props; - - const menuProps: IContextualMenuProps = { - directionalHintFixed: true, - className: "accountSwitchContextualMenu", - items: [ - { - key: "switchSubscription", - onRender: this._renderSubscriptionDropdown.bind(this) - }, - { - key: "switchAccount", - onRender: this._renderAccountDropDown.bind(this) - } - ] - }; - - const buttonStyles: IButtonStyles = { - root: { - fontSize: StyleConstants.DefaultFontSize, - height: 40, - padding: 0, - paddingLeft: 10, - marginRight: 5, - backgroundColor: StyleConstants.BaseDark, - color: StyleConstants.BaseLight - }, - rootHovered: { - backgroundColor: StyleConstants.BaseHigh, - color: StyleConstants.BaseLight - }, - rootFocused: { - backgroundColor: StyleConstants.BaseHigh, - color: StyleConstants.BaseLight - }, - rootPressed: { - backgroundColor: StyleConstants.BaseHigh, - color: StyleConstants.BaseLight - }, - rootExpanded: { - backgroundColor: StyleConstants.BaseHigh, - color: StyleConstants.BaseLight - }, - textContainer: { - flexGrow: "initial" - } - }; - - const buttonProps: IButtonProps = { - text: displayText || selectedAccountName, - menuProps: menuProps, - styles: buttonStyles, - className: "accountSwitchButton", - id: "accountSwitchButton" - }; - - return ; - } - - private _renderSubscriptionDropdown(): JSX.Element { - const { subscriptions, selectedSubscriptionId, isLoadingSubscriptions } = this.props; - const options: IDropdownOption[] = subscriptions.map(sub => { - return { - key: sub.subscriptionId, - text: sub.displayName, - data: sub - }; - }); - - const placeHolderText = isLoadingSubscriptions - ? "Loading subscriptions" - : !options || !options.length - ? "No subscriptions found in current directory" - : "Select subscription from list"; - - const dropdownProps: IDropdownProps = { - label: "Subscription", - className: "accountSwitchSubscriptionDropdown", - options: options, - onChange: this._onSubscriptionDropdownChange, - defaultSelectedKey: selectedSubscriptionId, - placeholder: placeHolderText, - styles: { - callout: "accountSwitchSubscriptionDropdownMenu" - } - }; - - return ; - } - - private _onSubscriptionDropdownChange = (e: React.FormEvent, option: IDropdownOption): void => { - if (!option) { - return; - } - - this.props.onSubscriptionChange(option.data); - }; - - private _renderAccountDropDown(): JSX.Element { - const { accounts, selectedAccountName, isLoadingAccounts } = this.props; - const options: IDropdownOption[] = accounts.map(account => { - return { - key: account.name, - text: account.name, - data: account - }; - }); - // Fabric UI will also try to select the first non-disabled option from dropdown. - // Add a option to prevent pop the message when user click on dropdown on first time. - options.unshift({ - key: "select from list", - text: "Select Cosmos DB account from list", - data: undefined - }); - - const placeHolderText = isLoadingAccounts - ? "Loading Cosmos DB accounts" - : !options || !options.length - ? "No Cosmos DB accounts found" - : "Select Cosmos DB account from list"; - - const dropdownProps: IDropdownProps = { - label: "Cosmos DB Account Name", - className: "accountSwitchAccountDropdown", - options: options, - onChange: this._onAccountDropdownChange, - defaultSelectedKey: selectedAccountName, - placeholder: placeHolderText, - styles: { - callout: "accountSwitchAccountDropdownMenu" - } - }; - - return ; - } - - private _onAccountDropdownChange = (e: React.FormEvent, option: IDropdownOption): void => { - if (!option) { - return; - } - - this.props.onAccountChange(option.data); - }; - - private _renderAccountName(): JSX.Element { - const { displayText, selectedAccountName } = this.props; - return {displayText || selectedAccountName}; - } -} diff --git a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponentAdapter.tsx b/src/Explorer/Controls/AccountSwitch/AccountSwitchComponentAdapter.tsx deleted file mode 100644 index 09acf6af7..000000000 --- a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponentAdapter.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { AccountSwitchComponent, AccountSwitchComponentProps } from "./AccountSwitchComponent"; - -export class AccountSwitchComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - public renderComponent(): JSX.Element { - return ; - } -} diff --git a/src/Explorer/Controls/AccountSwitch/__snapshots__/AccountSwitchComponent.test.tsx.snap b/src/Explorer/Controls/AccountSwitch/__snapshots__/AccountSwitchComponent.test.tsx.snap deleted file mode 100644 index 3a40ab7a3..000000000 --- a/src/Explorer/Controls/AccountSwitch/__snapshots__/AccountSwitchComponent.test.tsx.snap +++ /dev/null @@ -1,71 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`test render renders auth aad, with all information 1`] = ` - -`; - -exports[`test render renders auth security token, with selected account name 1`] = ` - - testaccount - -`; - -exports[`test render renders no auth type -> handle error in code 1`] = ` - -`; diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index 1560ae849..afe771bf4 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -38,7 +38,7 @@ export interface CommandButtonComponentProps { /** * Label for the button */ - commandButtonLabel: string; + commandButtonLabel?: string; /** * True if this button opens a tab or pane, false otherwise. diff --git a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx index a22f7e7ce..3783ef102 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx @@ -5,7 +5,6 @@ import { getSanitizedInputValue, hasDatabaseSharedThroughput, isDirty, - isDirtyTypes, MongoIndexTypes, MongoNotificationType, parseConflictResolutionMode, @@ -71,17 +70,18 @@ describe("SettingsUtils", () => { excludedPaths: [] } as DataModels.IndexingPolicy; - const cases = [ - ["baseline", "current"], - [0, 1], - [true, false], - [undefined, indexingPolicy], - [indexingPolicy, { ...indexingPolicy, automatic: false }] - ]; + it("works on all types", () => { + expect(isDirty("baseline", "baseline")).toEqual(false); + expect(isDirty(0, 0)).toEqual(false); + expect(isDirty(true, true)).toEqual(false); + expect(isDirty(undefined, undefined)).toEqual(false); + expect(isDirty(indexingPolicy, indexingPolicy)).toEqual(false); - test.each(cases)("", (baseline: isDirtyTypes, current: isDirtyTypes) => { - expect(isDirty(baseline, baseline)).toEqual(false); - expect(isDirty(baseline, current)).toEqual(true); + expect(isDirty("baseline", "current")).toEqual(true); + expect(isDirty(0, 1)).toEqual(true); + expect(isDirty(true, false)).toEqual(true); + expect(isDirty(undefined, indexingPolicy)).toEqual(true); + expect(isDirty(indexingPolicy, { ...indexingPolicy, automatic: false })).toEqual(true); }); }); diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 2e6c13bf4..251c3f614 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -1026,7 +1026,6 @@ exports[`SettingsComponent renders 1`] = ` "notificationConsoleData": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { @@ -1072,8 +1071,6 @@ exports[`SettingsComponent renders 1`] = ` "title": [Function], "visible": [Function], }, - "renewToken": [Function], - "renewTokenError": [Function], "resourceTokenCollection": [Function], "resourceTokenCollectionId": [Function], "resourceTokenDatabaseId": [Function], @@ -1166,7 +1163,6 @@ exports[`SettingsComponent renders 1`] = ` "shouldShowContextSwitchPrompt": [Function], "shouldShowDataAccessExpiryDialog": [Function], "shouldShowShareDialogContents": [Function], - "signInAad": [Function], "sparkClusterConnectionInfo": [Function], "splashScreenAdapter": SplashScreenComponentAdapter { "clearMostRecent": [Function], @@ -1233,7 +1229,6 @@ exports[`SettingsComponent renders 1`] = ` "toggleLeftPaneExpandedKeyPress": [Function], "toggleRead": [Function], "toggleReadWrite": [Function], - "tokenForRenewal": [Function], "uploadFilePane": UploadFilePane { "container": [Circular], "extensions": [Function], @@ -2301,7 +2296,6 @@ exports[`SettingsComponent renders 1`] = ` "notificationConsoleData": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { @@ -2347,8 +2341,6 @@ exports[`SettingsComponent renders 1`] = ` "title": [Function], "visible": [Function], }, - "renewToken": [Function], - "renewTokenError": [Function], "resourceTokenCollection": [Function], "resourceTokenCollectionId": [Function], "resourceTokenDatabaseId": [Function], @@ -2441,7 +2433,6 @@ exports[`SettingsComponent renders 1`] = ` "shouldShowContextSwitchPrompt": [Function], "shouldShowDataAccessExpiryDialog": [Function], "shouldShowShareDialogContents": [Function], - "signInAad": [Function], "sparkClusterConnectionInfo": [Function], "splashScreenAdapter": SplashScreenComponentAdapter { "clearMostRecent": [Function], @@ -2508,7 +2499,6 @@ exports[`SettingsComponent renders 1`] = ` "toggleLeftPaneExpandedKeyPress": [Function], "toggleRead": [Function], "toggleReadWrite": [Function], - "tokenForRenewal": [Function], "uploadFilePane": UploadFilePane { "container": [Circular], "extensions": [Function], @@ -3589,7 +3579,6 @@ exports[`SettingsComponent renders 1`] = ` "notificationConsoleData": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { @@ -3635,8 +3624,6 @@ exports[`SettingsComponent renders 1`] = ` "title": [Function], "visible": [Function], }, - "renewToken": [Function], - "renewTokenError": [Function], "resourceTokenCollection": [Function], "resourceTokenCollectionId": [Function], "resourceTokenDatabaseId": [Function], @@ -3729,7 +3716,6 @@ exports[`SettingsComponent renders 1`] = ` "shouldShowContextSwitchPrompt": [Function], "shouldShowDataAccessExpiryDialog": [Function], "shouldShowShareDialogContents": [Function], - "signInAad": [Function], "sparkClusterConnectionInfo": [Function], "splashScreenAdapter": SplashScreenComponentAdapter { "clearMostRecent": [Function], @@ -3796,7 +3782,6 @@ exports[`SettingsComponent renders 1`] = ` "toggleLeftPaneExpandedKeyPress": [Function], "toggleRead": [Function], "toggleReadWrite": [Function], - "tokenForRenewal": [Function], "uploadFilePane": UploadFilePane { "container": [Circular], "extensions": [Function], @@ -4864,7 +4849,6 @@ exports[`SettingsComponent renders 1`] = ` "notificationConsoleData": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], - "onSwitchToConnectionString": [Function], "onToggleKeyDown": [Function], "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { @@ -4910,8 +4894,6 @@ exports[`SettingsComponent renders 1`] = ` "title": [Function], "visible": [Function], }, - "renewToken": [Function], - "renewTokenError": [Function], "resourceTokenCollection": [Function], "resourceTokenCollectionId": [Function], "resourceTokenDatabaseId": [Function], @@ -5004,7 +4986,6 @@ exports[`SettingsComponent renders 1`] = ` "shouldShowContextSwitchPrompt": [Function], "shouldShowDataAccessExpiryDialog": [Function], "shouldShowShareDialogContents": [Function], - "signInAad": [Function], "sparkClusterConnectionInfo": [Function], "splashScreenAdapter": SplashScreenComponentAdapter { "clearMostRecent": [Function], @@ -5071,7 +5052,6 @@ exports[`SettingsComponent renders 1`] = ` "toggleLeftPaneExpandedKeyPress": [Function], "toggleRead": [Function], "toggleReadWrite": [Function], - "tokenForRenewal": [Function], "uploadFilePane": UploadFilePane { "container": [Circular], "extensions": [Function], diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx index b455a59fa..bf04650ce 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx @@ -1,6 +1,6 @@ -import React from "react"; import { shallow } from "enzyme"; -import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent"; +import React from "react"; +import { Descriptor, SmartUiComponent } from "./SmartUiComponent"; describe("SmartUiComponent", () => { const exampleData: Descriptor = { @@ -77,9 +77,7 @@ describe("SmartUiComponent", () => { } }; - const exampleCallbacks = (newValues: Map): void => { - console.log("New values:", newValues); - }; + const exampleCallbacks = (): void => undefined; it("should render", () => { const wrapper = shallow(); diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index c424a8968..a3965fe3b 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -214,8 +214,6 @@ export default class Explorer { public shouldShowShareDialogContents: ko.Observable; public shareAccessData: ko.Observable; public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise; - public renewTokenError: ko.Observable; - public tokenForRenewal: ko.Observable; public shareAccessToggleState: ko.Observable; public shareAccessUrl: ko.Observable; public shareUrlCopyHelperText: ko.Observable; @@ -314,7 +312,7 @@ export default class Explorer { this.isSynapseLinkUpdating = ko.observable(false); this.isAccountReady.subscribe(async (isAccountReady: boolean) => { if (isAccountReady) { - this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); + this.isAuthWithResourceToken() ? await this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); RouteHandler.getInstance().initHandler(); this.notebookWorkspaceManager = new NotebookWorkspaceManager(); this.arcadiaWorkspaces = ko.observableArray(); @@ -381,8 +379,6 @@ export default class Explorer { readWriteUrl: undefined, readUrl: undefined }); - this.tokenForRenewal = ko.observable(""); - this.renewTokenError = ko.observable(""); this.shareAccessUrl = ko.observable(); this.shareUrlCopyHelperText = ko.observable("Click to copy"); this.shareTokenCopyHelperText = ko.observable("Click to copy"); @@ -414,7 +410,6 @@ export default class Explorer { this.isSchemaEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableSchema)); this.isNotificationConsoleExpanded = ko.observable(false); - this.isAutoscaleDefaultEnabled = ko.observable(false); this.databases = ko.observableArray(); @@ -1095,25 +1090,6 @@ export default class Explorer { return true; } - public renewToken = (): void => { - TelemetryProcessor.trace(Action.ConnectEncryptionToken); - this.renewTokenError(""); - const id: string = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Initiating connection to account" - ); - this.renewExplorerShareAccess(this, this.tokenForRenewal()) - .fail((error: any) => { - const stringifiedError: string = getErrorMessage(error); - this.renewTokenError("Invalid connection string specified"); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to initiate connection to account: ${stringifiedError}` - ); - }) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); - }; - public generateSharedAccessData(): void { const id: string = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Generating share url"); AuthHeadersUtil.generateEncryptedToken().then( @@ -1265,16 +1241,6 @@ export default class Explorer { $("#contextSwitchPrompt").dialog("open"); } - public displayConnectExplorerForm(): void { - $("#divExplorer").hide(); - $("#connectExplorer").css("display", "flex"); - } - - public hideConnectExplorerForm(): void { - $("#connectExplorer").hide(); - $("#divExplorer").show(); - } - public isReadWriteToggled: () => boolean = (): boolean => { return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite; }; @@ -1364,21 +1330,17 @@ export default class Explorer { } } - public refreshDatabaseForResourceToken(): Q.Promise { + public async refreshDatabaseForResourceToken(): Promise { const databaseId = this.resourceTokenDatabaseId(); const collectionId = this.resourceTokenCollectionId(); if (!databaseId || !collectionId) { - return Q.reject(); + throw new Error("No collection ID or database ID for resource token"); } - const deferred: Q.Deferred = Q.defer(); - readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { + return readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection)); this.selectedNode(this.resourceTokenCollection()); - deferred.resolve(); }); - - return deferred.promise; } public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise { @@ -1848,7 +1810,7 @@ export default class Explorer { if (inputs != null) { // In development mode, save the iframe message from the portal in session storage. // This allows webpack hot reload to funciton properly - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV === "development" && configContext.platform === Platform.Portal) { sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); } @@ -1883,16 +1845,6 @@ export default class Explorer { subscriptionType: inputs.subscriptionType, quotaId: inputs.quotaId }); - TelemetryProcessor.traceSuccess( - Action.LoadDatabaseAccount, - { - resourceId: this.databaseAccount && this.databaseAccount().id, - dataExplorerArea: Constants.Areas.ResourceTree, - databaseAccount: this.databaseAccount && this.databaseAccount() - }, - inputs.loadDatabaseAccountTimestamp - ); - this.isAccountReady(true); } } @@ -1974,18 +1926,6 @@ export default class Explorer { this.commandBarComponentAdapter.onUpdateTabsButtons(buttons); } - public signInAad = () => { - TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" }); - sendMessage({ - type: MessageTypes.AadSignIn - }); - }; - - public onSwitchToConnectionString = () => { - $("#connectWithAad").hide(); - $("#connectWithConnectionString").show(); - }; - public clickHostedAccountSwitch = () => { sendMessage({ type: MessageTypes.UpdateAccountSwitch, diff --git a/src/Explorer/Menus/NavBar/MeControlComponent.test.tsx b/src/Explorer/Menus/NavBar/MeControlComponent.test.tsx deleted file mode 100644 index 614cf1011..000000000 --- a/src/Explorer/Menus/NavBar/MeControlComponent.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from "react"; -import { shallow, mount } from "enzyme"; -import { MeControlComponent, MeControlComponentProps } from "./MeControlComponent"; - -const createNotSignedInProps = (): MeControlComponentProps => { - return { - isUserSignedIn: false, - user: null, - onSignInClick: jest.fn(), - onSignOutClick: jest.fn(), - onSwitchDirectoryClick: jest.fn() - }; -}; - -const createSignedInProps = (): MeControlComponentProps => { - return { - isUserSignedIn: true, - user: { - name: "Test User", - email: "testuser@contoso.com", - tenantName: "Contoso", - imageUrl: "../../../../images/dotnet.png" - }, - onSignInClick: jest.fn(), - onSignOutClick: jest.fn(), - onSwitchDirectoryClick: jest.fn() - }; -}; - -describe("test render", () => { - it("renders not signed in", () => { - const props = createNotSignedInProps(); - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders signed in with full info", () => { - const props = createSignedInProps(); - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("change not signed in to signed in", () => { - const notSignInProps = createNotSignedInProps(); - - const wrapper = mount(); - expect(wrapper.exists(".mecontrolSigninButton")).toBe(true); - expect(wrapper.exists(".mecontrolHeaderButton")).toBe(false); - - const signInProps = createSignedInProps(); - - wrapper.setProps(signInProps); - expect(wrapper.exists(".mecontrolSigninButton")).toBe(false); - expect(wrapper.exists(".mecontrolHeaderButton")).toBe(true); - - wrapper.unmount; - }); - - it("render contextual menu", () => { - const signInProps = createSignedInProps(); - const wrapper = mount(); - - wrapper.find("button.mecontrolHeaderButton").simulate("click"); - expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true); - - wrapper.find("button.mecontrolHeaderButton").simulate("click"); - expect(wrapper.exists(".mecontrolContextualMenu")).toBe(false); - - wrapper.unmount; - }); -}); - -describe("test function got called", () => { - it("sign in click", () => { - const notSignInProps = createNotSignedInProps(); - const wrapper = mount(); - - wrapper.find("button.mecontrolSigninButton").simulate("click"); - expect(notSignInProps.onSignInClick).toBeCalled(); - expect(notSignInProps.onSignInClick).toHaveBeenCalled(); - }); - - it("sign out click", () => { - const signInProps = createSignedInProps(); - const wrapper = mount(); - - wrapper.find("button.mecontrolHeaderButton").simulate("click"); - expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true); - - wrapper.find("div.signOutLink").simulate("click"); - expect(signInProps.onSignOutClick).toBeCalled(); - expect(signInProps.onSignOutClick).toHaveBeenCalled(); - }); - - it("switch directory", () => { - const signInProps = createSignedInProps(); - const wrapper = mount(); - - wrapper.find("button.mecontrolHeaderButton").simulate("click"); - expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true); - - wrapper.find("div.switchDirectoryLink").simulate("click"); - expect(signInProps.onSwitchDirectoryClick).toBeCalled(); - expect(signInProps.onSwitchDirectoryClick).toHaveBeenCalled(); - }); -}); diff --git a/src/Explorer/Menus/NavBar/MeControlComponent.tsx b/src/Explorer/Menus/NavBar/MeControlComponent.tsx deleted file mode 100644 index 142ed3917..000000000 --- a/src/Explorer/Menus/NavBar/MeControlComponent.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import * as React from "react"; -import { DefaultButton, BaseButton, IButtonProps } from "office-ui-fabric-react/lib/Button"; -import { DirectionalHint, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu"; -import { FocusZone } from "office-ui-fabric-react/lib/FocusZone"; -import { IPersonaSharedProps, Persona, PersonaInitialsColor, PersonaSize } from "office-ui-fabric-react/lib/Persona"; - -export interface MeControlComponentProps { - /** - * Wheather user is signed in or not - */ - isUserSignedIn: boolean; - /** - * User info - */ - user: MeControlUser; - /** - * Click handler for sign in click - */ - onSignInClick: (e: React.MouseEvent) => void; - /** - * Click handler for sign out click - */ - onSignOutClick: (e: React.SyntheticEvent) => void; - /** - * Click handler for switch directory click - */ - onSwitchDirectoryClick: (e: React.SyntheticEvent) => void; -} - -export interface MeControlUser { - /** - * Display name for user - */ - name: string; - /** - * Display email for user - */ - email: string; - /** - * Display tenant for user - */ - tenantName: string; - /** - * image source for the profic photo - */ - imageUrl: string; -} - -export class MeControlComponent extends React.Component { - public render(): JSX.Element { - return this.props.isUserSignedIn ? this._renderProfileComponent() : this._renderSignInComponent(); - } - - private _renderProfileComponent(): JSX.Element { - const { user } = this.props; - - const menuProps: IContextualMenuProps = { - className: "mecontrolContextualMenu", - isBeakVisible: false, - directionalHintFixed: true, - directionalHint: DirectionalHint.bottomRightEdge, - calloutProps: { - minPagePadding: 0 - }, - items: [ - { - key: "Persona", - onRender: this._renderPersonaComponent - }, - { - key: "SwitchDirectory", - onRender: this._renderSwitchDirectory - }, - { - key: "SignOut", - onRender: this._renderSignOut - } - ] - }; - - const personaProps: IPersonaSharedProps = { - imageUrl: user.imageUrl, - text: user.email, - secondaryText: user.tenantName, - showSecondaryText: true, - showInitialsUntilImageLoads: true, - initialsColor: PersonaInitialsColor.teal, - size: PersonaSize.size28, - className: "mecontrolHeaderPersona" - }; - - const buttonProps: IButtonProps = { - id: "mecontrolHeader", - className: "mecontrolHeaderButton", - menuProps: menuProps, - onRenderMenuIcon: () => , - styles: { - rootHovered: { backgroundColor: "#393939" }, - rootFocused: { backgroundColor: "#393939" }, - rootPressed: { backgroundColor: "#393939" }, - rootExpanded: { backgroundColor: "#393939" } - } - }; - - return ( - - - - - - ); - } - - private _renderPersonaComponent = (): JSX.Element => { - const { user } = this.props; - const personaProps: IPersonaSharedProps = { - imageUrl: user.imageUrl, - text: user.name, - secondaryText: user.email, - showSecondaryText: true, - showInitialsUntilImageLoads: true, - initialsColor: PersonaInitialsColor.teal, - size: PersonaSize.size72, - className: "mecontrolContextualMenuPersona" - }; - - return ; - }; - - private _renderSwitchDirectory = (): JSX.Element => { - return ( -
| React.KeyboardEvent) => - this.props.onSwitchDirectoryClick(e) - } - > - Switch Directory -
- ); - }; - - private _renderSignOut = (): JSX.Element => { - return ( -
| React.KeyboardEvent) => this.props.onSignOutClick(e)} - > - Sign out -
- ); - }; - - private _renderSignInComponent = (): JSX.Element => { - const buttonProps: IButtonProps = { - className: "mecontrolSigninButton", - text: "Sign In", - onClick: (e: React.MouseEvent) => this.props.onSignInClick(e), - styles: { - rootHovered: { backgroundColor: "#393939", color: "#fff" }, - rootFocused: { backgroundColor: "#393939", color: "#fff" }, - rootPressed: { backgroundColor: "#393939", color: "#fff" } - } - }; - return ; - }; -} diff --git a/src/Explorer/Menus/NavBar/MeControlComponentAdapter.tsx b/src/Explorer/Menus/NavBar/MeControlComponentAdapter.tsx deleted file mode 100644 index fca1baffd..000000000 --- a/src/Explorer/Menus/NavBar/MeControlComponentAdapter.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This adapter is responsible to render the React component - * If the component signals a change through the callback passed in the properties, it must render the React component when appropriate - * and update any knockout observables passed from the parent. - */ -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { MeControlComponent, MeControlComponentProps } from "./MeControlComponent"; - -export class MeControlComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - public renderComponent(): JSX.Element { - return ; - } -} diff --git a/src/Explorer/Menus/NavBar/__snapshots__/MeControlComponent.test.tsx.snap b/src/Explorer/Menus/NavBar/__snapshots__/MeControlComponent.test.tsx.snap deleted file mode 100644 index b30cb2c68..000000000 --- a/src/Explorer/Menus/NavBar/__snapshots__/MeControlComponent.test.tsx.snap +++ /dev/null @@ -1,91 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`test render renders not signed in 1`] = ` - -`; - -exports[`test render renders signed in with full info 1`] = ` - - - - - -`; diff --git a/src/Explorer/Panes/RenewAdHocAccessPane.ts b/src/Explorer/Panes/RenewAdHocAccessPane.ts index f9476c341..d4ef767df 100644 --- a/src/Explorer/Panes/RenewAdHocAccessPane.ts +++ b/src/Explorer/Panes/RenewAdHocAccessPane.ts @@ -2,7 +2,7 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { ConnectionStringParser } from "../../Platform/Hosted/Helpers/ConnectionStringParser"; +import { parseConnectionString } from "../../Platform/Hosted/Helpers/ConnectionStringParser"; import { ContextualPaneBase } from "./ContextualPaneBase"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; @@ -48,9 +48,7 @@ export class RenewAdHocAccessPane extends ContextualPaneBase { }; private _shouldShowContextSwitchPrompt(): boolean { - const inputMetadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( - this.accessKey() - ); + const inputMetadata: DataModels.AccessInputMetadata = parseConnectionString(this.accessKey()); const apiKind: DataModels.ApiKind = this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()); const hasOpenedTabs: boolean = diff --git a/src/GitHub/GitHubOAuthService.test.ts b/src/GitHub/GitHubOAuthService.test.ts index 6c41891c5..0229465db 100644 --- a/src/GitHub/GitHubOAuthService.test.ts +++ b/src/GitHub/GitHubOAuthService.test.ts @@ -35,7 +35,7 @@ describe("GitHubOAuthService", () => { window.dataExplorer = { ...originalDataExplorer, logConsoleData: (data): void => - data.type === ConsoleDataType.Error ? console.error(data.message) : console.log(data.message) + data.type === ConsoleDataType.Error ? console.error(data.message) : console.error(data.message) } as Explorer; window.dataExplorer.notebookManager = new NotebookManager(); window.dataExplorer.notebookManager.junoClient = junoClient; diff --git a/src/HostedExplorer.ts b/src/HostedExplorer.ts deleted file mode 100644 index 30e97cf74..000000000 --- a/src/HostedExplorer.ts +++ /dev/null @@ -1,1174 +0,0 @@ -import "./Shared/appInsights"; -import * as _ from "underscore"; -import * as ko from "knockout"; -import hasher from "hasher"; -import { AccountSwitchComponentProps } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponent"; -import { AccountSwitchComponentAdapter } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponentAdapter"; -import { Action } from "./Shared/Telemetry/TelemetryConstants"; -import { ArmResourceUtils } from "./Platform/Hosted/ArmResourceUtils"; -import AuthHeadersUtil from "./Platform/Hosted/Authorization"; -import { AuthType } from "./AuthType"; -import { getArcadiaAuthToken } from "./Utils/AuthorizationUtils"; -import { ActionType, PaneKind } from "./Contracts/ActionContracts"; -import * as Constants from "./Common/Constants"; -import { ControlBarComponentAdapter } from "./Explorer/Menus/NavBar/ControlBarComponentAdapter"; -import { ConsoleDataType } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import { DatabaseAccount, Subscription, AccountKeys, Tenant } from "./Contracts/DataModels"; -import { - DefaultDirectoryDropdownComponent, - DefaultDirectoryDropdownProps -} from "./Explorer/Controls/Directory/DefaultDirectoryDropdownComponent"; -import { DialogComponentAdapter } from "./Explorer/Controls/DialogReactComponent/DialogComponentAdapter"; -import { DialogProps } from "./Explorer/Controls/DialogReactComponent/DialogComponent"; -import { DirectoryListProps } from "./Explorer/Controls/Directory/DirectoryListComponent"; -import { getErrorMessage } from "./Common/ErrorHandlingUtils"; -import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; -import { LocalStorageUtility, StorageKey, SessionStorageUtility } from "./Shared/StorageUtility"; -import * as Logger from "./Common/Logger"; -import { MeControlComponentProps } from "./Explorer/Menus/NavBar/MeControlComponent"; -import { MeControlComponentAdapter } from "./Explorer/Menus/NavBar/MeControlComponentAdapter"; -import { MessageTypes } from "./Contracts/ExplorerContracts"; -import * as ReactBindingHandler from "./Bindings/ReactBindingHandler"; -import { SwitchDirectoryPane, SwitchDirectoryPaneComponent } from "./Explorer/Panes/SwitchDirectoryPane"; -import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor"; -import { isInvalidParentFrameOrigin } from "./Utils/MessageValidation"; -import "../less/hostedexplorer.less"; -import "./Explorer/Menus/NavBar/MeControlComponent.less"; -import ConnectIcon from "../images/HostedConnectwhite.svg"; -import SettingsIcon from "../images/HostedSettings.svg"; -import FeedbackIcon from "../images/Feedback.svg"; -import SwitchDirectoryIcon from "../images/DirectorySwitch.svg"; -import { CommandButtonComponentProps } from "./Explorer/Controls/CommandButton/CommandButtonComponent"; - -ReactBindingHandler.Registerer.register(); -ko.components.register("switch-directory-pane", new SwitchDirectoryPaneComponent()); - -class HostedExplorer { - public navigationSelection: ko.Observable; - public isAccountActive: ko.Computed; - public controlBarComponentAdapter: ControlBarComponentAdapter; - public firewallWarningComponentAdapter: DialogComponentAdapter; - public dialogComponentAdapter: DialogComponentAdapter; - public meControlComponentAdapter: MeControlComponentAdapter; - public accountSwitchComponentAdapter: AccountSwitchComponentAdapter; - public switchDirectoryPane: SwitchDirectoryPane; - - private _firewallWarningDialogProps: ko.Observable; - private _dialogProps: ko.Observable; - private _meControlProps: ko.Observable; - private _accountSwitchProps: ko.Observable; - private _controlbarCommands: ko.ObservableArray; - private _directoryDropdownProps: ko.Observable; - private _directoryListProps: ko.Observable; - - constructor() { - this.navigationSelection = ko.observable("explorer"); - const updateExplorerHash = (newHash: string, oldHash: string) => this._updateExplorerWithHash(newHash); - // This pull icons from CDN, if we support standalone hosted in National Cloud, we need to change this - initializeIcons(/* optional base url */); - - this._controlbarCommands = ko.observableArray([ - { - id: "commandbutton-connect", - iconSrc: ConnectIcon, - iconAlt: "connect button", - onCommandClick: () => this.openConnectPane(), - commandButtonLabel: undefined, - ariaLabel: "connect button", - tooltipText: "Connect to a Cosmos DB account", - hasPopup: true, - disabled: false - }, - { - id: "commandbutton-settings", - iconSrc: SettingsIcon, - iconAlt: "setting button", - onCommandClick: () => this.openSettingsPane(), - commandButtonLabel: undefined, - ariaLabel: "setting button", - tooltipText: "Global settings", - hasPopup: true, - disabled: false - }, - { - id: "commandbutton-feedback", - iconSrc: FeedbackIcon, - iconAlt: "feeback button", - onCommandClick: () => - window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback"), - commandButtonLabel: undefined, - ariaLabel: "feeback button", - tooltipText: "Send feedback", - hasPopup: true, - disabled: false - } - ]); - this.controlBarComponentAdapter = new ControlBarComponentAdapter(this._controlbarCommands); - - this._directoryDropdownProps = ko.observable({ - defaultDirectoryId: undefined, - directories: [], - onDefaultDirectoryChange: this._onDefaultDirectoryChange - }); - - this._directoryListProps = ko.observable({ - directories: [], - selectedDirectoryId: undefined, - onNewDirectorySelected: this._onNewDirectorySelected - }); - - this.switchDirectoryPane = new SwitchDirectoryPane(this._directoryDropdownProps, this._directoryListProps); - - this._firewallWarningDialogProps = ko.observable({ - isModal: true, - visible: false, - title: "Data Explorer Access", - subText: - 'The way Data Explorer accesses your databases and containers has changed and you need to update your Firewall settings to add your current IP address to the firewall rules. Please open Firewall blade in Azure portal, click "Add my IP address" and click ‘Save’.', - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._closeFirewallWarningDialog, - onSecondaryButtonClick: this._closeFirewallWarningDialog - }); - this.firewallWarningComponentAdapter = new DialogComponentAdapter(); - this.firewallWarningComponentAdapter.parameters = this._firewallWarningDialogProps; - - this._dialogProps = ko.observable({ - isModal: false, - visible: false, - title: undefined, - subText: undefined, - primaryButtonText: undefined, - secondaryButtonText: undefined, - onPrimaryButtonClick: undefined, - onSecondaryButtonClick: undefined - }); - this.dialogComponentAdapter = new DialogComponentAdapter(); - this.dialogComponentAdapter.parameters = this._dialogProps; - - this._meControlProps = ko.observable({ - isUserSignedIn: false, - user: { - name: undefined, - email: undefined, - tenantName: undefined, - imageUrl: undefined - }, - onSignInClick: this._onSignInClick, - onSignOutClick: this._onSignOutClick, - onSwitchDirectoryClick: this._onSwitchDirectoryClick - }); - this.meControlComponentAdapter = new MeControlComponentAdapter(); - this.meControlComponentAdapter.parameters = this._meControlProps; - - this._accountSwitchProps = ko.observable({ - authType: AuthType.EncryptedToken, - selectedAccountName: "", - accounts: [], - isLoadingAccounts: false, - onAccountChange: this._onAccountChange, - selectedSubscriptionId: undefined, - subscriptions: [], - isLoadingSubscriptions: false, - onSubscriptionChange: this._onSubscriptionChange - }); - this.accountSwitchComponentAdapter = new AccountSwitchComponentAdapter(); - this.accountSwitchComponentAdapter.parameters = this._accountSwitchProps; - - this.isAccountActive = ko.computed(() => { - if ( - this._accountSwitchProps() && - (this._accountSwitchProps().displayText || this._accountSwitchProps().selectedAccountName) - ) { - return true; - } - return false; - }); - - hasher.initialized.add(updateExplorerHash); - hasher.changed.add(updateExplorerHash); - hasher.init(); - window.addEventListener("message", this._handleMessage.bind(this), false); - this._handleAadLogin(); - } - - public explorer_click() { - this.navigationSelection("explorer"); - } - - public openSettingsPane(): boolean { - this._sendMessageToExplorerFrame({ - openAction: { - actionType: ActionType.OpenPane, - paneKind: PaneKind.GlobalSettings - } - }); - - return false; - } - - public openConnectPane(): boolean { - this._sendMessageToExplorerFrame({ - openAction: { - actionType: ActionType.OpenPane, - paneKind: PaneKind.AdHocAccess - } - }); - - return false; - } - - public openDirectoryPane(): void { - this.switchDirectoryPane.open(); - } - - public openAzurePortal(src: any, event: MouseEvent): boolean { - // TODO: Get environment specific azure portal url from a config file - window.open("https://portal.azure.com", "_blank"); - return false; - } - - public onOpenAzurePortalKeyPress(src: any, event: KeyboardEvent): boolean { - if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { - this.openAzurePortal(src, undefined); - return false; - } - - return true; - } - - private _handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") { - return; - } - if (typeof event.data !== "object" || !("data" in event.data)) { - return; - } - - const message: any = event.data.data; - if (message === "ready") { - this._updateExplorerWithHash(decodeURIComponent(hasher.getHash())); - } else if (message && message.type) { - this._handleMessageTypes(message); - } - } - - private _handleMessageTypes(message: any) { - switch (message.type) { - case MessageTypes.AadSignIn: - AuthHeadersUtil.signIn(); - break; - case MessageTypes.UpdateLocationHash: - if (message.locationHash) { - hasher.replaceHash(message.locationHash); - } - break; - case MessageTypes.UpdateAccountSwitch: - if (message.props) { - this._updateAccountSwitchProps(message.props); - } - if (message.click) { - this._clickAccountSwitchControl(); - } - break; - case MessageTypes.UpdateDirectoryControl: - if (message.click) { - this._clickDirectoryControl(); - } - break; - case MessageTypes.GetAccessAadRequest: - this._handleGetAccessAadRequest(); - break; - case MessageTypes.ExplorerClickEvent: - this._simulateClick(); - break; - case MessageTypes.ForbiddenError: - this._displayFirewallWarningDialog(); - break; - case MessageTypes.GetArcadiaToken: - this._getArcadiaToken(message); - } - } - - private _updateDirectoryProps( - dropdownProps?: Partial, - listProps?: Partial - ) { - if (dropdownProps) { - const propsToUpdate = this._directoryDropdownProps(); - if (dropdownProps.defaultDirectoryId) { - propsToUpdate.defaultDirectoryId = dropdownProps.defaultDirectoryId; - } - if (dropdownProps.directories) { - propsToUpdate.directories = dropdownProps.directories; - } - this._directoryDropdownProps(propsToUpdate); - } - if (listProps) { - const propsToUpdate = this._directoryListProps(); - if (listProps.selectedDirectoryId) { - propsToUpdate.selectedDirectoryId = listProps.selectedDirectoryId; - } - if (listProps.directories) { - propsToUpdate.directories = listProps.directories; - } - this._directoryListProps(propsToUpdate); - } - } - - private _updateMeControlProps(props: Partial) { - if (!props) { - return; - } - - const propsToUpdate = this._meControlProps(); - if (props.isUserSignedIn != null) { - propsToUpdate.isUserSignedIn = props.isUserSignedIn; - } - - if (props.user) { - if (props.user.name != null) { - propsToUpdate.user.name = props.user.name; - } - if (props.user.email != null) { - propsToUpdate.user.email = props.user.email; - } - if (props.user.imageUrl != null) { - propsToUpdate.user.imageUrl = props.user.imageUrl; - } - if (props.user.tenantName != null) { - propsToUpdate.user.tenantName = props.user.tenantName; - } - } - - this._meControlProps(propsToUpdate); - } - - private _updateAccountSwitchProps(props: Partial) { - if (!props) { - return; - } - - const propsToUpdate = this._accountSwitchProps(); - if (props.authType) { - if (props.selectedAccountName != null) { - propsToUpdate.selectedAccountName = props.selectedAccountName; - } - if (props.authType === AuthType.EncryptedToken) { - propsToUpdate.authType = AuthType.EncryptedToken; - } else if (props.authType === AuthType.AAD) { - propsToUpdate.authType = AuthType.AAD; - if (props.displayText != null) { - propsToUpdate.displayText = props.displayText; - } - if (props.isLoadingAccounts != null) { - propsToUpdate.isLoadingAccounts = props.isLoadingAccounts; - } - if (props.accounts) { - propsToUpdate.accounts = props.accounts.sort((a, b) => (a.name < b.name ? -1 : 1)); - } - - if (props.isLoadingSubscriptions != null) { - propsToUpdate.isLoadingSubscriptions = props.isLoadingSubscriptions; - } - if (props.subscriptions) { - propsToUpdate.subscriptions = props.subscriptions.sort((a, b) => (a.displayName < b.displayName ? -1 : 1)); - } - if (props.selectedSubscriptionId != null) { - propsToUpdate.selectedSubscriptionId = props.selectedSubscriptionId; - } - } - } - - this._accountSwitchProps(propsToUpdate); - } - - private _onAccountChange = (newAccount: DatabaseAccount) => { - if (!newAccount) { - return; - } - this._openSwitchAccountModalDialog(newAccount); - TelemetryProcessor.traceStart(Action.AccountSwitch); - }; - - private _onSubscriptionChange = (newSubscription: Subscription) => { - if (!newSubscription) { - return; - } - this._switchSubscription(newSubscription); - TelemetryProcessor.trace(Action.SubscriptionSwitch); - }; - - private _openSwitchAccountModalDialog = (newAccount: DatabaseAccount) => { - const switchAccountDialogProps: DialogProps = { - isModal: true, - visible: true, - title: `Switch account to ${newAccount.name}`, - subText: - "Please save your work before you switch! When you switch to a different Azure Cosmos DB account, current Data Explorer tabs will be closed. Proceed anyway?", - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: () => this._onSwitchDialogOkClicked(newAccount), - onSecondaryButtonClick: this._onSwitchDialogCancelClicked - }; - this._dialogProps(switchAccountDialogProps); - }; - - private _onSwitchDialogCancelClicked = () => { - this._closeModalDialog(); - TelemetryProcessor.traceFailure(Action.AccountSwitch); - }; - - private _onSwitchDialogOkClicked = (newAccount: DatabaseAccount) => { - this._closeModalDialog(); - this._switchAccount(newAccount).then(accountResponse => { - this._sendMessageToExplorerFrame({ - type: MessageTypes.SwitchAccount, - account: accountResponse[0], - keys: accountResponse[1], - authorizationToken: accountResponse[2] - }); - }); - TelemetryProcessor.traceSuccess(Action.AccountSwitch); - }; - - private _closeModalDialog = () => { - this._dialogProps().visible = false; - this._dialogProps.valueHasMutated(); - }; - - private _closeFirewallWarningDialog = () => { - this._firewallWarningDialogProps().visible = false; - this._firewallWarningDialogProps.valueHasMutated(); - }; - - private _displayFirewallWarningDialog = () => { - this._firewallWarningDialogProps().visible = true; - this._firewallWarningDialogProps.valueHasMutated(); - }; - - private _updateExplorerWithHash(newHash: string): void { - this._sendMessageToExplorerFrame({ - type: MessageTypes.UpdateLocationHash, - locationHash: newHash - }); - } - - private _sendMessageToExplorerFrame(data: any): void { - const explorerFrame = document.getElementById("explorerMenu") as HTMLIFrameElement; - explorerFrame && - explorerFrame.contentDocument && - explorerFrame.contentDocument.referrer && - explorerFrame.contentWindow.postMessage( - { - signature: "pcIframe", - data: data - }, - explorerFrame.contentDocument.referrer || window.location.href - ); - } - - private _onSignInClick = () => { - if (SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken)) { - SessionStorageUtility.removeEntry(StorageKey.EncryptedKeyToken); - } - const windowUrl = window.location.href; - const params = new URLSearchParams(window.parent.location.search); - if (!!params && params.has("key")) { - const keyIndex = windowUrl.indexOf("key"); - const keyLength = encodeURIComponent(params.get("key")).length; - const metaDataLength = "key=".length; - const cleanUrl = windowUrl.slice(0, keyIndex) + windowUrl.slice(keyIndex + keyLength + metaDataLength); - window.history.pushState({}, document.title, cleanUrl); - } - AuthHeadersUtil.signIn(); - TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "HostedExplorer" }); - }; - - private _onSignOutClick = () => { - AuthHeadersUtil.signOut(); - TelemetryProcessor.trace(Action.SignOutAad, undefined, { area: "HostedExplorer" }); - }; - - private _onSwitchDirectoryClick = () => { - this._clickMeControl(); - this.openDirectoryPane(); - }; - - private async _getArcadiaToken(message: any): Promise { - try { - const token = await getArcadiaAuthToken(); - this._sendMessageToExplorerFrame({ - actionType: ActionType.TransmitCachedData, - message: { - id: message && message.id, - data: JSON.stringify(token) // target expects stringified value - } - }); - } catch (error) { - const errorMessage = getErrorMessage(error); - Logger.logError(errorMessage, "HostedExplorer/_getArcadiaToken"); - this._sendMessageToExplorerFrame({ - actionType: ActionType.TransmitCachedData, - message: { - id: message && message.id, - error: errorMessage - } - }); - } - } - - private _handleAadLogin() { - AuthHeadersUtil.processTokenResponse(); - if (AuthHeadersUtil.isUserSignedIn()) { - window.authType = AuthType.AAD; - const user = AuthHeadersUtil.getCachedUser(); - this._updateMeControlProps({ - isUserSignedIn: true, - user: { - name: user.profile.name, - email: user.userName, - tenantName: undefined, - imageUrl: undefined - } - }); - - AuthHeadersUtil.getPhotoFromGraphAPI().then(blob => { - const imageUrl = URL.createObjectURL(blob); - this._updateMeControlProps({ - isUserSignedIn: true, - user: { - name: undefined, - email: undefined, - tenantName: undefined, - imageUrl: imageUrl - } - }); - }); - } - } - - private _handleGetAccessAadRequest() { - this._getAccessAad().then( - response => { - this._sendMessageToExplorerFrame({ - type: MessageTypes.GetAccessAadResponse, - response - }); - }, - error => { - this._sendMessageToExplorerFrame({ - type: MessageTypes.GetAccessAadResponse, - error: getErrorMessage(error) - }); - } - ); - } - - private async _getAccessAad(): Promise<[DatabaseAccount, AccountKeys, string]> { - return this._getAccessCached().catch(() => this._getAccessNew()); - } - - private async _getAccessCached(): Promise<[DatabaseAccount, AccountKeys, string]> { - if (!this._hasCachedDatabaseAccount() || !this._hasCachedTenant()) { - throw new Error("No cached account or tenant found."); - } - - const accountResourceId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId); - let tenantId = LocalStorageUtility.getEntryString(StorageKey.TenantId); - tenantId = tenantId && tenantId.indexOf("lastVisited") > -1 ? tenantId.substring("lastVisited".length) : tenantId; - - try { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "Loading..." - }); - this._updateLoadingStatusText("Loading Account..."); - - const loadAccountResult = await this._loadAccount(accountResourceId, tenantId); - - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "", - selectedAccountName: loadAccountResult[0].name - }); - this._updateLoadingStatusText("Successfully loaded the account."); - - this._setAadControlBar(); - this._getTenantsHelper().then(tenants => { - this._getDefaultTenantHelper(tenants); - }); - this._getSubscriptionsHelper(tenantId, true, true).then(subs => - this._getDefaultSubscriptionHelper(subs, true, true) - ); - const subscriptionId: string = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; - this._getAccountsHelper(subscriptionId, true, true); - - return loadAccountResult; - } catch (error) { - LocalStorageUtility.removeEntry(StorageKey.DatabaseAccountId); - Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessCached"); - throw error; - } - } - - private async _loadAccount( - cosmosdbResourceId: string, - tenantId?: string - ): Promise<[DatabaseAccount, AccountKeys, string]> { - const getAccountPromise = ArmResourceUtils.getCosmosdbAccount(cosmosdbResourceId, tenantId); - const getKeysPromise = ArmResourceUtils.getCosmosdbKeys(cosmosdbResourceId, tenantId); - const getAuthToken = ArmResourceUtils.getAuthToken(tenantId); - - return Promise.all([getAccountPromise, getKeysPromise, getAuthToken]); - } - - private async _getAccessNew(): Promise<[DatabaseAccount, AccountKeys, string]> { - try { - const tenants = await this._getTenantsHelper(); - const defaultTenant = this._getDefaultTenantHelper(tenants); - - this._setAadControlBar(); - - const accountResponse = this._getAccessAfterTenantSelection(defaultTenant.tenantId); - return accountResponse; - } catch (error) { - Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessNew"); - throw error; - } - } - - private async _getAccessAfterTenantSelection(tenantId: string): Promise<[DatabaseAccount, AccountKeys, string]> { - try { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "Loading..." - }); - const authToken = await ArmResourceUtils.getAuthToken(tenantId); - const subscriptions = await this._getSubscriptionsHelper(tenantId, true, true); - const defaultSubscription = this._getDefaultSubscriptionHelper(subscriptions, true, true); - - const accounts = await this._getAccountsHelper(defaultSubscription.subscriptionId, true, true); - const defaultAccount = this._getDefaultAccountHelper(accounts, true, true); - - const keys = await this._getAccountKeysHelper(defaultAccount, true); - return [defaultAccount, keys, authToken]; - } catch (error) { - Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessAfterTenantSelection"); - throw error; - } - } - - private async _getTenantsHelper( - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Promise> { - if (setLoadingStatus) { - this._updateLoadingStatusText("Loading directories..."); - } - - try { - TelemetryProcessor.traceStart(Action.FetchTenants); - const tenants = await ArmResourceUtils.listTenants(); - TelemetryProcessor.traceSuccess(Action.FetchTenants); - - if (!tenants || !tenants.length) { - if (setLoadingStatus) { - this._updateLoadingStatusText("No directories found. Please sign up for Azure."); - } - return Promise.reject(new Error("No directories found")); - } - - if (setLoadingStatus) { - this._updateLoadingStatusText("Successfully loaded directories."); - } - if (setControl) { - this._updateDirectoryProps({ directories: tenants }, { directories: tenants }); - } - return tenants; - } catch (error) { - if (setLoadingStatus) { - this._updateLoadingStatusText("Failed to load directoreis."); - } - TelemetryProcessor.traceFailure(Action.FetchTenants); - throw error; - } - } - - private _getDefaultTenantHelper( - tenants: Tenant[], - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Tenant { - if (!tenants || !tenants.length) { - return undefined; - } - - let storedDefaultTenantId = LocalStorageUtility.getEntryString(StorageKey.TenantId); - const useLastVisitedAsDefault = - storedDefaultTenantId && storedDefaultTenantId.indexOf(DefaultDirectoryDropdownComponent.lastVisitedKey) > -1; - storedDefaultTenantId = useLastVisitedAsDefault - ? storedDefaultTenantId.substring(DefaultDirectoryDropdownComponent.lastVisitedKey.length) - : storedDefaultTenantId; - - let defaultTenant: Tenant = _.find(tenants, t => t.tenantId === storedDefaultTenantId); - if (!defaultTenant) { - defaultTenant = tenants[0]; - LocalStorageUtility.setEntryString( - StorageKey.TenantId, - `${DefaultDirectoryDropdownComponent.lastVisitedKey}${defaultTenant.tenantId}` - ); - } - - if (setControl) { - const dropdownDefaultDirectoryId = useLastVisitedAsDefault - ? DefaultDirectoryDropdownComponent.lastVisitedKey - : defaultTenant.tenantId; - - this._updateDirectoryProps( - { defaultDirectoryId: dropdownDefaultDirectoryId }, - { selectedDirectoryId: defaultTenant.tenantId } - ); - - this._updateMeControlProps({ - isUserSignedIn: true, - user: { - name: undefined, - email: undefined, - tenantName: defaultTenant && defaultTenant.displayName, - imageUrl: undefined - } - }); - } - if (setLoadingStatus) { - this._updateLoadingStatusText(`Connecting to directory: ${defaultTenant.displayName}`); - } - - return defaultTenant; - } - - private async _getSubscriptionsHelper( - tenantId?: string, - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Promise> { - if (setLoadingStatus) { - this._updateLoadingStatusText("Loading subscriptions..."); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingSubscriptions: true - }); - } - try { - TelemetryProcessor.traceStart(Action.FetchSubscriptions); - const subscriptions = await ArmResourceUtils.listSubscriptions(tenantId); - TelemetryProcessor.traceSuccess(Action.FetchSubscriptions); - - if (!subscriptions || !subscriptions.length) { - const message: string = "No Subscription Found"; - if (setLoadingStatus) { - this._updateLoadingStatusText( - `Please - switch to a different directory with Cosmos DB accounts, or - create an subscription under this directory`, - message - ); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingSubscriptions: false, - subscriptions: [], - accounts: [], - displayText: message - }); - } - return Promise.reject(new Error(message)); - } - if (setLoadingStatus) { - this._updateLoadingStatusText("Successfully loaded subscriptions."); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingSubscriptions: false, - subscriptions: subscriptions - }); - } - return subscriptions; - } catch (error) { - const failureMessage = "Failed to load subscriptions"; - if (setLoadingStatus) { - this._updateLoadingStatusText(failureMessage); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingSubscriptions: false, - displayText: failureMessage - }); - } - TelemetryProcessor.traceFailure(Action.FetchSubscriptions); - throw error; - } - } - - private _getDefaultSubscriptionHelper( - subscriptions: Subscription[], - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Subscription { - if (!subscriptions || !subscriptions.length) { - return undefined; - } - - const storedAccountId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId); - const storedSubId = storedAccountId && storedAccountId.split("subscriptions/")[1].split("/")[0]; - - let defaultSub = _.find(subscriptions, s => s.subscriptionId === storedSubId); - if (!defaultSub) { - defaultSub = subscriptions[0]; - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - selectedSubscriptionId: defaultSub.subscriptionId - }); - } - if (setLoadingStatus) { - this._updateLoadingStatusText(`Connecting to subscription: ${defaultSub.displayName}`); - } - - return defaultSub; - } - - private async _getAccountsHelper( - subscriptionId: string, - setControl: boolean = true, - setLoadingStatus: boolean = true - ): Promise> { - if (!subscriptionId) { - throw new Error("No subscription Id"); - } - - if (setLoadingStatus) { - this._updateLoadingStatusText("Loading Accounts..."); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingAccounts: true, - accounts: [] - }); - } - - try { - TelemetryProcessor.traceStart(Action.FetchAccounts); - const accounts = await ArmResourceUtils.listCosmosdbAccounts([subscriptionId]); - TelemetryProcessor.traceSuccess(Action.FetchAccounts); - - if (!accounts || !accounts.length) { - const message: string = "No Account Found"; - if (setLoadingStatus) { - this._updateLoadingStatusText( - `Please - switch to a different subscription with Cosmos DB accounts, or - - create an account in this subscription`, - message - ); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: message, - isLoadingAccounts: false, - accounts: [] - }); - } - return Promise.reject(new Error("No Account Found")); - } - if (setLoadingStatus) { - this._updateLoadingStatusText("Successfully loaded accounts."); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingAccounts: false, - accounts: accounts - }); - } - return accounts; - } catch (error) { - const failureMessage = "Failed to load accounts."; - if (setLoadingStatus) { - this._updateLoadingStatusText(failureMessage); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - isLoadingAccounts: false, - accounts: [], - displayText: failureMessage - }); - } - TelemetryProcessor.traceFailure(Action.FetchAccounts); - throw error; - } - } - - private _getDefaultAccountHelper( - accounts: DatabaseAccount[], - setControl: boolean = true, - setLoadingStatus: boolean = true - ): DatabaseAccount { - if (!accounts || !accounts.length) { - return undefined; - } - - let storedDefaultAccountId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId); - let defaultAccount = _.find(accounts, a => a.id === storedDefaultAccountId); - - if (!defaultAccount) { - defaultAccount = accounts[0]; - LocalStorageUtility.setEntryString(StorageKey.DatabaseAccountId, defaultAccount.id); - } - if (setControl) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "", - selectedAccountName: defaultAccount.name - }); - } - if (setLoadingStatus) { - this._updateLoadingStatusText(`Connecting to Azure Cosmos DB account: ${defaultAccount.name}`); - } - - return defaultAccount; - } - - private async _getAccountKeysHelper( - account: DatabaseAccount, - setLoadingStatus: boolean = true - ): Promise { - try { - if (setLoadingStatus) { - this._updateLoadingStatusText(`Getting authentication token for Azure Cosmos DB account: ${account.name}`); - } - - TelemetryProcessor.traceStart(Action.GetAccountKeys); - const keys = await ArmResourceUtils.getCosmosdbKeys(account.id); - TelemetryProcessor.traceSuccess(Action.GetAccountKeys); - - if (setLoadingStatus) { - this._updateLoadingStatusText( - `Successfully got authentication token for Azure Cosmos DB account: ${account.name}` - ); - } - return keys; - } catch (error) { - if (setLoadingStatus) { - this._updateLoadingStatusText( - `Failed to get authentication token for Azure Cosmos DB account: ${account.name}` - ); - } - TelemetryProcessor.traceFailure(Action.GetAccountKeys); - throw error; - } - } - - private _switchSubscription = async (newSubscription: Subscription): Promise> => { - if (!newSubscription) { - throw new Error("no subscription specified"); - } - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - selectedSubscriptionId: newSubscription.subscriptionId - }); - const id: string = _.uniqueId(); - this._logConsoleMessage( - ConsoleDataType.InProgress, - `Getting Cosmos DB accounts from subscription: ${newSubscription.displayName}`, - id - ); - - try { - const accounts = await this._getAccountsHelper(newSubscription.subscriptionId, true); - - this._logConsoleMessage(ConsoleDataType.Info, "Successfully fetched Cosmos DB accounts."); - this._clearInProgressMessageWithId(id); - - return accounts; - } catch (error) { - this._logConsoleMessage(ConsoleDataType.Error, `Failed to fetch accounts: ${getErrorMessage(error)}`); - this._clearInProgressMessageWithId(id); - - throw error; - } - }; - - private _switchAccount = async (newAccount: DatabaseAccount): Promise<[DatabaseAccount, AccountKeys, string]> => { - if (!newAccount) { - throw new Error("No account passed in"); - } - - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "Loading..." - }); - const id: string = _.uniqueId(); - this._logConsoleMessage(ConsoleDataType.InProgress, `Connecting to Cosmos DB account: ${newAccount.name}`, id); - - try { - const loadAccountResponse = await this._loadAccount(newAccount.id); - const account = loadAccountResponse[0]; - - LocalStorageUtility.setEntryString(StorageKey.DatabaseAccountId, account.id); - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "", - selectedAccountName: account.name - }); - this._logConsoleMessage(ConsoleDataType.Info, "Connection successful"); - this._clearInProgressMessageWithId(id); - - return loadAccountResponse; - } catch (error) { - this._updateAccountSwitchProps({ - authType: AuthType.AAD, - displayText: "Error loading account" - }); - this._updateLoadingStatusText(`Failed to load selected account: ${newAccount.name}`); - this._logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${getErrorMessage(error)}`); - this._clearInProgressMessageWithId(id); - throw error; - } - }; - - private _hasCachedDatabaseAccount(): boolean { - return LocalStorageUtility.hasItem(StorageKey.DatabaseAccountId); - } - - private _hasCachedTenant(): boolean { - return LocalStorageUtility.hasItem(StorageKey.TenantId); - } - - private _logConsoleMessage(consoleDataType: ConsoleDataType, message: string, id?: string) { - this._sendMessageToExplorerFrame({ - type: MessageTypes.SendNotification, - consoleDataType, - message, - id: id || undefined - }); - } - - private _clearInProgressMessageWithId(id: string) { - this._sendMessageToExplorerFrame({ - type: MessageTypes.ClearNotification, - id - }); - } - - private _updateLoadingStatusText(text: string, title?: string) { - this._sendMessageToExplorerFrame({ - type: MessageTypes.LoadingStatus, - text, - title - }); - } - - private _setAadControlBar() { - const switchDirectoryCommand: CommandButtonComponentProps = { - iconSrc: SwitchDirectoryIcon, - iconAlt: "switch directory button", - onCommandClick: () => this.openDirectoryPane(), - commandButtonLabel: undefined, - ariaLabel: "switch directory button", - tooltipText: "Switch Directory", - hasPopup: true, - disabled: false, - id: "directorySwitchButton" - }; - - this._controlbarCommands.splice(0, 1, switchDirectoryCommand); - } - - private _onDefaultDirectoryChange = (newDirectory: Tenant) => { - this._updateDirectoryProps({ defaultDirectoryId: newDirectory.tenantId }); - if (newDirectory.tenantId === DefaultDirectoryDropdownComponent.lastVisitedKey) { - const storedDirectoryId = LocalStorageUtility.getEntryString(StorageKey.TenantId); - LocalStorageUtility.setEntryString( - StorageKey.TenantId, - `${DefaultDirectoryDropdownComponent.lastVisitedKey}${storedDirectoryId}` - ); - return; - } - LocalStorageUtility.setEntryString(StorageKey.TenantId, newDirectory.tenantId); - TelemetryProcessor.trace(Action.DefaultTenantSwitch); - }; - - private _onNewDirectorySelected = (newDirectory: Tenant) => { - this.switchDirectoryPane.close(); - this._updateDirectoryProps(null, { selectedDirectoryId: newDirectory.tenantId }); - this._updateCacheOnNewDirectorySelected(newDirectory); - this._updateMeControlProps({ - user: { tenantName: newDirectory.displayName, name: undefined, email: undefined, imageUrl: undefined } - }); - this._getAccessAfterTenantSelection(newDirectory.tenantId).then( - accountResponse => { - this._sendMessageToExplorerFrame({ - type: MessageTypes.SwitchAccount, - account: accountResponse[0], - keys: accountResponse[1], - authorizationToken: accountResponse[2] - }); - }, - error => { - Logger.logError(getErrorMessage(error), "HostedExplorer/_onNewDirectorySelected"); - } - ); - TelemetryProcessor.trace(Action.TenantSwitch); - }; - - private _updateCacheOnNewDirectorySelected(newDirectory: Tenant) { - const storedDefaultTenantId = LocalStorageUtility.getEntryString(StorageKey.TenantId); - if (storedDefaultTenantId.indexOf(DefaultDirectoryDropdownComponent.lastVisitedKey) >= 0) { - LocalStorageUtility.setEntryString( - StorageKey.TenantId, - `${DefaultDirectoryDropdownComponent.lastVisitedKey}${newDirectory.tenantId}` - ); - } - LocalStorageUtility.removeEntry(StorageKey.DatabaseAccountId); - } - - private _clickDirectoryControl() { - document.getElementById("directorySwitchButton").click(); - } - - private _clickAccountSwitchControl() { - document.getElementById("accountSwitchButton").click(); - } - - private _clickMeControl() { - document.getElementById("mecontrolHeader").click(); - } - - /** - * The iframe swallows any click event which breaks the logic to dismiss the menu, so we simulate a click from the message - */ - private _simulateClick() { - const event = document.createEvent("Events"); - event.initEvent("click", true, false); - document.getElementsByTagName("header")[0].dispatchEvent(event); - } -} - -const hostedExplorer = new HostedExplorer(); -ko.applyBindings(hostedExplorer); diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx new file mode 100644 index 000000000..7fe0988c7 --- /dev/null +++ b/src/HostedExplorer.tsx @@ -0,0 +1,143 @@ +import { useBoolean } from "@uifabric/react-hooks"; +import { initializeIcons } from "office-ui-fabric-react"; +import * as React from "react"; +import { render } from "react-dom"; +import ChevronRight from "../images/chevron-right.svg"; +import "../less/hostedexplorer.less"; +import { AuthType } from "./AuthType"; +import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; +import { DatabaseAccount } from "./Contracts/DataModels"; +import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel"; +import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; +import "./Explorer/Menus/NavBar/MeControlComponent.less"; +import { useTokenMetadata } from "./hooks/usePortalAccessToken"; +import { MeControl } from "./Platform/Hosted/Components/MeControl"; +import "./Platform/Hosted/ConnectScreen.less"; +import "./Shared/appInsights"; +import { SignInButton } from "./Platform/Hosted/Components/SignInButton"; +import { useAADAuth } from "./hooks/useAADAuth"; +import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; +import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; +import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; + +initializeIcons(); + +const App: React.FunctionComponent = () => { + // For handling encrypted portal tokens sent via query paramter + const params = new URLSearchParams(window.location.search); + const [encryptedToken, setEncryptedToken] = React.useState(params && params.get("key")); + const encryptedTokenMetadata = useTokenMetadata(encryptedToken); + + // For showing/hiding panel + const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); + + const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); + const [databaseAccount, setDatabaseAccount] = React.useState(); + const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); + const [connectionString, setConnectionString] = React.useState(); + + const ref = React.useRef(); + + React.useEffect(() => { + // If ref.current is undefined no iframe has been rendered + if (ref.current) { + // In hosted mode, we can set global properties directly on the child iframe. + // This is not possible in the portal where the iframes have different origins + const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame; + // AAD authenticated uses ALWAYS using AAD authType + if (isLoggedIn) { + frameWindow.hostedConfig = { + authType: AuthType.AAD, + databaseAccount, + authorizationToken: armToken + }; + } else if (authType === AuthType.EncryptedToken) { + frameWindow.hostedConfig = { + authType: AuthType.EncryptedToken, + encryptedToken, + encryptedTokenMetadata + }; + } else if (authType === AuthType.ConnectionString) { + frameWindow.hostedConfig = { + authType: AuthType.ConnectionString, + encryptedToken, + encryptedTokenMetadata, + masterKey: extractMasterKeyfromConnectionString(connectionString) + }; + } else if (authType === AuthType.ResourceToken) { + frameWindow.hostedConfig = { + authType: AuthType.ResourceToken, + resourceToken: connectionString + }; + } + } + }); + + const showExplorer = + (isLoggedIn && databaseAccount) || + (encryptedTokenMetadata && encryptedTokenMetadata) || + (authType === AuthType.ResourceToken && connectionString); + + return ( + <> +
+
+
+ window.open("https://portal.azure.com", "_blank")} + tabIndex={0} + title="Go to Azure Portal" + > + Microsoft Azure + + Cosmos DB + {(isLoggedIn || encryptedTokenMetadata?.accountName) && ( + account separator + )} + {isLoggedIn && ( + + + + )} + {!isLoggedIn && encryptedTokenMetadata?.accountName && ( + + {encryptedTokenMetadata?.accountName} + + )} +
+ +
+ {isLoggedIn ? ( + + ) : ( + + )} +
+
+
+ {showExplorer && ( + // Ideally we would import and render data explorer like any other React component, however + // because it still has a significant amount of Knockout code, this would lead to memory leaks. + // Knockout does not have a way to tear down all of its binding and listeners with a single method. + // It's possible this can be changed once all knockout code has been removed. + + )} + {!isLoggedIn && !encryptedTokenMetadata && ( + + )} + {isLoggedIn && } + + ); +}; + +render(, document.getElementById("App")); diff --git a/src/HostedExplorerChildFrame.ts b/src/HostedExplorerChildFrame.ts new file mode 100644 index 000000000..70497fb2c --- /dev/null +++ b/src/HostedExplorerChildFrame.ts @@ -0,0 +1,32 @@ +import { AuthType } from "./AuthType"; +import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels"; + +export interface HostedExplorerChildFrame extends Window { + hostedConfig: AAD | ConnectionString | EncryptedToken | ResourceToken; +} + +interface AAD { + authType: AuthType.AAD; + databaseAccount: DatabaseAccount; + authorizationToken: string; +} + +interface ConnectionString { + authType: AuthType.ConnectionString; + // Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy + encryptedToken: string; + encryptedTokenMetadata: AccessInputMetadata; + // Master key is currently only used by Graph API. All other APIs use encrypted tokens and proxy with connection string + masterKey?: string; +} + +interface EncryptedToken { + authType: AuthType.EncryptedToken; + encryptedToken: string; + encryptedTokenMetadata: AccessInputMetadata; +} + +interface ResourceToken { + authType: AuthType.ResourceToken; + resourceToken: string; +} diff --git a/src/Main.tsx b/src/Main.tsx index 92e1619c4..bb6867133 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -44,7 +44,6 @@ import "./Libs/jquery"; import "bootstrap/dist/js/npm"; import "../externals/jquery.typeahead.min.js"; import "../externals/jquery-ui.min.js"; -import "../externals/adal.js"; import "promise-polyfill/src/polyfill"; import "abort-controller/polyfill"; import "whatwg-fetch"; @@ -56,71 +55,207 @@ import "url-polyfill/url-polyfill.min"; initializeIcons(); -import * as ko from "knockout"; -import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor"; -import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants"; - -import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer"; -import * as Emulator from "./Platform/Emulator/Main"; -import Hosted from "./Platform/Hosted/Main"; -import * as Portal from "./Platform/Portal/Main"; import { AuthType } from "./AuthType"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; import { applyExplorerBindings } from "./applyExplorerBindings"; -import { initializeConfiguration, Platform } from "./ConfigContext"; +import { configContext, initializeConfiguration, Platform } from "./ConfigContext"; import Explorer from "./Explorer/Explorer"; import React, { useEffect } from "react"; import ReactDOM from "react-dom"; -import errorImage from "../images/error.svg"; import copyImage from "../images/Copy.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import refreshImg from "../images/refresh-cosmos.svg"; import arrowLeftImg from "../images/imgarrowlefticon.svg"; import { KOCommentEnd, KOCommentIfStart } from "./koComment"; - -// TODO: Encapsulate and reuse all global variables as environment variables -window.authType = AuthType.AAD; +import { updateUserContext } from "./UserContext"; +import { CollectionCreation } from "./Shared/Constants"; +import { extractFeatures } from "./Platform/Hosted/extractFeatures"; +import { emulatorAccount } from "./Platform/Emulator/emulatorAccount"; +import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; +import { + getDatabaseAccountKindFromExperience, + getDatabaseAccountPropertiesFromMetadata +} from "./Platform/Hosted/HostedUtils"; +import { DefaultExperienceUtility } from "./Shared/DefaultExperienceUtility"; +import { parseResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils"; +import { AccountKind, DefaultAccountExperience, ServerIds } from "./Common/Constants"; +import { listKeys } from "./Utils/arm/generatedClients/2020-04-01/databaseAccounts"; const App: React.FunctionComponent = () => { useEffect(() => { - initializeConfiguration().then(config => { + initializeConfiguration().then(async config => { + let explorer: Explorer; if (config.platform === Platform.Hosted) { - try { - Hosted.initializeExplorer().then( - (explorer: Explorer) => { - applyExplorerBindings(explorer); - Hosted.configureTokenValidationDisplayPrompt(explorer); - }, - (error: unknown) => { - try { - const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess(); - window.dataExplorer = uninitializedExplorer; - ko.applyBindings(uninitializedExplorer); - BindingHandlersRegisterer.registerBindingHandlers(); - if (window.authType !== AuthType.AAD) { - uninitializedExplorer.isRefreshingExplorer(false); - uninitializedExplorer.displayConnectExplorerForm(); - } - } catch (e) { - console.log(e); - } - console.error(error); - } + const win = (window as unknown) as HostedExplorerChildFrame; + explorer = new Explorer(); + if (win.hostedConfig.authType === AuthType.EncryptedToken) { + // TODO: Remove window.authType + window.authType = AuthType.EncryptedToken; + // Impossible to tell if this is a try cosmos sub using an encrypted token + explorer.isTryCosmosDBSubscription(false); + updateUserContext({ + accessToken: encodeURIComponent(win.hostedConfig.encryptedToken) + }); + + const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( + win.hostedConfig.encryptedTokenMetadata.apiKind ); - } catch (e) { - console.log(e); + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: { + id: "", + // id: Main._databaseAccountId, + name: win.hostedConfig.encryptedTokenMetadata.accountName, + kind: getDatabaseAccountKindFromExperience(apiExperience), + properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), + tags: [] + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: undefined, + hasWriteAccess: true, // TODO: we should embed this information in the token ideally + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() + }); + explorer.isAccountReady(true); + } else if (win.hostedConfig.authType === AuthType.ResourceToken) { + window.authType = AuthType.ResourceToken; + // Resource tokens can only be used with SQL API + const apiExperience: string = DefaultAccountExperience.DocumentDB; + const parsedResourceToken = parseResourceTokenConnectionString(win.hostedConfig.resourceToken); + updateUserContext({ + resourceToken: parsedResourceToken.resourceToken, + endpoint: parsedResourceToken.accountEndpoint + }); + explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId); + explorer.resourceTokenCollectionId(parsedResourceToken.collectionId); + if (parsedResourceToken.partitionKey) { + explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey); + } + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: { + id: "", + name: parsedResourceToken.accountEndpoint, + kind: AccountKind.GlobalDocumentDB, + properties: { documentEndpoint: parsedResourceToken.accountEndpoint }, + tags: { defaultExperience: apiExperience } + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: undefined, + hasWriteAccess: true, // TODO: we should embed this information in the token ideally + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), + isAuthWithresourceToken: true + }); + explorer.isAccountReady(true); + explorer.isRefreshingExplorer(false); + } else if (win.hostedConfig.authType === AuthType.ConnectionString) { + // For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login + window.authType = AuthType.EncryptedToken; + // Impossible to tell if this is a try cosmos sub using an encrypted token + explorer.isTryCosmosDBSubscription(false); + updateUserContext({ + accessToken: encodeURIComponent(win.hostedConfig.encryptedToken) + }); + + const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( + win.hostedConfig.encryptedTokenMetadata.apiKind + ); + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: { + id: "", + // id: Main._databaseAccountId, + name: win.hostedConfig.encryptedTokenMetadata.accountName, + kind: getDatabaseAccountKindFromExperience(apiExperience), + properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata), + tags: [] + }, + subscriptionId: undefined, + resourceGroup: undefined, + masterKey: win.hostedConfig.masterKey, + hasWriteAccess: true, // TODO: we should embed this information in the token ideally + authorizationToken: undefined, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() + }); + explorer.isAccountReady(true); + } else if (win.hostedConfig.authType === AuthType.AAD) { + window.authType = AuthType.AAD; + const account = win.hostedConfig.databaseAccount; + const accountResourceId = account.id; + const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; + const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; + updateUserContext({ + authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`, + databaseAccount: win.hostedConfig.databaseAccount + }); + const keys = await listKeys(subscriptionId, resourceGroup, account.name); + explorer.initDataExplorerWithFrameInputs({ + databaseAccount: account, + subscriptionId, + resourceGroup, + masterKey: keys.primaryMasterKey, + hasWriteAccess: true, //TODO: 425017 - support read access + authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`, + features: extractFeatures(), + csmEndpoint: undefined, + dnsSuffix: undefined, + serverId: ServerIds.productionPortal, + extensionEndpoint: configContext.BACKEND_ENDPOINT, + subscriptionType: CollectionCreation.DefaultSubscriptionType, + quotaId: undefined, + addCollectionDefaultFlight: explorer.flight(), + isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() + }); + explorer.isAccountReady(true); } } else if (config.platform === Platform.Emulator) { window.authType = AuthType.MasterKey; - const explorer = Emulator.initializeExplorer(); - applyExplorerBindings(explorer); + explorer = new Explorer(); + explorer.databaseAccount(emulatorAccount); + explorer.isAccountReady(true); } else if (config.platform === Platform.Portal) { - TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {}); - const explorer = Portal.initializeExplorer(); - TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {}); - applyExplorerBindings(explorer); + explorer = new Explorer(); + + // In development mode, try to load the iframe message from session storage. + // This allows webpack hot reload to funciton properly + if (process.env.NODE_ENV === "development") { + const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); + if (initMessage) { + const message = JSON.parse(initMessage); + console.warn("Loaded cached portal iframe message from session storage"); + console.dir(message); + explorer.initDataExplorerWithFrameInputs(message); + } + } + + window.addEventListener("message", explorer.handleMessage.bind(explorer), false); } + applyExplorerBindings(explorer); }); }, []); @@ -177,7 +312,7 @@ const App: React.FunctionComponent = () => { aria-label="Share url link" className="shareLink" type="text" - read-only + read-only={true} data-bind="value: shareAccessUrl" /> { - {/* Collections Window Title/Command Bar - End */} - - {!window.dataExplorer?.isAuthWithResourceToken() && ( -
- )} - {window.dataExplorer?.isAuthWithResourceToken() && ( -
- )} +
+
{/* Collections Window - End */}
@@ -320,56 +452,6 @@ const App: React.FunctionComponent = () => { data-bind="react: notificationConsoleComponentAdapter" />
- {/* Explorer Connection - Encryption Token / AAD - Start */} -
-
-
-

- Azure Cosmos DB -

-

Welcome to Azure Cosmos DB

-
- -

- Connect to your account with connection string -

-
-
-

Connect to your account with connection string

-

- - - Error notification - - -

-

- -

-

- Sign In with Azure Account -

-
-
-
-
- {/* Explorer Connection - Encryption Token / AAD - End */} {/* Global loader - Start */}
diff --git a/src/Platform/Emulator/Main.ts b/src/Platform/Emulator/Main.ts deleted file mode 100644 index d3f5783a1..000000000 --- a/src/Platform/Emulator/Main.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Explorer from "../../Explorer/Explorer"; -import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants"; - -export function initializeExplorer(): Explorer { - const explorer = new Explorer(); - explorer.databaseAccount({ - name: "", - id: "", - location: "", - type: "", - kind: AccountKind.DocumentDB, - tags: { - [TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB - }, - properties: { - documentEndpoint: "", - tableEndpoint: "", - gremlinEndpoint: "", - cassandraEndpoint: "" - } - }); - - explorer.isAccountReady(true); - return explorer; -} diff --git a/src/Platform/Emulator/emulatorAccount.tsx b/src/Platform/Emulator/emulatorAccount.tsx new file mode 100644 index 000000000..1ff4437c8 --- /dev/null +++ b/src/Platform/Emulator/emulatorAccount.tsx @@ -0,0 +1,18 @@ +import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants"; + +export const emulatorAccount = { + name: "", + id: "", + location: "", + type: "", + kind: AccountKind.DocumentDB, + tags: { + [TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB + }, + properties: { + documentEndpoint: "", + tableEndpoint: "", + gremlinEndpoint: "", + cassandraEndpoint: "" + } +}; diff --git a/src/Platform/Hosted/ArmResourceUtils.ts b/src/Platform/Hosted/ArmResourceUtils.ts deleted file mode 100644 index dbe9a5f64..000000000 --- a/src/Platform/Hosted/ArmResourceUtils.ts +++ /dev/null @@ -1,180 +0,0 @@ -import AuthHeadersUtil from "./Authorization"; -import * as Constants from "../../Common/Constants"; -import * as Logger from "../../Common/Logger"; -import { Tenant, Subscription, DatabaseAccount, AccountKeys } from "../../Contracts/DataModels"; -import { configContext } from "../../ConfigContext"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; - -// TODO: 421864 - add a fetch wrapper -export abstract class ArmResourceUtils { - private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT; - private static readonly _armApiVersion: string = configContext.ARM_API_VERSION; - private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA; - - // TODO: 422867 - return continuation token instead of read through - public static async listTenants(): Promise> { - let tenants: Array = []; - - try { - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea); - let nextLink = `${ArmResourceUtils._armEndpoint}/tenants?api-version=2017-08-01`; - - while (nextLink) { - const response: Response = await fetch(nextLink, { headers: fetchHeaders }); - const result: TenantListResult = - response.status === 204 || response.status === 304 ? null : await response.json(); - if (!response.ok) { - throw result; - } - nextLink = result.nextLink; - tenants = [...tenants, ...result.value]; - } - return tenants; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/listTenants"); - throw error; - } - } - - // TODO: 422867 - return continuation token instead of read through - public static async listSubscriptions(tenantId?: string): Promise> { - let subscriptions: Array = []; - - try { - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId); - let nextLink = `${ArmResourceUtils._armEndpoint}/subscriptions?api-version=${ArmResourceUtils._armApiVersion}`; - - while (nextLink) { - const response: Response = await fetch(nextLink, { headers: fetchHeaders }); - const result: SubscriptionListResult = - response.status === 204 || response.status === 304 ? null : await response.json(); - if (!response.ok) { - throw result; - } - nextLink = result.nextLink; - const validSubscriptions = result.value.filter( - sub => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue" - ); - subscriptions = [...subscriptions, ...validSubscriptions]; - } - return subscriptions; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/listSubscriptions"); - throw error; - } - } - - // TODO: 422867 - return continuation token instead of read through - public static async listCosmosdbAccounts( - subscriptionIds: string[], - tenantId?: string - ): Promise> { - if (!subscriptionIds || !subscriptionIds.length) { - return Promise.reject("No subscription passed in"); - } - - let accounts: Array = []; - - try { - const subscriptionFilter = "subscriptionId eq '" + subscriptionIds.join("' or subscriptionId eq '") + "'"; - const urlFilter = `$filter=(${subscriptionFilter}) and (resourceType eq 'microsoft.documentdb/databaseaccounts')`; - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId); - let nextLink = `${ArmResourceUtils._armEndpoint}/resources?api-version=${ArmResourceUtils._armApiVersion}&${urlFilter}`; - - while (nextLink) { - const response: Response = await fetch(nextLink, { headers: fetchHeaders }); - const result: AccountListResult = - response.status === 204 || response.status === 304 ? null : await response.json(); - if (!response.ok) { - throw result; - } - nextLink = result.nextLink; - accounts = [...accounts, ...result.value]; - } - return accounts; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/listAccounts"); - throw error; - } - } - - public static async getCosmosdbAccount(cosmosdbResourceId: string, tenantId?: string): Promise { - if (!cosmosdbResourceId) { - return Promise.reject("No Cosmos DB resource id passed in"); - } - try { - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId); - const url = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}?api-version=${Constants.ArmApiVersions.documentDB}`; - - const response: Response = await fetch(url, { headers: fetchHeaders }); - const result: DatabaseAccount = response.status === 204 || response.status === 304 ? null : await response.json(); - if (!response.ok) { - throw result; - } - return result; - } catch (error) { - throw error; - } - } - - public static async getCosmosdbKeys(cosmosdbResourceId: string, tenantId?: string): Promise { - if (!cosmosdbResourceId) { - return Promise.reject("No Cosmos DB resource id passed in"); - } - - try { - const fetchHeaders = await ArmResourceUtils._getAuthHeader(ArmResourceUtils._armAuthArea, tenantId); - const readWriteKeysUrl = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/listKeys?api-version=${Constants.ArmApiVersions.documentDB}`; - const readOnlyKeysUrl = `${ArmResourceUtils._armEndpoint}/${cosmosdbResourceId}/readOnlyKeys?api-version=${Constants.ArmApiVersions.documentDB}`; - let response: Response = await fetch(readWriteKeysUrl, { headers: fetchHeaders, method: "POST" }); - if (response.status === Constants.HttpStatusCodes.Forbidden) { - // fetch read only keys for readers - response = await fetch(readOnlyKeysUrl, { headers: fetchHeaders, method: "POST" }); - } - const result: AccountKeys = - response.status === Constants.HttpStatusCodes.NoContent || - response.status === Constants.HttpStatusCodes.NotModified - ? null - : await response.json(); - if (!response.ok) { - throw result; - } - return result; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAccountKeys"); - throw error; - } - } - - public static async getAuthToken(tenantId?: string): Promise { - try { - const token = await AuthHeadersUtil.getAccessToken(ArmResourceUtils._armAuthArea, tenantId); - return token; - } catch (error) { - Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAuthToken"); - throw error; - } - } - - private static async _getAuthHeader(authArea: string, tenantId?: string): Promise { - const token = await AuthHeadersUtil.getAccessToken(authArea, tenantId); - let fetchHeaders = new Headers(); - fetchHeaders.append("authorization", `Bearer ${token}`); - return fetchHeaders; - } -} - -interface TenantListResult { - nextLink: string; - value: Tenant[]; -} - -interface SubscriptionListResult { - nextLink: string; - value: Subscription[]; -} - -interface AccountListResult { - nextLink: string; - value: DatabaseAccount[]; -} diff --git a/src/Platform/Hosted/Authorization.ts b/src/Platform/Hosted/Authorization.ts index fca39d2ba..06531d932 100644 --- a/src/Platform/Hosted/Authorization.ts +++ b/src/Platform/Hosted/Authorization.ts @@ -1,88 +1,11 @@ -import "expose-loader?AuthenticationContext!../../../externals/adal"; - import Q from "q"; import * as Constants from "../../Common/Constants"; -import * as DataModels from "../../Contracts/DataModels"; -import { AuthType } from "../../AuthType"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; -import * as Logger from "../../Common/Logger"; import { configContext } from "../../ConfigContext"; +import * as DataModels from "../../Contracts/DataModels"; +import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { userContext } from "../../UserContext"; export default class AuthHeadersUtil { - public static serverId: string = Constants.ServerIds.productionPortal; - - private static readonly _firstPartyAppId: string = "203f1145-856a-4232-83d4-a43568fba23d"; - private static readonly _aadEndpoint: string = configContext.AAD_ENDPOINT; - private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT; - private static readonly _arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT; - private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA; - private static readonly _graphEndpoint: string = configContext.GRAPH_ENDPOINT; - private static readonly _graphApiVersion: string = configContext.GRAPH_API_VERSION; - - private static _authContext: AuthenticationContext = new AuthenticationContext({ - instance: AuthHeadersUtil._aadEndpoint, - clientId: AuthHeadersUtil._firstPartyAppId, - postLogoutRedirectUri: window.location.origin, - endpoints: { - aad: AuthHeadersUtil._aadEndpoint, - graph: AuthHeadersUtil._graphEndpoint, - armAuthArea: AuthHeadersUtil._armAuthArea, - armEndpoint: AuthHeadersUtil._armEndpoint, - arcadiaEndpoint: AuthHeadersUtil._arcadiaEndpoint - }, - tenant: undefined, - cacheLocation: window.navigator.userAgent.indexOf("Edge") > -1 ? "localStorage" : undefined - }); - - public static getAccessInputMetadata(accessInput: string): Q.Promise { - const deferred: Q.Deferred = Q.defer(); - const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`; - const authType: string = (window).authType; - const headers: { [headerName: string]: string } = {}; - - if (authType === AuthType.EncryptedToken) { - headers[Constants.HttpHeaders.guestAccessToken] = accessInput; - } else { - headers[Constants.HttpHeaders.connectionString] = accessInput; - } - - $.ajax({ - url: url, - type: "GET", - headers: headers, - cache: false, - dataType: "text" - }).then( - (data: string, textStatus: string, xhr: JQueryXHR) => { - if (!data) { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to get access input metadata`); - deferred.reject(`Failed to get access input metadata`); - } - - try { - const metadata: DataModels.AccessInputMetadata = JSON.parse(JSON.parse(data)); - deferred.resolve(metadata); // TODO: update to a single JSON parse once backend response is stringified exactly once - } catch (error) { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, "Failed to parse access input metadata"); - deferred.reject("Failed to parse access input metadata"); - throw error; - } - }, - (xhr: JQueryXHR, textStatus: string, error: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while fetching access input metadata: ${JSON.stringify(xhr.responseText)}` - ); - deferred.reject(xhr.responseText); - } - ); - - return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs); - } - public static generateEncryptedToken(): Q.Promise { const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl(); const explorer = window.dataExplorer; @@ -118,154 +41,6 @@ export default class AuthHeadersUtil { }); } - public static isUserSignedIn(): boolean { - const user = AuthHeadersUtil._authContext.getCachedUser(); - return !!user; - } - - public static getCachedUser(): AuthenticationContext.UserInfo { - if (this.isUserSignedIn()) { - return AuthHeadersUtil._authContext.getCachedUser(); - } - return undefined; - } - - public static signIn() { - if (!AuthHeadersUtil.isUserSignedIn()) { - AuthHeadersUtil._authContext.login(); - } - } - - public static signOut() { - AuthHeadersUtil._authContext.logOut(); - } - - /** - * Process token from oauth after login or get cached - */ - public static processTokenResponse() { - const isCallback = AuthHeadersUtil._authContext.isCallback(window.location.hash); - if (isCallback && !AuthHeadersUtil._authContext.getLoginError()) { - AuthHeadersUtil._authContext.handleWindowCallback(); - } - } - - /** - * Get auth token to access apis (Graph, ARM) - * - * @param authEndpoint Default to ARM endpoint - * @param tenantId if tenant id provided, tenant id will set at global. Can be reset with 'common' - */ - public static async getAccessToken( - authEndpoint: string = AuthHeadersUtil._armAuthArea, - tenantId?: string - ): Promise { - const AuthorizationType: string = (window).authType; - if (AuthorizationType === AuthType.EncryptedToken) { - // setting authorization header to an undefined value causes the browser to exclude - // the header, which is expected here - throw new Error("auth type is encrypted token, should not get access token"); - } - - return new Promise(async (resolve, reject) => { - if (tenantId) { - // if tenant id passed in, we will use this tenant id for all the rest calls until next tenant id passed in - AuthHeadersUtil._authContext.config.tenant = tenantId; - } - - AuthHeadersUtil._authContext.acquireToken( - authEndpoint, - AuthHeadersUtil._authContext.config.tenant, - (errorResponse: any, token: any) => { - if (errorResponse && typeof errorResponse === "string") { - if (errorResponse.indexOf("login is required") >= 0 || errorResponse.indexOf("AADSTS50058") === 0) { - // Handle error AADSTS50058: A silent sign-in request was sent but no user is signed in. - // The user's cached token is invalid, hence we let the user login again. - AuthHeadersUtil._authContext.login(); - return; - } - if ( - this._isMultifactorAuthRequired(errorResponse) || - errorResponse.indexOf("AADSTS53000") > -1 || - errorResponse.indexOf("AADSTS65001") > -1 - ) { - // Handle error AADSTS50079 and AADSTS50076: User needs to use multifactor authentication and acquireToken fails silent. Redirect - // Handle error AADSTS53000: User needs to use compliant device to access resource when Conditional Access Policy is set up for user. - AuthHeadersUtil._authContext.acquireTokenRedirect( - authEndpoint, - AuthHeadersUtil._authContext.config.tenant - ); - return; - } - } - if (errorResponse || !token) { - Logger.logError(errorResponse, "Hosted/Authorization/_getAuthHeader"); - reject(errorResponse); - return; - } - resolve(token); - } - ); - }); - } - - public static async getPhotoFromGraphAPI(): Promise { - const token = await this.getAccessToken(AuthHeadersUtil._graphEndpoint); - const headers = new Headers(); - headers.append("Authorization", `Bearer ${token}`); - - try { - const response: Response = await fetch( - `${AuthHeadersUtil._graphEndpoint}/me/thumbnailPhoto?api-version=${AuthHeadersUtil._graphApiVersion}`, - { - method: "GET", - headers: headers - } - ); - if (!response.ok) { - throw response; - } - return response.blob(); - } catch (err) { - return new Blob(); - } - } - - private static async _getTenant(subId: string): Promise { - if (subId) { - try { - // Follow https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/azure-resource-manager/resource-manager-api-authentication.md - // TenantId will be returned in the header of the response. - const response: Response = await fetch( - `https://management.core.windows.net/subscriptions/${subId}?api-version=2015-01-01` - ); - if (!response.ok) { - throw response; - } - } catch (reason) { - if (reason.status === 401) { - const authUrl: string = reason.headers - .get("www-authenticate") - .split(",")[0] - .split("=")[1]; - // Fetch the tenant GUID ID and the length should be 36. - const tenantId: string = authUrl.substring(authUrl.lastIndexOf("/") + 1, authUrl.lastIndexOf("/") + 37); - return Promise.resolve(tenantId); - } - } - } - return Promise.resolve(undefined); - } - - private static _isMultifactorAuthRequired(errorResponse: string): boolean { - for (const code of ["AADSTS50079", "AADSTS50076"]) { - if (errorResponse.indexOf(code) === 0) { - return true; - } - } - return false; - } - private static _generateResourceUrl(): string { const databaseAccount = userContext.databaseAccount; const subscriptionId: string = userContext.subscriptionId; diff --git a/src/Platform/Hosted/Components/AccountSwitcher.test.tsx b/src/Platform/Hosted/Components/AccountSwitcher.test.tsx new file mode 100644 index 000000000..51f1fc687 --- /dev/null +++ b/src/Platform/Hosted/Components/AccountSwitcher.test.tsx @@ -0,0 +1,46 @@ +jest.mock("../../../hooks/useSubscriptions"); +jest.mock("../../../hooks/useDatabaseAccounts"); +import React from "react"; +import { render, fireEvent, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { AccountSwitcher } from "./AccountSwitcher"; +import { useSubscriptions } from "../../../hooks/useSubscriptions"; +import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts"; +import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels"; + +it("calls setAccount from parent component", () => { + const armToken = "fakeToken"; + const setDatabaseAccount = jest.fn(); + const subscriptions = [ + { subscriptionId: "testSub1", displayName: "Test Sub 1" }, + { subscriptionId: "testSub2", displayName: "Test Sub 2" } + ] as Subscription[]; + (useSubscriptions as jest.Mock).mockReturnValue(subscriptions); + const accounts = [{ name: "testAccount1" }, { name: "testAccount2" }] as DatabaseAccount[]; + (useDatabaseAccounts as jest.Mock).mockReturnValue(accounts); + + render(); + + fireEvent.click(screen.getByText("Select a Database Account")); + expect(screen.getByLabelText("Subscription")).toHaveTextContent("Select a Subscription"); + fireEvent.click(screen.getByText("Select a Subscription")); + fireEvent.click(screen.getByText(subscriptions[0].displayName)); + expect(screen.getByLabelText("Cosmos DB Account Name")).toHaveTextContent("Select an Account"); + fireEvent.click(screen.getByText("Select an Account")); + fireEvent.click(screen.getByText(accounts[0].name)); + expect(setDatabaseAccount).toHaveBeenCalledWith(accounts[0]); +}); + +it("No subscriptions", () => { + const armToken = "fakeToken"; + const setDatabaseAccount = jest.fn(); + const subscriptions = [] as Subscription[]; + (useSubscriptions as jest.Mock).mockReturnValue(subscriptions); + const accounts = [] as DatabaseAccount[]; + (useDatabaseAccounts as jest.Mock).mockReturnValue(accounts); + + render(); + + fireEvent.click(screen.getByText("Select a Database Account")); + expect(screen.getByLabelText("Subscription")).toHaveTextContent("No Subscriptions Found"); +}); diff --git a/src/Platform/Hosted/Components/AccountSwitcher.tsx b/src/Platform/Hosted/Components/AccountSwitcher.tsx new file mode 100644 index 000000000..da28c5eac --- /dev/null +++ b/src/Platform/Hosted/Components/AccountSwitcher.tsx @@ -0,0 +1,109 @@ +// TODO: Renable this rule for the file or turn it off everywhere +/* eslint-disable react/display-name */ + +import { StyleConstants } from "../../../Common/Constants"; +import { FunctionComponent, useState, useEffect } from "react"; +import * as React from "react"; +import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button"; +import { IContextualMenuItem } from "office-ui-fabric-react/lib/ContextualMenu"; +import { DatabaseAccount } from "../../../Contracts/DataModels"; +import { useSubscriptions } from "../../../hooks/useSubscriptions"; +import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts"; +import { SwitchSubscription } from "./SwitchSubscription"; +import { SwitchAccount } from "./SwitchAccount"; + +const buttonStyles: IButtonStyles = { + root: { + fontSize: StyleConstants.DefaultFontSize, + height: 40, + padding: 0, + paddingLeft: 10, + marginRight: 5, + backgroundColor: StyleConstants.BaseDark, + color: StyleConstants.BaseLight + }, + rootHovered: { + backgroundColor: StyleConstants.BaseHigh, + color: StyleConstants.BaseLight + }, + rootFocused: { + backgroundColor: StyleConstants.BaseHigh, + color: StyleConstants.BaseLight + }, + rootPressed: { + backgroundColor: StyleConstants.BaseHigh, + color: StyleConstants.BaseLight + }, + rootExpanded: { + backgroundColor: StyleConstants.BaseHigh, + color: StyleConstants.BaseLight + }, + textContainer: { + flexGrow: "initial" + } +}; + +interface Props { + armToken: string; + setDatabaseAccount: (account: DatabaseAccount) => void; +} + +export const AccountSwitcher: FunctionComponent = ({ armToken, setDatabaseAccount }: Props) => { + const subscriptions = useSubscriptions(armToken); + const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(() => + localStorage.getItem("cachedSubscriptionId") + ); + const selectedSubscription = subscriptions?.find(sub => sub.subscriptionId === selectedSubscriptionId); + const accounts = useDatabaseAccounts(selectedSubscription?.subscriptionId, armToken); + const [selectedAccountName, setSelectedAccountName] = useState(() => + localStorage.getItem("cachedDatabaseAccountName") + ); + const selectedAccount = accounts?.find(account => account.name === selectedAccountName); + + useEffect(() => { + if (selectedAccountName) { + localStorage.setItem("cachedDatabaseAccountName", selectedAccountName); + } + }, [selectedAccountName]); + + useEffect(() => { + if (selectedSubscriptionId) { + localStorage.setItem("cachedSubscriptionId", selectedSubscriptionId); + } + }, [selectedSubscriptionId]); + + useEffect(() => { + if (selectedAccount) { + setDatabaseAccount(selectedAccount); + } + }, [selectedAccount]); + + const buttonText = selectedAccount?.name || "Select a Database Account"; + + const items: IContextualMenuItem[] = [ + { + key: "switchSubscription", + onRender: () => + }, + { + key: "switchAccount", + onRender: (_, dismissMenu) => ( + + ) + } + ]; + + return ( + + ); +}; diff --git a/src/Platform/Hosted/Components/ConnectExplorer.test.tsx b/src/Platform/Hosted/Components/ConnectExplorer.test.tsx new file mode 100644 index 000000000..ed342a17b --- /dev/null +++ b/src/Platform/Hosted/Components/ConnectExplorer.test.tsx @@ -0,0 +1,18 @@ +jest.mock("../../../hooks/useDirectories"); +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import React from "react"; +import { ConnectExplorer } from "./ConnectExplorer"; + +it("shows the connect form", () => { + const connectionString = "fakeConnectionString"; + const login = jest.fn(); + const setConnectionString = jest.fn(); + const setEncryptedToken = jest.fn(); + const setAuthType = jest.fn(); + + render(); + expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeNull(); + fireEvent.click(screen.getByText("Connect to your account with connection string")); + expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeDefined(); +}); diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx new file mode 100644 index 000000000..bd9c99275 --- /dev/null +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -0,0 +1,94 @@ +import * as React from "react"; +import { useBoolean } from "@uifabric/react-hooks"; +import { HttpHeaders } from "../../../Common/Constants"; +import { GenerateTokenResponse } from "../../../Contracts/DataModels"; +import { configContext } from "../../../ConfigContext"; +import { AuthType } from "../../../AuthType"; +import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; + +interface Props { + connectionString: string; + login: () => void; + setEncryptedToken: (token: string) => void; + setConnectionString: (connectionString: string) => void; + setAuthType: (authType: AuthType) => void; +} + +export const ConnectExplorer: React.FunctionComponent = ({ + setEncryptedToken, + login, + setAuthType, + connectionString, + setConnectionString +}: Props) => { + const [isFormVisible, { setTrue: showForm }] = useBoolean(false); + + return ( +
+
+
+

+ Azure Cosmos DB +

+

Welcome to Azure Cosmos DB

+ {isFormVisible ? ( +
{ + event.preventDefault(); + + if (isResourceTokenConnectionString(connectionString)) { + setAuthType(AuthType.ResourceToken); + return; + } + + const headers = new Headers(); + headers.append(HttpHeaders.connectionString, connectionString); + const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken"; + const response = await fetch(url, { headers, method: "POST" }); + if (!response.ok) { + throw response; + } + // This API has a quirk where it must be parsed twice + const result: GenerateTokenResponse = JSON.parse(await response.json()); + setEncryptedToken(decodeURIComponent(result.readWrite || result.read)); + setAuthType(AuthType.ConnectionString); + }} + > +

Connect to your account with connection string

+

+ { + setConnectionString(event.target.value); + }} + /> + + Error notification + + +

+

+ +

+

+ Sign In with Azure Account +

+
+ ) : ( +
+ +

+ Connect to your account with connection string +

+
+ )} +
+
+
+ ); +}; diff --git a/src/Platform/Hosted/Components/DirectoryPickerPanel.test.tsx b/src/Platform/Hosted/Components/DirectoryPickerPanel.test.tsx new file mode 100644 index 000000000..a734bef71 --- /dev/null +++ b/src/Platform/Hosted/Components/DirectoryPickerPanel.test.tsx @@ -0,0 +1,31 @@ +jest.mock("../../../hooks/useDirectories"); +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import React from "react"; +import { Tenant } from "../../../Contracts/DataModels"; +import { useDirectories } from "../../../hooks/useDirectories"; +import { DirectoryPickerPanel } from "./DirectoryPickerPanel"; + +it("switches tenant for user", () => { + const armToken = "fakeToken"; + const switchTenant = jest.fn(); + const dismissPanel = jest.fn(); + const directories = [ + { displayName: "test1", tenantId: "test1-id" }, + { displayName: "test2", tenantId: "test2-id" } + ] as Tenant[]; + (useDirectories as jest.Mock).mockReturnValue(directories); + + render( + + ); + fireEvent.click(screen.getByLabelText(/test2-id/)); + expect(switchTenant).toHaveBeenCalledWith(directories[1].tenantId); + expect(dismissPanel).toHaveBeenCalled(); +}); diff --git a/src/Platform/Hosted/Components/DirectoryPickerPanel.tsx b/src/Platform/Hosted/Components/DirectoryPickerPanel.tsx new file mode 100644 index 000000000..41ddc9688 --- /dev/null +++ b/src/Platform/Hosted/Components/DirectoryPickerPanel.tsx @@ -0,0 +1,39 @@ +import { Panel, PanelType, ChoiceGroup } from "office-ui-fabric-react"; +import * as React from "react"; +import { useDirectories } from "../../../hooks/useDirectories"; + +interface Props { + isOpen: boolean; + dismissPanel: () => void; + tenantId: string; + armToken: string; + switchTenant: (tenantId: string) => void; +} + +export const DirectoryPickerPanel: React.FunctionComponent = ({ + isOpen, + dismissPanel, + armToken, + tenantId, + switchTenant +}: Props) => { + const directories = useDirectories(armToken); + return ( + + ({ key: dir.tenantId, text: `${dir.displayName} (${dir.tenantId})` }))} + selectedKey={tenantId} + onChange={(event, option) => { + switchTenant(option.key); + dismissPanel(); + }} + /> + + ); +}; diff --git a/src/Platform/Hosted/Components/FeedbackCommandButton.tsx b/src/Platform/Hosted/Components/FeedbackCommandButton.tsx new file mode 100644 index 000000000..00b6670b9 --- /dev/null +++ b/src/Platform/Hosted/Components/FeedbackCommandButton.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent"; +import FeedbackIcon from "../../../../images/Feedback.svg"; + +export const FeedbackCommandButton: React.FunctionComponent = () => { + return ( +
+ + window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback") + } + ariaLabel="feeback button" + tooltipText="Send feedback" + hasPopup={true} + disabled={false} + /> +
+ ); +}; diff --git a/src/Platform/Hosted/Components/MeControl.test.tsx b/src/Platform/Hosted/Components/MeControl.test.tsx new file mode 100644 index 000000000..4c5823e07 --- /dev/null +++ b/src/Platform/Hosted/Components/MeControl.test.tsx @@ -0,0 +1,17 @@ +jest.mock("../../../hooks/useDirectories"); +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import React from "react"; +import { MeControl } from "./MeControl"; +import { Account } from "msal"; + +it("renders", () => { + const account = {} as Account; + const logout = jest.fn(); + const openPanel = jest.fn(); + + render(); + fireEvent.click(screen.getByRole("button")); + expect(screen.getByText("Switch Directory")).toBeDefined(); + expect(screen.getByText("Sign Out")).toBeDefined(); +}); diff --git a/src/Platform/Hosted/Components/MeControl.tsx b/src/Platform/Hosted/Components/MeControl.tsx new file mode 100644 index 000000000..43677cc71 --- /dev/null +++ b/src/Platform/Hosted/Components/MeControl.tsx @@ -0,0 +1,68 @@ +import { + FocusZone, + DefaultButton, + DirectionalHint, + Persona, + PersonaInitialsColor, + PersonaSize +} from "office-ui-fabric-react"; +import * as React from "react"; +import { Account } from "msal"; +import { useGraphPhoto } from "../../../hooks/useGraphPhoto"; + +interface Props { + graphToken: string; + account: Account; + openPanel: () => void; + logout: () => void; +} + +export const MeControl: React.FunctionComponent = ({ openPanel, logout, account, graphToken }: Props) => { + const photo = useGraphPhoto(graphToken); + return ( + + + + + + ); +}; diff --git a/src/Platform/Hosted/Components/SignInButton.tsx b/src/Platform/Hosted/Components/SignInButton.tsx new file mode 100644 index 000000000..c13c8de3c --- /dev/null +++ b/src/Platform/Hosted/Components/SignInButton.tsx @@ -0,0 +1,21 @@ +import { DefaultButton } from "office-ui-fabric-react"; +import * as React from "react"; + +interface Props { + login: () => void; +} + +export const SignInButton: React.FunctionComponent = ({ login }: Props) => { + return ( + + ); +}; diff --git a/src/Platform/Hosted/Components/SwitchAccount.tsx b/src/Platform/Hosted/Components/SwitchAccount.tsx new file mode 100644 index 000000000..c3d98a6a8 --- /dev/null +++ b/src/Platform/Hosted/Components/SwitchAccount.tsx @@ -0,0 +1,39 @@ +import { Dropdown } from "office-ui-fabric-react/lib/Dropdown"; +import * as React from "react"; +import { FunctionComponent } from "react"; +import { DatabaseAccount } from "../../../Contracts/DataModels"; + +interface Props { + accounts: DatabaseAccount[]; + selectedAccount: DatabaseAccount; + setSelectedAccountName: (id: string) => void; + dismissMenu: () => void; +} + +export const SwitchAccount: FunctionComponent = ({ + accounts, + setSelectedAccountName, + selectedAccount, + dismissMenu +}: Props) => { + return ( + ({ + key: account.name, + text: account.name, + data: account + }))} + onChange={(_, option) => { + setSelectedAccountName(String(option.key)); + dismissMenu(); + }} + defaultSelectedKey={selectedAccount?.name} + placeholder={accounts && accounts.length === 0 ? "No Accounts Found" : "Select an Account"} + styles={{ + callout: "accountSwitchAccountDropdownMenu" + }} + /> + ); +}; diff --git a/src/Platform/Hosted/Components/SwitchSubscription.tsx b/src/Platform/Hosted/Components/SwitchSubscription.tsx new file mode 100644 index 000000000..e8ae29e94 --- /dev/null +++ b/src/Platform/Hosted/Components/SwitchSubscription.tsx @@ -0,0 +1,38 @@ +import { Dropdown } from "office-ui-fabric-react/lib/Dropdown"; +import * as React from "react"; +import { FunctionComponent } from "react"; +import { Subscription } from "../../../Contracts/DataModels"; + +interface Props { + subscriptions: Subscription[]; + selectedSubscription: Subscription; + setSelectedSubscriptionId: (id: string) => void; +} + +export const SwitchSubscription: FunctionComponent = ({ + subscriptions, + setSelectedSubscriptionId, + selectedSubscription +}: Props) => { + return ( + { + return { + key: sub.subscriptionId, + text: sub.displayName, + data: sub + }; + })} + onChange={(_, option) => { + setSelectedSubscriptionId(String(option.key)); + }} + defaultSelectedKey={selectedSubscription?.subscriptionId} + placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"} + styles={{ + callout: "accountSwitchSubscriptionDropdownMenu" + }} + /> + ); +}; diff --git a/src/Platform/Hosted/ConnectScreen.less b/src/Platform/Hosted/ConnectScreen.less new file mode 100644 index 000000000..0514e41ab --- /dev/null +++ b/src/Platform/Hosted/ConnectScreen.less @@ -0,0 +1,101 @@ +.connectExplorerContainer { + height: 100%; + width: 100%; +} +.connectExplorerContainer .connectExplorerFormContainer { + display: -webkit-flex; + display: -ms-flexbox; + display: -ms-flex; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + height: 100%; + width: 100%; +} +.connectExplorerContainer .connectExplorer { + text-align: center; + display: -webkit-flex; + display: -ms-flexbox; + display: -ms-flex; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + justify-content: center; + height: 100%; + margin-bottom: 60px; +} +.connectExplorerContainer .connectExplorer .welcomeText { + font-size: 14px; + color: #393939; + margin: 8px 8px 16px 8px; +} +.connectExplorerContainer .connectExplorer .switchConnectTypeText { + margin: 8px; + font-size: 12px; + color: #0058ad; + cursor: pointer; +} +.connectExplorerContainer .connectExplorer .connectStringText { + font-size: 12px; + color: #393939; +} +.connectExplorerContainer .connectExplorer .connectExplorerContent { + margin: 8px; + color: #393939; +} +.connectExplorerContainer .connectExplorer .connectExplorerContent .inputToken { + width: 300px; + padding: 0px 4px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} +.connectExplorerContainer .connectExplorer .connectExplorerContent .inputToken::placeholder { + font-style: italic; +} +.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip { + position: relative; + display: inline-block; + padding-left: 4px; + vertical-align: top; +} +.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip:hover .errorDetails { + visibility: visible; +} +.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails { + bottom: 24px; + width: 145px; + visibility: hidden; + background-color: #393939; + color: #ffffff; + position: absolute; + z-index: 1; + left: -10px; + padding: 6px; +} +.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails:after { + border-width: 10px 10px 0px 10px; + bottom: -8px; + content: ""; + position: absolute; + right: 100%; + border-style: solid; + left: 12px; + width: 0; + height: 0; + border-color: #3b3b3b transparent; +} +.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorImg { + height: 14px; + width: 14px; +} + +.filterbtnstyle { + background: #0058ad; + width: 90px; + height: 25px; + color: white; + border: solid 1px; +} diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts index 0291396d1..da534c045 100644 --- a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts +++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts @@ -1,12 +1,12 @@ import * as DataModels from "../../../Contracts/DataModels"; -import { ConnectionStringParser } from "./ConnectionStringParser"; +import { parseConnectionString } from "./ConnectionStringParser"; describe("ConnectionStringParser", () => { const mockAccountName: string = "Test"; const mockMasterKey: string = "some-key"; it("should parse a valid sql account connection string", () => { - const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( + const metadata = parseConnectionString( `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};` ); @@ -15,7 +15,7 @@ describe("ConnectionStringParser", () => { }); it("should parse a valid mongo account connection string", () => { - const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( + const metadata = parseConnectionString( `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255` ); @@ -24,7 +24,7 @@ describe("ConnectionStringParser", () => { }); it("should parse a valid compute mongo account connection string", () => { - const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( + const metadata = parseConnectionString( `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255` ); @@ -33,7 +33,7 @@ describe("ConnectionStringParser", () => { }); it("should parse a valid graph account connection string", () => { - const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( + const metadata = parseConnectionString( `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;` ); @@ -42,7 +42,7 @@ describe("ConnectionStringParser", () => { }); it("should parse a valid table account connection string", () => { - const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( + const metadata = parseConnectionString( `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;` ); @@ -51,7 +51,7 @@ describe("ConnectionStringParser", () => { }); it("should parse a valid cassandra account connection string", () => { - const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( + const metadata = parseConnectionString( `AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};` ); @@ -60,15 +60,13 @@ describe("ConnectionStringParser", () => { }); it("should fail to parse an invalid connection string", () => { - const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString( - "some-rogue-connection-string" - ); + const metadata = parseConnectionString("some-rogue-connection-string"); expect(metadata).toBe(undefined); }); it("should fail to parse an empty connection string", () => { - const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(""); + const metadata = parseConnectionString(""); expect(metadata).toBe(undefined); }); diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts index 423a85f09..861f724d1 100644 --- a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts +++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts @@ -1,50 +1,48 @@ import * as Constants from "../../../Common/Constants"; -import * as DataModels from "../../../Contracts/DataModels"; +import { AccessInputMetadata, ApiKind } from "../../../Contracts/DataModels"; -export class ConnectionStringParser { - public static parseConnectionString(connectionString: string): DataModels.AccessInputMetadata { - if (!!connectionString) { - try { - const accessInput: DataModels.AccessInputMetadata = {} as DataModels.AccessInputMetadata; - const connectionStringParts = connectionString.split(";"); +export function parseConnectionString(connectionString: string): AccessInputMetadata { + if (connectionString) { + try { + const accessInput = {} as AccessInputMetadata; + const connectionStringParts = connectionString.split(";"); - connectionStringParts.forEach((connectionStringPart: string) => { - if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) { - accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1]; - accessInput.apiKind = DataModels.ApiKind.SQL; - } else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) { - const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo); - accessInput.accountName = matches && matches.length > 1 && matches[2]; - accessInput.apiKind = DataModels.ApiKind.MongoDB; - } else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) { - const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute); - accessInput.accountName = matches && matches.length > 1 && matches[2]; - accessInput.apiKind = DataModels.ApiKind.MongoDBCompute; - } else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) { - Constants.EndpointsRegex.cassandra.forEach(regex => { - if (RegExp(regex).test(connectionStringPart)) { - accessInput.accountName = connectionStringPart.match(regex)[1]; - accessInput.apiKind = DataModels.ApiKind.Cassandra; - } - }); - } else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) { - accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1]; - accessInput.apiKind = DataModels.ApiKind.Table; - } else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) { - accessInput.apiKind = DataModels.ApiKind.Graph; - } - }); - - if (Object.keys(accessInput).length === 0) { - return undefined; + connectionStringParts.forEach((connectionStringPart: string) => { + if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) { + accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1]; + accessInput.apiKind = ApiKind.SQL; + } else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) { + const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo); + accessInput.accountName = matches && matches.length > 1 && matches[2]; + accessInput.apiKind = ApiKind.MongoDB; + } else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) { + const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute); + accessInput.accountName = matches && matches.length > 1 && matches[2]; + accessInput.apiKind = ApiKind.MongoDBCompute; + } else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) { + Constants.EndpointsRegex.cassandra.forEach(regex => { + if (RegExp(regex).test(connectionStringPart)) { + accessInput.accountName = connectionStringPart.match(regex)[1]; + accessInput.apiKind = ApiKind.Cassandra; + } + }); + } else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) { + accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1]; + accessInput.apiKind = ApiKind.Table; + } else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) { + accessInput.apiKind = ApiKind.Graph; } + }); - return accessInput; - } catch (error) { + if (Object.keys(accessInput).length === 0) { return undefined; } - } - return undefined; + return accessInput; + } catch (error) { + return undefined; + } } + + return undefined; } diff --git a/src/Platform/Hosted/Maint.test.ts b/src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts similarity index 55% rename from src/Platform/Hosted/Maint.test.ts rename to src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts index 4c520cb78..12cc90706 100644 --- a/src/Platform/Hosted/Maint.test.ts +++ b/src/Platform/Hosted/Helpers/ResourceTokenUtils.test.ts @@ -1,24 +1,10 @@ -import Main from "./Main"; - -describe("Main", () => { - it("correctly detects feature flags", () => { - // Search containing non-features, with Camelcase keys and uri encoded values - const params = new URLSearchParams( - "?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey" - ); - const features = Main.extractFeatures(params); - - expect(features).toEqual({ - notebookserverurl: "https://localhost:10001/12345/notebook", - notebookservertoken: "token", - enablenotebooks: "true" - }); - }); +import { isResourceTokenConnectionString, parseResourceTokenConnectionString } from "./ResourceTokenUtils"; +describe("parseResourceTokenConnectionString", () => { it("correctly parses resource token connection string", () => { const connectionString = "AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;"; - const properties = Main.parseResourceTokenConnectionString(connectionString); + const properties = parseResourceTokenConnectionString(connectionString); expect(properties).toEqual({ accountEndpoint: "fakeEndpoint", @@ -32,7 +18,7 @@ describe("Main", () => { it("correctly parses resource token connection string with partition key", () => { const connectionString = "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;PartitionKey=fakePartitionKey;"; - const properties = Main.parseResourceTokenConnectionString(connectionString); + const properties = parseResourceTokenConnectionString(connectionString); expect(properties).toEqual({ accountEndpoint: "fakeEndpoint", @@ -43,3 +29,16 @@ describe("Main", () => { }); }); }); + +describe("isResourceToken", () => { + it("valid resource connection string", () => { + const connectionString = + "AccountEndpoint=fakeEndpoint;DatabaseId=fakeDatabaseId;CollectionId=fakeCollectionId;type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;"; + expect(isResourceTokenConnectionString(connectionString)).toBe(true); + }); + + it("non-resource connection string", () => { + const connectionString = "AccountEndpoint=https://stfaul-sql.documents.azure.com:443/;AccountKey=foo;"; + expect(isResourceTokenConnectionString(connectionString)).toBe(false); + }); +}); diff --git a/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts b/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts new file mode 100644 index 000000000..8fb6f51c7 --- /dev/null +++ b/src/Platform/Hosted/Helpers/ResourceTokenUtils.ts @@ -0,0 +1,43 @@ +export interface ParsedResourceTokenConnectionString { + accountEndpoint: string; + collectionId: string; + databaseId: string; + partitionKey?: string; + resourceToken: string; +} + +export function parseResourceTokenConnectionString(connectionString: string): ParsedResourceTokenConnectionString { + let accountEndpoint: string; + let collectionId: string; + let databaseId: string; + let partitionKey: string; + let resourceToken: string; + const connectionStringParts = connectionString.split(";"); + connectionStringParts.forEach((part: string) => { + if (part.startsWith("type=resource")) { + resourceToken = part + ";"; + } else if (part.startsWith("AccountEndpoint=")) { + accountEndpoint = part.substring(16); + } else if (part.startsWith("DatabaseId=")) { + databaseId = part.substring(11); + } else if (part.startsWith("CollectionId=")) { + collectionId = part.substring(13); + } else if (part.startsWith("PartitionKey=")) { + partitionKey = part.substring(13); + } else if (part !== "") { + resourceToken += part + ";"; + } + }); + + return { + accountEndpoint, + collectionId, + databaseId, + partitionKey, + resourceToken + }; +} + +export function isResourceTokenConnectionString(connectionString: string): boolean { + return connectionString && connectionString.includes("type=resource"); +} diff --git a/src/Platform/Hosted/HostedUtils.test.ts b/src/Platform/Hosted/HostedUtils.test.ts index babee3a69..3ec2ba99a 100644 --- a/src/Platform/Hosted/HostedUtils.test.ts +++ b/src/Platform/Hosted/HostedUtils.test.ts @@ -1,9 +1,9 @@ import { AccessInputMetadata } from "../../Contracts/DataModels"; -import { HostedUtils } from "./HostedUtils"; +import { getDatabaseAccountPropertiesFromMetadata } from "./HostedUtils"; describe("getDatabaseAccountPropertiesFromMetadata", () => { it("should only return an object with the mongoEndpoint key if the apiKind is mongoCompute (5)", () => { - let mongoComputeAccount: AccessInputMetadata = { + const mongoComputeAccount: AccessInputMetadata = { accountName: "compute-batch2", apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255", apiKind: 5, @@ -11,21 +11,21 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => { expiryTimestamp: "1234", mongoEndpoint: "https://compute-batch2.mongo.cosmos.azure.com:443/" }; - expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({ + expect(getDatabaseAccountPropertiesFromMetadata(mongoComputeAccount)).toEqual({ mongoEndpoint: mongoComputeAccount.mongoEndpoint, documentEndpoint: mongoComputeAccount.documentEndpoint }); }); it("should not return an object with the mongoEndpoint key if the apiKind is mongo (1)", () => { - let mongoAccount: AccessInputMetadata = { + const mongoAccount: AccessInputMetadata = { accountName: "compute-batch2", apiEndpoint: "compute-batch2.mongo.cosmos.azure.com:10255", apiKind: 1, documentEndpoint: "https://compute-batch2.documents.azure.com:443/", expiryTimestamp: "1234" }; - expect(HostedUtils.getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({ + expect(getDatabaseAccountPropertiesFromMetadata(mongoAccount)).toEqual({ documentEndpoint: mongoAccount.documentEndpoint }); }); diff --git a/src/Platform/Hosted/HostedUtils.ts b/src/Platform/Hosted/HostedUtils.ts index 601ab5895..4cf09904e 100644 --- a/src/Platform/Hosted/HostedUtils.ts +++ b/src/Platform/Hosted/HostedUtils.ts @@ -1,35 +1,50 @@ -import * as Constants from "../../Common/Constants"; -import * as DataModels from "../../Contracts/DataModels"; -import { AccessInputMetadata } from "../../Contracts/DataModels"; +import { DefaultAccountExperience, CapabilityNames, AccountKind } from "../../Common/Constants"; +import { AccessInputMetadata, ApiKind } from "../../Contracts/DataModels"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; -export class HostedUtils { - static getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): any { - let properties = { documentEndpoint: metadata.documentEndpoint }; - const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind); +export function getDatabaseAccountPropertiesFromMetadata(metadata: AccessInputMetadata): unknown { + let properties = { documentEndpoint: metadata.documentEndpoint }; + const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(metadata.apiKind); - if (apiExperience === Constants.DefaultAccountExperience.Cassandra) { + if (apiExperience === DefaultAccountExperience.Cassandra) { + properties = Object.assign(properties, { + cassandraEndpoint: metadata.apiEndpoint, + capabilities: [{ name: CapabilityNames.EnableCassandra }] + }); + } else if (apiExperience === DefaultAccountExperience.Table) { + properties = Object.assign(properties, { + tableEndpoint: metadata.apiEndpoint, + capabilities: [{ name: CapabilityNames.EnableTable }] + }); + } else if (apiExperience === DefaultAccountExperience.Graph) { + properties = Object.assign(properties, { + gremlinEndpoint: metadata.apiEndpoint, + capabilities: [{ name: CapabilityNames.EnableGremlin }] + }); + } else if (apiExperience === DefaultAccountExperience.MongoDB) { + if (metadata.apiKind === ApiKind.MongoDBCompute) { properties = Object.assign(properties, { - cassandraEndpoint: metadata.apiEndpoint, - capabilities: [{ name: Constants.CapabilityNames.EnableCassandra }] + mongoEndpoint: metadata.mongoEndpoint }); - } else if (apiExperience === Constants.DefaultAccountExperience.Table) { - properties = Object.assign(properties, { - tableEndpoint: metadata.apiEndpoint, - capabilities: [{ name: Constants.CapabilityNames.EnableTable }] - }); - } else if (apiExperience === Constants.DefaultAccountExperience.Graph) { - properties = Object.assign(properties, { - gremlinEndpoint: metadata.apiEndpoint, - capabilities: [{ name: Constants.CapabilityNames.EnableGremlin }] - }); - } else if (apiExperience === Constants.DefaultAccountExperience.MongoDB) { - if (metadata.apiKind === DataModels.ApiKind.MongoDBCompute) { - properties = Object.assign(properties, { - mongoEndpoint: metadata.mongoEndpoint - }); - } } - return properties; } + return properties; +} + +export function getDatabaseAccountKindFromExperience(apiExperience: string): string { + if (apiExperience === DefaultAccountExperience.MongoDB) { + return AccountKind.MongoDB; + } + + if (apiExperience === DefaultAccountExperience.ApiForMongoDB) { + return AccountKind.MongoDB; + } + + return AccountKind.GlobalDocumentDB; +} + +export function extractMasterKeyfromConnectionString(connectionString: string): string { + // Only Gremlin uses the actual master key for connection to cosmos + const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$"); + return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined; } diff --git a/src/Platform/Hosted/Main.ts b/src/Platform/Hosted/Main.ts deleted file mode 100644 index 672efba95..000000000 --- a/src/Platform/Hosted/Main.ts +++ /dev/null @@ -1,598 +0,0 @@ -import * as Constants from "../../Common/Constants"; -import AuthHeadersUtil from "./Authorization"; -import Q from "q"; -import { - AccessInputMetadata, - AccountKeys, - ApiKind, - DatabaseAccount, - GenerateTokenResponse, - resourceTokenConnectionStringProperties -} from "../../Contracts/DataModels"; -import { AuthType } from "../../AuthType"; -import { CollectionCreation } from "../../Shared/Constants"; -import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; -import { DataExplorerInputsFrame } from "../../Contracts/ViewModels"; -import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; -import { HostedUtils } from "./HostedUtils"; -import { sendMessage } from "../../Common/MessageHandler"; -import { MessageTypes } from "../../Contracts/ExplorerContracts"; -import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility"; -import { SubscriptionUtilMappings } from "../../Shared/Constants"; -import "../../Explorer/Tables/DataTable/DataTableBindingManager"; -import Explorer from "../../Explorer/Explorer"; -import { updateUserContext } from "../../UserContext"; -import { configContext } from "../../ConfigContext"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; - -export default class Main { - private static _databaseAccountId: string; - private static _encryptedToken: string; - private static _accessInputMetadata: AccessInputMetadata; - private static _features: { [key: string]: string }; - // For AAD, Need to post message to hosted frame to do the auth - // Use local deferred variable as work around until we find better solution - private static _getAadAccessDeferred: Q.Deferred; - private static _explorer: Explorer; - - public static isUsingEncryptionToken(): boolean { - const params = new URLSearchParams(window.parent.location.search); - if ((!!params && params.has("key")) || Main._hasCachedEncryptedKey()) { - return true; - } - return false; - } - - public static initializeExplorer(): Q.Promise { - window.addEventListener("message", this._handleMessage.bind(this), false); - this._features = {}; - const params = new URLSearchParams(window.parent.location.search); - const deferred: Q.Deferred = Q.defer(); - let authType: string = null; - - // Encrypted token flow - if (!!params && params.has("key")) { - Main._encryptedToken = encodeURIComponent(params.get("key")); - SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken); - authType = AuthType.EncryptedToken; - } else if (Main._hasCachedEncryptedKey()) { - Main._encryptedToken = SessionStorageUtility.getEntryString(StorageKey.EncryptedKeyToken); - authType = AuthType.EncryptedToken; - } - - // Aad flow - if (AuthHeadersUtil.isUserSignedIn()) { - authType = AuthType.AAD; - } - - if (params) { - this._features = Main.extractFeatures(params); - } - - (window).authType = authType; - if (!authType) { - return Q.reject("Sign in needed"); - } - - const explorer: Explorer = this._instantiateExplorer(); - if (authType === AuthType.EncryptedToken) { - sendMessage({ - type: MessageTypes.UpdateAccountSwitch, - props: { - authType: AuthType.EncryptedToken, - displayText: "Loading..." - } - }); - updateUserContext({ - accessToken: Main._encryptedToken - }); - Main._getAccessInputMetadata(Main._encryptedToken).then( - () => { - const expiryTimestamp: number = - Main._accessInputMetadata && parseInt(Main._accessInputMetadata.expiryTimestamp); - if (authType === AuthType.EncryptedToken && (isNaN(expiryTimestamp) || expiryTimestamp <= 0)) { - return deferred.reject("Token expired"); - } - - Main._initDataExplorerFrameInputs(explorer); - deferred.resolve(explorer); - }, - (error: any) => { - console.error(error); - deferred.reject(error); - } - ); - } else if (authType === AuthType.AAD) { - sendMessage({ - type: MessageTypes.GetAccessAadRequest - }); - if (this._getAadAccessDeferred != null) { - // already request aad access, don't duplicate - return Q(null); - } - this._explorer = explorer; - this._getAadAccessDeferred = Q.defer(); - return this._getAadAccessDeferred.promise.finally(() => { - this._getAadAccessDeferred = null; - }); - } else { - Main._initDataExplorerFrameInputs(explorer); - deferred.resolve(explorer); - } - - return deferred.promise; - } - - public static extractFeatures(params: URLSearchParams): { [key: string]: string } { - const featureParamRegex = /feature.(.*)/i; - const features: { [key: string]: string } = {}; - params.forEach((value: string, param: string) => { - if (featureParamRegex.test(param)) { - const matches: string[] = param.match(featureParamRegex); - if (matches.length > 0) { - features[matches[1].toLowerCase()] = value; - } - } - }); - return features; - } - - public static configureTokenValidationDisplayPrompt(explorer: Explorer): void { - const authType: AuthType = (window).authType; - if ( - !explorer || - !Main._encryptedToken || - !Main._accessInputMetadata || - Main._accessInputMetadata.expiryTimestamp == null || - authType !== AuthType.EncryptedToken - ) { - return; - } - - Main._showGuestAccessTokenRenewalPromptInMs(explorer, parseInt(Main._accessInputMetadata.expiryTimestamp)); - } - - public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties { - let accountEndpoint: string; - let collectionId: string; - let databaseId: string; - let partitionKey: string; - let resourceToken: string; - const connectionStringParts = connectionString.split(";"); - connectionStringParts.forEach((part: string) => { - if (part.startsWith("type=resource")) { - resourceToken = part + ";"; - } else if (part.startsWith("AccountEndpoint=")) { - accountEndpoint = part.substring(16); - } else if (part.startsWith("DatabaseId=")) { - databaseId = part.substring(11); - } else if (part.startsWith("CollectionId=")) { - collectionId = part.substring(13); - } else if (part.startsWith("PartitionKey=")) { - partitionKey = part.substring(13); - } else if (part !== "") { - resourceToken += part + ";"; - } - }); - - return { - accountEndpoint, - collectionId, - databaseId, - partitionKey, - resourceToken - }; - } - - public static renewExplorerAccess = (explorer: Explorer, connectionString: string): Q.Promise => { - if (!connectionString) { - console.error("Missing or invalid connection string input"); - Q.reject("Missing or invalid connection string input"); - } - - if (Main._isResourceToken(connectionString)) { - return Main._renewExplorerAccessWithResourceToken(explorer, connectionString); - } - - const deferred: Q.Deferred = Q.defer(); - AuthHeadersUtil.generateUnauthenticatedEncryptedTokenForConnectionString(connectionString).then( - (encryptedToken: GenerateTokenResponse) => { - if (!encryptedToken || !encryptedToken.readWrite) { - deferred.reject("Encrypted token is empty or undefined"); - } - - Main._encryptedToken = encryptedToken.readWrite; - window.authType = AuthType.EncryptedToken; - - updateUserContext({ - accessToken: Main._encryptedToken - }); - Main._getAccessInputMetadata(Main._encryptedToken).then( - () => { - if (explorer.isConnectExplorerVisible()) { - explorer.notificationConsoleData([]); - explorer.hideConnectExplorerForm(); - } - - if (Main._accessInputMetadata.apiKind != ApiKind.Graph) { - // do not save encrypted token for graphs because we cannot extract master key in the client - SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken); - window.parent && - window.parent.history.replaceState( - { encryptedToken: encryptedToken }, - "", - `?key=${Main._encryptedToken}${(window.parent && window.parent.location.hash) || ""}` - ); // replace query params if any - } else { - SessionStorageUtility.removeEntry(StorageKey.EncryptedKeyToken); - window.parent && - window.parent.history.replaceState( - { encryptedToken: encryptedToken }, - "", - `?${(window.parent && window.parent.location.hash) || ""}` - ); // replace query params if any - } - - const masterKey: string = Main._getMasterKeyFromConnectionString(connectionString); - Main.configureTokenValidationDisplayPrompt(explorer); - Main._setExplorerReady(explorer, masterKey); - - deferred.resolve(); - }, - (error: any) => { - console.error(error); - deferred.reject(error); - } - ); - }, - (error: any) => { - deferred.reject(`Failed to generate encrypted token: ${getErrorMessage(error)}`); - } - ); - - return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs); - }; - - public static getUninitializedExplorerForGuestAccess(): Explorer { - const explorer = Main._instantiateExplorer(); - if (window.authType === AuthType.AAD) { - this._explorer = explorer; - } - (window).dataExplorer = explorer; - - return explorer; - } - - private static _initDataExplorerFrameInputs( - explorer: Explorer, - masterKey?: string /* master key extracted from connection string if available */, - account?: DatabaseAccount, - authorizationToken?: string /* access key */ - ): void { - const serverId: string = AuthHeadersUtil.serverId; - const authType: string = (window).authType; - const accountResourceId = - authType === AuthType.EncryptedToken - ? Main._databaseAccountId - : authType === AuthType.AAD && account - ? account.id - : ""; - const subscriptionId: string = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0]; - const resourceGroup: string = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; - - explorer.isTryCosmosDBSubscription(SubscriptionUtilMappings.FreeTierSubscriptionIds.indexOf(subscriptionId) >= 0); - if (authorizationToken && authorizationToken.indexOf("Bearer") !== 0) { - // Portal sends the auth token with bearer suffix, so we prepend the same to be consistent - authorizationToken = `Bearer ${authorizationToken}`; - } - - if (authType === AuthType.EncryptedToken) { - const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( - Main._accessInputMetadata.apiKind - ); - sendMessage({ - type: MessageTypes.UpdateAccountSwitch, - props: { - authType: AuthType.EncryptedToken, - selectedAccountName: Main._accessInputMetadata.accountName - } - }); - return explorer.initDataExplorerWithFrameInputs({ - databaseAccount: { - id: Main._databaseAccountId, - name: Main._accessInputMetadata.accountName, - kind: this._getDatabaseAccountKindFromExperience(apiExperience), - properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata), - tags: { defaultExperience: apiExperience } - }, - subscriptionId, - resourceGroup, - masterKey, - hasWriteAccess: true, // TODO: we should embed this information in the token ideally - authorizationToken: undefined, - features: this._features, - csmEndpoint: undefined, - dnsSuffix: null, - serverId: serverId, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() - }); - } - - if (authType === AuthType.AAD) { - const inputs: DataExplorerInputsFrame = { - databaseAccount: account, - subscriptionId, - resourceGroup, - masterKey, - hasWriteAccess: true, //TODO: 425017 - support read access - authorizationToken, - features: this._features, - csmEndpoint: undefined, - dnsSuffix: null, - serverId: serverId, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription() - }; - return explorer.initDataExplorerWithFrameInputs(inputs); - } - - if (authType === AuthType.ResourceToken) { - const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( - Main._accessInputMetadata.apiKind - ); - return explorer.initDataExplorerWithFrameInputs({ - databaseAccount: { - id: Main._databaseAccountId, - name: Main._accessInputMetadata.accountName, - kind: this._getDatabaseAccountKindFromExperience(apiExperience), - properties: HostedUtils.getDatabaseAccountPropertiesFromMetadata(Main._accessInputMetadata), - tags: { defaultExperience: apiExperience } - }, - subscriptionId, - resourceGroup, - masterKey, - hasWriteAccess: true, // TODO: we should embed this information in the token ideally - authorizationToken: undefined, - features: this._features, - csmEndpoint: undefined, - dnsSuffix: null, - serverId: serverId, - extensionEndpoint: configContext.BACKEND_ENDPOINT, - subscriptionType: CollectionCreation.DefaultSubscriptionType, - quotaId: undefined, - addCollectionDefaultFlight: explorer.flight(), - isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(), - isAuthWithresourceToken: true - }); - } - - throw new Error(`Unsupported AuthType ${authType}`); - } - - private static _instantiateExplorer(): Explorer { - const explorer = new Explorer(); - // workaround to resolve cyclic refs with view - explorer.renewExplorerShareAccess = Main.renewExplorerAccess; - window.addEventListener("message", explorer.handleMessage.bind(explorer), false); - - // Hosted needs click to dismiss any menu - if (window.authType === AuthType.AAD) { - window.addEventListener( - "click", - () => { - sendMessage({ - type: MessageTypes.ExplorerClickEvent - }); - }, - true - ); - } - - return explorer; - } - - private static _showGuestAccessTokenRenewalPromptInMs(explorer: Explorer, interval: number): void { - if (interval != null && !isNaN(interval)) { - setTimeout(() => { - explorer.displayGuestAccessTokenRenewalPrompt(); - }, interval); - } - } - - private static _hasCachedEncryptedKey(): boolean { - return SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken); - } - - private static _getDatabaseAccountKindFromExperience(apiExperience: string): string { - if (apiExperience === Constants.DefaultAccountExperience.MongoDB) { - return Constants.AccountKind.MongoDB; - } - - if (apiExperience === Constants.DefaultAccountExperience.ApiForMongoDB) { - return Constants.AccountKind.MongoDB; - } - - return Constants.AccountKind.GlobalDocumentDB; - } - - private static _getAccessInputMetadata(accessInput: string): Q.Promise { - const deferred: Q.Deferred = Q.defer(); - AuthHeadersUtil.getAccessInputMetadata(accessInput).then( - (metadata: any) => { - Main._accessInputMetadata = metadata; - deferred.resolve(); - }, - (error: any) => { - deferred.reject(error); - } - ); - - return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs); - } - - private static _getMasterKeyFromConnectionString(connectionString: string): string { - if (!connectionString || Main._accessInputMetadata == null || Main._accessInputMetadata.apiKind !== ApiKind.Graph) { - // client only needs master key for Graph API - return undefined; - } - - const matchedParts: string[] = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$"); - return (matchedParts.length > 1 && matchedParts[1]) || undefined; - } - - private static _isResourceToken(connectionString: string): boolean { - return connectionString && connectionString.includes("type=resource"); - } - - private static _renewExplorerAccessWithResourceToken = ( - explorer: Explorer, - connectionString: string - ): Q.Promise => { - window.authType = AuthType.ResourceToken; - - const properties: resourceTokenConnectionStringProperties = Main.parseResourceTokenConnectionString( - connectionString - ); - if ( - !properties.accountEndpoint || - !properties.resourceToken || - !properties.databaseId || - !properties.collectionId - ) { - console.error("Invalid connection string input"); - Q.reject("Invalid connection string input"); - } - updateUserContext({ - resourceToken: properties.resourceToken, - endpoint: properties.accountEndpoint - }); - explorer.resourceTokenDatabaseId(properties.databaseId); - explorer.resourceTokenCollectionId(properties.collectionId); - if (properties.partitionKey) { - explorer.resourceTokenPartitionKey(properties.partitionKey); - } - Main._accessInputMetadata = Main._getAccessInputMetadataFromAccountEndpoint(properties.accountEndpoint); - - if (explorer.isConnectExplorerVisible()) { - explorer.notificationConsoleData([]); - explorer.hideConnectExplorerForm(); - } - - Main._setExplorerReady(explorer); - return Q.resolve(); - }; - - private static _getAccessInputMetadataFromAccountEndpoint = (accountEndpoint: string): AccessInputMetadata => { - const documentEndpoint: string = accountEndpoint; - const result: RegExpMatchArray = accountEndpoint.match("https://([^\\.]+)\\..+"); - const accountName: string = result && result[1]; - const apiEndpoint: string = accountEndpoint.substring(8); - const apiKind: number = ApiKind.SQL; - - return { - accountName, - apiEndpoint, - apiKind, - documentEndpoint, - expiryTimestamp: "" - }; - }; - - private static _setExplorerReady( - explorer: Explorer, - masterKey?: string, - account?: DatabaseAccount, - authorizationToken?: string - ) { - Main._initDataExplorerFrameInputs(explorer, masterKey, account, authorizationToken); - explorer.isAccountReady.valueHasMutated(); - sendMessage("ready"); - } - - private static _shouldProcessMessage(event: MessageEvent): boolean { - if (typeof event.data !== "object") { - return false; - } - if (event.data["signature"] !== "pcIframe") { - return false; - } - if (!("data" in event.data)) { - return false; - } - if (typeof event.data["data"] !== "object") { - return false; - } - return true; - } - - private static _handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - if (!this._shouldProcessMessage(event)) { - return; - } - - const message: any = event.data.data; - if (message.type) { - if (message.type === MessageTypes.GetAccessAadResponse && (message.response || message.error)) { - if (message.response) { - Main._handleGetAccessAadSucceed(message.response); - } - if (message.error) { - Main._handleGetAccessAadFailed(message.error); - } - return; - } - if (message.type === MessageTypes.SwitchAccount && message.account && message.keys) { - Main._handleSwitchAccountSucceed(message.account, message.keys, message.authorizationToken); - return; - } - } - } - - private static _handleSwitchAccountSucceed(account: DatabaseAccount, keys: AccountKeys, authorizationToken: string) { - if (!this._explorer) { - console.error("no explorer found"); - return; - } - - this._explorer.hideConnectExplorerForm(); - - const masterKey = Main._getMasterKey(keys); - this._explorer.notificationConsoleData([]); - Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken); - } - - private static _handleGetAccessAadSucceed(response: [DatabaseAccount, AccountKeys, string]) { - if (!response || response.length < 1) { - return; - } - const account = response[0]; - const masterKey = Main._getMasterKey(response[1]); - const authorizationToken = response[2]; - Main._setExplorerReady(this._explorer, masterKey, account, authorizationToken); - this._getAadAccessDeferred.resolve(this._explorer); - } - - private static _getMasterKey(keys: AccountKeys): string { - return ( - keys?.primaryMasterKey ?? - keys?.secondaryMasterKey ?? - keys?.primaryReadonlyMasterKey ?? - keys?.secondaryReadonlyMasterKey - ); - } - - private static _handleGetAccessAadFailed(error: any) { - this._getAadAccessDeferred.reject(error); - } -} diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts new file mode 100644 index 000000000..3567134d2 --- /dev/null +++ b/src/Platform/Hosted/extractFeatures.test.ts @@ -0,0 +1,17 @@ +import { extractFeatures } from "./extractFeatures"; + +describe("extractFeatures", () => { + it("correctly detects feature flags", () => { + // Search containing non-features, with Camelcase keys and uri encoded values + const params = new URLSearchParams( + "?platform=Hosted&feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true&key=mykey" + ); + const features = extractFeatures(params); + + expect(features).toEqual({ + notebookserverurl: "https://localhost:10001/12345/notebook", + notebookservertoken: "token", + enablenotebooks: "true" + }); + }); +}); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts new file mode 100644 index 000000000..05581c27b --- /dev/null +++ b/src/Platform/Hosted/extractFeatures.ts @@ -0,0 +1,14 @@ +export function extractFeatures(params?: URLSearchParams): { [key: string]: string } { + params = params || new URLSearchParams(window.parent.location.search); + const featureParamRegex = /feature.(.*)/i; + const features: { [key: string]: string } = {}; + params.forEach((value: string, param: string) => { + if (featureParamRegex.test(param)) { + const matches: string[] = param.match(featureParamRegex); + if (matches.length > 0) { + features[matches[1].toLowerCase()] = value; + } + } + }); + return features; +} diff --git a/src/Platform/Portal/Main.ts b/src/Platform/Portal/Main.ts deleted file mode 100644 index 5d0b652ab..000000000 --- a/src/Platform/Portal/Main.ts +++ /dev/null @@ -1,23 +0,0 @@ -import "../../Explorer/Tables/DataTable/DataTableBindingManager"; -import Explorer from "../../Explorer/Explorer"; -import { handleMessage } from "../../Controls/Heatmap/Heatmap"; - -export function initializeExplorer(): Explorer { - const explorer = new Explorer(); - - // In development mode, try to load the iframe message from session storage. - // This allows webpack hot reload to funciton properly - if (process.env.NODE_ENV === "development") { - const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); - if (initMessage) { - const message = JSON.parse(initMessage); - console.warn("Loaded cached portal iframe message from session storage"); - console.dir(message); - explorer.initDataExplorerWithFrameInputs(message); - } - } - - window.addEventListener("message", explorer.handleMessage.bind(explorer), false); - - return explorer; -} diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 48f023dc9..eda4d766d 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -1,11 +1,9 @@ -import * as Constants from "../Common/Constants"; -import * as ViewModels from "../Contracts/ViewModels"; -import AuthHeadersUtil from "../Platform/Hosted/Authorization"; import { AuthType } from "../AuthType"; +import * as Constants from "../Common/Constants"; import * as Logger from "../Common/Logger"; import { configContext, Platform } from "../ConfigContext"; +import * as ViewModels from "../Contracts/ViewModels"; import { userContext } from "../UserContext"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { if (window.authType === AuthType.EncryptedToken) { @@ -21,19 +19,6 @@ export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMet } } -export async function getArcadiaAuthToken( - arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT, - tenantId?: string -): Promise { - try { - const token = await AuthHeadersUtil.getAccessToken(arcadiaEndpoint, tenantId); - return token; - } catch (error) { - Logger.logError(getErrorMessage(error), "AuthorizationUtils/getArcadiaAuthToken"); - throw error; - } -} - export function decryptJWTToken(token: string) { if (!token) { Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken"); diff --git a/src/Utils/GitHubUtils.ts b/src/Utils/GitHubUtils.ts index 7fc6f3ea9..2b9cc2d04 100644 --- a/src/Utils/GitHubUtils.ts +++ b/src/Utils/GitHubUtils.ts @@ -42,8 +42,6 @@ export function fromContentUri( matches = contentUri.match(LegacyContentUriPattern); if (matches && matches.length > 4) { - console.log(`Using legacy github content uri scheme ${contentUri}`); - return { owner: matches[1], repo: matches[2], diff --git a/src/Utils/UserUtils.test.ts b/src/Utils/UserUtils.test.ts deleted file mode 100644 index 8a87d8bcf..000000000 --- a/src/Utils/UserUtils.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import AuthHeadersUtil from "../Platform/Hosted/Authorization"; -import * as UserUtils from "./UserUtils"; - -describe("UserUtils", () => { - it("getFullName works in regular data explorer (inside portal)", () => { - const user: AuthenticationContext.UserInfo = { - userName: "userName", - profile: { - name: "name" - } - }; - AuthHeadersUtil.getCachedUser = jest.fn().mockReturnValue(user); - - expect(UserUtils.getFullName()).toBe("name"); - }); - - it("getFullName works in fullscreen data explorer (outside portal)", () => { - jest.mock("./AuthorizationUtils", () => { - (): { name: string } => ({ name: "name" }); - }); - - expect(UserUtils.getFullName()).toBe("name"); - }); -}); diff --git a/src/Utils/UserUtils.ts b/src/Utils/UserUtils.ts index 33015e83c..5275b2702 100644 --- a/src/Utils/UserUtils.ts +++ b/src/Utils/UserUtils.ts @@ -1,17 +1,8 @@ -import AuthHeadersUtil from "../Platform/Hosted/Authorization"; import { decryptJWTToken } from "./AuthorizationUtils"; import { userContext } from "../UserContext"; export function getFullName(): string { - let fullName: string; - const user = AuthHeadersUtil.getCachedUser(); - if (user) { - fullName = user.profile.name; - } else { - const authToken = userContext.authorizationToken; - const props = decryptJWTToken(authToken); - fullName = props.name; - } - - return fullName; + const authToken = userContext.authorizationToken; + const props = decryptJWTToken(authToken); + return props.name; } diff --git a/src/applyExplorerBindings.ts b/src/applyExplorerBindings.ts index 570a65aa6..56114f1dc 100644 --- a/src/applyExplorerBindings.ts +++ b/src/applyExplorerBindings.ts @@ -5,12 +5,12 @@ import Explorer from "./Explorer/Explorer"; export const applyExplorerBindings = (explorer: Explorer) => { if (!!explorer) { - // This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times. - // TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal - sendMessage("ready"); window.dataExplorer = explorer; BindingHandlersRegisterer.registerBindingHandlers(); ko.applyBindings(explorer); + // This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times. + // TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal + sendMessage("ready"); $("#divExplorer").show(); } }; diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts new file mode 100644 index 000000000..dce78d087 --- /dev/null +++ b/src/hooks/useAADAuth.ts @@ -0,0 +1,101 @@ +import * as React from "react"; +import { useBoolean } from "@uifabric/react-hooks"; +import { UserAgentApplication, Account, Configuration } from "msal"; + +const config: Configuration = { + cache: { + cacheLocation: "localStorage" + }, + auth: { + authority: "https://login.microsoftonline.com/common", + clientId: "203f1145-856a-4232-83d4-a43568fba23d" + } +}; + +if (process.env.NODE_ENV === "development") { + config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net"; +} + +const msal = new UserAgentApplication(config); + +const cachedAccount = msal.getAllAccounts()?.[0]; +const cachedTenantId = localStorage.getItem("cachedTenantId"); + +interface ReturnType { + isLoggedIn: boolean; + graphToken: string; + armToken: string; + login: () => void; + logout: () => void; + tenantId: string; + account: Account; + switchTenant: (tenantId: string) => void; +} + +export function useAADAuth(): ReturnType { + const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean( + Boolean(cachedAccount && cachedTenantId) || false + ); + const [account, setAccount] = React.useState(cachedAccount); + const [tenantId, setTenantId] = React.useState(cachedTenantId); + const [graphToken, setGraphToken] = React.useState(); + const [armToken, setArmToken] = React.useState(); + + const login = React.useCallback(async () => { + const response = await msal.loginPopup(); + setLoggedIn(); + setAccount(response.account); + setTenantId(response.tenantId); + localStorage.setItem("cachedTenantId", response.tenantId); + }, []); + + const logout = React.useCallback(() => { + setLoggedOut(); + localStorage.removeItem("cachedTenantId"); + msal.logout(); + }, []); + + const switchTenant = React.useCallback( + async id => { + const response = await msal.loginPopup({ + authority: `https://login.microsoftonline.com/${id}` + }); + setTenantId(response.tenantId); + setAccount(response.account); + }, + [account, tenantId] + ); + + React.useEffect(() => { + if (account && tenantId) { + Promise.all([ + msal.acquireTokenSilent({ + // There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority + forceRefresh: true, + authority: `https://login.microsoftonline.com/${tenantId}`, + scopes: ["https://graph.windows.net//.default"] + }), + msal.acquireTokenSilent({ + // There is a bug in MSALv1 that requires us to refresh the token. Their internal cache is not respecting authority + forceRefresh: true, + authority: `https://login.microsoftonline.com/${tenantId}`, + scopes: ["https://management.azure.com//.default"] + }) + ]).then(([graphTokenResponse, armTokenResponse]) => { + setGraphToken(graphTokenResponse.accessToken); + setArmToken(armTokenResponse.accessToken); + }); + } + }, [account, tenantId]); + + return { + account, + tenantId, + isLoggedIn, + graphToken, + armToken, + login, + logout, + switchTenant + }; +} diff --git a/src/hooks/useDatabaseAccounts.tsx b/src/hooks/useDatabaseAccounts.tsx new file mode 100644 index 000000000..513266b50 --- /dev/null +++ b/src/hooks/useDatabaseAccounts.tsx @@ -0,0 +1,38 @@ +import useSWR from "swr"; +import { DatabaseAccount } from "../Contracts/DataModels"; + +interface AccountListResult { + nextLink: string; + value: DatabaseAccount[]; +} + +export async function fetchDatabaseAccounts(subscriptionId: string, accessToken: string): Promise { + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + let accounts: Array = []; + + let nextLink = `https://management.azure.com/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-06-01-preview`; + + while (nextLink) { + const response: Response = await fetch(nextLink, { headers }); + const result: AccountListResult = + response.status === 204 || response.status === 304 ? undefined : await response.json(); + if (!response.ok) { + throw result; + } + nextLink = result.nextLink; + accounts = [...accounts, ...result.value]; + } + return accounts; +} + +export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined { + const { data } = useSWR( + () => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined), + (_, subscriptionId, armToken) => fetchDatabaseAccounts(subscriptionId, armToken) + ); + return data; +} diff --git a/src/hooks/useDirectories.tsx b/src/hooks/useDirectories.tsx new file mode 100644 index 000000000..70b36e18e --- /dev/null +++ b/src/hooks/useDirectories.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; +import { Tenant } from "../Contracts/DataModels"; + +interface TenantListResult { + nextLink: string; + value: Tenant[]; +} + +export async function fetchDirectories(accessToken: string): Promise { + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + let tenents: Array = []; + let nextLink = `https://management.azure.com/tenants?api-version=2020-01-01`; + + while (nextLink) { + const response = await fetch(nextLink, { headers }); + const result: TenantListResult = + response.status === 204 || response.status === 304 ? undefined : await response.json(); + if (!response.ok) { + throw result; + } + nextLink = result.nextLink; + tenents = [...tenents, ...result.value]; + } + return tenents; +} + +export function useDirectories(armToken: string): Tenant[] { + const [state, setState] = useState(); + + useEffect(() => { + if (armToken) { + fetchDirectories(armToken).then(response => setState(response)); + } + }, [armToken]); + return state || []; +} diff --git a/src/hooks/useGraphPhoto.tsx b/src/hooks/useGraphPhoto.tsx new file mode 100644 index 000000000..92b11925c --- /dev/null +++ b/src/hooks/useGraphPhoto.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; + +export async function fetchPhoto(accessToken: string): Promise { + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + headers.append("Content-Type", "image/jpg"); + + const options = { + method: "GET", + headers: headers + }; + + return fetch("https://graph.windows.net/me/thumbnailPhoto?api-version=1.6", options).then(response => + response.blob() + ); +} + +export function useGraphPhoto(graphToken: string): string { + const [photo, setPhoto] = useState(); + + useEffect(() => { + if (graphToken) { + fetchPhoto(graphToken).then(response => setPhoto(URL.createObjectURL(response))); + } + }, [graphToken]); + return photo; +} diff --git a/src/hooks/usePortalAccessToken.tsx b/src/hooks/usePortalAccessToken.tsx new file mode 100644 index 000000000..7a4d856a2 --- /dev/null +++ b/src/hooks/usePortalAccessToken.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { ApiEndpoints } from "../Common/Constants"; +import { configContext } from "../ConfigContext"; +import { AccessInputMetadata } from "../Contracts/DataModels"; + +const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`; + +export async function fetchAccessData(portalToken: string): Promise { + const headers = new Headers(); + // Portal encrypted token API quirk: The token header must be URL encoded + headers.append("x-ms-encrypted-auth-token", encodeURIComponent(portalToken)); + + const options = { + method: "GET", + headers: headers + }; + + return ( + fetch(url, options) + .then(response => response.json()) + // Portal encrypted token API quirk: The response is double JSON encoded + .then(json => JSON.parse(json)) + .catch(error => console.error(error)) + ); +} + +export function useTokenMetadata(token: string): AccessInputMetadata { + const [state, setState] = useState(); + + useEffect(() => { + if (token) { + fetchAccessData(token).then(response => setState(response)); + } + }, [token]); + return state; +} diff --git a/src/hooks/useSubscriptions.tsx b/src/hooks/useSubscriptions.tsx new file mode 100644 index 000000000..d5627c2d6 --- /dev/null +++ b/src/hooks/useSubscriptions.tsx @@ -0,0 +1,40 @@ +import { Subscription } from "../Contracts/DataModels"; +import useSWR from "swr"; + +interface SubscriptionListResult { + nextLink: string; + value: Subscription[]; +} + +export async function fetchSubscriptions(accessToken: string): Promise { + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + let subscriptions: Array = []; + let nextLink = `https://management.azure.com/subscriptions?api-version=2020-01-01`; + + while (nextLink) { + const response = await fetch(nextLink, { headers }); + const result: SubscriptionListResult = + response.status === 204 || response.status === 304 ? undefined : await response.json(); + if (!response.ok) { + throw result; + } + nextLink = result.nextLink; + const validSubscriptions = result.value.filter( + sub => sub.state === "Enabled" || sub.state === "Warned" || sub.state === "PastDue" + ); + subscriptions = [...subscriptions, ...validSubscriptions]; + } + return subscriptions; +} + +export function useSubscriptions(armToken: string): Subscription[] | undefined { + const { data } = useSWR( + () => (armToken ? ["subscriptions", armToken] : undefined), + (_, armToken) => fetchSubscriptions(armToken) + ); + return data; +} diff --git a/src/hostedExplorer.html b/src/hostedExplorer.html index 07763f12d..1c46ca907 100644 --- a/src/hostedExplorer.html +++ b/src/hostedExplorer.html @@ -7,62 +7,6 @@ - -
- -
- - - - - - -
- - -
- -
-
+
diff --git a/src/setupTests.ts b/src/setupTests.ts index 70040c7a5..1d06b89ca 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1,7 +1,9 @@ import Adapter from "enzyme-adapter-react-16"; import { configure } from "enzyme"; import "jest-canvas-mock"; +import { initializeIcons } from "office-ui-fabric-react"; configure({ adapter: new Adapter() }); +initializeIcons(); if (typeof window.URL.createObjectURL === "undefined") { Object.defineProperty(window.URL, "createObjectURL", { value: () => {} }); diff --git a/test/sql/resourceToken.spec.ts b/test/sql/resourceToken.spec.ts new file mode 100644 index 000000000..bf9907f75 --- /dev/null +++ b/test/sql/resourceToken.spec.ts @@ -0,0 +1,70 @@ +/* eslint-disable jest/expect-expect */ +import "expect-puppeteer"; +import { Frame } from "puppeteer"; +import { generateUniqueName } from "../utils/shared"; +import { CosmosClient, PermissionMode } from "@azure/cosmos"; + +jest.setTimeout(300000); +const RETRY_DELAY = 5000; +const CREATE_DELAY = 10000; + +describe("Collection Add and Delete SQL spec", () => { + it("creates a collection", async () => { + const dbId = generateUniqueName("db"); + const collectionId = generateUniqueName("col"); + const connectionString = process.env.PORTAL_RUNNER_CONNECTION_STRING; + const client = new CosmosClient(connectionString); + const endpoint = /AccountEndpoint=(.*);/.exec(connectionString)[1]; + const { database } = await client.databases.createIfNotExists({ id: dbId }); + const { container } = await database.containers.createIfNotExists({ id: collectionId }); + const { user } = await database.users.upsert({ id: "testUser" }); + const { resource: containerPermission } = await user.permissions.upsert({ + id: "partitionLevelPermission", + permissionMode: PermissionMode.All, + resource: container.url + }); + const resourceTokenConnectionString = `AccountEndpoint=${endpoint};DatabaseId=${database.id};CollectionId=${container.id};${containerPermission._token}`; + try { + await page.goto(process.env.DATA_EXPLORER_ENDPOINT); + await page.waitFor("div > p.switchConnectTypeText", { visible: true }); + await page.click("div > p.switchConnectTypeText"); + await page.type("input[class='inputToken']", resourceTokenConnectionString); + await page.click("input[value='Connect']"); + const handle = await page.waitForSelector("iframe"); + const frame = await handle.contentFrame(); + // validate created + // open database menu + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await frame.waitFor(CREATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); + expect(await frame.$(`span[title="${collectionId}"]`)).toBeDefined(); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const testName = (expect as any).getState().currentTestName; + await page.screenshot({ path: `failed-${testName}.jpg` }); + throw error; + } + }); +}); + +async function clickDBMenu(dbId: string, frame: Frame, retries = 0) { + const button = await frame.$(`div[data-test="${dbId}"]`); + await button.focus(); + const handler = await button.asElement(); + await handler.click(); + await ensureMenuIsOpen(dbId, frame, retries); + return button; +} + +async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) { + await frame.waitFor(RETRY_DELAY); + const button = await frame.$(`div[data-test="${dbId}"]`); + const classList = await frame.evaluate(button => { + return button.parentElement.classList; + }, button); + if (!Object.values(classList).includes("selected") && retries < 5) { + retries = retries + 1; + await clickDBMenu(dbId, frame, retries); + } +} diff --git a/test/utils/shared.ts b/test/utils/shared.ts index 1bc87bbef..2f7a92705 100644 --- a/test/utils/shared.ts +++ b/test/utils/shared.ts @@ -9,13 +9,13 @@ export async function login(connectionString: string): Promise { return page.mainFrame(); } // log in with connection string + await page.waitFor("div > p.switchConnectTypeText", { visible: true }); + await page.click("div > p.switchConnectTypeText"); + const connStr = connectionString; + await page.type("input[class='inputToken']", connStr); + await page.click("input[value='Connect']"); const handle = await page.waitForSelector("iframe"); const frame = await handle.contentFrame(); - await frame.waitFor("div > p.switchConnectTypeText", { visible: true }); - await frame.click("div > p.switchConnectTypeText"); - const connStr = connectionString; - await frame.type("input[class='inputToken']", connStr); - await frame.click("input[value='Connect']"); return frame; } diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 67c287714..ab1df0c27 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -8,7 +8,6 @@ }, "files": [ "./src/AuthType.ts", - "./src/Bindings/BindingHandlersRegisterer.ts", "./src/Bindings/ReactBindingHandler.ts", "./src/Common/ArrayHashMap.ts", "./src/Common/Constants.ts", diff --git a/utils/armClientGenerator/generator.ts b/utils/armClientGenerator/generator.ts index b4bd7023f..7a1d70c0b 100644 --- a/utils/armClientGenerator/generator.ts +++ b/utils/armClientGenerator/generator.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /// import { writeFileSync } from "fs"; import mkdirp from "mkdirp"; diff --git a/webpack.config.js b/webpack.config.js index a90e8896c..7ffaefaeb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -102,6 +102,7 @@ module.exports = function(env = {}, argv = {}) { if (mode === "development") { envVars.NODE_ENV = "development"; + typescriptRule.use[0].options.compilerOptions = { target: "ES2018" }; } const plugins = [ @@ -182,7 +183,7 @@ module.exports = function(env = {}, argv = {}) { main: "./src/Main.tsx", index: "./src/Index.ts", quickstart: "./src/quickstart.ts", - hostedExplorer: "./src/HostedExplorer.ts", + hostedExplorer: "./src/HostedExplorer.tsx", testExplorer: "./test/notebooks/testExplorer/TestExplorer.ts", heatmap: "./src/Controls/Heatmap/Heatmap.ts", terminal: "./src/Terminal/index.ts",