# APM panels

> Latency percentiles (p50, p95, p99), an error-rate panel with an SLO, a slow-endpoints table and a trace list with a span waterfall.

Source: https://docs.nasaqui.com/components/apm-panels

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/apm-panels.json
```

Four panels for application performance monitoring. `LatencyPercentiles` charts p50, p95 and p99 with a target line. `ErrorRatePanel` charts errors over requests, marks an SLO and lists the top error messages. `EndpointTable` ranks routes by p95 and flags slow ones. `TraceList` lists recent requests; selecting one draws its spans as a waterfall.

## When to use

- Application, API or service performance pages.
- Any latency distribution view with percentiles.

## When not to use

- Plain logs: use [`LogViewer`](https://docs.nasaqui.com/components/log-viewer).
- Business metrics: use [`MetricTiles`](https://docs.nasaqui.com/components/metric-tiles).

## Import

```tsx
import { LatencyPercentiles, ErrorRatePanel, EndpointTable, TraceList } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
import { LatencyPercentiles } from "@fadymondy/nasaq/web";

export function Latency() {
  return (
    <LatencyPercentiles
      targetMs={500}
      summary={{ p50: 121, p95: 486, p99: 1140 }}
      data={[
        { time: "2026-09-29T09:00", p50: 118, p95: 470, p99: 1050 },
        { time: "2026-09-29T09:05", p50: 124, p95: 492, p99: 1210 },
      ]}
    />
  );
}
```

## Anatomy

```
LatencyPercentiles     data-slot="latency-percentiles"   percentile chips + line chart + target line
ErrorRatePanel         data-slot="error-rate-panel"      rate, SLO status, chart, top errors
EndpointTable          data-slot="endpoint-table"        method, route, requests, p50, p95, error rate
TraceList              data-slot="trace-list"            rows, and the span waterfall of the selected trace
```

## API

### LatencyPercentiles

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `readonly LatencyPoint[]` | required | `{ time, p50, p95, p99 }` in milliseconds. |
| `summary / previous` | `LatencySummary` | none | Headline percentiles and the previous period's, for the change. |
| `targetMs` | `number` | none | Draws a target line. |
| `title / description / action` | `ReactNode` | none | Card header. |
| `loading / error / onRetry` | `boolean / ReactNode / () => void` | none | States. |
| `className` | `string` | none | Extra classes on the root. |
| `labels` | `Partial<ApmPanelsLabels>` | none | Replace any built-in English or Arabic string. |

### ErrorRatePanel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `readonly ErrorRatePoint[]` | required | `{ time, requests, errors }`. |
| `previousRate` | `number` | none | Previous period's error fraction. |
| `slo` | `number` | none | Objective as a fraction (0.01 is 1%); shows "Within SLO" or "Breaching SLO". |
| `topErrors` | `readonly TopError[]` | none | `{ id, message, count, endpoint?, lastSeenAt? }`. |
| `onErrorClick` | `(error) => void` | none | Makes the error rows buttons. |
| `loading / error / onRetry / labels / className` | none | none | As above. |

### EndpointTable

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rows` | `readonly EndpointRow[]` | required | `{ id, method, route, requests, throughput?, p50, p95, errorRate }`. |
| `slowMs` | `number` | `1000` | p95 at or above this is marked slow. |
| `onRowClick` | `(row) => void` | none | Makes rows clickable. |
| `pageSize` | `number` | `8` | Rows per page. |
| `title / description / loading / error / onRetry / labels / className` | none | none | As above. |

### TraceList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `traces` | `readonly TraceSummary[]` | required | `{ id, method, name, status, durationMs, startedAt, service?, spans? }`. |
| `selectedId / defaultSelectedId / onSelect` | `string \| null / string \| null / (trace \| null) => void` | none | Selection, controlled or not. A trace with `spans` shows its waterfall. |
| `slowMs` | `number` | `1000` | Durations at or above this are marked slow. |
| `title / description / loading / error / onRetry / labels / className` | none | none | As above. |

