Jersey knit
\\n37.5% cotton, 50% polyester, 12.5% rayon
\\nMade in the USA
', - price: '18.99', - keywords: 'Waze Women\'s Short Sleeve Tee, Waze Short Sleeve Tee, Waze Women\'s Tees, Waze Women\'s tee, waze ladies tees, waze ladies tee, waze short sleeve tees, waze short sleeve tee', - url: 'Waze+Womens+Short+Sleeve+Tee', - category: 'apparel', - subcategory: 'apparel' - }) - }, - partition: 0, - messageId: 'shopOrder', - headers: { - foo: { value: 'bar', binary: '' }, - bar: { value: '', binary: 'dmFsdWUgYXMgYmluYXJ5'} - }, - actions: [ + { + id: '5d549ffe-97cd-477d-8db6-27ed4963c08d', + traits: { + namespace: 'kafka', + name: 'Kafka World', + type: 'message', + topic: 'mokapi.shop.products' + }, + time: '2023-02-13T09:49:25.482366+01:00', + data: { + topic: 'mokapi.shop.products', + offset: 0, + key: { + value: 'GGOEWXXX0827' + }, + message: { + value: JSON.stringify( + { + id: 'GGOEWXXX0827', + name: 'Waze Women\'s Short Sleeve Tee', + description: 'Made of soft tri-blend jersey fabric, this great t-shirt will help you find your Waze. Made in USA.', + features: 'Jersey knit
\\n37.5% cotton, 50% polyester, 12.5% rayon
\\nMade in the USA
', + price: '18.99', + keywords: 'Waze Women\'s Short Sleeve Tee, Waze Short Sleeve Tee, Waze Women\'s Tees, Waze Women\'s tee, waze ladies tees, waze ladies tee, waze short sleeve tees, waze short sleeve tee', + url: 'Waze+Womens+Short+Sleeve+Tee', + category: 'apparel', + subcategory: 'apparel' + }) + }, + partition: 0, + messageId: 'shopOrder', + headers: { + foo: { value: 'bar', binary: '' }, + bar: { value: '', binary: 'dmFsdWUgYXMgYmluYXJ5' } + }, + actions: [ { duration: 20, tags: { @@ -364,9 +612,9 @@ export let events = [ event: "kafka" } } - ] - } - }, + ] + } + }, { id: '670ce171-808c-4f8e-a7cd-209b008aada5', traits: { @@ -377,6 +625,7 @@ export let events = [ }, time: '2023-02-13T09:49:25.482366+01:00', data: { + topic: 'mokapi.shop.products', offset: 1, key: { value: 'GGOEWXXX0828' @@ -413,6 +662,7 @@ export let events = [ }, time: '2025-02-13T09:49:25.482366+01:00', data: { + topic: 'mokapi.shop.avro', offset: 1, key: { binary: 'VEVTVDEgw6Qg8JCAgA==' @@ -428,5 +678,82 @@ export let events = [ partition: 0, messageId: 'avro' } + }, + { + id: '84490626-b806-4420-898c-d6b28d1f4ab1', + traits: { + namespace: 'kafka', + name: 'Kafka World', + type: 'request', + clientId: 'consumer', + }, + time: '2025-02-13T09:49:25.482366+01:00', + data: { + "api": "Kafka World", + "header": { + "requestKey": 10, + "requestName": "FindCoordinator", + "version": 2 + }, + "request": { + "key": "mokapi.shop.products", + "keyType": 0 + }, + "response": { + "errorCode": "", + "errorMessage": "", + "host": "localhost", + "port": 9092 + } + } + }, + { + id: '3373222f-e854-4dc6-b369-1f67b79cf47d', + traits: { + namespace: 'kafka', + name: 'Kafka World', + type: 'message', + topic: 'orders.{region}.{eventType}', + instance: 'orders.us.created' + }, + time: '2023-02-13T09:49:25.482366+01:00', + data: { + topic: 'orders.us.created', + offset: 0, + key: { + value: 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d' + }, + message: { + value: JSON.stringify( + { + orderId: 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', + amount: 149.99, + timestamp: '2026-10-17T10:15:30' + }) + }, + partition: 0, + messageId: 'shopOrder', + } + }, +] + +function getChannelInfo(c: any) { + return { + name: c.name, + ...c.summary === undefined ? {} : { summary: c.summary }, + ...('parameters' in c ? { parameters: c.parameters } : {}), + ...('instances' in c ? { instances: c.instances?.map((i: any) => ( + { + ...i, + metrics: { + messages_total: sum('kafka_messages_total', { name: 'service', value: clusters[0].name }, { name: 'topic', value: c.name }, { name: 'instance', value: i.name }), + message_timestamp: max('kafka_message_timestamp', { name: 'service', value: clusters[0].name }, { name: 'topic', value: c.name }, { name: 'instance', value: i.name }) + }, + } + )) } : {}), + metrics: { + messages_total: metrics.find(m => m.name === `kafka_messages_total{service="Kafka World",topic="${c.name}"}`)?.value ?? 0, + message_timestamp: metrics.find(m => m.name === `kafka_message_timestamp{service="Kafka World",topic="${c.name}"}`)?.value ?? 0 + } } - ] +} \ No newline at end of file diff --git a/webui/e2e/mocks/kafka.yml b/webui/e2e/mocks/kafka.yml index c475d4d69..4de815443 100644 --- a/webui/e2e/mocks/kafka.yml +++ b/webui/e2e/mocks/kafka.yml @@ -23,7 +23,30 @@ components: contact: $ref: './common.yml#/components/schemas/Contact' additionalProperties: false - + Channel: + get: + summary: Returns channel specification + operationId: kafkaChannel + parameters: + - in: path + name: cluster + required: true + schema: + type: string + - in: path + name: channel + required: true + schema: + type: string + responses: + '200': + description: The channel specification + content: + application/json: + schema: + $ref: '#/components/schemas/Channel' + '404': + description: Channel not found Topics: get: summary: Returns the list of topics that belongs to the specified Kafka cluster @@ -42,7 +65,7 @@ components: schema: type: array items: - $ref: '#/components/schemas/TopicInfo' + $ref: '#/components/schemas/ChannelInfo' Topic: parameters: - in: path @@ -65,6 +88,8 @@ components: application/json: schema: $ref: '#/components/schemas/Topic' + '404': + description: Topic not found post: summary: Produce messages to a topic, the partition will be chosen randomly operationId: kafkaProduceTopic @@ -376,7 +401,7 @@ components: topics: type: array items: - $ref: '#/components/schemas/TopicInfo' + $ref: '#/components/schemas/ChannelInfo' groups: type: array items: @@ -385,7 +410,7 @@ components: type: array items: $ref: './common.yml#/components/schemas/ConfigInfo' - TopicInfo: + ChannelInfo: type: object properties: name: @@ -396,25 +421,60 @@ components: type: integer messages: type: integer - Topic: + Channel: type: object properties: name: type: string + title: + type: string summary: type: string - partitions: - type: array - items: - $ref: '#/components/schemas/Partition' + description: + type: string messages: type: object additionalProperties: $ref: '#/components/schemas/MessageConfig' - bindings: - $ref: '#/components/schemas/TopicBindings' + parameters: + type: array + items: + type: string tags: $ref: '#/components/schemas/Tags' + metrics: + type: object + properties: + messages_total: + type: integer + message_timestamp: + type: integer + Topic: + type: object + properties: + name: + type: string + parameters: + type: object + additionalProperties: + type: string + bindings: + $ref: '#/components/schemas/TopicBindings' + partitions: + type: array + items: + $ref: '#/components/schemas/Partition' + groups: + type: array + items: + $ref: '#/components/schemas/GroupInfo' + metrics: + type: object + properties: + messages_total: + type: integer + message_timestamp: + type: integer Partition: type: object properties: @@ -444,7 +504,7 @@ components: metrics: type: object properties: - kafka_rebalance_timestamp: + rebalance_timestamp: type: integer topics: type: object @@ -478,7 +538,7 @@ components: metrics: type: object properties: - kafka_rebalance_timestamp: + rebalance_timestamp: type: integer topics: type: object @@ -601,3 +661,40 @@ components: type: string description: type: string + KafkaEvent: + type: object + properties: + offset: + type: integer + key: + $ref: '#/components/schemas/KafkaValue' + message: + $ref: '#/components/schemas/KafkaValue' + schemaId: + type: integer + partition: + type: integer + headers: + type: object + additionalProperties: + type: object + properties: + value: + type: string + binary: + type: string + deleted: + type: boolean + producerId: + type: number + producerEpoch: + type: number + sequenceNumber: + type: number + KafkaValue: + type: object + properties: + value: + type: string + binary: + type: string diff --git a/webui/e2e/mocks/ldap.js b/webui/e2e/mocks/ldap.ts similarity index 96% rename from webui/e2e/mocks/ldap.js rename to webui/e2e/mocks/ldap.ts index ba2556fe7..275550674 100644 --- a/webui/e2e/mocks/ldap.js +++ b/webui/e2e/mocks/ldap.ts @@ -1,4 +1,13 @@ -export let server = [ +import { app } from 'mokapi' + +export default function () { + app.http() + .operation('serviceLdap', (_, res) => { + res.data = servers[0] + }) +} + +export let servers = [ { name: "LDAP Testserver", description: "This is a sample LDAP server", diff --git a/webui/e2e/mocks/mail.js b/webui/e2e/mocks/mail.ts similarity index 64% rename from webui/e2e/mocks/mail.js rename to webui/e2e/mocks/mail.ts index 31caf47a4..4772f08d7 100644 --- a/webui/e2e/mocks/mail.js +++ b/webui/e2e/mocks/mail.ts @@ -1,3 +1,64 @@ +import { app } from 'mokapi' + +export default function () { + app.http() + .operation('serviceMail', (_, res) => { + res.data = services[0] + }) + .operation('mailboxes', (_, res) => { + res.data = services[0].mailboxes + }) + .operation('mailbox', (req, res) => { + for (const mb of services[0].mailboxes) { + if (mb.name === req.path.mailbox) { + res.data = { + ...mb, + folders: mb.folders.map(x => x.name) + } + return + } + } + res.rebuild(404) + }) + .operation('smtpMessages', (req, res) => { + for (const mb of services[0].mailboxes) { + if (mb.name === req.path.mailbox) { + const result = [] + for (const f of mb.folders) { + result.push(...f.messages) + } + res.data = result + return + } + } + res.rebuild(404) + }) + .operation('smtpMessage', (req, res) => { + const messageId = req.path.messageId + const mail = getMail(messageId) + if (mail == null) { + res.rebuild(404) + return + } + res.data = { + service: services[0].name, + data: mail + } + }) + .operation('smtpMessageAttachment', (req, res) => { + const messageId = req.path.messageId + const mail = getMail(messageId) + if (mail == null) { + res.rebuild(404) + return + } + res.data = { + service: services[0].name, + data: mail + } + }) +} + export let mails = [ { from: [{name: 'Alice', address: 'alice@mokapi.io'}], @@ -111,12 +172,13 @@ export let mailEvents = [ from: 'alice@mokapi.io', to: ['bob@mokapi.io'], messageId: '20230223-084925.763-4196@mokapi.io', - subject: 'A test mail' + subject: 'A test mail', + duration: 30016, }, }, ] -export function getMail(messageId) { +export function getMail(messageId: string) { for (let m of mails) { if (m.messageId === messageId) { return m @@ -125,8 +187,12 @@ export function getMail(messageId) { return null } -export function getAttachment(messageId, attachmentName) { +export function getAttachment(messageId: string, attachmentName: string) { const mail = getMail(messageId) + if (!mail) { + return null + } + for (let a of mail.attachments) { if (a.name === attachmentName) { return a diff --git a/webui/e2e/mocks/metrics.ts b/webui/e2e/mocks/metrics.ts index 466918a84..cf29fa2f2 100644 --- a/webui/e2e/mocks/metrics.ts +++ b/webui/e2e/mocks/metrics.ts @@ -1,6 +1,20 @@ const app_start = new Date() app_start.setHours(new Date().getHours() - 2) +import { app } from 'mokapi' + +export default function () { + app.http() + .operation('metrics', (req, res) => { + let names = req.query['names'] + res.data = !names ? metrics : metrics.filter(x => names.includes(x.name)) + }) + .operation('metricsType', (req, res) => { + let type = req.path['type'] + res.data = metrics.filter(x => x.name.startsWith(type + '_')) + }) +} + export let metrics = [ { name: 'app_start_timestamp', @@ -38,6 +52,7 @@ export let metrics = [ name: 'http_request_timestamp{service="Swagger Petstore",endpoint="/pet/findByStatus",method="POST"}', value: 1652237690 }, + // Kafka { name: 'kafka_messages_total{service="Kafka World",topic="mokapi.shop.products"}', value: 10 @@ -82,6 +97,15 @@ export let metrics = [ name: 'kafka_message_timestamp{service="Kafka World",topic="mokapi.shop.avro"}', value: 1652035690 }, + { + name: 'kafka_messages_total{service="Kafka World",topic="orders.{region}.{eventType}",instance="orders.us.created"}', + value: 1 + }, + { + name: 'kafka_message_timestamp{service="Kafka World",topic="orders.{region}.{eventType}",instance="orders.us.created"}', + value: 1676278165 + }, + // Mails { name: 'mail_mails_total{service="Mail Testserver"}', value: 3 @@ -108,11 +132,11 @@ export let metrics = [ value: 1672182690 }, { - name: 'mqtt_messages_total{service="MQTT Temperature Sensor API",topic="sensors/{sensorId}/data"}', + name: 'mqtt_messages_total{service="MQTT Temperature Sensor API",topic="sensors/{sensorId}/data",instance="sensors/123/data"}', value: 1 }, { - name: 'mqtt_message_timestamp{service="MQTT Temperature Sensor API",topic="sensors/{sensorId}/data"}', + name: 'mqtt_message_timestamp{service="MQTT Temperature Sensor API",topic="sensors/{sensorId}/data",instance="sensors/123/data"}', value: 1771058965 }, // websocket @@ -125,16 +149,16 @@ export let metrics = [ value: 1672182690 }, { - name: 'websocket_messages_total{service="WebSocket Chat API",channel="/chats/{chatId}"}', + name: 'websocket_messages_total{service="WebSocket Chat API",channel="/chats/{chatId}",instance="/chats/1234"}', value: 2 }, { - name: 'websocket_message_timestamp{service="WebSocket Chat API",channel="/chats/{chatId}"}', + name: 'websocket_message_timestamp{service="WebSocket Chat API",channel="/chats/{chatId}",instance="/chats/1234"}', value: 1771058323 }, ] -export function sum(name, ...labels){ +export function sum(name: string, ...labels: Label[]){ if (!metrics){ return 0 } @@ -152,7 +176,7 @@ export function sum(name, ...labels){ return sum } -export function max(name, ...labels) { +export function max(name: string, ...labels: Label[]) { if (!metrics){ return 0 } @@ -173,7 +197,7 @@ export function max(name, ...labels) { return max } -export function filter(name, ...labels) { +export function filter(name: string, ...labels: Label[]) { const result = []; if (!metrics){ return []; @@ -191,7 +215,7 @@ export function filter(name, ...labels) { return result; } -export function parseLabels(metric) { +export function parseLabels(metric: { name: string }) { const match = metric.name.match(/\{(.+)}/); if (!match || match.length < 2 || !match[1]) { return {}; @@ -205,7 +229,7 @@ export function parseLabels(metric) { ); } -function matchLabels(metric, labels){ +function matchLabels(metric: { name: string }, labels: Label[]){ for (var label of labels){ const s = `${label.name}="${label.value}"` if (!metric.name.includes(s)){ diff --git a/webui/e2e/mocks/mqtt.js b/webui/e2e/mocks/mqtt.ts similarity index 81% rename from webui/e2e/mocks/mqtt.js rename to webui/e2e/mocks/mqtt.ts index 4826f8e72..e3d2035a4 100644 --- a/webui/e2e/mocks/mqtt.js +++ b/webui/e2e/mocks/mqtt.ts @@ -1,4 +1,4 @@ -import { metrics } from 'metrics.ts'; +import { sum, max, metrics } from './metrics'; import { on } from 'mokapi'; const Measurement = { @@ -75,12 +75,13 @@ export let clusters = [ contentType: 'application/json', }, }, + parameters: ['sensorId'], instances: [ { name: 'sensors/123/data', parameters: { sensorId: '123' - } + }, } ] }, @@ -106,15 +107,24 @@ export default async function () { topics: clusters[0].topics.map(x => { return { name: x.name, - ...x.title ? { title: x.title } : {}, - ...x.summary ? { summary: x.summary } : {}, - ...x.description ? { description: x.description } : {}, + ...('title' in x ? { title: x.title } : {}), + ...('summary' in x ? { summary: x.summary } : {}), + ...('description' in x ? { description: x.description } : {}), messages: x.messages, - ...x.instances ? { instances: x.instances } : {}, + ...('parameters' in x ? { parameters: x.parameters} : {}), + ...('instances' in x ? { instances: x.instances?.map((i: any) => ( + { + ...i, + metrics: { + messages_total: sum('mqtt_messages_total', { name: 'service', value: clusters[0].name }, { name: 'topic', value: x.name }, { name: 'instance', value: i.name }), + message_timestamp: max('mqtt_message_timestamp', { name: 'service', value: clusters[0].name }, { name: 'topic', value: x.name }, { name: 'instance', value: i.name }) + }, + } + )) } : {}), metrics: { - mqtt_messages_total: metrics.find(m => m.name === `mqtt_messages_total{service="${clusters[0].name}",topic="${x.name}"}`).value, - mqtt_message_timestamp: metrics.find(m => m.name === `mqtt_message_timestamp{service="${clusters[0].name}",topic="${x.name}"}`).value - } + messages_total: sum('mqtt_messages_total', { name: 'service', value: clusters[0].name }, { name: 'topic', value: x.name }), + message_timestamp: max('mqtt_message_timestamp', { name: 'service', value: clusters[0].name }, { name: 'topic', value: x.name }) + }, } }), clients: clusters[0].clients @@ -155,6 +165,7 @@ export let events = [ name: 'MQTT Temperature Sensor API', type: 'message', topic: 'sensors/{sensorId}/data', + instance: 'sensors/123/data', sensorId: '123', clientId: 'mqtt-client-2', }, diff --git a/webui/e2e/mocks/tsconfig.json b/webui/e2e/mocks/tsconfig.json new file mode 100644 index 000000000..2d8745e61 --- /dev/null +++ b/webui/e2e/mocks/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "lib": ["es2020", "dom"], + "paths": { + "mokapi": ["../../../npm/go-mokapi/types/index"], + "mokapi/*": ["../../../npm/go-mokapi/types/*"] + }, + "types": ["../../../npm/go-mokapi/types/global"] + } +} \ No newline at end of file diff --git a/webui/e2e/mocks/types.d.ts b/webui/e2e/mocks/types.d.ts new file mode 100644 index 000000000..439c31afb --- /dev/null +++ b/webui/e2e/mocks/types.d.ts @@ -0,0 +1,36 @@ +interface Config { + name: string; + description?: string; + version?: string; + contact?: Contact; + status?: string; +} + +interface Info { + type: string; + name: string; + description?: string; + version?: string; + contact?: Contact; + status?: string; +} + +interface Contact { + name?: string; + email?: string; + url?: string; +} + +interface Label { + name: string; + value: string; +} + +type Traits = Record| Type | -Namespace | -Name | -Event | -Time | - -|||
|---|---|---|---|---|---|---|---|
| {{ event.traits.type }} | -{{ event.traits.namespace }} | -{{ event.traits.name }} | -- - - - - - | -- - {{ event.traits.method }} - - {{ event.traits.path }} - | -- - {{ (event.data as LdapEventData).request.operation }} - - {{ targetDn(event.data as LdapEventData) }} - | -{{ title(event) }} | -{{ format(event.time) }} | -
| {{ instanceLabel }} | +Last Message | +Messages | + +
|---|---|---|
|
+ |
+ {{ lastMessage(instance) }} | +{{ instance.metrics?.messages_total ?? 0 }} | +
Operation
@@ -50,25 +63,20 @@ const description = computed(() => useMarkdown(operation.value?.description || '
Operation ID
{{ operation.operationId }}
Service
-
-
Warning
Deprecated
Summary
diff --git a/webui/src/components/dashboard/http/HttpOperationsCard.vue b/webui/src/components/dashboard/http/HttpOperationsCard.vue index 5318d016b..59b866fdf 100644 --- a/webui/src/components/dashboard/http/HttpOperationsCard.vue +++ b/webui/src/components/dashboard/http/HttpOperationsCard.vue @@ -52,7 +52,7 @@ function showWarningColumn(){ return false } function lastRequest(op: HttpOperationInfo){ - const n = op.metrics.http_request_timestamp + const n = op.metrics.request_timestamp if (n == 0){ return '-' } @@ -60,11 +60,11 @@ function lastRequest(op: HttpOperationInfo){ } function requests(op: HttpOperationInfo){ - return op.metrics.http_requests_total + return op.metrics.requests_total } function errors(op: HttpOperationInfo){ - return op.metrics.http_requests_errors_total + return op.metrics.requests_errors_total } function operationCssClass(operation: HttpOperationInfo): string { if (operation.status === 'valid') { diff --git a/webui/src/components/dashboard/http/HttpPath.vue b/webui/src/components/dashboard/http/HttpPath.vue index 7d609459e..a3fccbdf2 100644 --- a/webui/src/components/dashboard/http/HttpPath.vue +++ b/webui/src/components/dashboard/http/HttpPath.vue @@ -4,6 +4,7 @@ import { useRoute } from '@/router'; import HttpOperationsCard from './HttpOperationsCard.vue'; import Requests from './Requests.vue'; import ServiceStatus from '../ServiceStatus.vue' +import { useBreadcrumb } from '@/composables/breadcrumb.ts'; const props = defineProps({ service: { type: Object as PropTypeService
-
-
Warning
Deprecated
diff --git a/webui/src/components/dashboard/http/HttpRequestMetricCard.vue b/webui/src/components/dashboard/http/HttpRequestMetricCard.vue index 74496c7e7..5cae05de7 100644 --- a/webui/src/components/dashboard/http/HttpRequestMetricCard.vue +++ b/webui/src/components/dashboard/http/HttpRequestMetricCard.vue @@ -40,7 +40,7 @@ const requests = computed(() => { let result = 0; for (const service of services.value) { - result += service.metrics.http_requests_total || 0 + result += service.metrics.requests_total || 0 } return result; }) @@ -51,7 +51,7 @@ const errors = computed(() => { let result = 0; for (const service of services.value) { - result += service.metrics.http_requests_errors_total || 0 + result += service.metrics.requests_errors_total || 0 } return result; }) diff --git a/webui/src/components/dashboard/http/HttpService.vue b/webui/src/components/dashboard/http/HttpService.vue index 2d902656f..1f3b4265d 100644 --- a/webui/src/components/dashboard/http/HttpService.vue +++ b/webui/src/components/dashboard/http/HttpService.vue @@ -1,5 +1,5 @@ +