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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions addon/components/widget/api-traffic.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::DevelopersChart @endpoint="api-traffic" @title={{t "developers.component.widget.dashboard.api-traffic.title"}} @subtitle={{t "developers.component.widget.dashboard.api-traffic.subtitle"}} />
5 changes: 5 additions & 0 deletions addon/components/widget/api-traffic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Component from '@glimmer/component';

export default class WidgetApiTrafficComponent extends Component {
widget = 'api-traffic';
}
30 changes: 30 additions & 0 deletions addon/components/widget/developer-activity.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="developers-dashboard-widget flex h-full flex-col overflow-hidden rounded-md border bg-white shadow-sm dark:bg-gray-800" ...attributes>
<div class="developers-widget-header">
<div class="min-w-0">
<div class="developers-widget-title">{{t "developers.component.widget.dashboard.developer-activity.title"}}</div>
<div class="developers-widget-subtitle">{{t "developers.component.widget.dashboard.developer-activity.subtitle"}}</div>
</div>
<button type="button" class="developers-widget-icon-button" {{on "click" this.refresh}} title={{t "developers.common.reload"}}>
<FaIcon @icon="rotate" class={{if this.load.isRunning "animate-spin"}} />
</button>
</div>
<div class="developers-widget-scroll-body">
{{#if this.load.isRunning}}
<div class="developers-empty-state"><Spinner /></div>
{{else if this.error}}
<div class="developers-empty-state text-rose-500">{{this.error}}</div>
{{else if this.items.length}}
{{#each this.items as |item|}}
<div class="developers-activity-row">
<div class="developers-activity-icon"><FaIcon @icon={{this.iconFor item.type}} /></div>
<div class="min-w-0">
<div class="developers-list-title">{{item.label}}</div>
<div class="developers-list-subtitle">{{item.type}} / {{item.status}}</div>
</div>
</div>
{{/each}}
{{else}}
<div class="developers-empty-state">{{t "developers.component.widget.dashboard.empty"}}</div>
{{/if}}
</div>
</div>
46 changes: 46 additions & 0 deletions addon/components/widget/developer-activity.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
22 changes: 22 additions & 0 deletions addon/components/widget/developers-chart.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div class="developers-dashboard-widget developers-chart-widget flex h-full flex-col rounded-md border bg-white shadow-sm dark:bg-gray-800" ...attributes>
<div class="developers-widget-header">
<div class="min-w-0">
<div class="developers-widget-title">{{@title}}</div>
<div class="developers-widget-subtitle">{{@subtitle}}</div>
</div>
<button type="button" class="developers-widget-icon-button" {{on "click" this.refresh}} title={{t "developers.common.reload"}}>
<FaIcon @icon="rotate" class={{if this.load.isRunning "animate-spin"}} />
</button>
</div>
<div class="developers-chart-body">
{{#if this.load.isRunning}}
<div class="flex h-full w-full items-center justify-center"><Spinner /></div>
{{else if this.error}}
<div class="flex h-full w-full items-center justify-center text-xs text-rose-500">{{this.error}}</div>
{{else}}
<div class="developers-chart-frame">
<Chart @type={{or @type "line"}} @labels={{this.labels}} @datasets={{this.datasets}} @options={{this.chartOptions}} />
</div>
{{/if}}
</div>
</div>
77 changes: 77 additions & 0 deletions addon/components/widget/developers-chart.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
24 changes: 24 additions & 0 deletions addon/components/widget/developers-kpi-tile.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div class="developers-dashboard-widget developers-kpi-tile {{this.accentClass}} h-full rounded-md border shadow-sm overflow-hidden" ...attributes>
<div class="flex h-full flex-col justify-between p-3">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<div class="developers-kpi-label truncate">{{this.title}}</div>
{{#if this.error}}
<div class="mt-2 text-xs font-semibold text-rose-600 dark:text-rose-300 truncate">{{this.error}}</div>
{{else if this.load.isRunning}}
<div class="mt-3"><Spinner /></div>
{{else}}
<div class="developers-kpi-value mt-2 truncate">{{this.value}}</div>
{{/if}}
</div>
<button type="button" class="developers-kpi-icon flex items-center justify-center rounded-md" {{on "click" this.refresh}} title={{t "developers.common.reload"}}>
<FaIcon @icon={{@icon}} @prefix={{or @iconPrefix "fas"}} class={{if this.load.isRunning "animate-spin"}} />
</button>
</div>

<div class="mt-3 flex items-center justify-between gap-2 text-[11px]">
<span class="text-gray-500 dark:text-gray-400 truncate">{{or @footnote (t "developers.component.widget.dashboard.period")}}</span>
<Badge @status={{this.deltaStatus}} @text={{this.deltaText}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="developers-kpi-delta text-[10px] px-2 py-0.5 font-bold" />
</div>
</div>
</div>
79 changes: 79 additions & 0 deletions addon/components/widget/developers-kpi-tile.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
30 changes: 30 additions & 0 deletions addon/components/widget/endpoint-health.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="developers-dashboard-widget flex h-full flex-col overflow-hidden rounded-md border bg-white shadow-sm dark:bg-gray-800" ...attributes>
<div class="developers-widget-header">
<div class="min-w-0">
<div class="developers-widget-title">{{t "developers.component.widget.dashboard.endpoint-health.title"}}</div>
<div class="developers-widget-subtitle">{{t "developers.component.widget.dashboard.endpoint-health.subtitle"}}</div>
</div>
<button type="button" class="developers-widget-icon-button" {{on "click" this.refresh}} title={{t "developers.common.reload"}}>
<FaIcon @icon="rotate" class={{if this.load.isRunning "animate-spin"}} />
</button>
</div>
<div class="developers-widget-scroll-body">
{{#if this.load.isRunning}}
<div class="developers-empty-state"><Spinner /></div>
{{else if this.error}}
<div class="developers-empty-state text-rose-500">{{this.error}}</div>
{{else if this.items.length}}
{{#each this.items as |item|}}
<div class="developers-list-row">
<div class="min-w-0">
<div class="developers-list-title">{{item.url}}</div>
<div class="developers-list-subtitle">{{item.status}} / {{item.mode}} / {{item.deliveries}} deliveries</div>
</div>
<div class="developers-list-metric {{this.statusClass item}}">{{item.success_rate}}%</div>
</div>
{{/each}}
{{else}}
<div class="developers-empty-state">{{t "developers.component.widget.dashboard.empty"}}</div>
{{/if}}
</div>
</div>
Loading
Loading