diff --git a/CHANGELOG.md b/CHANGELOG.md index df1294ad3..410d99f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) ### Changed - Fix format script and format codebase ([#782](https://github.com/cyverse/troposphere/pull/782)) - Travis will also check that the code is formatted from now on - +### Added + - Add ability to create, edit, and delete "Personal Access Tokens" from the advanced section on the "settings" view ([#789](https://github.com/cyverse/troposphere/pull/789)) ## [v33-0](https://github.com/cyverse/troposphere/compare/v32-0...v33-0) - 2018-08-06 ### Changed - Suggest adopting a changelog format @@ -64,7 +65,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) ([#750](https://github.com/cyverse/troposphere/pull/750)) - Solves problem where requests were being 'approved' while the resources were not being updated - ### Changed - Change ./manage.py maintenance to be non-interactive ([#769](https://github.com/cyverse/troposphere/pull/769)) diff --git a/troposphere/static/js/actions/APITokenActions.js b/troposphere/static/js/actions/APITokenActions.js new file mode 100644 index 000000000..3b3985f6e --- /dev/null +++ b/troposphere/static/js/actions/APITokenActions.js @@ -0,0 +1,65 @@ +import APITokenConstants from "constants/APITokenConstants"; +import APIToken from "models/APIToken"; +import NotificationController from "controllers/NotificationController"; +import Utils from "./Utils"; + +export default { + create: name => { + if (!name) throw new Error("Missing Token name"); + let apiToken = new APIToken({ + name + }); + + let promise = Promise.resolve(apiToken.save()); + promise.then(() => { + Utils.dispatch(APITokenConstants.ADD_TOKEN, {apiToken}); + }); + promise.catch(() => { + NotificationController.error( + "Error creating token.", + "Your login might be expired. If you continue to see this error " + + "after logging in again, contact support." + ); + }); + return promise; + }, + update: (apiToken, newAttributes) => { + let prevAttributes = Object.assign({}, apiToken.attributes); + + apiToken.set(newAttributes); + Utils.dispatch(APITokenConstants.UPDATE_TOKEN, {apiToken}); + apiToken + .save(newAttributes, {patch: true}) + .done(() => { + Utils.dispatch(APITokenConstants.UPDATE_TOKEN, {apiToken}); + }) + .fail(response => { + Utils.displayError({ + title: "Token could not be saved", + response + }); + apiToken.set(prevAttributes); + Utils.dispatch(APITokenConstants.UPDATE_TOKEN, {apiToken}); + }); + return apiToken; + }, + destroy: apiToken => { + // Destroy token optimistically + Utils.dispatch(APITokenConstants.REMOVE_TOKEN, {apiToken}); + + apiToken + .destroy() + .done(() => { + Utils.dispatch(APITokenConstants.REMOVE_TOKEN, {apiToken}); + }) + .fail(() => { + Utils.dispatch(APITokenConstants.UPDATE_TOKEN, {apiToken}); + NotificationController.error( + "Error deleting token.", + "Your login might be expired. If you continue to see this error " + + "after logging in again, contact support." + ); + }); + return apiToken; + } +}; diff --git a/troposphere/static/js/bootstrapper.js b/troposphere/static/js/bootstrapper.js index 49d2a5c31..95b61d169 100644 --- a/troposphere/static/js/bootstrapper.js +++ b/troposphere/static/js/bootstrapper.js @@ -91,6 +91,7 @@ stores.AllocationSourceStore = require("stores/AllocationSourceStore"); import actions from "actions"; actions.AccountActions = require("actions/AccountActions"); +actions.APITokenActions = require("actions/APITokenActions"); actions.BadgeActions = require("actions/BadgeActions"); actions.GroupActions = require("actions/GroupActions"); actions.ExternalLinkActions = require("actions/ExternalLinkActions"); diff --git a/troposphere/static/js/collections/APITokenCollection.js b/troposphere/static/js/collections/APITokenCollection.js new file mode 100644 index 000000000..ed6ab0c37 --- /dev/null +++ b/troposphere/static/js/collections/APITokenCollection.js @@ -0,0 +1,11 @@ +import Backbone from "backbone"; +import APIToken from "models/APIToken"; +import globals from "globals"; + +export default Backbone.Collection.extend({ + model: APIToken, + url: globals.API_V2_ROOT + "/access_tokens", + parse: function(data) { + return data.results; + } +}); diff --git a/troposphere/static/js/components/modals/api_token/APITokenCreate.jsx b/troposphere/static/js/components/modals/api_token/APITokenCreate.jsx new file mode 100644 index 000000000..97845af1f --- /dev/null +++ b/troposphere/static/js/components/modals/api_token/APITokenCreate.jsx @@ -0,0 +1,182 @@ +import React from "react"; +import BootstrapModalMixin from "components/mixins/BootstrapModalMixin"; +import actions from "actions"; +import {RaisedButton, CircularProgress} from "material-ui"; +import WarningIcon from "material-ui/svg-icons/alert/warning"; +import CopyButton from "components/common/ui/CopyButton"; + +export default React.createClass({ + mixins: [BootstrapModalMixin], + + getInitialState() { + return { + errorMsg: "", + name: "", + hash: "", + successView: false, + isSubmitting: false + }; + }, + + updateName(e) { + this.setState({ + name: e.target.value + }); + }, + + onSubmit() { + const {name} = this.state; + this.setState({ + isSubmitting: true + }); + let promise = actions.APITokenActions.create(name.trim()); + promise.then(this.onSuccess); + promise.catch(this.onError); + }, + + onSuccess(response) { + this.setState({ + isSubmitting: false, + successView: true, + hash: response.token + }); + }, + + onError(response) { + this.hide(); + }, + + renderFormView() { + return ( +
+

+ Give your Access Token a name to help you remember where you + are using it. After you create your token you will only have + one chance to copy it somewhere safe. +

+
+ +
+ + * Name Required +
+
+
+ ); + }, + + successStyles() { + return { + infoBlock: {display: "flex", marginBottom: "16px"}, + infoBlockIcon: {marginRight: "16px", flex: "1 0 24px"}, + figureCaption: {fontWeight: "600"}, + figureBody: { + borderRadius: "4px", + border: "solid rgba(0,0,0,.3) 1px", + padding: "8px", + fontSize: "16px", + fontFamily: "monospace" + } + }; + }, + + renderSuccessView() { + const {name, hash} = this.state; + const styles = this.successStyles(); + return ( +
+

{`Token "${name}" Created Successfully!`}

+
+ +

+ Make sure you copy this token now. You will not have + another chance after this modal is closed! +

+
+
+
+ Your Public Access Token +
+
+ {hash} + +
+
+
+ ); + }, + + renderSubmitButton() { + if (this.state.successView) { + return ( + + ); + } else if (this.state.isSubmitting) { + return ( + } + label="creating..." + /> + ); + } else { + return ( + + ); + } + }, + + render() { + const {successView, isSubmitting} = this.state; + + return ( +
+
+
+
+ {this.renderCloseButton} +

+ Create Personal Access Token +

+
+
+ {successView + ? this.renderSuccessView() + : this.renderFormView()} +
+
+ {successView || isSubmitting ? null : ( + + )} + {this.renderSubmitButton()} +
+
+
+
+ ); + } +}); diff --git a/troposphere/static/js/components/modals/api_token/APITokenDelete.jsx b/troposphere/static/js/components/modals/api_token/APITokenDelete.jsx new file mode 100644 index 000000000..959ebebb9 --- /dev/null +++ b/troposphere/static/js/components/modals/api_token/APITokenDelete.jsx @@ -0,0 +1,69 @@ +import React from "react"; +import Backbone from "backbone"; +import actions from "actions"; +import BootstrapModalMixin from "components/mixins/BootstrapModalMixin"; +import {RaisedButton} from "material-ui"; +import WarningIcon from "material-ui/svg-icons/alert/warning"; +import subscribe from "utilities/subscribe"; + +const APITokenDelete = React.createClass({ + propTypes: { + token: React.PropTypes.instanceOf(Backbone.Model) + }, + + mixins: [BootstrapModalMixin], + + onSubmit() { + this.hide(); + let token = this.props.token; + actions.APITokenActions.destroy(token); + }, + + render() { + const {token} = this.props; + const name = token.get("name"); + return ( +
+
+
+
+ {this.renderCloseButton()} +

+ Delete Personal Access Token +

+
+
+
+ {" "} +

+ {`Are you sure you want to delete Access Token "${name}"? Any applications using this Token will not be able to connect to your account`} +

+
+
+
+ + +
+
+
+
+ ); + } +}); + +export default subscribe(APITokenDelete, ["APITokenStore"]); diff --git a/troposphere/static/js/components/modals/api_token/APITokenEdit.jsx b/troposphere/static/js/components/modals/api_token/APITokenEdit.jsx new file mode 100644 index 000000000..a8ac95ba2 --- /dev/null +++ b/troposphere/static/js/components/modals/api_token/APITokenEdit.jsx @@ -0,0 +1,85 @@ +import React from "react"; +import BootstrapModalMixin from "components/mixins/BootstrapModalMixin"; +import actions from "actions"; +import {RaisedButton} from "material-ui"; + +export default React.createClass({ + mixins: [BootstrapModalMixin], + propTypes: { + token: React.PropTypes.object.isRequired + }, + + getInitialState() { + const {token} = this.props; + return { + errorMsg: "", + name: token.get("name") + }; + }, + + updateName(e) { + this.setState({ + name: e.target.value + }); + }, + + onSubmit() { + let token = this.props.token; + const {name} = this.state; + actions.APITokenActions.update(token, {name: name.trim()}); + this.hide(); + }, + + render() { + return ( +
+
+
+
+ {this.renderCloseButton} +

+ Edit Personal Access Token Name +

+
+
+

+ You can change the name of your Access Token. + Note that the Token Hash can not be changed. If + you want to change the hash and keep the name, + delete this Token and create a new Token with + the same name. +

+
+ +
+ + * Name Required +
+
+
+
+ + +
+
+
+
+ ); + } +}); diff --git a/troposphere/static/js/components/modals/instance/InstanceLaunchWizardModal.jsx b/troposphere/static/js/components/modals/instance/InstanceLaunchWizardModal.jsx index 31a96746c..dbbc3ee66 100644 --- a/troposphere/static/js/components/modals/instance/InstanceLaunchWizardModal.jsx +++ b/troposphere/static/js/components/modals/instance/InstanceLaunchWizardModal.jsx @@ -43,7 +43,7 @@ export default React.createClass({ image: React.PropTypes.instanceOf(Backbone.Model), project: React.PropTypes.instanceOf(Backbone.Model), onConfirm: React.PropTypes.func.isRequired, - initialView: React.PropTypes.string.isRequired + initialView: React.PropTypes.string.isRequired, }, getInitialState: function() { @@ -80,8 +80,9 @@ export default React.createClass({ providerSize: null, identityProvider: null, attachedScripts: [], + provisionOption: "full-provision", allocationSource: null, - waitingOnLaunch: false + waitingOnLaunch: false, }; }, @@ -113,7 +114,7 @@ export default React.createClass({ let imageVersionList; if (this.state.image) { imageVersionList = stores.ImageVersionStore.fetchWhere({ - image_id: this.state.image.id + image_id: this.state.image.id, }); } @@ -138,10 +139,10 @@ export default React.createClass({ let identityProvider, providerSizeList; if (provider) { identityProvider = stores.IdentityStore.findOne({ - "provider.id": provider.id + "provider.id": provider.id, }); providerSizeList = stores.SizeStore.fetchWhere({ - provider__id: provider.id + provider__id: provider.id, }); } @@ -173,7 +174,7 @@ export default React.createClass({ provider, providerSize, identityProvider, - allocationSource + allocationSource, }); }, @@ -210,31 +211,31 @@ export default React.createClass({ viewImageSelect: function() { this.setState({ - view: "IMAGE_VIEW" + view: "IMAGE_VIEW", }); }, viewProject: function() { this.setState({ - view: "PROJECT_VIEW" + view: "PROJECT_VIEW", }); }, viewBasic: function() { this.setState({ - view: "BASIC_VIEW" + view: "BASIC_VIEW", }); }, viewAdvanced: function() { this.setState({ - view: "ADVANCED_VIEW" + view: "ADVANCED_VIEW", }); }, viewLicense: function() { this.setState({ - view: "LICENSE_VIEW" + view: "LICENSE_VIEW", }); }, @@ -242,6 +243,10 @@ export default React.createClass({ // Event Handlers //========================= + onProvisionOptionChange: function(e, value) { + this.setState({provisionOption: value}); + }, + onSelectImage: function(image) { let instanceName = image.get("name"); @@ -250,7 +255,7 @@ export default React.createClass({ } let imageVersionList = stores.ImageVersionStore.fetchWhere({ - image_id: image.id + image_id: image.id, }); let imageVersion; @@ -274,10 +279,10 @@ export default React.createClass({ let identityProvider, providerSizeList; if (provider) { identityProvider = stores.IdentityStore.findOne({ - "provider.id": provider.id + "provider.id": provider.id, }); providerSizeList = stores.SizeStore.fetchWhere({ - provider__id: provider.id + provider__id: provider.id, }); } @@ -301,7 +306,7 @@ export default React.createClass({ provider, imageVersion, providerSize, - identityProvider + identityProvider, }, this.viewBasic ); @@ -313,14 +318,14 @@ export default React.createClass({ onNameChange: function(e) { this.setState({ - instanceName: e.target.value + instanceName: e.target.value, }); }, onNameBlur: function(e) { let instanceName = this.state.instanceName.trim(); this.setState({ - instanceName + instanceName, }); }, @@ -337,10 +342,10 @@ export default React.createClass({ let identityProvider, providerSizeList; if (provider) { identityProvider = stores.IdentityStore.findOne({ - "provider.id": provider.id + "provider.id": provider.id, }); providerSizeList = stores.SizeStore.fetchWhere({ - provider__id: provider.id + provider__id: provider.id, }); } @@ -361,19 +366,19 @@ export default React.createClass({ imageVersion, provider, providerSize, - identityProvider + identityProvider, }); }, onProjectChange: function(project) { this.setState({ - project + project, }); }, onAllocationSourceChange: function(source) { this.setState({ - allocationSource: source + allocationSource: source, }); }, @@ -383,7 +388,7 @@ export default React.createClass({ let provider = stores.ProviderStore.findWhere({id: providerId}); let providerSizeList = stores.SizeStore.fetchWhere({ - provider__id: providerId + provider__id: providerId, }); let imageVersion = this.state.imageVersion; @@ -402,13 +407,13 @@ export default React.createClass({ this.setState({ provider, providerSize, - identityProvider + identityProvider, }); }, onProviderChange: function(provider) { let providerSizeList = stores.SizeStore.fetchWhere({ - provider__id: provider.id + provider__id: provider.id, }); let imageVersion = this.state.imageVersion; @@ -426,7 +431,7 @@ export default React.createClass({ } let identityProvider = stores.IdentityStore.findOne({ - "provider.id": provider.id + "provider.id": provider.id, }); if (providerSizeList) { @@ -436,20 +441,20 @@ export default React.createClass({ this.setState({ provider, providerSize, - identityProvider + identityProvider, }); }, onSizeChange: function(providerSize) { this.setState({ - providerSize + providerSize, }); }, onRequestResources: function() { this.hide(); modals.HelpModals.requestMoreResources({ - identity: this.state.identityProvider.id + identity: this.state.identityProvider.id, }); }, @@ -457,7 +462,7 @@ export default React.createClass({ let attachedScripts = this.state.attachedScripts; if (attachedScripts.indexOf(value) === -1) { this.setState({ - attachedScripts: [...attachedScripts, value] + attachedScripts: [...attachedScripts, value], }); } }, @@ -466,7 +471,7 @@ export default React.createClass({ let attachedScripts = this.state.attachedScripts.filter(i => i != item); this.setState({ - attachedScripts + attachedScripts, }); }, @@ -476,7 +481,7 @@ export default React.createClass({ onClearAdvanced: function() { this.setState({ - attachedScripts: [] + attachedScripts: [], }); }, @@ -484,13 +489,13 @@ export default React.createClass({ this.viewBasic(); actions.ProjectActions.create({ name: name, - description + description, }); }, onLaunchFailed: function() { this.setState({ - waitingOnLaunch: false + waitingOnLaunch: false, }); }, @@ -541,7 +546,7 @@ export default React.createClass({ }, onFail: () => { this.onLaunchFailed(); - } + }, }; if (globals.USE_ALLOCATION_SOURCES) { @@ -555,7 +560,7 @@ export default React.createClass({ // enter into a "waiting" state to determine // result of launch operation this.setState({ - waitingOnLaunch: true + waitingOnLaunch: true, }); return; @@ -564,7 +569,7 @@ export default React.createClass({ // if we cannot launch, we are in a world of hurt // - show some indication of that this.setState({ - showValidationErr: true + showValidationErr: true, }); }, @@ -641,7 +646,7 @@ export default React.createClass({ "identityProvider", "providerSize", "imageVersion", - "attachedScripts" + "attachedScripts", ]; // Check if we are using AllocationSource and add to requierd fields @@ -729,7 +734,7 @@ export default React.createClass({ let imageVersionList; if (this.state.image) { imageVersionList = stores.ImageVersionStore.fetchWhere({ - image_id: this.state.image.id + image_id: this.state.image.id, }); if (imageVersionList) { @@ -749,7 +754,7 @@ export default React.createClass({ resourcesUsed = stores.InstanceStore.getTotalResources(provider.id); providerSizeList = stores.SizeStore.fetchWhere({ - provider__id: provider.id + provider__id: provider.id, }); } @@ -792,6 +797,7 @@ export default React.createClass({ onAllocationSourceChange: this.onAllocationSourceChange, onIdentityChange: this.onIdentityChange, onProviderChange: this.onProviderChange, + onProvisionOptionChange: this.onProvisionOptionChange, onRequestResources: this.onRequestResources, onSizeChange: this.onSizeChange, onSubmitLaunch: this.onSubmitLaunch, @@ -802,12 +808,13 @@ export default React.createClass({ providerList, providerSize, providerSizeList, + provisionOption: this.state.provisionOption, resourcesUsed, viewAdvanced: this.viewAdvanced, hasAdvancedOptions: this.hasAdvancedOptions(), allocationSource: this.state.allocationSource, allocationSourceList, - waitingOnLaunch + waitingOnLaunch, }} /> ); @@ -862,5 +869,5 @@ export default React.createClass({ ); - } + }, }); diff --git a/troposphere/static/js/components/modals/instance/launch/components/BasicInfoForm.jsx b/troposphere/static/js/components/modals/instance/launch/components/BasicInfoForm.jsx index 838223f97..de0921516 100644 --- a/troposphere/static/js/components/modals/instance/launch/components/BasicInfoForm.jsx +++ b/troposphere/static/js/components/modals/instance/launch/components/BasicInfoForm.jsx @@ -4,6 +4,7 @@ import Backbone from "backbone"; import context from "context"; import featureFlags from "utilities/featureFlags"; import SelectMenu from "components/common/ui/SelectMenu"; +import {RadioButton, RadioButtonGroup} from "material-ui/RadioButton"; export default React.createClass({ propTypes: { @@ -17,7 +18,7 @@ export default React.createClass({ instanceName: React.PropTypes.string, onNameChange: React.PropTypes.func, onVersionChange: React.PropTypes.func, - onProjectChange: React.PropTypes.func + onProjectChange: React.PropTypes.func, }, componentDidMount: function() { @@ -59,9 +60,11 @@ export default React.createClass({ project, projectList, instanceName, + provisionOption, showValidationErr, - waitingOnLaunch + waitingOnLaunch, } = this.props; + let hasErrorClass; let errorMessage = null; @@ -137,7 +140,30 @@ export default React.createClass({ {projectType}

+
+ + + + + Minimum Provision
(This might break + functionality) + + } + /> +
+
); - } + }, }); diff --git a/troposphere/static/js/components/settings/AdvancedSettingsPage.jsx b/troposphere/static/js/components/settings/AdvancedSettingsPage.jsx index 1688f09e0..d6aba98ca 100644 --- a/troposphere/static/js/components/settings/AdvancedSettingsPage.jsx +++ b/troposphere/static/js/components/settings/AdvancedSettingsPage.jsx @@ -1,6 +1,7 @@ import React from "react"; import featureFlags from "utilities/featureFlags"; import SSHConfiguration from "components/settings/advanced/SSHConfiguration"; +import TokenListView from "components/settings/advanced/TokenListView"; import ScriptListView from "components/settings/advanced/ScriptListView"; import ClientCredentials from "components/settings/advanced/ClientCredentials"; @@ -39,6 +40,7 @@ export default React.createClass({ {this.renderClientCredentials()} {this.renderScripts()} + ); diff --git a/troposphere/static/js/components/settings/advanced/TokenListView.jsx b/troposphere/static/js/components/settings/advanced/TokenListView.jsx new file mode 100644 index 000000000..1d2f33ae3 --- /dev/null +++ b/troposphere/static/js/components/settings/advanced/TokenListView.jsx @@ -0,0 +1,102 @@ +import React from "react"; +import ModalHelpers from "components/modals/ModalHelpers"; +import APITokenCreate from "components/modals/api_token/APITokenCreate"; +import APITokenDelete from "components/modals/api_token/APITokenDelete"; +import APITokenEdit from "components/modals/api_token/APITokenEdit"; +import subscribe from "utilities/subscribe"; + +const APITokenConfiguration = React.createClass({ + launchDeleteModal(token) { + ModalHelpers.renderModal(APITokenDelete, { + token + }); + }, + + launchCreateModal() { + ModalHelpers.renderModal(APITokenCreate); + }, + + launchEditModal(token) { + ModalHelpers.renderModal(APITokenEdit, { + token + }); + }, + + style() { + return { + td: { + wordWrap: "break-word", + whiteSpace: "normal" + }, + labelName: { + width: "100%" + } + }; + }, + + renderTokenRow(apiToken) { + let {td} = this.style(); + + let key = apiToken.get("name") + apiToken.cid; + return ( + + {apiToken.get("name")} + + + + {" "} + + + + + + ); + }, + + render() { + let {APITokenStore} = this.props.subscriptions, + apiTokens = APITokenStore.getAll(); + return ( +
+

Personal Access Tokens

+
+

+ Personal Access Tokens are API Tokens that can be used + instead of passwords for authentication. Other + aplications can use Personal Access Tokens to access + Atmosphere services under your account. +

+
+
+ + + + + + + + + {(apiTokens || []).map(this.renderTokenRow)} + + + + +
NameActions
+ + + + + +
+
+
+ ); + } +}); + +export default subscribe(APITokenConfiguration, ["APITokenStore"]); diff --git a/troposphere/static/js/constants/APITokenConstants.js b/troposphere/static/js/constants/APITokenConstants.js new file mode 100644 index 000000000..fb094bcab --- /dev/null +++ b/troposphere/static/js/constants/APITokenConstants.js @@ -0,0 +1,5 @@ +export default { + ADD_TOKEN: "ADD_TOKEN", + UPDATE_TOKEN: "UPDATE_TOKEN", + REMOVE_TOKEN: "REMOVE_TOKEN" +}; diff --git a/troposphere/static/js/models/APIToken.js b/troposphere/static/js/models/APIToken.js new file mode 100644 index 000000000..492373fff --- /dev/null +++ b/troposphere/static/js/models/APIToken.js @@ -0,0 +1,6 @@ +import Backbone from "backbone"; +import globals from "globals"; + +export default Backbone.Model.extend({ + urlRoot: globals.API_V2_ROOT + "/access_tokens" +}); diff --git a/troposphere/static/js/stores/APITokenStore.js b/troposphere/static/js/stores/APITokenStore.js new file mode 100644 index 000000000..520fcf2d8 --- /dev/null +++ b/troposphere/static/js/stores/APITokenStore.js @@ -0,0 +1,41 @@ +import BaseStore from "stores/BaseStore"; +import APITokenConstants from "constants/APITokenConstants"; +import APITokenCollection from "collections/APITokenCollection"; +import Dispatcher from "dispatchers/Dispatcher"; + +var APITokenStore = BaseStore.extend({ + collection: APITokenCollection +}); + +let store = new APITokenStore(); + +Dispatcher.register(function(dispatch) { + var actionType = dispatch.action.actionType; + var payload = dispatch.action.payload; + var options = dispatch.action.options || options; + + switch (actionType) { + case APITokenConstants.ADD_TOKEN: + store.add(payload.apiToken); + break; + + case APITokenConstants.REMOVE_TOKEN: + store.remove(payload.apiToken); + break; + + case APITokenConstants.UPDATE_TOKEN: + store.update(payload.apiToken); + break; + + default: + return true; + } + + if (!options.silent) { + store.emitChange(); + } + + return true; +}); + +export default store; diff --git a/troposphere/static/js/utilities/subscribe.js b/troposphere/static/js/utilities/subscribe.js index d4aac2c3a..4b726f250 100644 --- a/troposphere/static/js/utilities/subscribe.js +++ b/troposphere/static/js/utilities/subscribe.js @@ -38,6 +38,7 @@ import AdminResourceRequestStore from "stores/AdminResourceRequestStore"; import IdentityMembershipStore from "stores/IdentityMembershipStore"; import StatusStore from "stores/StatusStore"; import SSHKeyStore from "stores/SSHKeyStore"; +import APITokenStore from "stores/APITokenStore"; import QuotaStore from "stores/QuotaStore"; import SizeStore from "stores/SizeStore"; import TagStore from "stores/TagStore"; @@ -90,6 +91,7 @@ let stores = { IdentityMembershipStore, StatusStore, SSHKeyStore, + APITokenStore, QuotaStore, SizeStore, TagStore,