diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e11201b..fa2f721 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,21 +8,19 @@ on: pull_request: branches: [ main ] +env: + NODE_VERSION: 22.x + jobs: build: runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [18.x] # Build on Node.js 18 - steps: - uses: actions/checkout@v2 - - name: Setup Node.js ${{ matrix.node-version }} + - name: Setup Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node-version }} + node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v2.0.1 @@ -42,10 +40,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup Node.js 18.x + - name: Setup Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v2 with: - node-version: 18.x + node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v2.0.1 @@ -71,10 +69,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup Node.js 18.x + - name: Setup Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v2 with: - node-version: 18.x + node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v2.0.1 diff --git a/addon/components/widget/api-traffic.hbs b/addon/components/widget/api-traffic.hbs new file mode 100644 index 0000000..c73c1ca --- /dev/null +++ b/addon/components/widget/api-traffic.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/api-traffic.js b/addon/components/widget/api-traffic.js new file mode 100644 index 0000000..815054a --- /dev/null +++ b/addon/components/widget/api-traffic.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetApiTrafficComponent extends Component { + widget = 'api-traffic'; +} diff --git a/addon/components/widget/developer-activity.hbs b/addon/components/widget/developer-activity.hbs new file mode 100644 index 0000000..3805512 --- /dev/null +++ b/addon/components/widget/developer-activity.hbs @@ -0,0 +1,30 @@ +
+
+
+
{{t "developers.component.widget.dashboard.developer-activity.title"}}
+
{{t "developers.component.widget.dashboard.developer-activity.subtitle"}}
+
+ +
+
+ {{#if this.load.isRunning}} +
+ {{else if this.error}} +
{{this.error}}
+ {{else if this.items.length}} + {{#each this.items as |item|}} +
+
+
+
{{item.label}}
+
{{item.type}} / {{item.status}}
+
+
+ {{/each}} + {{else}} +
{{t "developers.component.widget.dashboard.empty"}}
+ {{/if}} +
+
diff --git a/addon/components/widget/developer-activity.js b/addon/components/widget/developer-activity.js new file mode 100644 index 0000000..6a9cced --- /dev/null +++ b/addon/components/widget/developer-activity.js @@ -0,0 +1,46 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; + +export default class WidgetDeveloperActivityComponent extends Component { + @service fetch; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.load.perform(); + } + + get items() { + return this.data?.items ?? []; + } + + iconFor(type) { + if (type === 'webhook') { + return 'webhook'; + } + + if (type === 'event') { + return 'bolt'; + } + + return 'terminal'; + } + + @task *load() { + try { + this.data = yield this.fetch.get('metrics/dev/activity', { limit: 14 }); + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load activity'; + } + } + + @action refresh() { + this.load.perform(); + } +} diff --git a/addon/components/widget/developers-chart.hbs b/addon/components/widget/developers-chart.hbs new file mode 100644 index 0000000..4194167 --- /dev/null +++ b/addon/components/widget/developers-chart.hbs @@ -0,0 +1,22 @@ +
+
+
+
{{@title}}
+
{{@subtitle}}
+
+ +
+
+ {{#if this.load.isRunning}} +
+ {{else if this.error}} +
{{this.error}}
+ {{else}} +
+ +
+ {{/if}} +
+
diff --git a/addon/components/widget/developers-chart.js b/addon/components/widget/developers-chart.js new file mode 100644 index 0000000..1f0891b --- /dev/null +++ b/addon/components/widget/developers-chart.js @@ -0,0 +1,77 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; + +const COLORS = [ + ['#2563eb', 'rgba(37, 99, 235, 0.15)'], + ['#10b981', 'rgba(16, 185, 129, 0.15)'], + ['#ef4444', 'rgba(239, 68, 68, 0.15)'], + ['#f59e0b', 'rgba(245, 158, 11, 0.15)'], +]; + +export default class WidgetDevelopersChartComponent extends Component { + @service fetch; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.load.perform(); + } + + get labels() { + return this.data?.labels ?? []; + } + + get datasets() { + return (this.data?.datasets ?? []).map((dataset, index) => { + const color = COLORS[index % COLORS.length]; + + return { + ...dataset, + borderColor: color[0], + backgroundColor: color[1], + borderWidth: 2, + fill: true, + tension: 0.35, + pointRadius: 0, + pointHoverRadius: 4, + }; + }); + } + + get chartOptions() { + return { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'index', intersect: false }, + plugins: { + legend: { + display: true, + position: 'top', + labels: { usePointStyle: true, boxWidth: 8, boxHeight: 8 }, + }, + }, + scales: { + x: { grid: { display: false }, ticks: { maxRotation: 0 } }, + y: { beginAtZero: true, grid: { color: 'rgba(148, 163, 184, 0.16)' } }, + }, + }; + } + + @task *load() { + try { + this.data = yield this.fetch.get(`metrics/dev/${this.args.endpoint}`, { period: this.args.period ?? '30d' }); + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load chart'; + } + } + + @action refresh() { + this.load.perform(); + } +} diff --git a/addon/components/widget/developers-kpi-tile.hbs b/addon/components/widget/developers-kpi-tile.hbs new file mode 100644 index 0000000..b77d8fd --- /dev/null +++ b/addon/components/widget/developers-kpi-tile.hbs @@ -0,0 +1,24 @@ +
+
+
+
+
{{this.title}}
+ {{#if this.error}} +
{{this.error}}
+ {{else if this.load.isRunning}} +
+ {{else}} +
{{this.value}}
+ {{/if}} +
+ +
+ +
+ {{or @footnote (t "developers.component.widget.dashboard.period")}} + +
+
+
diff --git a/addon/components/widget/developers-kpi-tile.js b/addon/components/widget/developers-kpi-tile.js new file mode 100644 index 0000000..1e6628b --- /dev/null +++ b/addon/components/widget/developers-kpi-tile.js @@ -0,0 +1,79 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; + +export default class WidgetDevelopersKpiTileComponent extends Component { + @service fetch; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.load.perform(); + } + + get metric() { + return this.data?.metrics?.[this.args.metric] ?? {}; + } + + get title() { + return this.args.title ?? this.metric.label ?? 'Metric'; + } + + get value() { + const value = this.metric.value ?? 0; + + if (this.metric.format === 'percent') { + return `${value}%`; + } + + if (this.metric.format === 'duration') { + return `${Number(value).toLocaleString()}ms`; + } + + return Number(value).toLocaleString(); + } + + get deltaText() { + const delta = this.metric.delta_percent; + + if (typeof delta !== 'number') { + return 'Current'; + } + + return `${delta > 0 ? '+' : ''}${delta}%`; + } + + get deltaStatus() { + const delta = this.metric.delta_percent; + + if (typeof delta !== 'number' || delta === 0) { + return 'info'; + } + + const positive = delta > 0; + const isGood = this.metric.inverse ? !positive : positive; + + return isGood ? 'success' : 'danger'; + } + + get accentClass() { + return `developers-kpi-accent-${this.args.accent ?? 'blue'}`; + } + + @task *load() { + try { + this.data = yield this.fetch.get('metrics/dev/kpis', { period: this.args.period ?? '30d' }); + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load metric'; + } + } + + @action refresh() { + this.load.perform(); + } +} diff --git a/addon/components/widget/endpoint-health.hbs b/addon/components/widget/endpoint-health.hbs new file mode 100644 index 0000000..4db9ccd --- /dev/null +++ b/addon/components/widget/endpoint-health.hbs @@ -0,0 +1,30 @@ +
+
+
+
{{t "developers.component.widget.dashboard.endpoint-health.title"}}
+
{{t "developers.component.widget.dashboard.endpoint-health.subtitle"}}
+
+ +
+
+ {{#if this.load.isRunning}} +
+ {{else if this.error}} +
{{this.error}}
+ {{else if this.items.length}} + {{#each this.items as |item|}} +
+
+
{{item.url}}
+
{{item.status}} / {{item.mode}} / {{item.deliveries}} deliveries
+
+
{{item.success_rate}}%
+
+ {{/each}} + {{else}} +
{{t "developers.component.widget.dashboard.empty"}}
+ {{/if}} +
+
diff --git a/addon/components/widget/endpoint-health.js b/addon/components/widget/endpoint-health.js new file mode 100644 index 0000000..8f65eac --- /dev/null +++ b/addon/components/widget/endpoint-health.js @@ -0,0 +1,42 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; + +export default class WidgetEndpointHealthComponent extends Component { + @service fetch; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.load.perform(); + } + + get items() { + return this.data?.items ?? []; + } + + statusClass(item) { + if (item.failures > 0 || item.success_rate < 95) { + return 'text-rose-600 dark:text-rose-300'; + } + + return 'text-emerald-600 dark:text-emerald-300'; + } + + @task *load() { + try { + this.data = yield this.fetch.get('metrics/dev/endpoint-health', { period: this.args.period ?? '30d' }); + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load endpoints'; + } + } + + @action refresh() { + this.load.perform(); + } +} diff --git a/addon/components/widget/event-stream.hbs b/addon/components/widget/event-stream.hbs new file mode 100644 index 0000000..b7207be --- /dev/null +++ b/addon/components/widget/event-stream.hbs @@ -0,0 +1,35 @@ +
+
+
+
{{t "developers.component.widget.dashboard.event-stream.title"}}
+
{{t "developers.component.widget.dashboard.event-stream.subtitle"}}
+
+ +
+
+ {{#if this.load.isRunning}} +
+ {{else if this.error}} +
{{this.error}}
+ {{else}} +
{{t "developers.component.widget.dashboard.event-stream.types"}}
+ {{#each this.types as |item|}} +
+
{{item.label}}
+
{{item.value}}
+
+ {{else}} +
{{t "developers.component.widget.dashboard.empty"}}
+ {{/each}} +
{{t "developers.component.widget.dashboard.event-stream.sources"}}
+ {{#each this.sources as |item|}} +
+
{{item.label}}
+
{{item.value}}
+
+ {{/each}} + {{/if}} +
+
diff --git a/addon/components/widget/event-stream.js b/addon/components/widget/event-stream.js new file mode 100644 index 0000000..360cea6 --- /dev/null +++ b/addon/components/widget/event-stream.js @@ -0,0 +1,38 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; + +export default class WidgetEventStreamComponent extends Component { + @service fetch; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.load.perform(); + } + + get types() { + return this.data?.types ?? []; + } + + get sources() { + return this.data?.sources ?? []; + } + + @task *load() { + try { + this.data = yield this.fetch.get('metrics/dev/events', { period: this.args.period ?? '30d' }); + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load events'; + } + } + + @action refresh() { + this.load.perform(); + } +} diff --git a/addon/components/widget/kpi-active-api-keys.hbs b/addon/components/widget/kpi-active-api-keys.hbs new file mode 100644 index 0000000..43aa579 --- /dev/null +++ b/addon/components/widget/kpi-active-api-keys.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-active-api-keys.js b/addon/components/widget/kpi-active-api-keys.js new file mode 100644 index 0000000..94bd09a --- /dev/null +++ b/addon/components/widget/kpi-active-api-keys.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiActiveApiKeysComponent extends Component { + widget = 'active-api-keys'; +} diff --git a/addon/components/widget/kpi-active-webhooks.hbs b/addon/components/widget/kpi-active-webhooks.hbs new file mode 100644 index 0000000..72b67c0 --- /dev/null +++ b/addon/components/widget/kpi-active-webhooks.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-active-webhooks.js b/addon/components/widget/kpi-active-webhooks.js new file mode 100644 index 0000000..0d6b2d5 --- /dev/null +++ b/addon/components/widget/kpi-active-webhooks.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiActiveWebhooksComponent extends Component { + widget = 'active-webhooks'; +} diff --git a/addon/components/widget/kpi-api-error-rate.hbs b/addon/components/widget/kpi-api-error-rate.hbs new file mode 100644 index 0000000..eb08c2b --- /dev/null +++ b/addon/components/widget/kpi-api-error-rate.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-api-error-rate.js b/addon/components/widget/kpi-api-error-rate.js new file mode 100644 index 0000000..e7a9a4f --- /dev/null +++ b/addon/components/widget/kpi-api-error-rate.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiApiErrorRateComponent extends Component { + widget = 'api-error-rate'; +} diff --git a/addon/components/widget/kpi-api-latency.hbs b/addon/components/widget/kpi-api-latency.hbs new file mode 100644 index 0000000..e64160e --- /dev/null +++ b/addon/components/widget/kpi-api-latency.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-api-latency.js b/addon/components/widget/kpi-api-latency.js new file mode 100644 index 0000000..d015781 --- /dev/null +++ b/addon/components/widget/kpi-api-latency.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiApiLatencyComponent extends Component { + widget = 'api-latency'; +} diff --git a/addon/components/widget/kpi-api-requests.hbs b/addon/components/widget/kpi-api-requests.hbs new file mode 100644 index 0000000..ba1c2b2 --- /dev/null +++ b/addon/components/widget/kpi-api-requests.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-api-requests.js b/addon/components/widget/kpi-api-requests.js new file mode 100644 index 0000000..fdfadac --- /dev/null +++ b/addon/components/widget/kpi-api-requests.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiApiRequestsComponent extends Component { + widget = 'api-requests'; +} diff --git a/addon/components/widget/kpi-events-emitted.hbs b/addon/components/widget/kpi-events-emitted.hbs new file mode 100644 index 0000000..875244c --- /dev/null +++ b/addon/components/widget/kpi-events-emitted.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-events-emitted.js b/addon/components/widget/kpi-events-emitted.js new file mode 100644 index 0000000..654c23b --- /dev/null +++ b/addon/components/widget/kpi-events-emitted.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiEventsEmittedComponent extends Component { + widget = 'events-emitted'; +} diff --git a/addon/components/widget/kpi-webhook-failures.hbs b/addon/components/widget/kpi-webhook-failures.hbs new file mode 100644 index 0000000..816a4b2 --- /dev/null +++ b/addon/components/widget/kpi-webhook-failures.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-webhook-failures.js b/addon/components/widget/kpi-webhook-failures.js new file mode 100644 index 0000000..b8b991f --- /dev/null +++ b/addon/components/widget/kpi-webhook-failures.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiWebhookFailuresComponent extends Component { + widget = 'webhook-failures'; +} diff --git a/addon/components/widget/kpi-webhook-success.hbs b/addon/components/widget/kpi-webhook-success.hbs new file mode 100644 index 0000000..ec8c427 --- /dev/null +++ b/addon/components/widget/kpi-webhook-success.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-webhook-success.js b/addon/components/widget/kpi-webhook-success.js new file mode 100644 index 0000000..e59987c --- /dev/null +++ b/addon/components/widget/kpi-webhook-success.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiWebhookSuccessComponent extends Component { + widget = 'webhook-success'; +} diff --git a/addon/components/widget/quick-resources.hbs b/addon/components/widget/quick-resources.hbs new file mode 100644 index 0000000..dfac2ea --- /dev/null +++ b/addon/components/widget/quick-resources.hbs @@ -0,0 +1,17 @@ +
+
+
+
{{t "developers.component.widget.dashboard.quick-resources.title"}}
+
{{t "developers.component.widget.dashboard.quick-resources.subtitle"}}
+
+
+
+ {{#each this.resources as |resource|}} + + + {{resource.label}} + + + {{/each}} +
+
diff --git a/addon/components/widget/quick-resources.js b/addon/components/widget/quick-resources.js new file mode 100644 index 0000000..6d75b8b --- /dev/null +++ b/addon/components/widget/quick-resources.js @@ -0,0 +1,11 @@ +import Component from '@glimmer/component'; + +export default class WidgetQuickResourcesComponent extends Component { + resources = [ + { label: 'API Keys', route: 'api-keys.index', icon: 'key' }, + { label: 'Webhooks', route: 'webhooks.index', icon: 'globe' }, + { label: 'Logs', route: 'logs.index', icon: 'terminal' }, + { label: 'Events', route: 'events.index', icon: 'bolt' }, + { label: 'Sockets', route: 'sockets.index', icon: 'plug' }, + ]; +} diff --git a/addon/components/widget/webhook-delivery.hbs b/addon/components/widget/webhook-delivery.hbs new file mode 100644 index 0000000..31c604a --- /dev/null +++ b/addon/components/widget/webhook-delivery.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/webhook-delivery.js b/addon/components/widget/webhook-delivery.js new file mode 100644 index 0000000..0a2de4e --- /dev/null +++ b/addon/components/widget/webhook-delivery.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class WidgetWebhookDeliveryComponent extends Component { + widget = 'webhook-delivery'; +} diff --git a/addon/controllers/api-keys/index.js b/addon/controllers/api-keys/index.js index 5035964..9e52881 100644 --- a/addon/controllers/api-keys/index.js +++ b/addon/controllers/api-keys/index.js @@ -28,7 +28,7 @@ export default class ApiKeysIndexController extends Controller { * * @var {Array} */ - queryParams = ['page', 'limit', 'sort']; + queryParams = ['page', 'limit', 'sort', 'query', 'view_api_key']; /** * Expiration options for api keys @@ -72,6 +72,13 @@ export default class ApiKeysIndexController extends Controller { */ @tracked query; + /** + * Deep-linked API key to open in the edit modal. + * + * @var {String} + */ + @tracked view_api_key; + /** * Checks if console environment is in live mode * @@ -509,4 +516,32 @@ export default class ApiKeysIndexController extends Controller { @action reload() { return this.hostRouter.refresh(); } + + @action async openDeepLinkedApiKey() { + const apiKeyId = this.view_api_key; + + if (!apiKeyId) { + return; + } + + try { + const apiKey = this.store.peekRecord('api-credential', apiKeyId) ?? (await this.store.findRecord('api-credential', apiKeyId)); + this.editApiKey(apiKey, { + onDecline: this.clearDeepLinkedApiKey, + onFinish: this.clearDeepLinkedApiKey, + }); + } catch (_) { + this.notifications.warning('Unable to open the selected API key.'); + this.clearDeepLinkedApiKey(); + } + } + + @action clearDeepLinkedApiKey() { + if (!this.view_api_key) { + return; + } + + this.view_api_key = null; + this.hostRouter.transitionTo({ queryParams: { view_api_key: null } }); + } } diff --git a/addon/controllers/application.js b/addon/controllers/application.js new file mode 100644 index 0000000..b4a16cc --- /dev/null +++ b/addon/controllers/application.js @@ -0,0 +1,94 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class ApplicationController extends Controller { + @service intl; + @service abilities; + @service fetch; + + get navigationItems() { + return [ + { + label: this.intl.t('developers.application.sidebar.items.home'), + description: 'Developer dashboard and API health overview.', + icon: 'home', + route: 'console.developers.home', + keywords: ['dashboard', 'overview', 'metrics'], + }, + { + label: this.intl.t('developers.application.sidebar.items.api-keys'), + description: 'Create and manage API credentials.', + icon: 'key', + route: 'console.developers.api-keys', + permission: 'developers list api-key', + visible: this.can('developers see api-key'), + keywords: ['credentials', 'access keys', 'sandbox', 'live keys'], + }, + { + label: this.intl.t('developers.application.sidebar.items.webhooks'), + description: 'Configure webhook endpoints.', + icon: 'globe-asia', + route: 'console.developers.webhooks', + permission: 'developers list webhook', + visible: this.can('developers see webhook'), + keywords: ['endpoints', 'callbacks', 'notifications'], + }, + { + label: this.intl.t('developers.application.sidebar.items.websockets'), + description: 'Inspect websocket channels.', + icon: 'plug', + route: 'console.developers.sockets', + permission: 'developers list socket', + visible: this.can('developers see socket'), + keywords: ['sockets', 'channels', 'realtime'], + }, + { + label: this.intl.t('developers.application.sidebar.items.logs'), + description: 'Review API request logs.', + icon: 'file-lines', + route: 'console.developers.logs', + permission: 'developers list log', + visible: this.can('developers see log'), + keywords: ['requests', 'traffic', 'debugging'], + }, + { + label: this.intl.t('developers.application.sidebar.items.events'), + description: 'Browse platform events.', + icon: 'calendar-day', + route: 'console.developers.events', + permission: 'developers list event', + visible: this.can('developers see event'), + keywords: ['event stream', 'activity', 'webhook events'], + }, + ]; + } + + can(permission) { + try { + return this.abilities.can(permission); + } catch (_) { + return true; + } + } + + @action + async searchNavigation({ query, limit = 12 }) { + const trimmedQuery = query?.trim(); + + if (!trimmedQuery) { + return []; + } + + try { + const response = await this.fetch.get('developers/search', { + query: trimmedQuery, + limit, + }); + + return response.results ?? []; + } catch (_) { + return []; + } + } +} diff --git a/addon/extension.js b/addon/extension.js index 96f9627..a29aca3 100644 --- a/addon/extension.js +++ b/addon/extension.js @@ -20,7 +20,7 @@ export default { { title: 'Webhooks', description: 'Configure webhook endpoints to receive real-time event notifications.', - icon: 'webhook', + icon: 'globe', route: 'console.developers.webhooks', }, { @@ -44,19 +44,180 @@ export default { ], }); - // Register widgets + // Register dashboard and widgets const widgets = [ + new Widget({ + id: 'developers-kpi-api-requests', + name: 'API Requests', + description: 'Total API requests for the current period.', + icon: 'code', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/kpi-api-requests'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'developers-kpi-api-error-rate', + name: 'API Error Rate', + description: 'Percentage of API requests returning an error.', + icon: 'triangle-exclamation', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/kpi-api-error-rate'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'developers-kpi-api-latency', + name: 'Avg API Latency', + description: 'Average API response duration for the current period.', + icon: 'gauge-high', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/kpi-api-latency'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'developers-kpi-webhook-success', + name: 'Webhook Success Rate', + description: 'Percentage of webhook deliveries returning a 2xx response.', + icon: 'webhook', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/kpi-webhook-success'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'developers-kpi-active-api-keys', + name: 'Active API Keys', + description: 'Active API credentials in this organization.', + icon: 'key', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/kpi-active-api-keys'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'developers-kpi-active-webhooks', + name: 'Active Webhooks', + description: 'Enabled webhook endpoints in this organization.', + icon: 'plug-circle-check', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/kpi-active-webhooks'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'developers-kpi-webhook-failures', + name: 'Webhook Failures', + description: 'Failed webhook deliveries for the current period.', + icon: 'webhook', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/kpi-webhook-failures'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'developers-kpi-events-emitted', + name: 'Events Emitted', + description: 'Platform events emitted in the current period.', + icon: 'bolt', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/kpi-events-emitted'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'developers-api-traffic', + name: 'API Traffic', + description: 'API request volume, successes, and errors over time.', + icon: 'chart-line', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/api-traffic'), + grid_options: { w: 6, h: 8, minW: 5, minH: 7 }, + category: 'Analytics', + default: true, + }), + new Widget({ + id: 'developers-webhook-delivery', + name: 'Webhook Delivery Health', + description: 'Webhook delivery volume, success, failure, and retry health.', + icon: 'chart-column', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/webhook-delivery'), + grid_options: { w: 6, h: 8, minW: 5, minH: 7 }, + category: 'Analytics', + default: true, + }), + new Widget({ + id: 'developers-endpoint-health', + name: 'Endpoint Health', + description: 'Webhook endpoint delivery health and recent failures.', + icon: 'heart-pulse', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/endpoint-health'), + grid_options: { w: 6, h: 8, minW: 5, minH: 7 }, + category: 'Operations', + default: true, + }), + new Widget({ + id: 'developers-event-stream', + name: 'Event Stream', + description: 'Top event types and event sources in the selected period.', + icon: 'bolt', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/event-stream'), + grid_options: { w: 6, h: 8, minW: 5, minH: 7 }, + category: 'Operations', + default: true, + }), + new Widget({ + id: 'developers-activity', + name: 'Developer Activity', + description: 'Recent API requests, webhook deliveries, and events.', + icon: 'timeline', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/developer-activity'), + grid_options: { w: 6, h: 8, minW: 5, minH: 7 }, + category: 'Operations', + default: true, + }), + new Widget({ + id: 'developers-quick-resources', + name: 'Quick Resources', + description: 'Shortcuts into API keys, webhooks, logs, events, and sockets.', + icon: 'link', + component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/quick-resources'), + grid_options: { w: 6, h: 8, minW: 5, minH: 7 }, + category: 'Resources', + default: true, + }), new Widget({ id: 'dev-api-metrics-widget', - name: 'Developer API Metrics', - description: 'Key metrics from API Usage.', + name: 'Developer API Metrics (Legacy)', + description: 'Legacy grouped monitoring widget. Replaced by individual developer dashboard widgets.', icon: 'code', component: new ExtensionComponent('@fleetbase/dev-engine', 'widget/api-metrics'), grid_options: { w: 12, h: 12, minW: 8, minH: 12 }, options: { title: 'API Metrics' }, + category: 'Legacy', + default: false, }), ]; - widgetService.registerWidgets('dashboard', widgets); + const getWidgetById = (id = null, mutate = null) => { + if (!id) return null; + const widget = widgets.find((w) => w.id === id); + const clone = new Widget(widget.toObject()); + if (typeof mutate === 'function') { + mutate(clone); + } + return clone; + }; + + widgetService.registerDashboard('developers'); + widgetService.registerWidgets('developers', widgets); + // widgetService.registerWidgets('dashboard', [ + // getWidgetById('developers-kpi-api-error-rate'), + // getWidgetById('developers-kpi-api-latency'), + // getWidgetById('developers-kpi-webhook-success'), + // getWidgetById('developers-api-traffic', (widget) => { + // widget.withGridOptions({ w: 6, h: 7, minW: 5, minH: 6 }); + // }), + // ]); }, }; diff --git a/addon/routes/api-keys/index.js b/addon/routes/api-keys/index.js index 327ab8b..91dacb8 100644 --- a/addon/routes/api-keys/index.js +++ b/addon/routes/api-keys/index.js @@ -1,6 +1,7 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; +import { later } from '@ember/runloop'; export default class ApiKeysIndexRoute extends Route { @service store; @@ -21,6 +22,9 @@ export default class ApiKeysIndexRoute extends Route { query: { refreshModel: true, }, + view_api_key: { + refreshModel: false, + }, sort: { refreshModel: true, }, @@ -38,10 +42,15 @@ export default class ApiKeysIndexRoute extends Route { } model(params) { - return this.store.query('api-credential', { ...params }); + const queryParams = { ...params }; + delete queryParams.view_api_key; + + return this.store.query('api-credential', { ...queryParams }); } setupController(controller) { + super.setupController(...arguments); controller.testMode = this.currentUser.getOption('sandbox', false); + later(controller, controller.openDeepLinkedApiKey); } } diff --git a/addon/styles/dev-engine.css b/addon/styles/dev-engine.css index 99f2e9e..4a66599 100644 --- a/addon/styles/dev-engine.css +++ b/addon/styles/dev-engine.css @@ -49,4 +49,324 @@ body[data-theme='dark'] .webhook-attempts-date-filter-container > .date-filter-l position: relative; height: 100%; width: 100%; -} \ No newline at end of file +} + +.developers-dashboard-widget { + min-width: 0; + border-color: #e5e7eb; + background-color: #fff; + animation: developers-widget-fade-up 320ms cubic-bezier(0.16, 1, 0.3, 1) both; + transition: + border-color 180ms ease, + box-shadow 180ms ease; +} + +.developers-dashboard-widget:hover { + border-color: #cbd5e1; + box-shadow: 0 10px 28px -22px rgb(15 23 42 / 45%); +} + +body[data-theme='dark'] .developers-dashboard-widget { + border-color: #374151; + background-color: #1f2937; +} + +@keyframes developers-widget-fade-up { + from { + opacity: 0; + transform: translateY(8px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.developers-dashboard-page { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + gap: 1rem; + border-bottom: 1px solid #e5e7eb; + background-color: #fff; + padding: 1rem; + box-shadow: 0 1px 2px rgb(15 23 42 / 6%); +} + +body[data-theme='dark'] .developers-dashboard-page { + border-bottom-color: #1f2937; + background-color: #111827; + box-shadow: 0 1px 2px rgb(2 6 23 / 30%); +} + +.developers-dashboard-title h1 { + color: #111827; + font-size: 1.05rem; + font-weight: 800; + line-height: 1.2; +} + +body[data-theme='dark'] .developers-dashboard-title h1 { + color: #f9fafb; +} + +.developers-dashboard-actions { + justify-content: flex-end; +} + +.developers-dashboard-create-wrapper { + padding: 1rem; +} + +.developers-widget-header { + display: flex; + min-height: 3.25rem; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + border-bottom: 1px solid #e5e7eb; + padding: 0.75rem; +} + +body[data-theme='dark'] .developers-widget-header { + border-bottom-color: #374151; +} + +.developers-widget-title { + overflow: hidden; + color: #111827; + font-size: 0.82rem; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +body[data-theme='dark'] .developers-widget-title { + color: #f9fafb; +} + +.developers-widget-subtitle, +.developers-list-subtitle { + overflow: hidden; + color: #6b7280; + font-size: 0.69rem; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +body[data-theme='dark'] .developers-widget-subtitle, +body[data-theme='dark'] .developers-list-subtitle { + color: #9ca3af; +} + +.developers-widget-icon-button, +.developers-kpi-icon { + width: 1.75rem; + height: 1.75rem; + flex: 0 0 1.75rem; + color: #64748b; +} + +.developers-widget-icon-button:hover, +.developers-kpi-icon:hover { + color: #111827; +} + +body[data-theme='dark'] .developers-widget-icon-button:hover, +body[data-theme='dark'] .developers-kpi-icon:hover { + color: #f9fafb; +} + +.developers-kpi-label { + color: #6b7280; + font-size: 0.625rem; + font-weight: 800; + line-height: 1.15; + text-transform: uppercase; +} + +body[data-theme='dark'] .developers-kpi-label { + color: #9ca3af; +} + +.developers-kpi-value { + color: #111827; + font-size: 1.45rem; + font-weight: 800; + line-height: 1; +} + +body[data-theme='dark'] .developers-kpi-value { + color: #f9fafb; +} + +.developers-kpi-delta { + max-width: 5.5rem; +} + +.developers-kpi-accent-blue { + background-image: linear-gradient(135deg, rgb(37 99 235 / 12%) 0%, rgb(37 99 235 / 0%) 64%); +} + +.developers-kpi-accent-rose { + background-image: linear-gradient(135deg, rgb(225 29 72 / 12%) 0%, rgb(225 29 72 / 0%) 64%); +} + +.developers-kpi-accent-amber { + background-image: linear-gradient(135deg, rgb(245 158 11 / 14%) 0%, rgb(245 158 11 / 0%) 64%); +} + +.developers-kpi-accent-emerald { + background-image: linear-gradient(135deg, rgb(16 185 129 / 12%) 0%, rgb(16 185 129 / 0%) 64%); +} + +.developers-kpi-accent-indigo { + background-image: linear-gradient(135deg, rgb(79 70 229 / 12%) 0%, rgb(79 70 229 / 0%) 64%); +} + +.developers-kpi-accent-cyan { + background-image: linear-gradient(135deg, rgb(6 182 212 / 12%) 0%, rgb(6 182 212 / 0%) 64%); +} + +.developers-chart-widget { + overflow: hidden; +} + +.developers-chart-body { + display: flex; + min-height: 0; + min-width: 0; + flex: 1 1 auto; + align-items: stretch; + justify-content: stretch; + padding: 0.75rem; +} + +.developers-chart-frame { + position: relative; + min-height: 0; + width: 100%; + flex: 1 1 auto; +} + +.developers-dashboard-widget .ui-chart { + position: relative; + width: 100%; + height: 100%; + min-height: 0; +} + +.developers-dashboard-widget .ui-chart > canvas { + position: absolute; + inset: 0; + width: 100% !important; + height: 100% !important; +} + +.developers-widget-scroll-body { + min-height: 0; + flex: 1 1 auto; + overflow-y: auto; + padding: 0.5rem; +} + +.developers-list-row, +.developers-activity-row, +.developers-resource-link { + display: flex; + min-width: 0; + align-items: center; + gap: 0.65rem; + border-radius: 0.375rem; + padding: 0.5rem; +} + +.developers-resource-link { + color: #111827; + font-size: 0.78rem; + font-weight: 700; +} + +.developers-list-row + .developers-list-row, +.developers-activity-row + .developers-activity-row, +.developers-resource-link + .developers-resource-link { + margin-top: 0.25rem; +} + +.developers-list-row:hover, +.developers-activity-row:hover, +.developers-resource-link:hover { + background-color: #f8fafc; +} + +body[data-theme='dark'] .developers-resource-link { + color: #f9fafb; +} + +body[data-theme='dark'] .developers-list-row:hover, +body[data-theme='dark'] .developers-activity-row:hover, +body[data-theme='dark'] .developers-resource-link:hover { + background-color: #111827; +} + +.developers-list-title { + min-width: 0; + overflow: hidden; + color: #111827; + font-size: 0.75rem; + font-weight: 700; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +body[data-theme='dark'] .developers-list-title { + color: #f9fafb; +} + +.developers-list-metric { + margin-left: auto; + flex: 0 0 auto; + font-size: 0.78rem; + font-weight: 800; +} + +.developers-activity-icon { + display: flex; + width: 1.75rem; + height: 1.75rem; + flex: 0 0 1.75rem; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + background-color: #eff6ff; + color: #2563eb; + font-size: 0.75rem; +} + +body[data-theme='dark'] .developers-activity-icon { + background-color: rgb(37 99 235 / 16%); + color: #93c5fd; +} + +.developers-mini-section { + padding: 0.25rem 0.5rem; + color: #64748b; + font-size: 0.62rem; + font-weight: 800; + text-transform: uppercase; +} + +.developers-empty-state { + display: flex; + min-height: 5rem; + align-items: center; + justify-content: center; + color: #64748b; + font-size: 0.75rem; + font-weight: 600; +} diff --git a/addon/templates/application.hbs b/addon/templates/application.hbs index c35e643..c4cc71f 100644 --- a/addon/templates/application.hbs +++ b/addon/templates/application.hbs @@ -1,15 +1,7 @@ - - {{t "developers.application.sidebar.items.home"}} - {{t "developers.application.sidebar.items.api-keys"}} - {{t "developers.application.sidebar.items.webhooks"}} - {{t "developers.application.sidebar.items.websockets"}} - {{t "developers.application.sidebar.items.logs"}} - {{t "developers.application.sidebar.items.events"}} - - + {{outlet}} - \ No newline at end of file + diff --git a/addon/templates/home/index.hbs b/addon/templates/home/index.hbs index 8cfcee8..1f2674f 100644 --- a/addon/templates/home/index.hbs +++ b/addon/templates/home/index.hbs @@ -1,3 +1,14 @@ - - - \ No newline at end of file + + + + {{outlet}} + diff --git a/app/components/widget/api-traffic.js b/app/components/widget/api-traffic.js new file mode 100644 index 0000000..5783957 --- /dev/null +++ b/app/components/widget/api-traffic.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/api-traffic'; diff --git a/app/components/widget/developer-activity.js b/app/components/widget/developer-activity.js new file mode 100644 index 0000000..0682b8e --- /dev/null +++ b/app/components/widget/developer-activity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/developer-activity'; diff --git a/app/components/widget/developers-chart.js b/app/components/widget/developers-chart.js new file mode 100644 index 0000000..8f41481 --- /dev/null +++ b/app/components/widget/developers-chart.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/developers-chart'; diff --git a/app/components/widget/developers-kpi-tile.js b/app/components/widget/developers-kpi-tile.js new file mode 100644 index 0000000..7d7fcd8 --- /dev/null +++ b/app/components/widget/developers-kpi-tile.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/developers-kpi-tile'; diff --git a/app/components/widget/endpoint-health.js b/app/components/widget/endpoint-health.js new file mode 100644 index 0000000..28c5c53 --- /dev/null +++ b/app/components/widget/endpoint-health.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/endpoint-health'; diff --git a/app/components/widget/event-stream.js b/app/components/widget/event-stream.js new file mode 100644 index 0000000..6f1cf54 --- /dev/null +++ b/app/components/widget/event-stream.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/event-stream'; diff --git a/app/components/widget/kpi-active-api-keys.js b/app/components/widget/kpi-active-api-keys.js new file mode 100644 index 0000000..1c56d7e --- /dev/null +++ b/app/components/widget/kpi-active-api-keys.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/kpi-active-api-keys'; diff --git a/app/components/widget/kpi-active-webhooks.js b/app/components/widget/kpi-active-webhooks.js new file mode 100644 index 0000000..eebe9f2 --- /dev/null +++ b/app/components/widget/kpi-active-webhooks.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/kpi-active-webhooks'; diff --git a/app/components/widget/kpi-api-error-rate.js b/app/components/widget/kpi-api-error-rate.js new file mode 100644 index 0000000..822aae7 --- /dev/null +++ b/app/components/widget/kpi-api-error-rate.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/kpi-api-error-rate'; diff --git a/app/components/widget/kpi-api-latency.js b/app/components/widget/kpi-api-latency.js new file mode 100644 index 0000000..4ab224a --- /dev/null +++ b/app/components/widget/kpi-api-latency.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/kpi-api-latency'; diff --git a/app/components/widget/kpi-api-requests.js b/app/components/widget/kpi-api-requests.js new file mode 100644 index 0000000..c2435bd --- /dev/null +++ b/app/components/widget/kpi-api-requests.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/kpi-api-requests'; diff --git a/app/components/widget/kpi-events-emitted.js b/app/components/widget/kpi-events-emitted.js new file mode 100644 index 0000000..3cf0b0a --- /dev/null +++ b/app/components/widget/kpi-events-emitted.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/kpi-events-emitted'; diff --git a/app/components/widget/kpi-webhook-failures.js b/app/components/widget/kpi-webhook-failures.js new file mode 100644 index 0000000..176b96d --- /dev/null +++ b/app/components/widget/kpi-webhook-failures.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/kpi-webhook-failures'; diff --git a/app/components/widget/kpi-webhook-success.js b/app/components/widget/kpi-webhook-success.js new file mode 100644 index 0000000..81e46f6 --- /dev/null +++ b/app/components/widget/kpi-webhook-success.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/kpi-webhook-success'; diff --git a/app/components/widget/quick-resources.js b/app/components/widget/quick-resources.js new file mode 100644 index 0000000..c54a738 --- /dev/null +++ b/app/components/widget/quick-resources.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/quick-resources'; diff --git a/app/components/widget/webhook-delivery.js b/app/components/widget/webhook-delivery.js new file mode 100644 index 0000000..6f0d8dc --- /dev/null +++ b/app/components/widget/webhook-delivery.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/components/widget/webhook-delivery'; diff --git a/app/controllers/application.js b/app/controllers/application.js new file mode 100644 index 0000000..09b6f58 --- /dev/null +++ b/app/controllers/application.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/dev-engine/controllers/application'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70ad693..3e27283 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9421,7 +9421,7 @@ snapshots: dependencies: postcss: 8.5.6 - '@ember-data/adapter@4.12.8(@ember-data/store@4.12.8(@babel/core@7.28.5)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))': + '@ember-data/adapter@4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))': dependencies: '@ember-data/private-build-infra': 4.12.8 '@ember-data/store': 4.12.8(@babel/core@7.28.5)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)) @@ -9552,7 +9552,7 @@ snapshots: '@ember-data/rfc395-data@0.0.4': {} - '@ember-data/serializer@4.12.8(@ember-data/store@4.12.8(@babel/core@7.28.5)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))': + '@ember-data/serializer@4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))': dependencies: '@ember-data/private-build-infra': 4.12.8 '@ember-data/store': 4.12.8(@babel/core@7.28.5)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)) @@ -13698,7 +13698,7 @@ snapshots: ember-data@4.12.8(@babel/core@7.28.5)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0): dependencies: - '@ember-data/adapter': 4.12.8(@ember-data/store@4.12.8(@babel/core@7.28.5)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))) + '@ember-data/adapter': 4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))) '@ember-data/debug': 4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(webpack@5.103.0) '@ember-data/graph': 4.12.8(@ember-data/store@4.12.8) '@ember-data/json-api': 4.12.8(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) @@ -13706,7 +13706,7 @@ snapshots: '@ember-data/model': 4.12.8(@babel/core@7.28.5)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)) '@ember-data/private-build-infra': 4.12.8 '@ember-data/request': 4.12.8 - '@ember-data/serializer': 4.12.8(@ember-data/store@4.12.8(@babel/core@7.28.5)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))) + '@ember-data/serializer': 4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))) '@ember-data/store': 4.12.8(@babel/core@7.28.5)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)) '@ember-data/tracking': 4.12.8 '@ember/edition-utils': 1.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..bf39b1a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +allowBuilds: + '@fortawesome/fontawesome-common-types': false + '@fortawesome/fontawesome-svg-core': false + '@fortawesome/free-brands-svg-icons': false + '@fortawesome/free-solid-svg-icons': false + core-js: false + fsevents: false +minimumReleaseAge: 0 \ No newline at end of file diff --git a/tests/integration/components/widget/developers-kpi-tile-test.js b/tests/integration/components/widget/developers-kpi-tile-test.js new file mode 100644 index 0000000..1f17b45 --- /dev/null +++ b/tests/integration/components/widget/developers-kpi-tile-test.js @@ -0,0 +1,36 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'dummy/tests/helpers'; +import { render } from '@ember/test-helpers'; +import Service from '@ember/service'; +import { hbs } from 'ember-cli-htmlbars'; + +class FetchStub extends Service { + get() { + return Promise.resolve({ + metrics: { + api_requests: { + label: 'API Requests', + value: 42, + format: 'count', + delta_percent: 12, + }, + }, + }); + } +} + +module('Integration | Component | widget/developers-kpi-tile', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:fetch', FetchStub); + }); + + test('it renders a dashboard KPI metric', async function (assert) { + await render(hbs``); + + assert.dom('.developers-kpi-tile').exists(); + assert.dom('.developers-kpi-label').hasText('API Requests'); + assert.dom('.developers-kpi-value').hasText('42'); + }); +}); diff --git a/tests/integration/components/widget/quick-resources-test.js b/tests/integration/components/widget/quick-resources-test.js new file mode 100644 index 0000000..c1a5824 --- /dev/null +++ b/tests/integration/components/widget/quick-resources-test.js @@ -0,0 +1,17 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'dummy/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | widget/quick-resources', function (hooks) { + setupRenderingTest(hooks); + + test('it renders developer resource shortcuts', async function (assert) { + await render(hbs``); + + assert.dom('.developers-dashboard-widget').exists(); + assert.dom('.developers-resource-link').exists({ count: 5 }); + assert.dom('.developers-resource-link').includesText('API Keys'); + assert.dom('.developers-resource-link').includesText('Webhooks'); + }); +}); diff --git a/tests/unit/controllers/api-keys/index-test.js b/tests/unit/controllers/api-keys/index-test.js index efd7283..382d956 100644 --- a/tests/unit/controllers/api-keys/index-test.js +++ b/tests/unit/controllers/api-keys/index-test.js @@ -1,12 +1,125 @@ import { module, test } from 'qunit'; import { setupTest } from 'dummy/tests/helpers'; +class IntlStub { + t(key) { + return key; + } +} + +class CurrentUserStub { + options = {}; + user = {}; + + getOption(key, defaultValue = null) { + return key in this.options ? this.options[key] : defaultValue; + } + + setOption(key, value) { + this.options[key] = value; + } +} + +class AbilitiesStub { + cannot() { + return false; + } +} + +class StoreStub { + record = { id: 'api_key_uuid', name: 'Live API Key' }; + findRequests = []; + + peekRecord() { + return null; + } + + findRecord(modelName, id) { + this.findRequests.push({ modelName, id }); + return Promise.resolve(this.record); + } +} + +class HostRouterStub { + transitions = []; + + transitionTo(...args) { + this.transitions.push(args); + return Promise.resolve(); + } + + refresh() { + return Promise.resolve(); + } +} + +class ModalsManagerStub { + show() {} + confirm() {} +} + +class NotificationsStub { + warnings = []; + + warning(message) { + this.warnings.push(message); + } + + serverError() {} + success() {} +} + +class EmptyServiceStub {} + module('Unit | Controller | api-keys/index', function (hooks) { setupTest(hooks); - // TODO: Replace this with your real tests. + hooks.beforeEach(function () { + this.owner.register('service:intl', IntlStub); + this.owner.register('service:current-user', CurrentUserStub); + this.owner.register('service:abilities', AbilitiesStub); + this.owner.register('service:store', StoreStub); + this.owner.register('service:host-router', HostRouterStub); + this.owner.register('service:modals-manager', ModalsManagerStub); + this.owner.register('service:notifications', NotificationsStub); + this.owner.register('service:crud', EmptyServiceStub); + this.owner.register('service:fetch', EmptyServiceStub); + this.owner.register('service:theme', EmptyServiceStub); + this.owner.register('service:universe', EmptyServiceStub); + }); + test('it exists', function (assert) { let controller = this.owner.lookup('controller:api-keys/index'); assert.ok(controller); }); + + test('it opens a deep-linked API key in the existing edit modal', async function (assert) { + const controller = this.owner.lookup('controller:api-keys/index'); + const store = this.owner.lookup('service:store'); + + controller.view_api_key = 'api_key_uuid'; + controller.editApiKey = (apiKey, options) => { + assert.strictEqual(apiKey, store.record); + assert.strictEqual(typeof options.onDecline, 'function'); + assert.strictEqual(typeof options.onFinish, 'function'); + }; + + await controller.openDeepLinkedApiKey(); + + assert.deepEqual(store.findRequests, [{ modelName: 'api-credential', id: 'api_key_uuid' }]); + }); + + test('it clears only the API key deep-link query param', function (assert) { + const controller = this.owner.lookup('controller:api-keys/index'); + const hostRouter = this.owner.lookup('service:host-router'); + + controller.query = 'live'; + controller.view_api_key = 'api_key_uuid'; + + controller.clearDeepLinkedApiKey(); + + assert.strictEqual(controller.view_api_key, null); + assert.strictEqual(controller.query, 'live', 'table query is preserved'); + assert.deepEqual(hostRouter.transitions, [[{ queryParams: { view_api_key: null } }]]); + }); }); diff --git a/tests/unit/controllers/application-test.js b/tests/unit/controllers/application-test.js new file mode 100644 index 0000000..83cd3b8 --- /dev/null +++ b/tests/unit/controllers/application-test.js @@ -0,0 +1,114 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; + +class IntlStub { + translations = { + 'developers.application.sidebar.items.home': 'Dashboard', + 'developers.application.sidebar.items.api-keys': 'API Keys', + 'developers.application.sidebar.items.webhooks': 'Webhooks', + 'developers.application.sidebar.items.websockets': 'WebSockets', + 'developers.application.sidebar.items.logs': 'Logs', + 'developers.application.sidebar.items.events': 'Events', + }; + + t(key) { + return this.translations[key] ?? key; + } +} + +class AbilitiesStub { + denied = new Set(); + + can(permission) { + return !this.denied.has(permission); + } +} + +class FetchStub { + requests = []; + response = { + results: [ + { + label: 'Live API Key', + description: 'flb_live_123', + icon: 'key', + type: 'API Key', + route: 'console.developers.api-keys.index', + breadcrumb: 'Developers > API Keys', + queryParams: { query: 'live', view_api_key: 'api_key_uuid' }, + }, + ], + }; + + get(url, params) { + this.requests.push({ url, params }); + return Promise.resolve(this.response); + } +} + +module('Unit | Controller | application', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:intl', IntlStub); + this.owner.register('service:abilities', AbilitiesStub); + this.owner.register('service:fetch', FetchStub); + }); + + test('it builds developer sidebar navigator items with host routes', function (assert) { + const controller = this.owner.lookup('controller:application'); + const items = controller.navigationItems; + + assert.deepEqual( + items.map((item) => item.label), + ['Dashboard', 'API Keys', 'Webhooks', 'WebSockets', 'Logs', 'Events'], + 'root items keep the developer labels' + ); + assert.deepEqual( + items.map((item) => item.route), + ['console.developers.home', 'console.developers.api-keys', 'console.developers.webhooks', 'console.developers.sockets', 'console.developers.logs', 'console.developers.events'], + 'root items keep the console host route names' + ); + assert.strictEqual(items[1].permission, 'developers list api-key'); + assert.true(items[1].visible, 'api keys item is visible when the see permission is allowed'); + }); + + test('it marks developer navigator items hidden when see permissions are denied', function (assert) { + const abilities = this.owner.lookup('service:abilities'); + abilities.denied.add('developers see webhook'); + + const controller = this.owner.lookup('controller:application'); + const webhooks = controller.navigationItems.find((item) => item.route === 'console.developers.webhooks'); + + assert.false(webhooks.visible); + }); + + test('it fetches developer resource search results for the sidebar navigator', async function (assert) { + const controller = this.owner.lookup('controller:application'); + const fetch = this.owner.lookup('service:fetch'); + const results = await controller.searchNavigation({ query: ' live ', limit: 12 }); + + assert.deepEqual(fetch.requests, [{ url: 'developers/search', params: { query: 'live', limit: 12 } }], 'calls the developer search endpoint with the trimmed query'); + assert.deepEqual(results, fetch.response.results, 'returns navigator-ready endpoint results'); + }); + + test('it does not fetch developer resource search results for blank queries', async function (assert) { + const controller = this.owner.lookup('controller:application'); + const fetch = this.owner.lookup('service:fetch'); + const results = await controller.searchNavigation({ query: ' ', limit: 12 }); + + assert.deepEqual(results, []); + assert.deepEqual(fetch.requests, [], 'blank queries do not call the adapter'); + }); + + test('it returns empty developer search results when the adapter fails', async function (assert) { + const controller = this.owner.lookup('controller:application'); + const fetch = this.owner.lookup('service:fetch'); + + fetch.get = () => Promise.reject(new Error('adapter failed')); + + const results = await controller.searchNavigation({ query: 'live', limit: 12 }); + + assert.deepEqual(results, []); + }); +}); diff --git a/translations/ar-ae.yaml b/translations/ar-ae.yaml index 0a18a1b..2f15983 100644 --- a/translations/ar-ae.yaml +++ b/translations/ar-ae.yaml @@ -148,6 +148,39 @@ developers: metrics: date-created: تاريخ الإنشاء widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: المراقبة api-requests: طلبات API diff --git a/translations/bg-bg.yaml b/translations/bg-bg.yaml index b465094..84db0bf 100644 --- a/translations/bg-bg.yaml +++ b/translations/bg-bg.yaml @@ -156,6 +156,39 @@ developers: metrics: date-created: Дата на създаване widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: Мониторинг api-requests: API заявки diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 32bdc6f..505aed4 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -138,6 +138,39 @@ developers: metrics: date-created: Date Created widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: Monitoring api-requests: API Requests @@ -249,4 +282,4 @@ developers: webhooks: Webhooks websockets: WebSockets logs: Logs - events: Events \ No newline at end of file + events: Events diff --git a/translations/es-es.yaml b/translations/es-es.yaml index a57e7f5..d1bf532 100644 --- a/translations/es-es.yaml +++ b/translations/es-es.yaml @@ -160,6 +160,39 @@ developers: metrics: date-created: Fecha de creación widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: Monitorización api-requests: Solicitudes API diff --git a/translations/fr-fr.yaml b/translations/fr-fr.yaml index eb808a3..3715803 100644 --- a/translations/fr-fr.yaml +++ b/translations/fr-fr.yaml @@ -160,6 +160,39 @@ developers: metrics: date-created: Date de création widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: Surveillance api-requests: Requêtes API diff --git a/translations/mn-mn.yaml b/translations/mn-mn.yaml index 2a00aee..31ef24c 100644 --- a/translations/mn-mn.yaml +++ b/translations/mn-mn.yaml @@ -152,6 +152,39 @@ developers: metrics: date-created: Үүсгэсэн огноо widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: Хянах api-requests: API Хүсэлтүүд diff --git a/translations/pt-br.yaml b/translations/pt-br.yaml index 93e5515..c9a50c0 100644 --- a/translations/pt-br.yaml +++ b/translations/pt-br.yaml @@ -155,6 +155,39 @@ developers: metrics: date-created: Data de Criação widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: Monitoramento api-requests: Requisições API diff --git a/translations/ru-ru.yaml b/translations/ru-ru.yaml index e7597d3..7832d35 100644 --- a/translations/ru-ru.yaml +++ b/translations/ru-ru.yaml @@ -153,6 +153,39 @@ developers: metrics: date-created: Дата создания widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: Мониторинг api-requests: API запросы diff --git a/translations/vi-vn.yaml b/translations/vi-vn.yaml index afa7fca..2cbbdf5 100644 --- a/translations/vi-vn.yaml +++ b/translations/vi-vn.yaml @@ -152,6 +152,39 @@ developers: metrics: date-created: Ngày tạo widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: Giám sát api-requests: Yêu cầu API diff --git a/translations/zh-cn.yaml b/translations/zh-cn.yaml index 232de3a..d6f5944 100644 --- a/translations/zh-cn.yaml +++ b/translations/zh-cn.yaml @@ -138,6 +138,39 @@ developers: metrics: date-created: 创建日期 widget: + dashboard: + name: Developers Dashboard + period: 30d + empty: No data available + kpi: + api-requests: API Requests + api-error-rate: API Error Rate + api-latency: Avg API Latency + webhook-success: Webhook Success + active-api-keys: Active API Keys + active-webhooks: Active Webhooks + webhook-failures: Webhook Failures + events-emitted: Events Emitted + api-traffic: + title: API Traffic + subtitle: Request volume, successes, and errors + webhook-delivery: + title: Webhook Delivery Health + subtitle: Delivery volume, retries, and failures + endpoint-health: + title: Endpoint Health + subtitle: Recent webhook endpoint reliability + event-stream: + title: Event Stream + subtitle: Top event types and sources + types: Event types + sources: Sources + developer-activity: + title: Developer Activity + subtitle: Recent logs, webhooks, and events + quick-resources: + title: Quick Resources + subtitle: Jump into developer tools api-metrics: title: 监控 api-requests: API请求