## Examples

Error rate against an SLO:

```tsx
import { ErrorRatePanel } from "@fadymondy/nasaq/web";

export const Errors = () => (
  <ErrorRatePanel
    slo={0.01}
    data={[{ time: "2026-09-29T09:00", requests: 5200, errors: 41 }, { time: "2026-09-29T09:05", requests: 5100, errors: 190 }]}
    topErrors={[{ id: "e1", message: "TimeoutError: query exceeded 1200ms", count: 214, endpoint: "GET /v1/analytics" }]}
  />
);
```

Trace with spans:

```tsx
import { TraceList } from "@fadymondy/nasaq/web";

export const Traces = () => (
  <TraceList
    defaultSelectedId="t1"
    traces={[{ id: "t1", method: "POST", name: "/v1/reports/export", status: 200, durationMs: 2480, startedAt: Date.now(), spans: [
      { id: "a", name: "POST /v1/reports/export", service: "api", startMs: 0, durationMs: 2480 },
      { id: "b", parentId: "a", name: "SELECT issues", service: "postgres", startMs: 50, durationMs: 640 },
    ] }]}
  />
);
```

## Accessibility

Charts have text alternatives and the percentile chips are text. Tables are real tables; clickable rows and trace rows are buttons reached with Tab. A failing span carries an error icon and label as well as colour, and slow endpoints get a "Slow" label.

## RTL & i18n

- Charts use the shared RTL axis handling.
- Routes, methods, span names and status codes are bidi-isolated and stay left-to-right.
- Durations use Arabic units ("مللي ث", "ث") in Arabic.

## Styling & tokens

- Colours come from tokens (`--primary`, `--nq-success`, `--nq-warning`, `--nq-danger`, `--nq-tag-*`); never pass raw hex.
- Percentile colours come from the chart config: p50 `--nq-success`, p95 `--nq-warning`, p99 `--nq-danger`.
- Error spans use `bg-destructive text-destructive-foreground`.

## Do / Don't

- Do pass percentiles in milliseconds.
- Do give traces `spans` so users can drill in.
- Don't average latencies; use percentiles.

## Related

