Skip to content

Commit c88c4c6

Browse files
authored
Merge pull request #47 from fleetbase/release/v0.2.16
release: Developers v0.2.16
2 parents 11a59fe + cac8533 commit c88c4c6

11 files changed

Lines changed: 637 additions & 286 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
tags:
77
- 'v*'
88
pull_request:
9-
branches: [ main ]
9+
branches: [ main, 'release/v*', 'dev-v*' ]
1010

1111
env:
1212
NODE_VERSION: 22.x

‎RELEASE.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
> v0.2.15 ~ "API key expiry actually persists"
1+
> v0.2.16 ~ "API key and webhook dialogs open from anywhere"
22
33
---
44
## Highlights
55

6-
- **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))
7-
- **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.
6+
- **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))
87

98
---
109
## Need help?

‎addon/controllers/api-keys/index.js‎

Lines changed: 6 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { format as formatDate } from 'date-fns';
1111
import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';
1212

1313
export default class ApiKeysIndexController extends Controller {
14+
@service apiKeyActions;
1415
@service currentUser;
1516
@service intl;
1617
@service modalsManager;
@@ -261,83 +262,13 @@ export default class ApiKeysIndexController extends Controller {
261262
this.currentUser.setOption('testKey', value);
262263
}
263264

264-
/**
265-
* Toggles modal to create a new API key
266-
*
267-
* @void
268-
*/
269-
@action createApiKey() {
270-
const formPermission = 'developers create api-key';
271-
const apiKey = this.store.createRecord('api-credential', {
272-
test_mode: this.testMode,
273-
});
274-
275-
this.editApiKey(apiKey, {
276-
title: this.intl.t('developers.api-keys.index.new-api-key-title'),
277-
acceptButtonIcon: 'check',
278-
acceptButtonIconPrefix: 'fas',
279-
acceptButtonDisabled: this.abilities.cannot(formPermission),
280-
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
281-
successMessage: this.intl.t('developers.api-keys.index.new-api-key-message'),
282-
formPermission,
283-
apiKey,
284-
confirm: async (modal) => {
285-
modal.startLoading();
286-
287-
if (this.abilities.cannot(formPermission)) {
288-
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
289-
}
290-
291-
try {
292-
await apiKey.save();
293-
this.notifications.success(modal.getOption('successMessage'));
294-
return this.hostRouter.refresh();
295-
} catch (error) {
296-
this.notifications.serverError(error);
297-
modal.stopLoading();
298-
}
299-
},
300-
});
265+
// Creating and editing keys lives in the api-key-actions service so other engines can open these dialogs too.
266+
@action createApiKey(...args) {
267+
return this.apiKeyActions.createApiKey(...args);
301268
}
302269

303-
/**
304-
* Toggles modal to create a new API key
305-
*
306-
* @void
307-
*/
308-
@action editApiKey(apiKey, options = {}) {
309-
const formPermission = 'developers update api-key';
310-
this.modalsManager.show('modals/api-key-form', {
311-
title: this.intl.t('developers.api-keys.index.edit-api-key-title'),
312-
acceptButtonIcon: 'save',
313-
acceptButtonDisabled: this.abilities.cannot(formPermission),
314-
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
315-
successMessage: this.intl.t('developers.api-keys.index.edit-api-key-message'),
316-
expirationOptions: this.expirationOptions,
317-
testMode: this.currentUser.getOption('sandbox') || false,
318-
apiKey,
319-
formPermission,
320-
setExpiration: ({ target }) => {
321-
apiKey.expires_at = target.value || null;
322-
},
323-
confirm: async (modal) => {
324-
modal.startLoading();
325-
326-
if (this.abilities.cannot(formPermission)) {
327-
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
328-
}
329-
330-
try {
331-
await apiKey.save();
332-
this.notifications.success(modal.getOption('successMessage'));
333-
return this.hostRouter.refresh();
334-
} catch (error) {
335-
this.notifications.serverError(error);
336-
modal.stopLoading();
337-
}
338-
},
339-
...options,
340-
});
270+
@action editApiKey(...args) {
271+
return this.apiKeyActions.editApiKey(...args);
341272
}
342273

343274
/**

‎addon/controllers/webhooks/index.js‎

Lines changed: 8 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import fromStore from '@fleetbase/ember-core/decorators/legacy-from-store';
1010
import fetchFrom from '@fleetbase/ember-core/decorators/legacy-fetch-from';
1111

1212
export default class WebhooksIndexController extends BaseController {
13+
@service webhookActions;
1314
@service currentUser;
1415
@service intl;
1516
@service modalsManager;
@@ -177,154 +178,17 @@ export default class WebhooksIndexController extends BaseController {
177178
this.query = value;
178179
}
179180

180-
/**
181-
* Toggles modal to create a new API key
182-
*
183-
* @void
184-
*/
185-
@action createWebhook() {
186-
const formPermission = 'developers create webhook';
187-
const webhook = this.store.createRecord('webhook-endpoint', {
188-
events: [],
189-
mode: this.currentUser.getOption('sandbox') ? 'test' : 'live',
190-
});
191-
192-
this.editWebhook(webhook, {
193-
title: this.intl.t('developers.webhooks.index.add-webhook'),
194-
acceptButtonText: this.intl.t('developers.webhooks.index.add-webhook-button-text'),
195-
acceptButtonIcon: 'check',
196-
acceptButtonIconPrefix: 'fas',
197-
acceptButtonDisabled: this.abilities.cannot(formPermission),
198-
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
199-
formPermission,
200-
webhook,
201-
confirm: async (modal) => {
202-
modal.startLoading();
203-
204-
if (this.abilities.cannot(formPermission)) {
205-
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
206-
}
207-
208-
try {
209-
await webhook.save();
210-
this.notifications.success(this.intl.t('developers.webhooks.index.new-webhook-success-message'));
211-
return this.hostRouter.refresh();
212-
} catch (error) {
213-
this.notifications.serverError(error);
214-
modal.stopLoading();
215-
}
216-
},
217-
});
181+
// Webhook dialogs live in the webhook-actions service so other engines can open them too.
182+
@action createWebhook(...args) {
183+
return this.webhookActions.createWebhook(...args);
218184
}
219185

220-
/**
221-
* Triggers dialog to edit webhook
222-
*
223-
* @param {WebhookEndpointModel} webhook
224-
* @param {Object} options
225-
* @void
226-
*/
227-
@action async editWebhook(webhook, options = {}) {
228-
await this.apiCredentials;
229-
230-
const formPermission = 'developers update webhook';
231-
this.modalsManager.show('modals/webhook-form', {
232-
title: this.intl.t('developers.webhooks.index.edit-webhook-endpoint'),
233-
acceptButtonText: this.intl.t('developers.webhooks.index.edit-webhook-endpoint-button-text'),
234-
acceptButtonIcon: 'save',
235-
acceptButtonDisabled: this.abilities.cannot(formPermission),
236-
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
237-
formPermission,
238-
declineButtonIcon: 'times',
239-
declineButtonIconPrefix: 'fas',
240-
eventOptions: this.groupedApiEvents,
241-
versionOptions: this.apiVersions,
242-
apiCredentialOptions: this.apiCredentials,
243-
webhook,
244-
setVersion: ({ target }) => {
245-
webhook.version = target.value || null;
246-
},
247-
setApiCredential: ({ target }) => {
248-
webhook.api_credential_uuid = target.value || null;
249-
},
250-
searchEvents: (query) => {
251-
if (typeof query !== 'string') {
252-
return;
253-
}
254-
const resources = Object.keys(this.groupedApiEvents);
255-
const filteredEvents = {};
256-
resources.forEach((eventResource) => {
257-
filteredEvents[eventResource] = this.groupedApiEvents[eventResource].filter((event) => {
258-
return event.toLowerCase().includes(query.toLowerCase());
259-
});
260-
// if 0 events remove from filter
261-
if (filteredEvents[eventResource].length === 0) {
262-
delete filteredEvents[eventResource];
263-
}
264-
});
265-
this.modalsManager.setOption('eventOptions', filteredEvents);
266-
},
267-
addEvent: (event) => {
268-
if (webhook.events.includes(event)) {
269-
return;
270-
}
271-
272-
webhook.events.pushObject(event);
273-
},
274-
removeEvent: (event) => {
275-
webhook.events.removeObject(event);
276-
},
277-
clearEvents: () => {
278-
webhook.events.clear();
279-
},
280-
receiveAllEvents: () => {
281-
webhook.events.pushObjects(this.webhookEvents);
282-
},
283-
confirm: async (modal) => {
284-
modal.startLoading();
285-
286-
if (this.abilities.cannot(formPermission)) {
287-
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
288-
}
289-
290-
try {
291-
await webhook.save();
292-
this.notifications.success(this.intl.t('developers.webhooks.index.new-webhook-success-message'));
293-
return this.hostRouter.refresh();
294-
} catch (error) {
295-
this.notifications.serverError(error);
296-
modal.stopLoading();
297-
}
298-
},
299-
...options,
300-
});
186+
@action editWebhook(...args) {
187+
return this.webhookActions.editWebhook(...args);
301188
}
302189

303-
/**
304-
* Toggles dialog to delete webhook
305-
*
306-
* @param {WebhookEndpointModel} webhook
307-
* @param {Object} options
308-
* @void
309-
*/
310-
@action deleteWebhook(webhook, options = {}) {
311-
this.modalsManager.confirm({
312-
title: this.intl.t('developers.webhooks.index.delete-webhook-endpoint'),
313-
body: this.intl.t('developers.webhooks.index.delete-webhook-endpoint-body'),
314-
confirm: async (modal) => {
315-
modal.startLoading();
316-
317-
try {
318-
await webhook.destroyRecord();
319-
this.notifications.success(this.intl.t('developers.webhooks.index.delete-webhook-success-message'));
320-
return this.hostRouter.refresh();
321-
} catch (error) {
322-
this.notifications.serverError(error);
323-
modal.stopLoading();
324-
}
325-
},
326-
...options,
327-
});
190+
@action deleteWebhook(...args) {
191+
return this.webhookActions.deleteWebhook(...args);
328192
}
329193

330194
/**

‎addon/services/api-key-actions.js‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
2+
import { action } from '@ember/object';
3+
4+
/**
5+
* API key actions shared by the Developers console and other engines (for example Fleetbase AI).
6+
*/
7+
export default class ApiKeyActionsService extends ResourceActionService {
8+
expirationOptions = ['never', 'immediately', 'in 1 hour', 'in 24 hours', 'in 3 days', 'in 7 days'];
9+
10+
constructor() {
11+
super(...arguments);
12+
this.initialize('api-credential', { permissionPrefix: 'developers', mountPrefix: 'console.developers' });
13+
}
14+
15+
transition = {
16+
list: () => this.transitionTo('api-keys.index'),
17+
};
18+
19+
modal = {
20+
create: (...args) => this.createApiKey(...args),
21+
edit: (...args) => this.editApiKey(...args),
22+
};
23+
24+
/**
25+
* Toggles modal to create a new API key
26+
*
27+
* @void
28+
*/
29+
@action createApiKey() {
30+
const formPermission = 'developers create api-key';
31+
const apiKey = this.store.createRecord('api-credential', {
32+
test_mode: this.currentUser.getOption('sandbox') || false,
33+
});
34+
35+
this.editApiKey(apiKey, {
36+
title: this.intl.t('developers.api-keys.index.new-api-key-title'),
37+
acceptButtonIcon: 'check',
38+
acceptButtonIconPrefix: 'fas',
39+
acceptButtonDisabled: this.abilities.cannot(formPermission),
40+
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
41+
successMessage: this.intl.t('developers.api-keys.index.new-api-key-message'),
42+
formPermission,
43+
apiKey,
44+
confirm: async (modal) => {
45+
modal.startLoading();
46+
47+
if (this.abilities.cannot(formPermission)) {
48+
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
49+
}
50+
51+
try {
52+
await apiKey.save();
53+
this.notifications.success(modal.getOption('successMessage'));
54+
return this.hostRouter.refresh();
55+
} catch (error) {
56+
this.notifications.serverError(error);
57+
modal.stopLoading();
58+
}
59+
},
60+
});
61+
}
62+
63+
/**
64+
* Opens the dialog to edit an API key
65+
*
66+
* @void
67+
*/
68+
@action editApiKey(apiKey, options = {}) {
69+
const formPermission = 'developers update api-key';
70+
this.modalsManager.show('modals/api-key-form', {
71+
title: this.intl.t('developers.api-keys.index.edit-api-key-title'),
72+
acceptButtonIcon: 'save',
73+
acceptButtonDisabled: this.abilities.cannot(formPermission),
74+
acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null,
75+
successMessage: this.intl.t('developers.api-keys.index.edit-api-key-message'),
76+
expirationOptions: this.expirationOptions,
77+
testMode: this.currentUser.getOption('sandbox') || false,
78+
apiKey,
79+
formPermission,
80+
setExpiration: ({ target }) => {
81+
apiKey.expires_at = target.value || null;
82+
},
83+
confirm: async (modal) => {
84+
modal.startLoading();
85+
86+
if (this.abilities.cannot(formPermission)) {
87+
return this.notifications.warning(this.intl.t('common.permissions-required-for-changes'));
88+
}
89+
90+
try {
91+
await apiKey.save();
92+
this.notifications.success(modal.getOption('successMessage'));
93+
return this.hostRouter.refresh();
94+
} catch (error) {
95+
this.notifications.serverError(error);
96+
modal.stopLoading();
97+
}
98+
},
99+
...options,
100+
});
101+
}
102+
}

0 commit comments

Comments
 (0)