From e709a391af0a8960ad12b2e134798c4f7fb71464 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 30 Sep 2026 20:16:38 +0800 Subject: [PATCH] feat: resource view registries for developer tables and pages Extensions can add columns, row actions, bulk actions and toolbar buttons through developers:table::, and header buttons and menu items to the detail pages through developers:details::. - api-keys, webhooks, events, logs and sockets move to Layout::Resource::Tabular; the test-mode controls use its header block - webhook, event, log and socket pages render header buttons from controller getters so registered ones merge in - events and logs passed a permission string to @searchDisabled, which is always truthy, so search was always disabled; it now checks the permission - stable ids on built-in items, query params include registered filters Requires @fleetbase/ember-core 0.3.25 and @fleetbase/ember-ui 0.4.5. --- addon/controllers/api-keys/index.js | 58 +++++++++++++++- addon/controllers/base-controller.js | 27 ++++++++ addon/controllers/events/index.js | 6 +- addon/controllers/events/view.js | 18 ++++- addon/controllers/logs/index.js | 9 ++- addon/controllers/logs/view.js | 16 ++++- addon/controllers/sockets/index.js | 20 ++++++ addon/controllers/sockets/view.js | 15 ++++- addon/controllers/webhooks/index.js | 32 ++++++++- addon/controllers/webhooks/view.js | 37 +++++++++++ addon/extension.js | 4 ++ addon/services/api-key-actions.js | 2 +- addon/services/webhook-actions.js | 2 +- addon/templates/api-keys/index.hbs | 99 +++++++++++++--------------- addon/templates/events/index.hbs | 42 +++++------- addon/templates/events/view.hbs | 4 +- addon/templates/logs/index.hbs | 42 +++++------- addon/templates/logs/view.hbs | 4 +- addon/templates/sockets/index.hbs | 21 +++--- addon/templates/sockets/view.hbs | 4 +- addon/templates/webhooks/index.hbs | 35 +++++----- addon/templates/webhooks/view.hbs | 10 +-- 22 files changed, 349 insertions(+), 158 deletions(-) diff --git a/addon/controllers/api-keys/index.js b/addon/controllers/api-keys/index.js index b49b2e6..7eb0ded 100644 --- a/addon/controllers/api-keys/index.js +++ b/addon/controllers/api-keys/index.js @@ -29,7 +29,7 @@ export default class ApiKeysIndexController extends Controller { * * @var {Array} */ - queryParams = ['page', 'limit', 'sort', 'query', 'view_api_key']; + queryParams = this.apiKeyActions.queryParamsFor(['page', 'limit', 'sort', 'query', 'view_api_key']); /** * Expiration options for api keys @@ -105,6 +105,50 @@ export default class ApiKeysIndexController extends Controller { return this.currentUser.getOption('testKey'); } + /** + * Header buttons. Extensions add to them through `developers:table:api-key:actions`. + * + * @var {Array} + */ + get actionButtons() { + return [ + { + id: 'create', + type: 'primary', + icon: 'plus', + iconPrefix: 'fas', + text: this.intl.t('developers.common.new'), + onClick: this.createApiKey, + permission: 'developers create api-key', + }, + { + id: 'export', + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + text: this.intl.t('developers.common.export'), + onClick: this.exportApiKeys, + permission: 'developers export api-key', + }, + ]; + } + + /** + * Bulk actions for the selected rows. Extensions add to them through `developers:table:api-key:bulk-actions`. + * + * @var {Array} + */ + get bulkActions() { + return [ + { + id: 'bulk-delete', + label: this.intl.t('developers.api-keys.index.delete-api'), + class: 'text-red-500', + fn: this.bulkDeleteApiCredentials, + permission: 'developers delete api-key', + }, + ]; + } + /** * Columns for table component. * @@ -112,6 +156,7 @@ export default class ApiKeysIndexController extends Controller { */ @tracked columns = [ { + id: 'name', sticky: true, label: this.intl.t('developers.common.name'), valuePath: 'name', @@ -122,6 +167,7 @@ export default class ApiKeysIndexController extends Controller { sortable: false, }, { + id: 'key', label: this.intl.t('developers.api-keys.index.public-key'), valuePath: 'key', sortable: false, @@ -129,6 +175,7 @@ export default class ApiKeysIndexController extends Controller { cellComponent: 'click-to-copy', }, { + id: 'secret', label: this.intl.t('developers.api-keys.index.secret-key'), valuePath: 'secret', sortable: false, @@ -141,6 +188,7 @@ export default class ApiKeysIndexController extends Controller { }, }, { + id: 'environment', label: this.intl.t('developers.api-keys.index.enviroment'), valuePath: 'environment', sortable: false, @@ -148,6 +196,7 @@ export default class ApiKeysIndexController extends Controller { cellComponent: 'table/cell/status', }, { + id: 'expires-at', label: this.intl.t('developers.api-keys.index.expiry'), valuePath: 'expiresAt', sortable: false, @@ -156,6 +205,7 @@ export default class ApiKeysIndexController extends Controller { cellClassNames: 'overflow-visible', }, { + id: 'last-used', label: this.intl.t('developers.api-keys.index.last-used'), valuePath: 'lastUsed', sortable: false, @@ -164,6 +214,7 @@ export default class ApiKeysIndexController extends Controller { cellClassNames: 'overflow-visible', }, { + id: 'created-at', label: this.intl.t('developers.common.created'), valuePath: 'createdAt', sortable: false, @@ -172,6 +223,7 @@ export default class ApiKeysIndexController extends Controller { cellClassNames: 'overflow-visible', }, { + id: 'row-actions', label: '', cellComponent: 'table/cell/dropdown', ddButtonText: false, @@ -185,21 +237,25 @@ export default class ApiKeysIndexController extends Controller { width: 60, actions: [ { + id: 'edit-api-key', label: this.intl.t('developers.api-keys.index.edit-key'), fn: this.editApiKey, permission: 'developers view api-key', }, { + id: 'roll-api-key', label: this.intl.t('developers.api-keys.index.roll-key'), fn: this.rollApiKey, permission: 'developers roll api-key', }, { + id: 'view-request-logs', label: this.intl.t('developers.api-keys.index.view-logs'), fn: this.viewRequestLogs, permission: 'developers view log', }, { + id: 'delete-api-key', label: this.intl.t('developers.api-keys.index.delete-key'), fn: this.deleteApiKey, className: 'text-red-700 hover:text-red-800', diff --git a/addon/controllers/base-controller.js b/addon/controllers/base-controller.js index 068ed73..4ad6c73 100644 --- a/addon/controllers/base-controller.js +++ b/addon/controllers/base-controller.js @@ -1,6 +1,8 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; +import { getOwner } from '@ember/application'; +import lookupResourceView, { mergeHeaderButtons } from '@fleetbase/ember-ui/utils/resource-view'; export default class BaseController extends Controller { /** @@ -28,4 +30,29 @@ export default class BaseController extends Controller { @action transitionToRoute(route, ...args) { return this.universe.transitionToEngineRoute('@fleetbase/dev-engine', route, ...args); } + + /** + * The query params an index controller declares, plus the filter params of + * columns extensions registered under `developers:table:`. + * + * @param {String} resource e.g. 'event' + * @param {Array} baseQueryParams + * @returns {Array} + */ + registeredQueryParams(resource, baseQueryParams = []) { + return lookupResourceView(getOwner(this))?.queryParamsFor('developers', resource, baseQueryParams) ?? baseQueryParams; + } + + /** + * A details page's header buttons, with what extensions registered under + * `developers:details:` merged in. + * + * @param {String} resource e.g. 'webhook' + * @param {Object} record The record the page shows + * @param {Array} buttons The page's own buttons + * @returns {Array} + */ + detailsActionButtons(resource, record, buttons = []) { + return mergeHeaderButtons(lookupResourceView(getOwner(this)), `developers:details:${resource}`, buttons, { resource: record, controller: this }, { withMenu: true }); + } } diff --git a/addon/controllers/events/index.js b/addon/controllers/events/index.js index c2c51d4..5eef967 100644 --- a/addon/controllers/events/index.js +++ b/addon/controllers/events/index.js @@ -26,7 +26,7 @@ export default class EventsIndexController extends BaseController { * * @var {Array} */ - queryParams = ['query', 'page', 'limit', 'sort', 'event', 'created_at']; + queryParams = this.registeredQueryParams('event', ['query', 'page', 'limit', 'sort', 'event', 'created_at']); /** * The current page of data being viewed @@ -77,12 +77,14 @@ export default class EventsIndexController extends BaseController { */ @tracked columns = [ { + id: 'description', sticky: true, label: this.intl.t('developers.common.event'), valuePath: 'description', sortable: false, }, { + id: 'event', label: this.intl.t('developers.common.code'), valuePath: 'event', sortable: false, @@ -91,6 +93,7 @@ export default class EventsIndexController extends BaseController { filterOptions: this.webhookEvents, }, { + id: 'public-id', label: this.intl.t('developers.common.id'), valuePath: 'public_id', cellComponent: 'click-to-copy', @@ -98,6 +101,7 @@ export default class EventsIndexController extends BaseController { sortable: false, }, { + id: 'created-at', label: this.intl.t('developers.common.date'), valuePath: 'createdAt', filterParam: 'created_at', diff --git a/addon/controllers/events/view.js b/addon/controllers/events/view.js index 49d2d01..8f47013 100644 --- a/addon/controllers/events/view.js +++ b/addon/controllers/events/view.js @@ -1,7 +1,21 @@ -import Controller from '@ember/controller'; +import BaseController from '../base-controller'; +import { inject as service } from '@ember/service'; import { action } from '@ember/object'; -export default class EventsViewController extends Controller { +export default class EventsViewController extends BaseController { + @service intl; + + /** + * Header buttons. Extensions add to them through `developers:details:event`. + * + * @var {Array} + */ + get actionButtons() { + return this.detailsActionButtons('event', this.model, [ + { id: 'back', icon: 'long-arrow-left', iconPrefix: 'fas', text: this.intl.t('developers.common.back'), onClick: this.goBack }, + ]); + } + @action goBack() { return window.history.back(); } diff --git a/addon/controllers/logs/index.js b/addon/controllers/logs/index.js index 453c8c3..25ce336 100644 --- a/addon/controllers/logs/index.js +++ b/addon/controllers/logs/index.js @@ -34,7 +34,7 @@ export default class LogsIndexController extends BaseController { * * @var {Array} */ - queryParams = ['query', 'page', 'limit', 'sort', 'version', 'key', 'method', 'created_at']; + queryParams = this.registeredQueryParams('log', ['query', 'page', 'limit', 'sort', 'version', 'key', 'method', 'created_at']); /** * The current page of data being viewed @@ -100,12 +100,14 @@ export default class LogsIndexController extends BaseController { @computed('apiCredentials.@each.id', 'apiVersions.[]') get columns() { return [ { + id: 'description', sticky: true, label: this.intl.t('developers.common.description'), valuePath: 'description', sortable: false, }, { + id: 'status', label: this.intl.t('developers.common.status'), valuePath: 'status', sortable: false, @@ -113,6 +115,7 @@ export default class LogsIndexController extends BaseController { cellClassNames: 'uppercase', }, { + id: 'public-id', label: this.intl.t('developers.common.id'), valuePath: 'public_id', cellComponent: 'click-to-copy', @@ -120,6 +123,7 @@ export default class LogsIndexController extends BaseController { sortable: false, }, { + id: 'api-credential-name', label: this.intl.t('developers.logs.index.api-credential'), valuePath: 'api_credential_name', cellComponent: 'click-to-copy', @@ -132,6 +136,7 @@ export default class LogsIndexController extends BaseController { filterOptions: this.apiCredentials, }, { + id: 'method', label: this.intl.t('developers.logs.index.http-method'), valuePath: 'method', filterable: true, @@ -139,6 +144,7 @@ export default class LogsIndexController extends BaseController { filterOptions: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], }, { + id: 'version', label: this.intl.t('developers.common.version'), valuePath: 'version', filterable: true, @@ -146,6 +152,7 @@ export default class LogsIndexController extends BaseController { filterOptions: this.apiVersions, }, { + id: 'created-at', label: this.intl.t('developers.common.date'), valuePath: 'createdAt', filterParam: 'created_at', diff --git a/addon/controllers/logs/view.js b/addon/controllers/logs/view.js index 77d0dd4..9a68c59 100644 --- a/addon/controllers/logs/view.js +++ b/addon/controllers/logs/view.js @@ -1,7 +1,19 @@ -import Controller from '@ember/controller'; +import BaseController from '../base-controller'; +import { inject as service } from '@ember/service'; import { action } from '@ember/object'; -export default class LogsViewController extends Controller { +export default class LogsViewController extends BaseController { + @service intl; + + /** + * Header buttons. Extensions add to them through `developers:details:log`. + * + * @var {Array} + */ + get actionButtons() { + return this.detailsActionButtons('log', this.model, [{ id: 'back', icon: 'long-arrow-left', iconPrefix: 'fas', text: this.intl.t('developers.common.back'), onClick: this.goBack }]); + } + @action goBack() { return window.history.back(); } diff --git a/addon/controllers/sockets/index.js b/addon/controllers/sockets/index.js index 2692d0e..f5caba3 100644 --- a/addon/controllers/sockets/index.js +++ b/addon/controllers/sockets/index.js @@ -13,6 +13,25 @@ export default class SocketsIndexController extends BaseController { @service hostRouter; @service abilities; + /** + * Header buttons. Extensions add to them through `developers:table:socket:actions`. + * + * @var {Array} + */ + get actionButtons() { + return [ + { + id: 'listen-on-custom-channel', + type: 'magic', + icon: 'headphones', + iconPrefix: 'fas', + text: this.intl.t('developers.sockets.index.button-text'), + onClick: this.listenOnCustomChannel, + permission: 'developers view socket', + }, + ]; + } + /** * All columns applicable for orders * @@ -32,6 +51,7 @@ export default class SocketsIndexController extends BaseController { cellClassNames: 'no-underline', }, { + id: 'name', label: this.intl.t('developers.sockets.index.channel'), valuePath: 'name', width: '88%', diff --git a/addon/controllers/sockets/view.js b/addon/controllers/sockets/view.js index 1ab83f7..040aa7a 100644 --- a/addon/controllers/sockets/view.js +++ b/addon/controllers/sockets/view.js @@ -1,14 +1,25 @@ -import Controller from '@ember/controller'; +import BaseController from '../base-controller'; import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { format } from 'date-fns'; -export default class SocketsViewController extends Controller { +export default class SocketsViewController extends BaseController { @service hostRouter; @service intl; @service socket; + /** + * Header buttons. Extensions add to them through `developers:details:socket`. + * + * @var {Array} + */ + get actionButtons() { + return this.detailsActionButtons('socket', this.model, [ + { id: 'back', icon: 'long-arrow-left', iconPrefix: 'fas', text: this.intl.t('developers.common.back'), onClick: this.goBack }, + ]); + } + /** * Incoming events logged from socket * diff --git a/addon/controllers/webhooks/index.js b/addon/controllers/webhooks/index.js index 83f2ebe..eed6a0d 100644 --- a/addon/controllers/webhooks/index.js +++ b/addon/controllers/webhooks/index.js @@ -62,7 +62,7 @@ export default class WebhooksIndexController extends BaseController { * * @var {Array} */ - queryParams = ['page', 'limit', 'sort', 'query']; + queryParams = this.webhookActions.queryParamsFor(['page', 'limit', 'sort', 'query']); /** * All webhook events @@ -87,6 +87,25 @@ export default class WebhooksIndexController extends BaseController { */ @fromStore('api-credential', { limit: -1 }) apiCredentials; + /** + * Header buttons. Extensions add to them through `developers:table:webhook:actions`. + * + * @var {Array} + */ + get actionButtons() { + return [ + { + id: 'create', + type: 'primary', + icon: 'plus', + iconPrefix: 'fas', + text: this.intl.t('developers.webhooks.index.button-text'), + onClick: this.createWebhook, + permission: 'developers create webhook', + }, + ]; + } + /** * Columns for table component. * @@ -94,6 +113,7 @@ export default class WebhooksIndexController extends BaseController { */ @tracked columns = [ { + id: 'url', label: this.intl.t('developers.common.url'), valuePath: 'url', width: '40%', @@ -104,6 +124,7 @@ export default class WebhooksIndexController extends BaseController { cellClassNames: 'no-underline', }, { + id: 'status', label: this.intl.t('developers.common.status'), valuePath: 'status', width: '15%', @@ -111,15 +132,17 @@ export default class WebhooksIndexController extends BaseController { cellComponent: 'table/cell/status', }, { + id: 'mode', label: this.intl.t('developers.common.mode'), valuePath: 'mode', width: '15%', sortable: false, cellComponent: 'table/cell/status', }, - { label: this.intl.t('developers.common.version'), valuePath: 'version', width: '10%', sortable: false }, - { label: this.intl.t('developers.common.created'), valuePath: 'createdAt', sortable: false, width: '10%' }, + { id: 'version', label: this.intl.t('developers.common.version'), valuePath: 'version', width: '10%', sortable: false }, + { id: 'created-at', label: this.intl.t('developers.common.created'), valuePath: 'createdAt', sortable: false, width: '10%' }, { + id: 'row-actions', label: '', cellComponent: 'table/cell/dropdown', ddButtonText: false, @@ -132,16 +155,19 @@ export default class WebhooksIndexController extends BaseController { align: 'right', actions: [ { + id: 'view-webhook', label: this.intl.t('developers.webhooks.index.view-logs'), fn: this.viewWebhook, permission: 'developers view webhook', }, { + id: 'edit-webhook', label: this.intl.t('developers.webhooks.index.edit-webhook'), fn: this.editWebhook, permission: 'developers update webhook', }, { + id: 'delete-webhook', label: this.intl.t('developers.webhooks.index.delete-webhook'), fn: this.deleteWebhook, permission: 'developers delete webhook', diff --git a/addon/controllers/webhooks/view.js b/addon/controllers/webhooks/view.js index e877c00..39c0884 100644 --- a/addon/controllers/webhooks/view.js +++ b/addon/controllers/webhooks/view.js @@ -49,6 +49,43 @@ export default class WebhooksViewController extends BaseController { return groupApiEvents(this.webhookEvents); } + /** + * Header buttons. Extensions add to them through `developers:details:webhook`. + * + * @var {Array} + */ + get actionButtons() { + const webhook = this.model; + const toggle = webhook?.isEnabled + ? { + id: 'disable', + icon: 'ban', + text: this.intl.t('developers.webhooks.view.disable-button-text'), + onClick: () => this.disableWebhook(webhook), + permission: 'developers disable webhook', + } + : { + id: 'enable', + icon: 'ban', + text: this.intl.t('developers.webhooks.view.enable-button-text'), + onClick: () => this.enableWebhook(webhook), + permission: 'developers enable webhook', + }; + + return this.detailsActionButtons('webhook', webhook, [ + { id: 'refresh', icon: 'refresh', onClick: this.reload, helpText: this.intl.t('common.reload-data') }, + toggle, + { + id: 'delete', + icon: 'times', + iconPrefix: 'fas', + text: this.intl.t('developers.webhooks.view.delete-button-text'), + onClick: () => this.deleteWebhook(webhook), + permission: 'developers delete webhook', + }, + ]); + } + /** * Toggles dialog to delete webhook * diff --git a/addon/extension.js b/addon/extension.js index a29aca3..04447f0 100644 --- a/addon/extension.js +++ b/addon/extension.js @@ -219,5 +219,9 @@ export default { // widget.withGridOptions({ w: 6, h: 7, minW: 5, minH: 6 }); // }), // ]); + + // Declare the registries extensions can add table columns, actions and + // buttons to, e.g. `developers:table:api-key:columns` or `developers:details:webhook:actions`. + universe.getService('universe/resource-view-service')?.declare('developers', ['api-key', 'event', 'log', 'socket', 'webhook']); }, }; diff --git a/addon/services/api-key-actions.js b/addon/services/api-key-actions.js index a76abea..90f7cb6 100644 --- a/addon/services/api-key-actions.js +++ b/addon/services/api-key-actions.js @@ -9,7 +9,7 @@ export default class ApiKeyActionsService extends ResourceActionService { constructor() { super(...arguments); - this.initialize('api-credential', { permissionPrefix: 'developers', mountPrefix: 'console.developers' }); + this.initialize('api-credential', { permissionPrefix: 'developers', mountPrefix: 'console.developers', registryResource: 'api-key' }); } transition = { diff --git a/addon/services/webhook-actions.js b/addon/services/webhook-actions.js index d15d7e2..6e61639 100644 --- a/addon/services/webhook-actions.js +++ b/addon/services/webhook-actions.js @@ -8,7 +8,7 @@ import groupApiEvents from '@fleetbase/ember-core/utils/group-api-events'; export default class WebhookActionsService extends ResourceActionService { constructor() { super(...arguments); - this.initialize('webhook-endpoint', { permissionPrefix: 'developers', mountPrefix: 'console.developers' }); + this.initialize('webhook-endpoint', { permissionPrefix: 'developers', mountPrefix: 'console.developers', registryResource: 'webhook' }); } transition = { diff --git a/addon/templates/api-keys/index.hbs b/addon/templates/api-keys/index.hbs index 7b6ed13..713597b 100644 --- a/addon/templates/api-keys/index.hbs +++ b/addon/templates/api-keys/index.hbs @@ -1,56 +1,45 @@ - -
- - {{#if this.isTestMode}} - - + - {{/each}} - - - {{/if}} -
-