Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
tags:
- 'v*'
pull_request:
branches: [ main ]
branches: [ main, 'release/v*', 'dev-v*' ]

env:
NODE_VERSION: 22.x
Expand Down
5 changes: 2 additions & 3 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
> v0.2.15 ~ "API key expiry actually persists"
> v0.2.16 ~ "API key and webhook dialogs open from anywhere"

---
## Highlights

- **API key expiration works again.** Selecting an expiry in the developers console (`immediately`, `in 1 hour`, `in 24 hours`, …) silently saved `NULL` — Ember Data's `date` transform discarded the relative expiration strings before they reached the API. A new `expiration` transform passes them through for the server to resolve, so `expires_at` is persisted for every option. Pair with fleetbase/core-api#246 for `immediately` to revoke a key reliably at the boundary instant. ([#43](https://github.com/fleetbase/dev-engine/pull/43))
- **The engine's test suite is runnable.** `ember test` previously crashed before executing a single test; the engine now eager-loads for its own test runs (hosts still get the lazy engine), and regression tests cover the expiration serialization path.
- **API key and webhook dialogs are reusable services.** New `api-key-actions` and `webhook-actions` services hold the create, edit and delete dialogs that lived in the index controllers, so other engines can open them — Fleetbase AI uses this to open **New API Key** and **New Webhook** from its confirmation cards. The Developers pages delegate to the same services, so they behave as before. ([#46](https://github.com/fleetbase/dev-engine/pull/46))

---
## Need help?
Expand Down
81 changes: 6 additions & 75 deletions addon/controllers/api-keys/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { format as formatDate } from 'date-fns';
import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';

export default class ApiKeysIndexController extends Controller {
@service apiKeyActions;
@service currentUser;
@service intl;
@service modalsManager;
Expand Down Expand Up @@ -261,83 +262,13 @@ export default class ApiKeysIndexController extends Controller {
this.currentUser.setOption('testKey', value);
}

/**
* Toggles modal to create a new API key
*
* @void
*/
@action createApiKey() {
const formPermission = 'developers create api-key';
const apiKey = this.store.createRecord('api-credential', {
test_mode: this.testMode,
});

this.editApiKey(apiKey, {
title: this.intl.t('developers.api-keys.index.new-api-key-title'),
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
successMessage: this.intl.t('developers.api-keys.index.new-api-key-message'),
formPermission,
apiKey,
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await apiKey.save();
this.notifications.success(modal.getOption('successMessage'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
// Creating and editing keys lives in the api-key-actions service so other engines can open these dialogs too.
@action createApiKey(...args) {
return this.apiKeyActions.createApiKey(...args);
}

/**
* Toggles modal to create a new API key
*
* @void
*/
@action editApiKey(apiKey, options = {}) {
const formPermission = 'developers update api-key';
this.modalsManager.show('modals/api-key-form', {
title: this.intl.t('developers.api-keys.index.edit-api-key-title'),
acceptButtonIcon: 'save',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
successMessage: this.intl.t('developers.api-keys.index.edit-api-key-message'),
expirationOptions: this.expirationOptions,
testMode: this.currentUser.getOption('sandbox') || false,
apiKey,
formPermission,
setExpiration: ({ target }) => {
apiKey.expires_at = target.value || null;
},
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await apiKey.save();
this.notifications.success(modal.getOption('successMessage'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
@action editApiKey(...args) {
return this.apiKeyActions.editApiKey(...args);
}

/**
Expand Down
152 changes: 8 additions & 144 deletions addon/controllers/webhooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import fromStore from '@fleetbase/ember-core/decorators/legacy-from-store';
import fetchFrom from '@fleetbase/ember-core/decorators/legacy-fetch-from';

export default class WebhooksIndexController extends BaseController {
@service webhookActions;
@service currentUser;
@service intl;
@service modalsManager;
Expand Down Expand Up @@ -177,154 +178,17 @@ export default class WebhooksIndexController extends BaseController {
this.query = value;
}

/**
* Toggles modal to create a new API key
*
* @void
*/
@action createWebhook() {
const formPermission = 'developers create webhook';
const webhook = this.store.createRecord('webhook-endpoint', {
events: [],
mode: this.currentUser.getOption('sandbox') ? 'test' : 'live',
});

this.editWebhook(webhook, {
title: this.intl.t('developers.webhooks.index.add-webhook'),
acceptButtonText: this.intl.t('developers.webhooks.index.add-webhook-button-text'),
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
formPermission,
webhook,
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await webhook.save();
this.notifications.success(this.intl.t('developers.webhooks.index.new-webhook-success-message'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
// Webhook dialogs live in the webhook-actions service so other engines can open them too.
@action createWebhook(...args) {
return this.webhookActions.createWebhook(...args);
}

/**
* Triggers dialog to edit webhook
*
* @param {WebhookEndpointModel} webhook
* @param {Object} options
* @void
*/
@action async editWebhook(webhook, options = {}) {
await this.apiCredentials;

const formPermission = 'developers update webhook';
this.modalsManager.show('modals/webhook-form', {
title: this.intl.t('developers.webhooks.index.edit-webhook-endpoint'),
acceptButtonText: this.intl.t('developers.webhooks.index.edit-webhook-endpoint-button-text'),
acceptButtonIcon: 'save',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
formPermission,
declineButtonIcon: 'times',
declineButtonIconPrefix: 'fas',
eventOptions: this.groupedApiEvents,
versionOptions: this.apiVersions,
apiCredentialOptions: this.apiCredentials,
webhook,
setVersion: ({ target }) => {
webhook.version = target.value || null;
},
setApiCredential: ({ target }) => {
webhook.api_credential_uuid = target.value || null;
},
searchEvents: (query) => {
if (typeof query !== 'string') {
return;
}
const resources = Object.keys(this.groupedApiEvents);
const filteredEvents = {};
resources.forEach((eventResource) => {
filteredEvents[eventResource] = this.groupedApiEvents[eventResource].filter((event) => {
return event.toLowerCase().includes(query.toLowerCase());
});
// if 0 events remove from filter
if (filteredEvents[eventResource].length === 0) {
delete filteredEvents[eventResource];
}
});
this.modalsManager.setOption('eventOptions', filteredEvents);
},
addEvent: (event) => {
if (webhook.events.includes(event)) {
return;
}

webhook.events.pushObject(event);
},
removeEvent: (event) => {
webhook.events.removeObject(event);
},
clearEvents: () => {
webhook.events.clear();
},
receiveAllEvents: () => {
webhook.events.pushObjects(this.webhookEvents);
},
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await webhook.save();
this.notifications.success(this.intl.t('developers.webhooks.index.new-webhook-success-message'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
@action editWebhook(...args) {
return this.webhookActions.editWebhook(...args);
}

/**
* Toggles dialog to delete webhook
*
* @param {WebhookEndpointModel} webhook
* @param {Object} options
* @void
*/
@action deleteWebhook(webhook, options = {}) {
this.modalsManager.confirm({
title: this.intl.t('developers.webhooks.index.delete-webhook-endpoint'),
body: this.intl.t('developers.webhooks.index.delete-webhook-endpoint-body'),
confirm: async (modal) => {
modal.startLoading();

try {
await webhook.destroyRecord();
this.notifications.success(this.intl.t('developers.webhooks.index.delete-webhook-success-message'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
@action deleteWebhook(...args) {
return this.webhookActions.deleteWebhook(...args);
}

/**
Expand Down
102 changes: 102 additions & 0 deletions addon/services/api-key-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
import { action } from '@ember/object';

/**
* API key actions shared by the Developers console and other engines (for example Fleetbase AI).
*/
export default class ApiKeyActionsService extends ResourceActionService {
expirationOptions = ['never', 'immediately', 'in 1 hour', 'in 24 hours', 'in 3 days', 'in 7 days'];

constructor() {
super(...arguments);
this.initialize('api-credential', { permissionPrefix: 'developers', mountPrefix: 'console.developers' });
}

transition = {
list: () => this.transitionTo('api-keys.index'),
};

modal = {
create: (...args) => this.createApiKey(...args),
edit: (...args) => this.editApiKey(...args),
};

/**
* Toggles modal to create a new API key
*
* @void
*/
@action createApiKey() {
const formPermission = 'developers create api-key';
const apiKey = this.store.createRecord('api-credential', {
test_mode: this.currentUser.getOption('sandbox') || false,
});

this.editApiKey(apiKey, {
title: this.intl.t('developers.api-keys.index.new-api-key-title'),
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
successMessage: this.intl.t('developers.api-keys.index.new-api-key-message'),
formPermission,
apiKey,
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await apiKey.save();
this.notifications.success(modal.getOption('successMessage'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
}

/**
* Opens the dialog to edit an API key
*
* @void
*/
@action editApiKey(apiKey, options = {}) {
const formPermission = 'developers update api-key';
this.modalsManager.show('modals/api-key-form', {
title: this.intl.t('developers.api-keys.index.edit-api-key-title'),
acceptButtonIcon: 'save',
acceptButtonDisabled: this.abilities.cannot(formPermission),
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
successMessage: this.intl.t('developers.api-keys.index.edit-api-key-message'),
expirationOptions: this.expirationOptions,
testMode: this.currentUser.getOption('sandbox') || false,
apiKey,
formPermission,
setExpiration: ({ target }) => {
apiKey.expires_at = target.value || null;
},
confirm: async (modal) => {
modal.startLoading();

if (this.abilities.cannot(formPermission)) {
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
}

try {
await apiKey.save();
this.notifications.success(modal.getOption('successMessage'));
return this.hostRouter.refresh();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
}
}
Loading
Loading