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.
+
+ 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.
+
+ 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.
+