@mantiq/heartbeat 0.5.22 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/Heartbeat.ts +15 -27
- package/src/HeartbeatServiceProvider.ts +66 -0
- package/src/contracts/Entry.ts +19 -0
- package/src/contracts/HeartbeatConfig.ts +2 -0
- package/src/dashboard/DashboardController.ts +71 -12
- package/src/dashboard/pages/CachePage.ts +61 -5
- package/src/dashboard/pages/CommandDetailPage.ts +216 -0
- package/src/dashboard/pages/CommandsPage.ts +72 -0
- package/src/dashboard/pages/EventsPage.ts +59 -6
- package/src/dashboard/pages/ExceptionsPage.ts +37 -6
- package/src/dashboard/pages/JobsPage.ts +61 -5
- package/src/dashboard/pages/LogsPage.ts +116 -0
- package/src/dashboard/pages/ModelsPage.ts +112 -0
- package/src/dashboard/pages/NotificationsPage.ts +87 -0
- package/src/dashboard/pages/OverviewPage.ts +109 -45
- package/src/dashboard/pages/PerformancePage.ts +151 -20
- package/src/dashboard/pages/QueriesPage.ts +92 -8
- package/src/dashboard/pages/RequestDetailPage.ts +227 -3
- package/src/dashboard/pages/RequestsPage.ts +90 -3
- package/src/dashboard/pages/SchedulesPage.ts +71 -0
- package/src/dashboard/shared/components.ts +140 -0
- package/src/dashboard/shared/layout.ts +327 -108
- package/src/index.ts +9 -0
- package/src/middleware/HeartbeatMiddleware.ts +26 -9
- package/src/migrations/CreateHeartbeatTables.ts +14 -0
- package/src/models/EntryModel.ts +1 -1
- package/src/storage/HeartbeatStore.ts +174 -1
- package/src/testing/HeartbeatFake.ts +6 -0
- package/src/tracing/Tracer.ts +32 -0
- package/src/watchers/CommandWatcher.ts +23 -0
- package/src/watchers/EventWatcher.ts +13 -0
- package/src/watchers/ScheduleWatcher.ts +1 -0
- package/src/widget/DebugWidget.ts +53 -30
|
@@ -73,3 +73,143 @@ export function formatBytes(bytes: number): string {
|
|
|
73
73
|
if (bytes < 1_073_741_824) return `${(bytes / 1_048_576).toFixed(1)} MB`
|
|
74
74
|
return `${(bytes / 1_073_741_824).toFixed(2)} GB`
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
export function pagination(total: number, page: number, perPage: number, baseUrl: string): string {
|
|
78
|
+
const totalPages = Math.ceil(total / perPage)
|
|
79
|
+
if (totalPages <= 1) return ''
|
|
80
|
+
|
|
81
|
+
const items: string[] = []
|
|
82
|
+
|
|
83
|
+
// Previous
|
|
84
|
+
if (page > 1) {
|
|
85
|
+
items.push(`<a href="${baseUrl}${baseUrl.includes('?') ? '&' : '?'}page=${page - 1}">« Prev</a>`)
|
|
86
|
+
} else {
|
|
87
|
+
items.push('<span class="disabled">« Prev</span>')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Page numbers (show max 7)
|
|
91
|
+
const start = Math.max(1, page - 3)
|
|
92
|
+
const end = Math.min(totalPages, start + 6)
|
|
93
|
+
for (let i = start; i <= end; i++) {
|
|
94
|
+
if (i === page) {
|
|
95
|
+
items.push(`<span class="active">${i}</span>`)
|
|
96
|
+
} else {
|
|
97
|
+
items.push(`<a href="${baseUrl}${baseUrl.includes('?') ? '&' : '?'}page=${i}">${i}</a>`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Next
|
|
102
|
+
if (page < totalPages) {
|
|
103
|
+
items.push(`<a href="${baseUrl}${baseUrl.includes('?') ? '&' : '?'}page=${page + 1}">Next »</a>`)
|
|
104
|
+
} else {
|
|
105
|
+
items.push('<span class="disabled">Next »</span>')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return `<div class="pagination">${items.join('')}</div>`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function filterBar(options: {
|
|
112
|
+
action: string
|
|
113
|
+
searchPlaceholder?: string
|
|
114
|
+
searchValue?: string
|
|
115
|
+
filters?: Array<{ name: string; label: string; options: Array<{ value: string; label: string }>; selected?: string }>
|
|
116
|
+
}): string {
|
|
117
|
+
const parts: string[] = [`<form class="filter-bar" method="get" action="${escapeHtml(options.action)}">`]
|
|
118
|
+
|
|
119
|
+
if (options.searchPlaceholder) {
|
|
120
|
+
parts.push(`<input type="text" name="search" placeholder="${escapeHtml(options.searchPlaceholder)}" value="${escapeHtml(options.searchValue ?? '')}" />`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.filters) {
|
|
124
|
+
for (const filter of options.filters) {
|
|
125
|
+
parts.push(`<select name="${escapeHtml(filter.name)}" onchange="this.form.submit()">`)
|
|
126
|
+
parts.push(`<option value="">${escapeHtml(filter.label)}</option>`)
|
|
127
|
+
for (const opt of filter.options) {
|
|
128
|
+
const sel = opt.value === filter.selected ? ' selected' : ''
|
|
129
|
+
parts.push(`<option value="${escapeHtml(opt.value)}"${sel}>${escapeHtml(opt.label)}</option>`)
|
|
130
|
+
}
|
|
131
|
+
parts.push('</select>')
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
parts.push('<button type="submit" style="background:var(--bg-2);border:1px solid var(--border-0);border-radius:6px;padding:6px 12px;font-size:12px;color:var(--fg-1);cursor:pointer">Filter</button>')
|
|
136
|
+
parts.push('</form>')
|
|
137
|
+
return parts.join('')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function breadcrumbs(items: Array<{ label: string; href?: string }>): string {
|
|
141
|
+
return '<div class="breadcrumbs">' + items.map((item, i) => {
|
|
142
|
+
if (i === items.length - 1 || !item.href) {
|
|
143
|
+
return `<span>${escapeHtml(item.label)}</span>`
|
|
144
|
+
}
|
|
145
|
+
return `<a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a><span class="sep">/</span>`
|
|
146
|
+
}).join('') + '</div>'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function emptyState(icon: string, title: string, description: string): string {
|
|
150
|
+
return `<div class="empty-state"><div class="icon">${icon}</div><div class="title">${escapeHtml(title)}</div><div class="desc">${escapeHtml(description)}</div></div>`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function collapsibleSection(title: string, count: number, color: string, content: string, defaultOpen = true): string {
|
|
154
|
+
const openClass = defaultOpen ? ' open' : ''
|
|
155
|
+
return `<div class="collapsible">
|
|
156
|
+
<div class="collapsible-header${openClass}" onclick="this.classList.toggle('open');this.nextElementSibling.classList.toggle('open')">
|
|
157
|
+
<span class="chevron">▶</span>
|
|
158
|
+
<span style="color:${color};font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em">${escapeHtml(title)}</span>
|
|
159
|
+
<span style="font-size:11px;color:var(--fg-3)">${count}</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="collapsible-body${openClass}">${content}</div>
|
|
162
|
+
</div>`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function sqlHighlight(sql: string): string {
|
|
166
|
+
const keywords = /\b(SELECT|FROM|WHERE|AND|OR|NOT|IN|IS|NULL|AS|ON|JOIN|LEFT|RIGHT|INNER|OUTER|CROSS|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|ALTER|DROP|INDEX|GROUP|BY|ORDER|HAVING|LIMIT|OFFSET|UNION|ALL|DISTINCT|COUNT|SUM|AVG|MIN|MAX|LIKE|BETWEEN|EXISTS|CASE|WHEN|THEN|ELSE|END|ASC|DESC|PRIMARY|KEY|FOREIGN|REFERENCES|UNIQUE|DEFAULT|CHECK|CONSTRAINT|TRANSACTION|BEGIN|COMMIT|ROLLBACK)\b/gi
|
|
167
|
+
const strings = /('[^']*')/g
|
|
168
|
+
const nums = /\b(\d+(?:\.\d+)?)\b/g
|
|
169
|
+
|
|
170
|
+
let result = escapeHtml(sql)
|
|
171
|
+
result = result.replace(strings, '<span class="sql-str">$1</span>')
|
|
172
|
+
result = result.replace(keywords, '<span class="sql-kw">$1</span>')
|
|
173
|
+
result = result.replace(nums, '<span class="sql-num">$1</span>')
|
|
174
|
+
return result
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function diffView(changes: Record<string, { old: any; new: any }> | null): string {
|
|
178
|
+
if (!changes || Object.keys(changes).length === 0) return '<span style="color:var(--fg-3)">No changes</span>'
|
|
179
|
+
|
|
180
|
+
const rows = Object.entries(changes).map(([key, { old: oldVal, new: newVal }]) => {
|
|
181
|
+
const o = oldVal === null || oldVal === undefined ? 'null' : String(oldVal)
|
|
182
|
+
const n = newVal === null || newVal === undefined ? 'null' : String(newVal)
|
|
183
|
+
return `<tr><td style="font-weight:500">${escapeHtml(key)}</td><td style="color:#f87171;text-decoration:line-through">${escapeHtml(o)}</td><td style="color:#34d399">${escapeHtml(n)}</td></tr>`
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
return `<table class="tbl" style="font-size:11px"><thead><tr><th>Field</th><th>Old</th><th>New</th></tr></thead><tbody>${rows.join('')}</tbody></table>`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function waterfallChart(items: Array<{ label: string; start: number; end: number; color: string }>, totalDuration: number): string {
|
|
190
|
+
if (items.length === 0 || totalDuration <= 0) return ''
|
|
191
|
+
|
|
192
|
+
const rows = items.map((item) => {
|
|
193
|
+
const left = (item.start / totalDuration) * 100
|
|
194
|
+
const width = Math.max(((item.end - item.start) / totalDuration) * 100, 0.5)
|
|
195
|
+
const dur = (item.end - item.start).toFixed(1)
|
|
196
|
+
return `<div class="waterfall-row">
|
|
197
|
+
<div class="waterfall-label" title="${escapeHtml(item.label)}">${escapeHtml(truncate(item.label, 18))}</div>
|
|
198
|
+
<div class="waterfall-track"><div class="waterfall-bar" style="left:${left}%;width:${width}%;background:${item.color}"></div></div>
|
|
199
|
+
<div class="waterfall-dur">${dur}ms</div>
|
|
200
|
+
</div>`
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
return `<div class="waterfall">${rows.join('')}</div>`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function originBadge(originType: string, originId: string | null, basePath: string): string {
|
|
207
|
+
if (!originId) return '<span style="color:var(--fg-3);font-size:11px">standalone</span>'
|
|
208
|
+
|
|
209
|
+
const colors: Record<string, string> = { request: '#818cf8', command: '#fb923c', schedule: '#fbbf24', job: '#60a5fa' }
|
|
210
|
+
const color = colors[originType] ?? 'var(--fg-3)'
|
|
211
|
+
const shortId = originId.slice(0, 8)
|
|
212
|
+
const href = originType === 'request' ? `${basePath}/requests/${originId}` : originType === 'command' ? `${basePath}/commands/${originId}` : '#'
|
|
213
|
+
|
|
214
|
+
return `<a href="${href}" style="font-size:11px;color:${color};text-decoration:none;border:1px solid ${color};border-radius:4px;padding:1px 6px" title="${escapeHtml(originId)}">${escapeHtml(originType)}:${shortId}</a>`
|
|
215
|
+
}
|