@bpmnkit/operate 0.0.5
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/README.md +131 -0
- package/dist/components/badge.d.ts +2 -0
- package/dist/components/badge.js +2 -0
- package/dist/components/card.d.ts +2 -0
- package/dist/components/card.js +2 -0
- package/dist/components/chart.d.ts +6 -0
- package/dist/components/chart.js +161 -0
- package/dist/components/filter-table.d.ts +20 -0
- package/dist/components/filter-table.js +215 -0
- package/dist/components/table.d.ts +2 -0
- package/dist/components/table.js +2 -0
- package/dist/css.d.ts +2 -0
- package/dist/css.js +1071 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/mock-data.d.ts +18 -0
- package/dist/mock-data.js +509 -0
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +308 -0
- package/dist/router.d.ts +9 -0
- package/dist/router.js +49 -0
- package/dist/stores/base.d.ts +20 -0
- package/dist/stores/base.js +32 -0
- package/dist/stores/dashboard.d.ts +6 -0
- package/dist/stores/dashboard.js +19 -0
- package/dist/stores/decisions.d.ts +9 -0
- package/dist/stores/decisions.js +19 -0
- package/dist/stores/definitions.d.ts +9 -0
- package/dist/stores/definitions.js +19 -0
- package/dist/stores/incidents.d.ts +10 -0
- package/dist/stores/incidents.js +27 -0
- package/dist/stores/instances.d.ts +15 -0
- package/dist/stores/instances.js +33 -0
- package/dist/stores/jobs.d.ts +10 -0
- package/dist/stores/jobs.js +19 -0
- package/dist/stores/tasks.d.ts +10 -0
- package/dist/stores/tasks.js +19 -0
- package/dist/stream.d.ts +12 -0
- package/dist/stream.js +42 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.js +2 -0
- package/dist/views/dashboard.d.ts +6 -0
- package/dist/views/dashboard.js +98 -0
- package/dist/views/decision-detail.d.ts +15 -0
- package/dist/views/decision-detail.js +167 -0
- package/dist/views/decisions.d.ts +7 -0
- package/dist/views/decisions.js +78 -0
- package/dist/views/definition-detail.d.ts +15 -0
- package/dist/views/definition-detail.js +339 -0
- package/dist/views/definitions.d.ts +7 -0
- package/dist/views/definitions.js +70 -0
- package/dist/views/header.d.ts +9 -0
- package/dist/views/header.js +49 -0
- package/dist/views/incident-detail.d.ts +14 -0
- package/dist/views/incident-detail.js +540 -0
- package/dist/views/incidents.d.ts +7 -0
- package/dist/views/incidents.js +108 -0
- package/dist/views/instance-detail.d.ts +16 -0
- package/dist/views/instance-detail.js +728 -0
- package/dist/views/instances.d.ts +7 -0
- package/dist/views/instances.js +249 -0
- package/dist/views/jobs.d.ts +6 -0
- package/dist/views/jobs.js +60 -0
- package/dist/views/messages.d.ts +11 -0
- package/dist/views/messages.js +431 -0
- package/dist/views/nav.d.ts +10 -0
- package/dist/views/nav.js +60 -0
- package/dist/views/task-detail.d.ts +13 -0
- package/dist/views/task-detail.js +187 -0
- package/dist/views/tasks.d.ts +7 -0
- package/dist/views/tasks.js +72 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/bpmn-sdk/monorepo/main/doc/logos/logo-2-gateway.svg" width="72" height="72" alt="BPMN Kit logo">
|
|
3
|
+
<h1>@bpmnkit/operate</h1>
|
|
4
|
+
<p>Monitoring and operations frontend for Camunda 8 clusters — real-time SSE, zero dependencies</p>
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@bpmnkit/operate)
|
|
7
|
+
[](https://github.com/bpmnkit/monorepo/blob/main/LICENSE)
|
|
8
|
+
[](https://github.com/bpmnkit/monorepo)
|
|
9
|
+
|
|
10
|
+
[Documentation](https://bpmn-sdk-docs.pages.dev) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/operate/CHANGELOG.md)
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
`@bpmnkit/operate` is a zero-dependency monitoring and operations frontend for Camunda 8. Mount it into any HTML element to get a full process monitoring UI — live dashboard, instance browser, incident management, job queue, and user tasks.
|
|
18
|
+
|
|
19
|
+
It pairs with the `@bpmnkit/proxy` local server, which polls the Camunda REST API server-side and pushes updates via **Server-Sent Events**. The frontend stays clean with no polling timers.
|
|
20
|
+
|
|
21
|
+
A **mock mode** (`mock: true`) ships fixture data without any running proxy or cluster — useful for demos and local development.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Dashboard** — real-time stats: active instances, open incidents, active jobs, pending tasks
|
|
26
|
+
- **Process Definitions** — deployed process list with name, version, and tenant
|
|
27
|
+
- **Process Instances** — paginated list with state filter (Active / Completed / Terminated)
|
|
28
|
+
- **Instance Detail** — BPMN canvas via `@bpmnkit/canvas` with live token-highlight overlay; active elements glow amber, visited elements show green tint
|
|
29
|
+
- **Incidents** — error type, message, process, and resolution state
|
|
30
|
+
- **Jobs** — job type, worker, retries, state, error message
|
|
31
|
+
- **User Tasks** — name, assignee, state, due date, priority
|
|
32
|
+
- **Profile switcher** — header dropdown populated from the proxy `/profiles` endpoint; switches reconnect all SSE streams
|
|
33
|
+
- **Mock/demo mode** — fully self-contained fixture data, no cluster required
|
|
34
|
+
- **SSE architecture** — proxy polls server-side; frontend opens one `EventSource` per view, gets pushed updates
|
|
35
|
+
- **Hash router** — `#/`, `#/instances`, `#/instances/:key`, `#/definitions`, etc.
|
|
36
|
+
- **Themeable** — light / dark / auto via `--bpmnkit-*` CSS custom properties from `@bpmnkit/ui`; theme choice persisted to `localStorage`
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install @bpmnkit/operate @bpmnkit/proxy
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### Demo mode (no cluster needed)
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { createOperate } from "@bpmnkit/operate"
|
|
50
|
+
|
|
51
|
+
createOperate({
|
|
52
|
+
container: document.getElementById("app")!,
|
|
53
|
+
mock: true,
|
|
54
|
+
theme: "auto",
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Connected to a real Camunda cluster via proxy
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { createOperate } from "@bpmnkit/operate"
|
|
62
|
+
|
|
63
|
+
createOperate({
|
|
64
|
+
container: document.getElementById("app")!,
|
|
65
|
+
proxyUrl: "http://localhost:3033", // default
|
|
66
|
+
profile: "production", // optional, uses active profile if omitted
|
|
67
|
+
pollInterval: 15_000, // ms between server-side polls (default: 30 000)
|
|
68
|
+
theme: "dark",
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The proxy must be running (`pnpm proxy`) and have at least one profile configured via the `casen` CLI.
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Browser ──── EventSource ────▶ @bpmnkit/proxy ──── CamundaClient ────▶ Camunda cluster
|
|
78
|
+
◀─── SSE events ────── (polls on interval, pushes results)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
1. Each view opens an SSE connection to `/operate/stream?topic=<view>&interval=<ms>`.
|
|
82
|
+
2. The proxy creates a `CamundaClient` using the configured profile's auth credentials.
|
|
83
|
+
3. On each polling tick, the proxy fetches the relevant Camunda data and emits a `{ type: "data", payload }` SSE event.
|
|
84
|
+
4. The store updates and the view re-renders.
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### `createOperate(options)`
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
interface OperateOptions {
|
|
92
|
+
container: HTMLElement
|
|
93
|
+
proxyUrl?: string // default: "http://localhost:3033"
|
|
94
|
+
profile?: string // profile name; uses active profile if omitted
|
|
95
|
+
theme?: "light" | "dark" | "auto" // default: "auto"
|
|
96
|
+
pollInterval?: number // ms between polls; default: 30 000
|
|
97
|
+
mock?: boolean // use built-in fixture data; default: false
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Returns an `OperateApi`:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
interface OperateApi {
|
|
105
|
+
el: HTMLElement
|
|
106
|
+
setProfile(name: string | null): void
|
|
107
|
+
setTheme(theme: "light" | "dark" | "auto"): void
|
|
108
|
+
navigate(path: string): void // e.g. "/instances/123456789"
|
|
109
|
+
destroy(): void
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Related Packages
|
|
116
|
+
|
|
117
|
+
| Package | Description |
|
|
118
|
+
|---------|-------------|
|
|
119
|
+
| [`@bpmnkit/core`](https://www.npmjs.com/package/@bpmnkit/core) | BPMN/DMN/Form parser, builder, layout engine |
|
|
120
|
+
| [`@bpmnkit/canvas`](https://www.npmjs.com/package/@bpmnkit/canvas) | Zero-dependency SVG BPMN viewer |
|
|
121
|
+
| [`@bpmnkit/editor`](https://www.npmjs.com/package/@bpmnkit/editor) | Full-featured interactive BPMN editor |
|
|
122
|
+
| [`@bpmnkit/engine`](https://www.npmjs.com/package/@bpmnkit/engine) | Lightweight BPMN process execution engine |
|
|
123
|
+
| [`@bpmnkit/feel`](https://www.npmjs.com/package/@bpmnkit/feel) | FEEL expression language parser & evaluator |
|
|
124
|
+
| [`@bpmnkit/plugins`](https://www.npmjs.com/package/@bpmnkit/plugins) | 22 composable canvas plugins |
|
|
125
|
+
| [`@bpmnkit/api`](https://www.npmjs.com/package/@bpmnkit/api) | Camunda 8 REST API TypeScript client |
|
|
126
|
+
| [`@bpmnkit/ascii`](https://www.npmjs.com/package/@bpmnkit/ascii) | Render BPMN diagrams as Unicode ASCII art |
|
|
127
|
+
| [`@bpmnkit/profiles`](https://www.npmjs.com/package/@bpmnkit/profiles) | Shared auth, profile storage, and client factories for CLI & proxy |
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
[MIT](https://github.com/bpmnkit/monorepo/blob/main/LICENSE) © bpmn-sdk
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const METRICS = [
|
|
2
|
+
{ key: "activeInstances", label: "Active Instances", cssVar: "--bpmnkit-accent" },
|
|
3
|
+
{ key: "openIncidents", label: "Open Incidents", cssVar: "--op-c-amber" },
|
|
4
|
+
{ key: "activeJobs", label: "Active Jobs", cssVar: "--op-c-green" },
|
|
5
|
+
{ key: "pendingTasks", label: "Pending Tasks", cssVar: "--op-c-purple" },
|
|
6
|
+
];
|
|
7
|
+
const MARGIN = { top: 12, right: 16, bottom: 36, left: 36 };
|
|
8
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
9
|
+
function niceMax(raw) {
|
|
10
|
+
if (raw <= 0)
|
|
11
|
+
return 4;
|
|
12
|
+
const mag = 10 ** Math.floor(Math.log10(raw));
|
|
13
|
+
const nice = Math.ceil(raw / mag) * mag;
|
|
14
|
+
return Math.max(nice, 4);
|
|
15
|
+
}
|
|
16
|
+
function makeSvgEl(tag) {
|
|
17
|
+
return document.createElementNS(SVG_NS, tag);
|
|
18
|
+
}
|
|
19
|
+
// ── Chart factory ─────────────────────────────────────────────────────────────
|
|
20
|
+
export function createBarChart(container) {
|
|
21
|
+
const el = document.createElement("div");
|
|
22
|
+
el.className = "op-chart";
|
|
23
|
+
container.appendChild(el);
|
|
24
|
+
const svg = makeSvgEl("svg");
|
|
25
|
+
svg.setAttribute("class", "op-chart-svg");
|
|
26
|
+
svg.setAttribute("aria-hidden", "true");
|
|
27
|
+
el.appendChild(svg);
|
|
28
|
+
let lastData = null;
|
|
29
|
+
let svgWidth = 0;
|
|
30
|
+
let svgHeight = 0;
|
|
31
|
+
function clearSvg() {
|
|
32
|
+
while (svg.firstChild)
|
|
33
|
+
svg.removeChild(svg.firstChild);
|
|
34
|
+
}
|
|
35
|
+
function draw(data) {
|
|
36
|
+
clearSvg();
|
|
37
|
+
if (svgWidth <= 0 || svgHeight <= 0)
|
|
38
|
+
return;
|
|
39
|
+
if (!data) {
|
|
40
|
+
// Loading dots
|
|
41
|
+
const cx = svgWidth / 2;
|
|
42
|
+
const cy = svgHeight / 2;
|
|
43
|
+
for (let i = 0; i < 3; i++) {
|
|
44
|
+
const circle = makeSvgEl("circle");
|
|
45
|
+
circle.setAttribute("cx", String(cx + (i - 1) * 16));
|
|
46
|
+
circle.setAttribute("cy", String(cy));
|
|
47
|
+
circle.setAttribute("r", "4");
|
|
48
|
+
circle.setAttribute("class", "op-chart-dot-pulse");
|
|
49
|
+
circle.setAttribute("style", `animation-delay: ${i * 0.2}s`);
|
|
50
|
+
svg.appendChild(circle);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const plotW = Math.max(1, svgWidth - MARGIN.left - MARGIN.right);
|
|
55
|
+
const plotH = Math.max(1, svgHeight - MARGIN.top - MARGIN.bottom);
|
|
56
|
+
// Y scale
|
|
57
|
+
let yMax = 0;
|
|
58
|
+
for (const m of METRICS) {
|
|
59
|
+
const v = data[m.key];
|
|
60
|
+
if (v > yMax)
|
|
61
|
+
yMax = v;
|
|
62
|
+
}
|
|
63
|
+
yMax = niceMax(yMax);
|
|
64
|
+
function toY(v) {
|
|
65
|
+
return MARGIN.top + plotH - (v / yMax) * plotH;
|
|
66
|
+
}
|
|
67
|
+
// Y grid + labels
|
|
68
|
+
for (let i = 0; i <= 4; i++) {
|
|
69
|
+
const v = Math.round((i / 4) * yMax);
|
|
70
|
+
const y = toY(v);
|
|
71
|
+
const line = makeSvgEl("line");
|
|
72
|
+
line.setAttribute("x1", String(MARGIN.left));
|
|
73
|
+
line.setAttribute("x2", String(MARGIN.left + plotW));
|
|
74
|
+
line.setAttribute("y1", String(y));
|
|
75
|
+
line.setAttribute("y2", String(y));
|
|
76
|
+
line.setAttribute("class", i === 0 ? "op-chart-axis" : "op-chart-grid");
|
|
77
|
+
svg.appendChild(line);
|
|
78
|
+
const lbl = makeSvgEl("text");
|
|
79
|
+
lbl.setAttribute("x", String(MARGIN.left - 6));
|
|
80
|
+
lbl.setAttribute("y", String(y + 4));
|
|
81
|
+
lbl.setAttribute("text-anchor", "end");
|
|
82
|
+
lbl.setAttribute("class", "op-chart-axis-label");
|
|
83
|
+
lbl.textContent = String(v);
|
|
84
|
+
svg.appendChild(lbl);
|
|
85
|
+
}
|
|
86
|
+
// X axis baseline
|
|
87
|
+
const xBase = makeSvgEl("line");
|
|
88
|
+
xBase.setAttribute("x1", String(MARGIN.left));
|
|
89
|
+
xBase.setAttribute("x2", String(MARGIN.left + plotW));
|
|
90
|
+
xBase.setAttribute("y1", String(MARGIN.top + plotH));
|
|
91
|
+
xBase.setAttribute("y2", String(MARGIN.top + plotH));
|
|
92
|
+
xBase.setAttribute("class", "op-chart-axis");
|
|
93
|
+
svg.appendChild(xBase);
|
|
94
|
+
// Bars — one per metric, evenly spaced
|
|
95
|
+
const n = METRICS.length;
|
|
96
|
+
const groupGap = Math.max(8, plotW * 0.06);
|
|
97
|
+
const barW = Math.max(1, (plotW - groupGap * (n + 1)) / n);
|
|
98
|
+
for (let i = 0; i < METRICS.length; i++) {
|
|
99
|
+
const m = METRICS[i];
|
|
100
|
+
if (!m)
|
|
101
|
+
continue;
|
|
102
|
+
const v = data[m.key];
|
|
103
|
+
const barH = (v / yMax) * plotH;
|
|
104
|
+
const x = MARGIN.left + groupGap * (i + 1) + barW * i;
|
|
105
|
+
const y = MARGIN.top + plotH - barH;
|
|
106
|
+
const rect = makeSvgEl("rect");
|
|
107
|
+
rect.setAttribute("x", String(x));
|
|
108
|
+
rect.setAttribute("y", String(y));
|
|
109
|
+
rect.setAttribute("width", String(barW));
|
|
110
|
+
rect.setAttribute("height", String(Math.max(1, barH)));
|
|
111
|
+
rect.setAttribute("fill", `var(${m.cssVar})`);
|
|
112
|
+
rect.setAttribute("opacity", "0.85");
|
|
113
|
+
rect.setAttribute("rx", "2");
|
|
114
|
+
svg.appendChild(rect);
|
|
115
|
+
// Value label above bar
|
|
116
|
+
if (barH > 0) {
|
|
117
|
+
const valLbl = makeSvgEl("text");
|
|
118
|
+
valLbl.setAttribute("x", String(x + barW / 2));
|
|
119
|
+
valLbl.setAttribute("y", String(Math.max(MARGIN.top + 10, y - 4)));
|
|
120
|
+
valLbl.setAttribute("text-anchor", "middle");
|
|
121
|
+
valLbl.setAttribute("class", "op-chart-axis-label");
|
|
122
|
+
valLbl.textContent = String(v);
|
|
123
|
+
svg.appendChild(valLbl);
|
|
124
|
+
}
|
|
125
|
+
// X label below bar
|
|
126
|
+
const xLbl = makeSvgEl("text");
|
|
127
|
+
xLbl.setAttribute("x", String(x + barW / 2));
|
|
128
|
+
xLbl.setAttribute("y", String(MARGIN.top + plotH + 20));
|
|
129
|
+
xLbl.setAttribute("text-anchor", "middle");
|
|
130
|
+
xLbl.setAttribute("class", "op-chart-axis-label");
|
|
131
|
+
// Abbreviate for space
|
|
132
|
+
const short = m.label.replace("Active ", "").replace("Open ", "").replace("Pending ", "");
|
|
133
|
+
xLbl.textContent = short;
|
|
134
|
+
svg.appendChild(xLbl);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function drawLast() {
|
|
138
|
+
draw(lastData);
|
|
139
|
+
}
|
|
140
|
+
const ro = new ResizeObserver((entries) => {
|
|
141
|
+
const entry = entries[0];
|
|
142
|
+
if (!entry)
|
|
143
|
+
return;
|
|
144
|
+
svgWidth = Math.floor(entry.contentRect.width);
|
|
145
|
+
svgHeight = Math.floor(entry.contentRect.height);
|
|
146
|
+
svg.setAttribute("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
|
|
147
|
+
drawLast();
|
|
148
|
+
});
|
|
149
|
+
ro.observe(svg);
|
|
150
|
+
return {
|
|
151
|
+
update(data) {
|
|
152
|
+
lastData = data;
|
|
153
|
+
draw(data);
|
|
154
|
+
},
|
|
155
|
+
destroy() {
|
|
156
|
+
ro.disconnect();
|
|
157
|
+
el.remove();
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=chart.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface FilterColumn<T> {
|
|
2
|
+
label: string;
|
|
3
|
+
width?: string;
|
|
4
|
+
render(row: T): string | HTMLElement;
|
|
5
|
+
/** If provided, column header becomes clickable for sort */
|
|
6
|
+
sortValue?: (row: T) => string | number | null | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function createFilterTable<T>(options: {
|
|
9
|
+
columns: FilterColumn<T>[];
|
|
10
|
+
/** Function returning searchable text for a row */
|
|
11
|
+
searchFn?: (row: T) => string;
|
|
12
|
+
onRowClick?: (row: T) => void;
|
|
13
|
+
emptyText?: string;
|
|
14
|
+
/** Initial page size (default: 10) */
|
|
15
|
+
pageSize?: number;
|
|
16
|
+
}): {
|
|
17
|
+
el: HTMLElement;
|
|
18
|
+
setRows(rows: T[]): void;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=filter-table.d.ts.map
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export function createFilterTable(options) {
|
|
2
|
+
let allRows = [];
|
|
3
|
+
let searchQuery = "";
|
|
4
|
+
let sortState = null;
|
|
5
|
+
let currentPage = 0;
|
|
6
|
+
let pageSize = options.pageSize ?? 10;
|
|
7
|
+
const el = document.createElement("div");
|
|
8
|
+
el.className = "op-filter-table";
|
|
9
|
+
// Toolbar row: search input + count
|
|
10
|
+
const toolbar = document.createElement("div");
|
|
11
|
+
toolbar.className = "op-toolbar";
|
|
12
|
+
const searchInput = document.createElement("input");
|
|
13
|
+
searchInput.type = "text";
|
|
14
|
+
searchInput.className = "op-search";
|
|
15
|
+
searchInput.placeholder = "Search…";
|
|
16
|
+
toolbar.appendChild(searchInput);
|
|
17
|
+
const countEl = document.createElement("span");
|
|
18
|
+
countEl.className = "op-search-count";
|
|
19
|
+
toolbar.appendChild(countEl);
|
|
20
|
+
el.appendChild(toolbar);
|
|
21
|
+
// Table wrap
|
|
22
|
+
const tableWrap = document.createElement("div");
|
|
23
|
+
tableWrap.className = "bpmnkit-table-wrap";
|
|
24
|
+
el.appendChild(tableWrap);
|
|
25
|
+
// Header with sort support
|
|
26
|
+
const headerEl = document.createElement("div");
|
|
27
|
+
headerEl.className = "bpmnkit-table-header";
|
|
28
|
+
const headerCells = [];
|
|
29
|
+
for (let i = 0; i < options.columns.length; i++) {
|
|
30
|
+
const col = options.columns[i];
|
|
31
|
+
if (!col)
|
|
32
|
+
continue;
|
|
33
|
+
const th = document.createElement("div");
|
|
34
|
+
th.className = "bpmnkit-table-th";
|
|
35
|
+
if (col.width)
|
|
36
|
+
th.style.width = col.width;
|
|
37
|
+
if (col.sortValue) {
|
|
38
|
+
th.classList.add("op-th-sort");
|
|
39
|
+
const labelSpan = document.createElement("span");
|
|
40
|
+
labelSpan.textContent = col.label;
|
|
41
|
+
th.appendChild(labelSpan);
|
|
42
|
+
const icon = document.createElement("span");
|
|
43
|
+
icon.className = "op-sort-icon";
|
|
44
|
+
icon.textContent = "↕";
|
|
45
|
+
th.appendChild(icon);
|
|
46
|
+
const idx = i;
|
|
47
|
+
th.addEventListener("click", () => {
|
|
48
|
+
if (sortState?.col === idx) {
|
|
49
|
+
sortState = sortState.dir === "asc" ? { col: idx, dir: "desc" } : null;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
sortState = { col: idx, dir: "asc" };
|
|
53
|
+
}
|
|
54
|
+
currentPage = 0;
|
|
55
|
+
updateSortIcons();
|
|
56
|
+
renderRows();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
th.textContent = col.label;
|
|
61
|
+
}
|
|
62
|
+
headerCells.push(th);
|
|
63
|
+
headerEl.appendChild(th);
|
|
64
|
+
}
|
|
65
|
+
tableWrap.appendChild(headerEl);
|
|
66
|
+
const bodyEl = document.createElement("div");
|
|
67
|
+
bodyEl.className = "bpmnkit-table-body";
|
|
68
|
+
tableWrap.appendChild(bodyEl);
|
|
69
|
+
// Pagination bar (outside scroll area, always visible)
|
|
70
|
+
const paginationEl = document.createElement("div");
|
|
71
|
+
paginationEl.className = "op-pagination";
|
|
72
|
+
el.appendChild(paginationEl);
|
|
73
|
+
function updateSortIcons() {
|
|
74
|
+
for (let i = 0; i < headerCells.length; i++) {
|
|
75
|
+
const icon = headerCells[i]?.querySelector(".op-sort-icon");
|
|
76
|
+
if (!icon)
|
|
77
|
+
continue;
|
|
78
|
+
if (sortState?.col === i) {
|
|
79
|
+
icon.textContent = sortState.dir === "asc" ? "↑" : "↓";
|
|
80
|
+
icon.classList.add("op-sort-icon--active");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
icon.textContent = "↕";
|
|
84
|
+
icon.classList.remove("op-sort-icon--active");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function applyFilterSort() {
|
|
89
|
+
let rows = allRows;
|
|
90
|
+
if (searchQuery && options.searchFn) {
|
|
91
|
+
const q = searchQuery.toLowerCase();
|
|
92
|
+
rows = rows.filter((row) => options.searchFn?.(row).toLowerCase().includes(q));
|
|
93
|
+
}
|
|
94
|
+
if (sortState !== null) {
|
|
95
|
+
const st = sortState;
|
|
96
|
+
const col = options.columns[st.col];
|
|
97
|
+
if (col?.sortValue) {
|
|
98
|
+
const dir = st.dir === "asc" ? 1 : -1;
|
|
99
|
+
rows = [...rows].sort((a, b) => {
|
|
100
|
+
const av = col.sortValue?.(a) ?? "";
|
|
101
|
+
const bv = col.sortValue?.(b) ?? "";
|
|
102
|
+
if (av < bv)
|
|
103
|
+
return -1 * dir;
|
|
104
|
+
if (av > bv)
|
|
105
|
+
return 1 * dir;
|
|
106
|
+
return 0;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return rows;
|
|
111
|
+
}
|
|
112
|
+
function renderPagination(total, start, end, totalPages) {
|
|
113
|
+
paginationEl.innerHTML = "";
|
|
114
|
+
const sizeLabel = document.createElement("span");
|
|
115
|
+
sizeLabel.textContent = "Rows:";
|
|
116
|
+
paginationEl.appendChild(sizeLabel);
|
|
117
|
+
const sizeSelect = document.createElement("select");
|
|
118
|
+
sizeSelect.className = "op-page-size";
|
|
119
|
+
for (const sz of [10, 25, 50, 100]) {
|
|
120
|
+
const opt = document.createElement("option");
|
|
121
|
+
opt.value = String(sz);
|
|
122
|
+
opt.textContent = String(sz);
|
|
123
|
+
if (sz === pageSize)
|
|
124
|
+
opt.selected = true;
|
|
125
|
+
sizeSelect.appendChild(opt);
|
|
126
|
+
}
|
|
127
|
+
sizeSelect.addEventListener("change", () => {
|
|
128
|
+
pageSize = Number(sizeSelect.value);
|
|
129
|
+
currentPage = 0;
|
|
130
|
+
renderRows();
|
|
131
|
+
});
|
|
132
|
+
paginationEl.appendChild(sizeSelect);
|
|
133
|
+
const prevBtn = document.createElement("button");
|
|
134
|
+
prevBtn.className = "op-pagination-btn";
|
|
135
|
+
prevBtn.textContent = "‹";
|
|
136
|
+
prevBtn.disabled = currentPage === 0;
|
|
137
|
+
prevBtn.addEventListener("click", () => {
|
|
138
|
+
currentPage--;
|
|
139
|
+
renderRows();
|
|
140
|
+
});
|
|
141
|
+
paginationEl.appendChild(prevBtn);
|
|
142
|
+
const infoEl = document.createElement("span");
|
|
143
|
+
infoEl.className = "op-pagination-info";
|
|
144
|
+
infoEl.textContent = total > 0 ? `${start + 1}–${end} of ${total}` : "0 results";
|
|
145
|
+
paginationEl.appendChild(infoEl);
|
|
146
|
+
const nextBtn = document.createElement("button");
|
|
147
|
+
nextBtn.className = "op-pagination-btn";
|
|
148
|
+
nextBtn.textContent = "›";
|
|
149
|
+
nextBtn.disabled = currentPage >= totalPages - 1;
|
|
150
|
+
nextBtn.addEventListener("click", () => {
|
|
151
|
+
currentPage++;
|
|
152
|
+
renderRows();
|
|
153
|
+
});
|
|
154
|
+
paginationEl.appendChild(nextBtn);
|
|
155
|
+
}
|
|
156
|
+
function renderRows() {
|
|
157
|
+
const filtered = applyFilterSort();
|
|
158
|
+
const total = filtered.length;
|
|
159
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
160
|
+
if (currentPage >= totalPages)
|
|
161
|
+
currentPage = totalPages - 1;
|
|
162
|
+
const start = currentPage * pageSize;
|
|
163
|
+
const pageRows = filtered.slice(start, start + pageSize);
|
|
164
|
+
const end = start + pageRows.length;
|
|
165
|
+
countEl.textContent =
|
|
166
|
+
filtered.length !== allRows.length
|
|
167
|
+
? `${filtered.length} / ${allRows.length}`
|
|
168
|
+
: String(allRows.length);
|
|
169
|
+
bodyEl.innerHTML = "";
|
|
170
|
+
if (total === 0) {
|
|
171
|
+
const empty = document.createElement("div");
|
|
172
|
+
empty.className = "bpmnkit-table-empty";
|
|
173
|
+
empty.textContent = searchQuery ? "No results" : (options.emptyText ?? "No data");
|
|
174
|
+
bodyEl.appendChild(empty);
|
|
175
|
+
renderPagination(0, 0, 0, 1);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const row of pageRows) {
|
|
179
|
+
const tr = document.createElement("div");
|
|
180
|
+
tr.className = "bpmnkit-table-row";
|
|
181
|
+
if (options.onRowClick) {
|
|
182
|
+
tr.classList.add("bpmnkit-table-row--clickable");
|
|
183
|
+
tr.addEventListener("click", () => options.onRowClick?.(row));
|
|
184
|
+
}
|
|
185
|
+
for (const col of options.columns) {
|
|
186
|
+
const td = document.createElement("div");
|
|
187
|
+
td.className = "bpmnkit-table-td";
|
|
188
|
+
if (col.width)
|
|
189
|
+
td.style.width = col.width;
|
|
190
|
+
const content = col.render(row);
|
|
191
|
+
if (typeof content === "string")
|
|
192
|
+
td.textContent = content;
|
|
193
|
+
else
|
|
194
|
+
td.appendChild(content);
|
|
195
|
+
tr.appendChild(td);
|
|
196
|
+
}
|
|
197
|
+
bodyEl.appendChild(tr);
|
|
198
|
+
}
|
|
199
|
+
renderPagination(total, start, end, totalPages);
|
|
200
|
+
}
|
|
201
|
+
searchInput.addEventListener("input", () => {
|
|
202
|
+
searchQuery = searchInput.value;
|
|
203
|
+
currentPage = 0;
|
|
204
|
+
renderRows();
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
el,
|
|
208
|
+
setRows(rows) {
|
|
209
|
+
allRows = rows;
|
|
210
|
+
currentPage = 0;
|
|
211
|
+
renderRows();
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=filter-table.js.map
|
package/dist/css.d.ts
ADDED