- [`time-series-panel`](https://docs.nasaqui.com/components/time-series-panel)
- [`log-viewer`](https://docs.nasaqui.com/components/log-viewer)
- [`data-table`](https://docs.nasaqui.com/components/data-table)
- [`apm-page`](https://docs.nasaqui.com/components/apm-page)

## Lab

https://docs.nasaqui.com/?path=/docs/components-monitoring-apm-panels--docs

## Code

### React

```tsx
import { LatencyPercentiles } from "@fadymondy/nasaq/web";

export function Latency() {
  return (
    <LatencyPercentiles
      targetMs={500}
      summary={{ p50: 121, p95: 486, p99: 1140 }}
      data={[
        { time: "2026-09-29T09:00", p50: 118, p95: 470, p99: 1050 },
        { time: "2026-09-29T09:05", p50: 124, p95: 492, p99: 1210 },
      ]}
    />
  );
}
```

### shadcn

```tsx
import { LatencyPercentiles } from "@/components/ui/apm-panels";

export function Latency() {
  return (
    <LatencyPercentiles
      targetMs={500}
      summary={{ p50: 121, p95: 486, p99: 1140 }}
      data={[
        { time: "2026-09-29T09:00", p50: 118, p95: 470, p99: 1050 },
        { time: "2026-09-29T09:05", p50: 124, p95: 492, p99: 1210 },
      ]}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqLatencyPercentiles } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqLatencyPercentiles
    :target-ms="500"
    :summary="{ p50: 121, p95: 486, p99: 1140 }"
    :data="[
      { time: '2026-09-29T09:00', p50: 118, p95: 470, p99: 1050 },
      { time: '2026-09-29T09:05', p50: 124, p95: 492, p99: 1210 },
    ]"
  />
</template>
```

### Blade

```blade
<x-nq::apm-panels.latency-percentiles
    :target-ms="500"
    :summary="['p50' => 121, 'p95' => 486, 'p99' => 1140]"
    :data="[
        ['time' => '2026-09-29T09:00', 'p50' => 118, 'p95' => 470, 'p99' => 1050],
        ['time' => '2026-09-29T09:05', 'p50' => 124, 'p95' => 492, 'p99' => 1210],
    ]"
/>
```

### HTML + Alpine

```html
<div data-slot="latency-percentiles" class="flex flex-col gap-4 rounded-card border border-border bg-card py-4 text-card-foreground"><div data-slot="card-header" class="grid auto-rows-min items-start gap-1 px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]"><h3 data-slot="card-title" class="text-label text-foreground">Latency</h3>
        <div data-slot="card-description" class="text-body-sm text-muted-foreground">Response time percentiles</div></div>
    <div data-slot="card-content" class="px-4 flex flex-col gap-4"><dl class="grid grid-cols-3 gap-3">
                                            <div data-percentile="p50" class="flex min-w-0 flex-col gap-0.5 rounded-control border border-border px-3 py-2">
                    <dt class="flex items-center gap-1.5 text-caption text-muted-foreground">
                        <span aria-hidden="true" class="size-2 rounded-full" style="background: var(--nq-tag-teal)"></span>
                        <bdi dir="ltr" class="text-label text-foreground">p50</bdi>
                        <span class="hidden truncate sm:inline">Median</span>
                    </dt>
                    <dd class="text-h3 text-foreground tabular-nums" dir="ltr">121 ms</dd>
                                    </div>
                                            <div data-percentile="p95" class="flex min-w-0 flex-col gap-0.5 rounded-control border border-border px-3 py-2">
                    <dt class="flex items-center gap-1.5 text-caption text-muted-foreground">
                        <span aria-hidden="true" class="size-2 rounded-full" style="background: var(--nq-tag-amber)"></span>
                        <bdi dir="ltr" class="text-label text-foreground">p95</bdi>
                        <span class="hidden truncate sm:inline">Slowest 5%</span>
                    </dt>
                    <dd class="text-h3 text-foreground tabular-nums" dir="ltr">486 ms</dd>
                                    </div>
                                            <div data-percentile="p99" class="flex min-w-0 flex-col gap-0.5 rounded-control border border-border px-3 py-2">
                    <dt class="flex items-center gap-1.5 text-caption text-muted-foreground">
                        <span aria-hidden="true" class="size-2 rounded-full" style="background: var(--nq-tag-pink)"></span>
                        <bdi dir="ltr" class="text-label text-foreground">p99</bdi>
                        <span class="hidden truncate sm:inline">Slowest 1%</span>
                    </dt>
                    <dd class="text-h3 text-foreground tabular-nums" dir="ltr">1.14 s</dd>
                                    </div>
                    </dl>
        <div data-slot="chart"  role="img" aria-label="Latency percentiles over time"   style="--color-p50: var(--nq-tag-teal); --color-p95: var(--nq-tag-amber); --color-p99: var(--nq-tag-pink)"     class="flex w-full text-caption [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-dot]:stroke-card [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector]:stroke-card [&_.recharts-surface]:outline-hidden aspect-auto flex-col justify-start gap-2 h-64">
    <div class="flex min-h-0 flex-1 gap-2">
        <div aria-hidden="true" class="flex w-12 shrink-0 flex-col justify-between text-end text-muted-foreground tabular-nums">
            <span>2K</span><span>1.5K</span><span>1K</span><span>500</span><span>0</span>        </div>
        <div class="relative min-w-0 flex-1 border-b border-border">
            <svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true" class="absolute inset-0 size-full overflow-visible rtl:-scale-x-100">
                                    <line x1="0" x2="100" y1="0.00" y2="0.00" stroke="var(--border)" stroke-width="1" vector-effect="non-scaling-stroke" />
                                    <line x1="0" x2="100" y1="25.00" y2="25.00" stroke="var(--border)" stroke-width="1" vector-effect="non-scaling-stroke" />
                                    <line x1="0" x2="100" y1="50.00" y2="50.00" stroke="var(--border)" stroke-width="1" vector-effect="non-scaling-stroke" />
                                    <line x1="0" x2="100" y1="75.00" y2="75.00" stroke="var(--border)" stroke-width="1" vector-effect="non-scaling-stroke" />
                                                    <line x1="0" x2="100" y1="75.00" y2="75.00" stroke="var(--nq-warning)" stroke-width="1.5" stroke-dasharray="2 4" vector-effect="non-scaling-stroke" />
                                                                        <polyline points="0.00,94.10 100.00,93.80" fill="none" stroke="var(--color-p50)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
                                                        <polyline points="0.00,76.50 100.00,75.40" fill="none" stroke="var(--color-p95)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
                                                        <polyline points="0.00,47.50 100.00,39.50" fill="none" stroke="var(--color-p99)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
                            </svg>
            <span class="absolute end-1 text-caption text-nq-warning-text" style="top: 75.00%">Target p95 500</span>                                                <span data-slot="chart-dot" title="9:00 AM: p50 118" class="absolute size-2 -translate-y-1/2 rounded-full bg-[var(--color-key)] opacity-0 hover:opacity-100 ltr:-translate-x-1/2 rtl:translate-x-1/2" style="inset-inline-start: 0.00%; top: 94.10%; --color-key: var(--color-p50)"></span>
                                    <span data-slot="chart-dot" title="9:05 AM: p50 124" class="absolute size-2 -translate-y-1/2 rounded-full bg-[var(--color-key)] opacity-0 hover:opacity-100 ltr:-translate-x-1/2 rtl:translate-x-1/2" style="inset-inline-start: 100.00%; top: 93.80%; --color-key: var(--color-p50)"></span>
                                                                <span data-slot="chart-dot" title="9:00 AM: p95 470" class="absolute size-2 -translate-y-1/2 rounded-full bg-[var(--color-key)] opacity-0 hover:opacity-100 ltr:-translate-x-1/2 rtl:translate-x-1/2" style="inset-inline-start: 0.00%; top: 76.50%; --color-key: var(--color-p95)"></span>
                                    <span data-slot="chart-dot" title="9:05 AM: p95 492" class="absolute size-2 -translate-y-1/2 rounded-full bg-[var(--color-key)] opacity-0 hover:opacity-100 ltr:-translate-x-1/2 rtl:translate-x-1/2" style="inset-inline-start: 100.00%; top: 75.40%; --color-key: var(--color-p95)"></span>
                                                                <span data-slot="chart-dot" title="9:00 AM: p99 1,050" class="absolute size-2 -translate-y-1/2 rounded-full bg-[var(--color-key)] opacity-0 hover:opacity-100 ltr:-translate-x-1/2 rtl:translate-x-1/2" style="inset-inline-start: 0.00%; top: 47.50%; --color-key: var(--color-p99)"></span>
                                    <span data-slot="chart-dot" title="9:05 AM: p99 1,210" class="absolute size-2 -translate-y-1/2 rounded-full bg-[var(--color-key)] opacity-0 hover:opacity-100 ltr:-translate-x-1/2 rtl:translate-x-1/2" style="inset-inline-start: 100.00%; top: 39.50%; --color-key: var(--color-p99)"></span>
                                    </div>
    </div>
    <div aria-hidden="true" class="flex justify-between ps-14 text-muted-foreground">
        <span>9:00 AM</span><span>9:05 AM</span>    </div>
</div></div></div>
```
