The following recipe walks through how you can allow users to update an option from within their integration.
This recipe will assume you are allowing a user to update an option with the key apiKey. The code can easily be modified to support updating any option. When implemented, your users will see the following prompt when the API key for the integration they are using is about to expire:
Adding Styles
We will first add the following styles to our stylesheet.
With our styles in place we are ready to add our template code. The template code will display a message to the user and provide an input where the user can enter a new API key. The template is only shown if the apiKeyExpired computed property is set to true.
templates/block.hbs
{{!-- Begin API Key Update --}}
{{#if apiKeyExpired}}
{{#if state.option.errorMessage}}
<div class="alert alert-danger error-message mt-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="error-message-title">
Unexpected Error Updating ApiKey
</div>
<div>
{{fa-icon icon="times" fixedWidth=true click=(action (set state "option.errorMessage" "")) class="close-icon"}}
</div>
</div>
<div class="error-scrolling-container">
<pre>{{state.option.errorMessage}}</pre>
</div>
</div>
{{/if}}
<div>
Your API Key is about to expire. Please go to <a href="https://YOUR_URL_HERE">the API key portal</a> to generate a new API
key. Add your new API key here and click "Save" to update this integration.
</div>
<div class="input-container mb-0">
<label class="small ifta-label">
API Key
<span class="required">*</span>
</label>
{{input
class=(concat "ifta-field" (if state.option.apiKeyError " error"))
value=state.option.newApiKey
placeholder=""
disabled=state.option.isUpdating
required=true
}}
</div>
{{#if state.option.apiKeyError}}
<div class="ifta-error-message">
{{state.option.apiKeyError}}
</div>
{{/if}}
<div class="my-2">
<button
class="btn btn-polarity save-btn"
{{action "saveApiKey"}}
disabled={{if state.option.isUpdating true}}
>
{{#if state.option.isUpdating}}
{{fa-icon icon="spinner-third" spin=true fixedWidth=true}}
{{else}}
Save
{{/if}}
</button>
</div>
{{/if}}
{{#if state.option.apiKeyUpdated}}
<div class="alert alert-success mt-2 p-2 d-flex justify-content-between align-items-center">
<div>
{{fa-icon icon="badge-check" fixedWidth=true}} Your API key has been successfully updated.
</div>
<div>
{{fa-icon icon="times" fixedWidth=true click=(action (set state "option.apiKeyUpdated" false)) class="close-icon"}}
</div>
</div>
{{/if}}
{{!-- End API Key Update --}}
Implementing Component Logic
Next we need to focus on our component code.
Injecting Services
To begin, we will be using several built-in services. We can inject these services into our component using the Ember.inject.service() method at the top of the component file.
The first service we inject is the session service which will allow us to make REST API requests on behalf of the user. This is required to make the REST request necessary to update the logged in user's option.
Store Service
The store service is used to push our updated option value into the in-memory store used by the Polarity web application. This is important to make sure that after we update the option value on the server, the option value is also updated locally on the client.
Flash Messages
The flashMessages service is a service that makes it easy to show a toast notification to the user. We can use it to inform the user when we've successfully updated the option as well as let the user know when an error occurred. Flash messages are best used for short, transient messages.
Initializing Variables
We need a boolean value indicating whether or not our API key is expired. For this sample, we will use a computed property that returns a value, __apiKeyExpired , set by the server in the integration's integration.js file. If this value is true then the computed property apiKeyExpired will return true and our template code will be shown to the user.
components/block.js
apiKeyExpired: Ember.computed.alias('block.data.details.__apiKeyExpired', function () {
// return whether the API key is expired or about to expire. This could be a value
// passed by the server, or you could compute the value based on a session timestamp.
// For demo purposes, we return the value `__apiKeyExpired` set by the server
return this.get('block.data.details.__apiKeyExpired');
}),
We'll also add an alias for the block._state property which we will initialize below:
components/block.js
state: Ember.computed.alias('block._state'),
In addition to the apiKeyExpired computed property we need to add an init method which is called when the component is first rendered. This init method will initialize an object where we will store all our variables related to updating the options.
If your integration already has an init method, just ensure that the init method is initializing the block._state variable (referenced by our state alias in the example above), and that the state variable also has the option and option.newApiKey variables initialized (lines 3, 4, 5).
Update Option Method
In our component code we will add a method called updateOption that is able to update a given option based on the option's unique id. This method can be added outside of the actions hash within the block.js component file.
The updateOption method constructs the proper payload to update the option specified by optionId and then uses the native fetch API to issue a REST API request to the Polarity server. It uses the logged in user's existing session by fetching it via the session helper this.session.authenticatedHeaders().
components/block.js
/**
* Updates the provided option to a new value by using the Polarity REST API. Once the
* option is successfully updated server-side, this method also updates the internal data store
* used by the Polarity web application.
*
* @param optionId {string}, integration option id to update
* @param newOptionValue {string}, new value for the option
* @param userCanEdit {boolean}, true if the user can edit the option
* @param adminOnly {boolean}, true if only the admin can edit the option
* @returns {Promise<Response>}
*/
async updateOption(optionId, newOptionValue, userCanEdit, adminOnly) {
const payload = {
data: [
{
id: optionId,
type: 'integration-option',
attributes: {
value: newOptionValue,
'user-can-edit': userCanEdit,
'admin-only': adminOnly
}
}
]
};
let response = await fetch('/api/integration-options', {
method: 'PATCH',
headers: this.session.authenticatedHeaders(),
body: JSON.stringify(payload)
});
let result = await response.json();
if (!response.ok) {
let errors = result.errors;
let errorMessage = 'Failed to save integration options due to invalid option values';
for (const error of errors) {
let pointer;
if (error && error.source && error.source.pointer) {
pointer = error.source.pointer;
}
if (pointer !== undefined) {
errorMessage = `${error.detail}`;
} else if (error.title !== undefined && error.detail !== undefined) {
// E.G. "Failed to update integration options: Integration is not running"
errorMessage = `${error.title}: ${error.detail}`;
}
}
response.__errorMessage = errorMessage;
} else {
// Push our updated option in the Ember store so the updated value is accessible to the rest of the
// application
this.store.pushPayload(payload);
}
return response;
},
This method can throw an error in the even the fetch call fails. It can also return an error on the __errorMessage property which is added to the fetch response object.
Save API Key Action
Now that we have our updateOption method in place, we can wire up our saveApiKey action. This method is an action on the components "action" hash that is triggered when the user clicks on the "Save" button we added our template.
The saveApiKey action will call the updateOption method and also handle updating any state that should be displayed in the template such as an updating icon or error messages.
components/block.js
/**
* Takes the new API key value provided by the user and updates all the managed integration options
* with the new API value. Finally, it updates the API key value of the Polarity Credential Manager so
* the integration can monitor if the API key is about to expire.
* @returns {Promise<void>}
*/
async saveApiKey() {
// Check to make sure the user has provided an API Key
let newApiKey = this.get('state.option.newApiKey').trim();
if (!newApiKey) {
this.set('state.option.apiKeyError', 'API Key is required');
return;
}
this.set('state.option.apiKeyError', '');
// Modify the `optionKey` to match the key of the option you want to update. The key value is the value of the
// `key` property for the option in the integration's config.json
const optionKey = 'apiKey';
// The option's id is constructed by taking this integration's id and concatenating it with the option
// id separated by a dash.
const optionIdToUpdate = `${this.get('block.integrationId')}-${optionKey}`;
try {
this.set('state.option.isUpdating', true);
const response = await this.updateOption(optionIdToUpdate, newApiKey, true, false);
if (response.__errorMessage) {
this.flashMessage(response.__errorMessage, 'danger');
} else {
this.flashMessage('Your API Key has been successfully updated', 'success');
this.set('state.option.apiKeyUpdated', true);
// Set our key to no longer expired so we no longer show the update interface
this.set('details.__apiKeyExpired', false);
this.set('state.option.newApiKey', '');
}
} catch (updateError) {
console.error('Error encountered updating option', updateError);
this.set('state.option.errorMessage', JSON.stringify(updateError, null, 2));
} finally {
this.set('state.option.isUpdating', false);
}
},
Remember that the saveApiKey method is an action and should be included as part of your actions hash:
Note that within the saveApiKey method on line 33 we set the __apiKeyExpired value to false. In your own implementation you may be storing this value on a different variable. You will want to make sure you set any variable indicating that the apiKey is still expired or expiring to false so that the template no longer shows the update API key form to the user.
Also note that on line 18 we are setting the option key to apiKey. If your option key is something different (e.g., password), then you can modify this line to match the key for your option.
Flash Messages
The final method we will add is for displaying our toast notifications. This method should be located outside of the actions hash. The method uses the flashMessages service that we injected earlier to display toast notifications.
components/block.js
/**
* Flash a message on the screen for a specific issue
* @param message
* @param type 'info', 'danger', or 'success'
*/
flashMessage(message, type = 'info') {
this.flashMessages.add({
message: `${this.block.acronym}: ${message}`,
type: `unv-${type}`,
icon:
type === 'success'
? 'check-circle'
: type === 'danger'
? 'exclamation-circle'
: 'info-circle',
timeout: 3000
});
}
Putting it all together
The final component code will look like this:
components/block.js
polarity.export = PolarityComponent.extend({
session: Ember.inject.service('session'),
store: Ember.inject.service('store'),
flashMessages: Ember.inject.service('flashMessages'),
state: Ember.computed.alias('block._state'),
apiKeyExpired: Ember.computed.alias('block.data.details.__apiKeyExpired', function () {
// return whether the API key is expired or about to expire. This could be a value
// passed by the server, or you could compute the value based on a session timestamp.
// For demo purposes, we return the value `__apiKeyExpired` set by the server
return this.get('block.data.details.__apiKeyExpired');
}),
init() {
if (!this.get('state')) {
this.set('state', {});
this.set('state.option', {});
this.set('state.option.newApiKey', '');
}
this._super(...arguments);
},
actions: {
/**
* Takes the new API key value provided by the user and updates all the managed integration options
* with the new API value. Finally, it updates the API key value of the Polarity Credential Manager so
* the integration can monitor if the API key is about to expire.
* @returns {Promise<void>}
*/
async saveApiKey() {
// Check to make sure the user has provided an API Key
let newApiKey = this.get('state.option.newApiKey').trim();
if (!newApiKey) {
this.set('state.option.apiKeyError', 'API Key is required');
return;
}
this.set('state.option.apiKeyError', '');
// Modify the `optionKey` to match the key of the option you want to update. The key value is the value of the
// `key` property for the option in the integration's config.json
const optionKey = 'apiKey';
// The option's id is constructed by taking this integration's id and concatenating it with the option
// id separated by a dash.
const optionIdToUpdate = `${this.get('block.integrationId')}-${optionKey}`;
try {
this.set('state.option.isUpdating', true);
const response = await this.updateOption(optionIdToUpdate, newApiKey, true, false);
if (response.__errorMessage) {
this.flashMessage(response.__errorMessage, 'danger');
} else {
this.flashMessage('Your API Key has been successfully updated', 'success');
this.set('state.option.apiKeyUpdated', true);
// Set our key to no longer expired so we no longer show the update interface
this.set('details.__apiKeyExpired', false);
this.set('state.option.newApiKey', '');
}
} catch (updateError) {
console.error('Error encountered updating option', updateError);
this.set('state.option.errorMessage', JSON.stringify(updateError, null, 2));
} finally {
this.set('state.option.isUpdating', false);
}
}
},
/**
* Updates the provided option to a new value by using the Polarity REST API. Once the
* option is successfully updated server-side, this method also updates the internal data store
* used by the Polarity web application.
*
* @param optionId {string}, integration option id to update
* @param newOptionValue {string}, new value for the option
* @param userCanEdit {boolean}, true if the user can edit the option
* @param adminOnly {boolean}, true if only the admin can edit the option
* @returns {Promise<Response>}
*/
async updateOption(optionId, newOptionValue, userCanEdit, adminOnly) {
const payload = {
data: [
{
id: optionId,
type: 'integration-option',
attributes: {
value: newOptionValue,
'user-can-edit': userCanEdit,
'admin-only': adminOnly
}
}
]
};
let response = await fetch('/api/integration-options', {
method: 'PATCH',
headers: this.session.authenticatedHeaders(),
body: JSON.stringify(payload)
});
let result = await response.json();
if (!response.ok) {
let errors = result.errors;
let errorMessage = 'Failed to save integration options due to invalid option values';
for (const error of errors) {
let pointer;
if (error && error.source && error.source.pointer) {
pointer = error.source.pointer;
}
if (pointer !== undefined) {
errorMessage = `${error.detail}`;
} else if (error.title !== undefined && error.detail !== undefined) {
// E.G. "Failed to update integration options: Integration is not running"
errorMessage = `${error.title}: ${error.detail}`;
}
}
response.__errorMessage = errorMessage;
} else {
// Push our updated option in the Ember store so the updated value is accessible to the rest of the
// application
this.store.pushPayload(payload);
}
return response;
},
/**
* Flash a message on the screen for a specific issue
* @param message
* @param type 'info', 'danger', or 'success'
*/
flashMessage(message, type = 'info') {
this.flashMessages.add({
message: `${this.block.acronym}: ${message}`,
type: `unv-${type}`,
icon:
type === 'success'
? 'check-circle'
: type === 'danger'
? 'exclamation-circle'
: 'info-circle',
timeout: 3000
});
}
});
The Final Result
When the apiKeyExpired computed property is set to true, the user will be prompted to update their API Key:
Once they enter their API key and click "Save", they will be shown a success message like this: