Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6248ae6a8 | ||
|
|
9824b7d60d | ||
|
|
c95f059857 |
No files matched your search
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Tests for the table Developer view's ESB section wiring
|
||||
|
||||
import React from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { toast } from 'sonner'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { QContextProvider } from '@/lib/context/q-context'
|
||||
import { queryClient, queryKeys } from '@/lib/query-client'
|
||||
import { server } from '@/mocks/node'
|
||||
import TableDeveloperViewPage from './page'
|
||||
|
||||
const { params } = vi.hoisted(() => ({ params: { slug: 'order' } }))
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => params,
|
||||
usePathname: () => `/app/${params.slug}/dev`,
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
vi.mock('sonner', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('sonner')>()),
|
||||
toast: { error: vi.fn(), success: vi.fn(), info: vi.fn(), warning: vi.fn(), dismiss: vi.fn() },
|
||||
}))
|
||||
|
||||
/**
|
||||
* Renders the page with a query client.
|
||||
*
|
||||
* @param client - The query client; a fresh one without retries by default.
|
||||
* @returns The query client, for inspecting query state.
|
||||
*/
|
||||
function renderPage(
|
||||
client: QueryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
): QueryClient {
|
||||
render(
|
||||
<QueryClientProvider client={client}>
|
||||
<QContextProvider>
|
||||
<TableDeveloperViewPage />
|
||||
</QContextProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
return client
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the table's metadata has rendered and its ESB request has settled.
|
||||
*
|
||||
* @param client - The page's query client.
|
||||
* @param tableName - The table shown.
|
||||
*/
|
||||
async function settled(client: QueryClient, tableName: string): Promise<void> {
|
||||
await screen.findByText('Fields')
|
||||
await waitFor(() =>
|
||||
expect(client.getQueryState(queryKeys.esbTable(tableName))?.status).toBe('success')
|
||||
)
|
||||
}
|
||||
|
||||
describe('TableDeveloperViewPage ESB section', () => {
|
||||
beforeEach(() => {
|
||||
params.slug = 'order'
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
})
|
||||
|
||||
it('shows the ESB section returned for the table', async () => {
|
||||
renderPage()
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: 'Enterprise Service Bus' })
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('table', { name: 'Publications' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('table', { name: 'Subscribers' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('omits the section when the user may not read the table', async () => {
|
||||
server.use(
|
||||
http.get('/qqq/v1/esb/table/order', () =>
|
||||
HttpResponse.json({ error: 'Permission denied.' }, { status: 403 })
|
||||
)
|
||||
)
|
||||
const client = renderPage()
|
||||
await settled(client, 'order')
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'Enterprise Service Bus' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('omits the section when the table has no ESB metadata', async () => {
|
||||
params.slug = 'person'
|
||||
const client = renderPage()
|
||||
await settled(client, 'person')
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'Enterprise Service Bus' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('omits the section without a toast when a backend without the ESB module answers with its SPA page', async () => {
|
||||
server.use(
|
||||
http.get('/qqq/v1/esb/table/order', () =>
|
||||
HttpResponse.html('<!doctype html><html><body>SPA</body></html>')
|
||||
)
|
||||
)
|
||||
const client = renderPage(queryClient)
|
||||
await settled(client, 'order')
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'Enterprise Service Bus' })
|
||||
).not.toBeInTheDocument()
|
||||
expect(toast.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file TableDeveloperView page — shows raw table metadata as formatted JSON for debugging field definitions and permissions.
|
||||
* @file TableDeveloperView page — shows raw table metadata as formatted JSON for debugging field definitions and permissions,
|
||||
* and the table's ESB publications and subscribers when it has any.
|
||||
*/
|
||||
|
||||
'use client'
|
||||
@@ -27,7 +28,9 @@ import { Code, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { useRouteParams } from '@/lib/hooks/use-route-params'
|
||||
import { useQContext } from '@/lib/context/q-context'
|
||||
import { loadTableMetaData } from '@/lib/api/metadata'
|
||||
import { getEsbForTable } from '@/lib/api/esb'
|
||||
import { queryKeys } from '@/lib/query-client'
|
||||
import { EsbSection } from '@/components/esb/EsbSection'
|
||||
|
||||
/**
|
||||
* Renders a developer debug view for a QQQ table, showing summary statistics
|
||||
@@ -35,6 +38,8 @@ import { queryKeys } from '@/lib/query-client'
|
||||
*
|
||||
* @returns A composed page that assembles:
|
||||
* - A 4-column stats grid (`<MetaStat>` cards: field count, section count, permissions, primary key)
|
||||
* - An `<EsbSection>` with the table's ESB publications and subscribers, omitted when the
|
||||
* table has none to show (403, 404, or a backend without the ESB module)
|
||||
* - A collapsible `<JsonBlock>` panel rendering the full table metadata as formatted JSON
|
||||
* - A loading spinner while metadata is fetching, and a destructive error panel on failure
|
||||
*/
|
||||
@@ -53,6 +58,11 @@ export default function TableDeveloperViewPage() {
|
||||
staleTime: 1000 * 60 * 5,
|
||||
})
|
||||
|
||||
const { data: esb } = useQuery({
|
||||
queryKey: queryKeys.esbTable(slug),
|
||||
queryFn: () => getEsbForTable(slug),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6" data-qqq-id={`table-dev-${slug}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -101,6 +111,8 @@ export default function TableDeveloperViewPage() {
|
||||
<MetaStat label="Primary Key" value={data.primaryKeyField ?? '—'} />
|
||||
</div>
|
||||
|
||||
<EsbSection data={esb ?? null} />
|
||||
|
||||
{/* Raw JSON */}
|
||||
<JsonBlock label="Full Table Metadata" value={data} defaultOpen={false} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file EsbCounters — compact list of a destination's or trigger's per-node ESB counters.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import type { EsbCounter } from '@/types'
|
||||
import { formatDateTime } from '@/lib/utils/datetime-utils'
|
||||
|
||||
/** Which side of a destination the counters describe. */
|
||||
export type EsbCountersKind = 'publish' | 'consume'
|
||||
|
||||
/** Numeric counts shown for each kind, with their labels. */
|
||||
const COUNTS: Record<EsbCountersKind, ReadonlyArray<readonly [keyof EsbCounter, string]>> = {
|
||||
publish: [
|
||||
['published', 'Published'],
|
||||
['publishFailures', 'Publish failures'],
|
||||
],
|
||||
consume: [
|
||||
['consumed', 'Consumed'],
|
||||
['succeeded', 'Succeeded'],
|
||||
['failed', 'Failed'],
|
||||
['retried', 'Retried'],
|
||||
['deadLettered', 'Dead-lettered'],
|
||||
['inFlight', 'In flight'],
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders counters as a wrapping description list: publish counts for a publication,
|
||||
* consume counts and processing times for a trigger, then last activity and last error.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @param props.counters - The counter snapshot.
|
||||
* @param props.kind - `publish` for a publication's destination, `consume` for a trigger.
|
||||
* @returns The counters as `<dl>` term/value pairs.
|
||||
*/
|
||||
export function EsbCounters({ counters, kind }: { counters: EsbCounter; kind: EsbCountersKind }) {
|
||||
const items = COUNTS[kind].map(([key, label]) => [label, String(counters[key])])
|
||||
if (kind === 'consume') {
|
||||
items.push(
|
||||
['Avg time', `${Math.round(counters.avgMs)} ms`],
|
||||
['Max time', `${Math.round(counters.maxMs)} ms`]
|
||||
)
|
||||
}
|
||||
items.push(['Last activity', formatDateTime(counters.lastActivity) ?? '—'])
|
||||
|
||||
return (
|
||||
<dl className="flex flex-wrap gap-x-4 gap-y-1 text-xs">
|
||||
{items.map(([label, value]) => (
|
||||
<div key={label} className="flex gap-1">
|
||||
<dt className="text-muted-foreground">{label}</dt>
|
||||
<dd className="font-medium tabular-nums text-foreground">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
{counters.lastError && (
|
||||
<div className="flex basis-full gap-1">
|
||||
<dt className="text-muted-foreground">Last error</dt>
|
||||
<dd className="break-words text-destructive">{counters.lastError}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Tests for the table Developer-view ESB section
|
||||
|
||||
import React from 'react'
|
||||
import { act, render, screen, within } from '@testing-library/react'
|
||||
import { axe } from 'jest-axe'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { orderEsb } from '@/mocks/fixtures/esb'
|
||||
import { EsbSection } from './EsbSection'
|
||||
|
||||
/**
|
||||
* Reads the value of one counter inside a row.
|
||||
*
|
||||
* @param scope - The row (or cell) holding the counters.
|
||||
* @param label - The counter's label.
|
||||
* @returns The counter's displayed value.
|
||||
*/
|
||||
function counter(scope: HTMLElement, label: string): string | null | undefined {
|
||||
return within(scope).getByText(label, { selector: 'dt' }).nextElementSibling?.textContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body rows of the table with the given accessible name.
|
||||
*
|
||||
* @param name - The table's accessible name.
|
||||
* @returns Its rows, excluding the header row.
|
||||
*/
|
||||
function bodyRows(name: string): HTMLElement[] {
|
||||
return within(screen.getByRole('table', { name })).getAllByRole('row').slice(1)
|
||||
}
|
||||
|
||||
describe('EsbSection', () => {
|
||||
it('renders nothing when the table has no ESB section', () => {
|
||||
const { container } = render(<EsbSection data={null} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('lists publications with destination, type, events, counters and queue depth', () => {
|
||||
render(<EsbSection data={orderEsb} />)
|
||||
expect(screen.getByRole('heading', { name: 'Enterprise Service Bus' })).toBeInTheDocument()
|
||||
const [topic, queue] = bodyRows('Publications')
|
||||
expect(bodyRows('Publications')).toHaveLength(2)
|
||||
|
||||
const topicCells = within(topic).getAllByRole('cell')
|
||||
expect(topicCells[0]).toHaveTextContent('orderEvents')
|
||||
expect(topicCells[1]).toHaveTextContent('Topic')
|
||||
expect(topicCells[2]).toHaveTextContent('INSERT, UPDATE, DELETE')
|
||||
expect(counter(topic, 'Published')).toBe('128')
|
||||
expect(counter(topic, 'Publish failures')).toBe('2')
|
||||
|
||||
const queueCells = within(queue).getAllByRole('cell')
|
||||
expect(queueCells[0]).toHaveTextContent('orderFulfillment')
|
||||
expect(queueCells[1]).toHaveTextContent('Queue')
|
||||
expect(queueCells[2]).toHaveTextContent('INSERT')
|
||||
expect(counter(queue, 'Published')).toBe('40')
|
||||
expect(queueCells[4]).toHaveTextContent('7')
|
||||
})
|
||||
|
||||
it('shows "—" for queue depth when the broker reports no queue info', () => {
|
||||
render(<EsbSection data={orderEsb} />)
|
||||
const [topic, queue] = bodyRows('Publications')
|
||||
expect(within(topic).getAllByRole('cell')[4]).toHaveTextContent(/^—$/)
|
||||
expect(within(queue).getAllByRole('cell')[4]).toHaveTextContent(/^7$/)
|
||||
})
|
||||
|
||||
it('lists subscribers with process link, state, counters and dead-letter count', () => {
|
||||
render(<EsbSection data={orderEsb} />)
|
||||
const [fulfill, cancel] = bodyRows('Subscribers')
|
||||
expect(bodyRows('Subscribers')).toHaveLength(2)
|
||||
|
||||
const fulfillCells = within(fulfill).getAllByRole('cell')
|
||||
expect(within(fulfillCells[0]).getByRole('link', { name: 'Fulfill Order' })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/fulfillOrder'
|
||||
)
|
||||
expect(fulfillCells[0]).toHaveTextContent('Single, concurrency 2, 3 attempts')
|
||||
expect(fulfillCells[1]).toHaveTextContent('orderFulfillment')
|
||||
expect(fulfillCells[2]).toHaveTextContent('Running')
|
||||
expect(counter(fulfill, 'Consumed')).toBe('33')
|
||||
expect(counter(fulfill, 'Succeeded')).toBe('30')
|
||||
expect(counter(fulfill, 'Failed')).toBe('3')
|
||||
expect(counter(fulfill, 'Retried')).toBe('4')
|
||||
expect(counter(fulfill, 'Dead-lettered')).toBe('1')
|
||||
expect(counter(fulfill, 'In flight')).toBe('1')
|
||||
expect(counter(fulfill, 'Avg time')).toBe('120 ms')
|
||||
expect(counter(fulfill, 'Max time')).toBe('950 ms')
|
||||
expect(counter(fulfill, 'Last error')).toBe('Warehouse API timed out')
|
||||
expect(fulfillCells[4]).toHaveTextContent(/^1$/)
|
||||
|
||||
const cancelCells = within(cancel).getAllByRole('cell')
|
||||
expect(within(cancelCells[0]).getByRole('link', { name: 'Cancel Order' })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/cancelOrder'
|
||||
)
|
||||
expect(cancelCells[1]).toHaveTextContent('orderEvents')
|
||||
expect(cancelCells[2]).toHaveTextContent('Paused')
|
||||
expect(within(cancel).queryByText('Last error')).not.toBeInTheDocument()
|
||||
expect(cancelCells[4]).toHaveTextContent(/^—$/)
|
||||
})
|
||||
|
||||
it('says so when there are no publications or subscribers', () => {
|
||||
render(<EsbSection data={{ ...orderEsb, publications: [], subscribers: [] }} />)
|
||||
expect(screen.queryByRole('table')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('This table does not publish to any destination.')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('No processes you can access are triggered by these destinations.')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('has no accessibility violations', async () => {
|
||||
const { container } = render(<EsbSection data={orderEsb} />)
|
||||
// next/link updates its prefetch state while axe runs; act() flushes it.
|
||||
let results: Awaited<ReturnType<typeof axe>> | undefined
|
||||
await act(async () => {
|
||||
results = await axe(container)
|
||||
})
|
||||
expect(results).toHaveNoViolations()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file EsbSection — the ESB part of a table's Developer view: the destinations the table
|
||||
* publishes to and the processes those destinations trigger, with per-node counters.
|
||||
*/
|
||||
|
||||
import React, { useId } from 'react'
|
||||
import { Network } from 'lucide-react'
|
||||
|
||||
import type { EsbDestinationType, EsbTableResponse } from '@/types'
|
||||
import { EsbCounters } from './EsbCounters'
|
||||
import { EsbTriggerRow } from './EsbTriggerRow'
|
||||
|
||||
/** Label for each destination type. */
|
||||
const DESTINATION_TYPES: Record<EsbDestinationType, string> = { QUEUE: 'Queue', TOPIC: 'Topic' }
|
||||
|
||||
/** Props for {@link EsbSection}. */
|
||||
interface EsbSectionProps {
|
||||
/** The table's ESB data, or `null` when the table has none or the user may not see it. */
|
||||
data: EsbTableResponse | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a table's ESB publications and subscribers, or nothing when `data` is `null`.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @param props.data - Response of `GET /esb/table/{table}`, or `null`.
|
||||
* @returns A section with a Publications table (destination, type, events, counters, queue
|
||||
* depth) and a Subscribers table (process, destination, state, counters, dead letters),
|
||||
* each replaced by a sentence when empty; or `null`.
|
||||
*/
|
||||
export function EsbSection({ data }: EsbSectionProps) {
|
||||
const headingId = useId()
|
||||
if (!data) return null
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby={headingId}
|
||||
className="space-y-4 rounded-xl border border-border bg-card p-4"
|
||||
data-qqq-id={`esb-section-${data.table}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-muted-foreground" aria-hidden="true" />
|
||||
<h3 id={headingId} className="text-lg font-semibold text-foreground">
|
||||
Enterprise Service Bus
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<EsbList
|
||||
title="Publications"
|
||||
columns={['Destination', 'Type', 'Events', 'Counters', 'Queue depth']}
|
||||
empty="This table does not publish to any destination."
|
||||
>
|
||||
{data.publications.map(({ destination, events }) => (
|
||||
<tr
|
||||
key={destination.name}
|
||||
className="border-b border-border/50 align-top last:border-0"
|
||||
data-qqq-id={`esb-publication-${destination.name}`}
|
||||
>
|
||||
<td className="px-2 py-2 font-mono text-xs text-foreground">{destination.name}</td>
|
||||
<td className="px-2 py-2 text-foreground">
|
||||
{DESTINATION_TYPES[destination.type] ?? destination.type}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-foreground">{events.join(', ')}</td>
|
||||
<td className="px-2 py-2">
|
||||
<EsbCounters counters={destination.counters} kind="publish" />
|
||||
</td>
|
||||
<td className="px-2 py-2 tabular-nums text-foreground">
|
||||
{destination.queueInfo ? destination.queueInfo.messageCount : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</EsbList>
|
||||
|
||||
<EsbList
|
||||
title="Subscribers"
|
||||
columns={['Process', 'Destination', 'State', 'Counters', 'Dead letters']}
|
||||
empty="No processes you can access are triggered by these destinations."
|
||||
>
|
||||
{data.subscribers.map((trigger) => (
|
||||
<EsbTriggerRow key={trigger.name} trigger={trigger} />
|
||||
))}
|
||||
</EsbList>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A titled table of ESB rows, or a sentence when there are none.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @param props.title - Heading, also the table's accessible name.
|
||||
* @param props.columns - Column headings.
|
||||
* @param props.empty - Sentence shown when there are no rows.
|
||||
* @param props.children - The table rows.
|
||||
* @returns The heading followed by the table or the empty sentence.
|
||||
*/
|
||||
function EsbList({
|
||||
title,
|
||||
columns,
|
||||
empty,
|
||||
children,
|
||||
}: {
|
||||
title: string
|
||||
columns: string[]
|
||||
empty: string
|
||||
children: React.ReactNode[]
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-foreground">{title}</h4>
|
||||
{children.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{empty}</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm" aria-label={title}>
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={column}
|
||||
scope="col"
|
||||
className="whitespace-nowrap px-2 py-1.5 font-medium text-muted-foreground"
|
||||
>
|
||||
{column}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{children}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file EsbTriggerRow — one table row for a process triggered by an ESB destination.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import type { EsbTrigger, EsbTriggerMode, EsbTriggerState } from '@/types'
|
||||
import { CHIP_COLOR_CLASSES, type ChipColor } from '@/lib/utils/adornment-utils'
|
||||
import { cn } from '@/lib/utils/cn'
|
||||
import { EsbCounters } from './EsbCounters'
|
||||
|
||||
/** Label and chip color for each trigger state. */
|
||||
const STATES: Record<EsbTriggerState, { label: string; color: ChipColor }> = {
|
||||
RUNNING: { label: 'Running', color: 'success' },
|
||||
PAUSED: { label: 'Paused', color: 'warning' },
|
||||
CONNECTING: { label: 'Connecting', color: 'info' },
|
||||
STOPPED: { label: 'Stopped', color: 'default' },
|
||||
}
|
||||
|
||||
/** Label for each trigger mode. */
|
||||
const MODES: Record<EsbTriggerMode, string> = { SINGLE: 'Single', BATCH: 'Batch' }
|
||||
|
||||
/**
|
||||
* Renders a trigger as a row of five cells: the process (linked, with its mode, concurrency
|
||||
* and attempts), the destination it consumes, its state, its counters, and the number of
|
||||
* messages in its dead-letter queue (`—` when the broker cannot report it).
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @param props.trigger - The trigger to show.
|
||||
* @returns A `<tr>` for a table with Process, Destination, State, Counters and Dead letters columns.
|
||||
*/
|
||||
export function EsbTriggerRow({ trigger }: { trigger: EsbTrigger }) {
|
||||
const state = STATES[trigger.state] ?? { label: trigger.state, color: 'default' }
|
||||
const attempts = `${trigger.maxAttempts} ${trigger.maxAttempts === 1 ? 'attempt' : 'attempts'}`
|
||||
|
||||
return (
|
||||
<tr
|
||||
className="border-b border-border/50 align-top last:border-0"
|
||||
data-qqq-id={`esb-trigger-${trigger.name}`}
|
||||
>
|
||||
<td className="px-2 py-2">
|
||||
<Link
|
||||
href={`/app/${encodeURIComponent(trigger.processName)}`}
|
||||
className="font-medium text-primary underline-offset-2 hover:underline focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
{trigger.processLabel}
|
||||
</Link>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{MODES[trigger.mode] ?? trigger.mode}, concurrency {trigger.concurrency}, {attempts}
|
||||
</p>
|
||||
</td>
|
||||
<td className="px-2 py-2 font-mono text-xs text-foreground">{trigger.destination.name}</td>
|
||||
<td className="px-2 py-2">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium',
|
||||
CHIP_COLOR_CLASSES[state.color]
|
||||
)}
|
||||
>
|
||||
{state.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<EsbCounters counters={trigger.counters} kind="consume" />
|
||||
</td>
|
||||
<td className="px-2 py-2 tabular-nums text-foreground">
|
||||
{trigger.deadLetter.messageCount ?? '—'}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Tests for the ESB API client
|
||||
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { orderEsb } from '@/mocks/fixtures/esb'
|
||||
import { server } from '@/mocks/node'
|
||||
import { getEsbForTable } from './esb'
|
||||
|
||||
describe('getEsbForTable', () => {
|
||||
it('returns the table ESB section from GET /qqq/v1/esb/table/{table}', async () => {
|
||||
let path = ''
|
||||
server.use(
|
||||
http.get('/qqq/v1/esb/table/:table', ({ request }) => {
|
||||
path = new URL(request.url).pathname
|
||||
return HttpResponse.json(orderEsb)
|
||||
})
|
||||
)
|
||||
await expect(getEsbForTable('order')).resolves.toEqual(orderEsb)
|
||||
expect(path).toBe('/qqq/v1/esb/table/order')
|
||||
})
|
||||
|
||||
it('URL-encodes the table name', async () => {
|
||||
let path = ''
|
||||
server.use(
|
||||
http.get('/qqq/v1/esb/table/:table', ({ request }) => {
|
||||
path = new URL(request.url).pathname
|
||||
return HttpResponse.json({ ...orderEsb, table: 'my table' })
|
||||
})
|
||||
)
|
||||
await getEsbForTable('my table')
|
||||
expect(path).toBe('/qqq/v1/esb/table/my%20table')
|
||||
})
|
||||
|
||||
it.each([403, 404])('returns null on %i', async (status) => {
|
||||
server.use(
|
||||
http.get('/qqq/v1/esb/table/order', () =>
|
||||
HttpResponse.json({ error: 'Permission denied.' }, { status })
|
||||
)
|
||||
)
|
||||
await expect(getEsbForTable('order')).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('rethrows other failures with the backend message', async () => {
|
||||
server.use(
|
||||
http.get('/qqq/v1/esb/table/order', () =>
|
||||
HttpResponse.json({ error: 'Broker management API unavailable' }, { status: 500 })
|
||||
)
|
||||
)
|
||||
await expect(getEsbForTable('order')).rejects.toThrow('Broker management API unavailable')
|
||||
})
|
||||
|
||||
it('returns null for the SPA index page a backend without the ESB module answers', async () => {
|
||||
server.use(
|
||||
http.get('/qqq/v1/esb/table/order', () =>
|
||||
HttpResponse.html('<!doctype html><html><body>SPA</body></html>')
|
||||
)
|
||||
)
|
||||
await expect(getEsbForTable('order')).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['an empty body', ''],
|
||||
['a JSON object without publications and subscribers', { error: 'unexpected' }],
|
||||
['a JSON array', []],
|
||||
])('returns null for %s', async (_label, body) => {
|
||||
server.use(
|
||||
http.get('/qqq/v1/esb/table/order', () =>
|
||||
typeof body === 'string' ? HttpResponse.text(body) : HttpResponse.json(body)
|
||||
)
|
||||
)
|
||||
await expect(getEsbForTable('order')).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ESB API — permission-checked `/qqq/v1/esb` endpoints served by the `qqq-esb` module.
|
||||
*/
|
||||
|
||||
import type { EsbTableResponse } from '@/types'
|
||||
import { getErrorStatusCode } from '@/lib/utils/error-utils'
|
||||
import apiClient from './client'
|
||||
|
||||
/**
|
||||
* Loads a table's ESB publications and subscribing triggers from `GET /esb/table/{table}`.
|
||||
*
|
||||
* With the `qqq-esb` module installed, the backend answers 403 when the user may not read the
|
||||
* table and 404 when the table has no ESB metadata. Without the module the route does not exist,
|
||||
* and the default Javalin root-SPA not-found handler answers 200 with the SPA's `index.html`.
|
||||
* All three mean there is nothing to show, so none of them is an error.
|
||||
*
|
||||
* @param tableName - Backend table name.
|
||||
* @returns The table's ESB section, or `null` on 403, on 404, or when a 2xx body is not an ESB table object.
|
||||
* @throws The request error for any other failure.
|
||||
*/
|
||||
export async function getEsbForTable(tableName: string): Promise<EsbTableResponse | null> {
|
||||
let data: unknown
|
||||
try {
|
||||
data = await apiClient.get<unknown>(`/esb/table/${encodeURIComponent(tableName)}`)
|
||||
} catch (error) {
|
||||
const status = getErrorStatusCode(error)
|
||||
if (status === 403 || status === 404) return null
|
||||
throw error
|
||||
}
|
||||
return isEsbTableResponse(data) ? data : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a response body has the shape of an ESB table response.
|
||||
*
|
||||
* @param data - The parsed response body.
|
||||
* @returns True for an object with `publications` and `subscribers` arrays.
|
||||
*/
|
||||
function isEsbTableResponse(data: unknown): data is EsbTableResponse {
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) return false
|
||||
const body = data as Partial<Record<keyof EsbTableResponse, unknown>>
|
||||
return Array.isArray(body.publications) && Array.isArray(body.subscribers)
|
||||
}
|
||||
@@ -176,6 +176,7 @@ export const queryClient = new QueryClient({
|
||||
* - `['qqq', 'widgets', widgetName, ...]` — widget data queries
|
||||
* - `['qqq', 'possibleValues', ...]` — possible-value / enum queries
|
||||
* - `['qqq', 'search', ...]` — global search queries
|
||||
* - `['qqq', 'esb', ...]` — ESB publications, triggers and counters
|
||||
*/
|
||||
export const queryKeys = {
|
||||
/**
|
||||
@@ -335,6 +336,21 @@ export const queryKeys = {
|
||||
audits: (tableName: string, primaryKey: string | number) =>
|
||||
[...queryKeys.tableRecord(tableName, primaryKey), 'audits'] as const,
|
||||
|
||||
// ESB
|
||||
/**
|
||||
* Key for all ESB queries.
|
||||
*
|
||||
* @returns The ESB namespace query key tuple.
|
||||
*/
|
||||
esb: () => [...queryKeys.all(), 'esb'] as const,
|
||||
/**
|
||||
* Key for a table's ESB publications and subscribers.
|
||||
*
|
||||
* @param tableName - Backend table name.
|
||||
* @returns The table ESB query key tuple.
|
||||
*/
|
||||
esbTable: (tableName: string) => [...queryKeys.esb(), 'table', tableName] as const,
|
||||
|
||||
// Search
|
||||
/**
|
||||
* Key for all global search queries.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ESB fixture data — `GET /qqq/v1/esb/table/order` for the demo `order` table: a topic
|
||||
* without broker management data, a managed queue, and one trigger on each.
|
||||
*/
|
||||
|
||||
import type { EsbCapabilities, EsbCounter, EsbDestination, EsbTableResponse } from '@/types'
|
||||
|
||||
/**
|
||||
* Builds a counter snapshot with every count at zero.
|
||||
*
|
||||
* @param overrides - Counts that differ from zero.
|
||||
* @returns A complete counter snapshot.
|
||||
*/
|
||||
function counter(overrides: Partial<EsbCounter> = {}): EsbCounter {
|
||||
return {
|
||||
published: 0,
|
||||
publishFailures: 0,
|
||||
consumed: 0,
|
||||
succeeded: 0,
|
||||
failed: 0,
|
||||
retried: 0,
|
||||
deadLettered: 0,
|
||||
inFlight: 0,
|
||||
lastActivity: null,
|
||||
avgMs: 0,
|
||||
maxMs: 0,
|
||||
lastError: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const artemisCapabilities: EsbCapabilities = {
|
||||
browse: true,
|
||||
pauseQueue: true,
|
||||
purge: true,
|
||||
deleteSelected: true,
|
||||
deleteOlderThan: true,
|
||||
move: true,
|
||||
}
|
||||
|
||||
const orderEvents: EsbDestination = {
|
||||
name: 'orderEvents',
|
||||
type: 'TOPIC',
|
||||
provider: 'artemis',
|
||||
brokerName: 'orderEvents',
|
||||
counters: counter({ published: 128, publishFailures: 2, lastActivity: '2026-09-25T12:00:00Z' }),
|
||||
queueInfo: null,
|
||||
capabilities: artemisCapabilities,
|
||||
}
|
||||
|
||||
const orderFulfillment: EsbDestination = {
|
||||
name: 'orderFulfillment',
|
||||
type: 'QUEUE',
|
||||
provider: 'artemis',
|
||||
brokerName: 'orderFulfillment',
|
||||
counters: counter({ published: 40, lastActivity: '2026-09-25T11:58:30Z' }),
|
||||
queueInfo: { messageCount: 7, consumerCount: 2, paused: false },
|
||||
capabilities: artemisCapabilities,
|
||||
}
|
||||
|
||||
export const orderEsb: EsbTableResponse = {
|
||||
table: 'order',
|
||||
publications: [
|
||||
{ destination: orderEvents, events: ['INSERT', 'UPDATE', 'DELETE'] },
|
||||
{ destination: orderFulfillment, events: ['INSERT'] },
|
||||
],
|
||||
subscribers: [
|
||||
{
|
||||
name: 'fulfillOrder.orderFulfillment',
|
||||
processName: 'fulfillOrder',
|
||||
processLabel: 'Fulfill Order',
|
||||
destination: orderFulfillment,
|
||||
mode: 'SINGLE',
|
||||
concurrency: 2,
|
||||
maxAttempts: 3,
|
||||
state: 'RUNNING',
|
||||
counters: counter({
|
||||
consumed: 33,
|
||||
succeeded: 30,
|
||||
failed: 3,
|
||||
retried: 4,
|
||||
deadLettered: 1,
|
||||
inFlight: 1,
|
||||
lastActivity: '2026-09-25T11:58:31Z',
|
||||
avgMs: 120,
|
||||
maxMs: 950,
|
||||
lastError: 'Warehouse API timed out',
|
||||
}),
|
||||
deadLetter: { brokerName: 'orderFulfillment.dlq', messageCount: 1 },
|
||||
},
|
||||
{
|
||||
name: 'cancelOrder.orderEvents',
|
||||
processName: 'cancelOrder',
|
||||
processLabel: 'Cancel Order',
|
||||
destination: orderEvents,
|
||||
mode: 'BATCH',
|
||||
concurrency: 1,
|
||||
maxAttempts: 5,
|
||||
state: 'PAUSED',
|
||||
counters: counter({
|
||||
consumed: 12,
|
||||
succeeded: 12,
|
||||
lastActivity: '2026-09-25T10:15:00Z',
|
||||
avgMs: 45,
|
||||
maxMs: 80,
|
||||
}),
|
||||
deadLetter: { brokerName: 'orderEvents.cancelOrder.orderEvents.dlq', messageCount: null },
|
||||
},
|
||||
],
|
||||
permissions: { canOperate: false, canDelete: false },
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file MSW handlers for the ESB endpoints (`GET /qqq/v1/esb/table/:tableName`).
|
||||
*/
|
||||
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import type { EsbTableResponse } from '@/types'
|
||||
import { orderEsb } from '../fixtures/esb'
|
||||
|
||||
const BASE = '/qqq/v1'
|
||||
|
||||
const esbTables: Record<string, EsbTableResponse> = {
|
||||
order: orderEsb,
|
||||
}
|
||||
|
||||
export const esbHandlers = [
|
||||
// GET /esb/table/:tableName — 404 for tables without ESB metadata, as the backend answers
|
||||
http.get(`${BASE}/esb/table/:tableName`, ({ params }) => {
|
||||
const { tableName } = params as { tableName: string }
|
||||
const data = esbTables[tableName]
|
||||
if (!data) {
|
||||
return HttpResponse.json(
|
||||
{ error: `Table '${tableName}' has no ESB metadata` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
return HttpResponse.json(data)
|
||||
}),
|
||||
]
|
||||
@@ -24,6 +24,7 @@ import { tableHandlers } from './tables'
|
||||
import { possibleValuesHandlers } from './possible-values'
|
||||
import { processHandlers } from './processes'
|
||||
import { widgetHandlers } from './widgets'
|
||||
import { esbHandlers } from './esb'
|
||||
|
||||
export const handlers = [
|
||||
...authHandlers,
|
||||
@@ -33,4 +34,5 @@ export const handlers = [
|
||||
...tableHandlers,
|
||||
...processHandlers,
|
||||
...widgetHandlers,
|
||||
...esbHandlers,
|
||||
]
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ESB types — mirror the `/qqq/v1/esb` endpoint contract served by the `qqq-esb` module
|
||||
* (QRun-IO/qqq#739). Field names and shapes match the JSON exactly.
|
||||
*/
|
||||
|
||||
/** Per-node activity counters for a destination or trigger. */
|
||||
export interface EsbCounter {
|
||||
published: number
|
||||
publishFailures: number
|
||||
consumed: number
|
||||
succeeded: number
|
||||
failed: number
|
||||
retried: number
|
||||
deadLettered: number
|
||||
inFlight: number
|
||||
/** ISO-8601 instant of the last activity, or `null` when there has been none. */
|
||||
lastActivity: string | null
|
||||
avgMs: number
|
||||
maxMs: number
|
||||
lastError: string | null
|
||||
}
|
||||
|
||||
/** Whether a destination is a point-to-point queue or a publish/subscribe topic. */
|
||||
export type EsbDestinationType = 'QUEUE' | 'TOPIC'
|
||||
|
||||
/** Broker-reported queue state, available when the provider has a management URL. */
|
||||
export interface EsbQueueInfo {
|
||||
messageCount: number
|
||||
consumerCount: number
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
/** Management actions the destination's broker adapter supports. */
|
||||
export interface EsbCapabilities {
|
||||
browse: boolean
|
||||
pauseQueue: boolean
|
||||
purge: boolean
|
||||
deleteSelected: boolean
|
||||
deleteOlderThan: boolean
|
||||
move: boolean
|
||||
}
|
||||
|
||||
/** A queue or topic configured in QQQ metadata. */
|
||||
export interface EsbDestination {
|
||||
name: string
|
||||
type: EsbDestinationType
|
||||
/** Name of the ESB provider the destination lives on. */
|
||||
provider: string
|
||||
/** Broker-side destination name. */
|
||||
brokerName: string
|
||||
counters: EsbCounter
|
||||
/** Broker queue state, or `null` when the broker has no management API configured. */
|
||||
queueInfo: EsbQueueInfo | null
|
||||
capabilities: EsbCapabilities
|
||||
}
|
||||
|
||||
/** How a trigger hands messages to its process. */
|
||||
export type EsbTriggerMode = 'SINGLE' | 'BATCH'
|
||||
|
||||
/** Runtime state of a trigger's consumers on this node. */
|
||||
export type EsbTriggerState = 'RUNNING' | 'PAUSED' | 'CONNECTING' | 'STOPPED'
|
||||
|
||||
/** A trigger's dead-letter queue. */
|
||||
export interface EsbDeadLetter {
|
||||
brokerName: string
|
||||
/** Messages waiting in the dead-letter queue, or `null` when the broker cannot report it. */
|
||||
messageCount: number | null
|
||||
}
|
||||
|
||||
/** A process triggered by messages on a destination. */
|
||||
export interface EsbTrigger {
|
||||
/** `<processName>.<destinationName>`. */
|
||||
name: string
|
||||
processName: string
|
||||
processLabel: string
|
||||
destination: EsbDestination
|
||||
mode: EsbTriggerMode
|
||||
concurrency: number
|
||||
maxAttempts: number
|
||||
state: EsbTriggerState
|
||||
counters: EsbCounter
|
||||
deadLetter: EsbDeadLetter
|
||||
}
|
||||
|
||||
/** What the current user may do with the ESB objects in a response. */
|
||||
export interface EsbPermissions {
|
||||
canOperate: boolean
|
||||
canDelete: boolean
|
||||
}
|
||||
|
||||
/** Record events a table can publish. */
|
||||
export type EsbTableEvent = 'INSERT' | 'UPDATE' | 'DELETE'
|
||||
|
||||
/** Lifecycle events a process can publish. */
|
||||
export type EsbProcessEvent = 'STARTED' | 'COMPLETED' | 'FAILED'
|
||||
|
||||
/** A destination a table or process publishes to, and the events it sends there. */
|
||||
export interface EsbPublication<E extends string = string> {
|
||||
destination: EsbDestination
|
||||
events: E[]
|
||||
}
|
||||
|
||||
/** `GET /qqq/v1/esb/table/{table}`. */
|
||||
export interface EsbTableResponse {
|
||||
table: string
|
||||
publications: EsbPublication<EsbTableEvent>[]
|
||||
/** Triggers consuming this table's destinations, limited to processes the user can access. */
|
||||
subscribers: EsbTrigger[]
|
||||
permissions: EsbPermissions
|
||||
}
|
||||
|
||||
/** `GET /qqq/v1/esb/process/{process}`. */
|
||||
export interface EsbProcessResponse {
|
||||
process: string
|
||||
publications: EsbPublication<EsbProcessEvent>[]
|
||||
triggers: EsbTrigger[]
|
||||
permissions: EsbPermissions
|
||||
}
|
||||
|
||||
/** Supported broker types. */
|
||||
export type EsbProviderType = 'ACTIVEMQ_ARTEMIS' | 'RABBITMQ'
|
||||
|
||||
/** A configured broker connection. */
|
||||
export interface EsbProvider {
|
||||
name: string
|
||||
type: EsbProviderType
|
||||
connected: boolean
|
||||
managementEnabled: boolean
|
||||
}
|
||||
|
||||
/** A table or process that publishes to a destination. */
|
||||
export interface EsbPublisher {
|
||||
kind: 'TABLE' | 'PROCESS'
|
||||
name: string
|
||||
events: string[]
|
||||
}
|
||||
|
||||
/** A destination with everything that publishes to and consumes from it. */
|
||||
export interface EsbOverviewDestination extends EsbDestination {
|
||||
publishers: EsbPublisher[]
|
||||
triggers: EsbTrigger[]
|
||||
}
|
||||
|
||||
/** `GET /qqq/v1/esb/overview`. */
|
||||
export interface EsbOverviewResponse {
|
||||
providers: EsbProvider[]
|
||||
destinations: EsbOverviewDestination[]
|
||||
permissions: EsbPermissions
|
||||
}
|
||||
|
||||
/** A CloudEvents 1.0 structured event as QQQ publishes it. */
|
||||
export interface EsbCloudEvent {
|
||||
specversion: string
|
||||
id: string
|
||||
source: string
|
||||
type: string
|
||||
time?: string
|
||||
subject?: string
|
||||
datacontenttype?: string
|
||||
data?: unknown
|
||||
/** Extension attribute: `id` of the event whose triggered run produced this one. */
|
||||
qqqcausationid?: string
|
||||
}
|
||||
|
||||
/** One message browsed from a queue or dead-letter queue. */
|
||||
export interface EsbMessage {
|
||||
messageId: string
|
||||
timestamp: string
|
||||
deliveryCount: number
|
||||
/** The parsed CloudEvent, or `null` when the body is not one. */
|
||||
event: EsbCloudEvent | null
|
||||
/** The message body, truncated to 10,000 characters. */
|
||||
rawBody: string
|
||||
/** JMS properties, including `qqqError` and related properties on dead letters. */
|
||||
properties: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** `GET /qqq/v1/esb/deadLetters/{trigger}` and `GET /qqq/v1/esb/messages/{destination}`. */
|
||||
export interface EsbMessagePage {
|
||||
messages: EsbMessage[]
|
||||
hasMore: boolean
|
||||
}
|
||||
@@ -96,3 +96,29 @@ export type {
|
||||
BlockData,
|
||||
BlockWidgetData,
|
||||
} from './widgets'
|
||||
|
||||
export type {
|
||||
EsbCounter,
|
||||
EsbDestinationType,
|
||||
EsbQueueInfo,
|
||||
EsbCapabilities,
|
||||
EsbDestination,
|
||||
EsbTriggerMode,
|
||||
EsbTriggerState,
|
||||
EsbDeadLetter,
|
||||
EsbTrigger,
|
||||
EsbPermissions,
|
||||
EsbTableEvent,
|
||||
EsbProcessEvent,
|
||||
EsbPublication,
|
||||
EsbTableResponse,
|
||||
EsbProcessResponse,
|
||||
EsbProviderType,
|
||||
EsbProvider,
|
||||
EsbPublisher,
|
||||
EsbOverviewDestination,
|
||||
EsbOverviewResponse,
|
||||
EsbCloudEvent,
|
||||
EsbMessage,
|
||||
EsbMessagePage,
|
||||
} from './esb'
|
||||
Reference in new issue
Block a user