@caido-utils/components 0.1.0 → 0.2.1
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 +416 -0
- package/dist/RequestEditor/Container.vue +113 -26
- package/dist/ResponseEditor/Container.d.vue.ts +1 -0
- package/dist/ResponseEditor/Container.vue +129 -26
- package/dist/ResponseEditor/Container.vue.d.ts +1 -0
- package/dist/editors/format.d.ts +1 -0
- package/dist/editors/format.js +25 -0
- package/package.json +4 -3
package/README.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# @caido-utils/components
|
|
2
|
+
|
|
3
|
+
Vue 3 components for Caido plugin frontends.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @caido-utils/components
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer dependencies:** `vue`, `primevue`, `@caido/sdk-frontend`, `@codemirror/view`
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Dialog, Table, HttpqlInput, Card } from "@caido-utils/components";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### ButtonGroup
|
|
20
|
+
|
|
21
|
+
Toggle button group. Always requires a selection (no empty state).
|
|
22
|
+
|
|
23
|
+
```vue
|
|
24
|
+
<ButtonGroup v-model="view" :options="options" />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### Props
|
|
28
|
+
|
|
29
|
+
| Name | Type | Default | Description |
|
|
30
|
+
| ------------ | ------------------------------------ | --------- | ------------------------ |
|
|
31
|
+
| `modelValue` | `string` | required | Currently selected value |
|
|
32
|
+
| `options` | `{ label: string; value: string }[]` | required | Available options |
|
|
33
|
+
| `size` | `"small" \| "large"` | `"small"` | Button size |
|
|
34
|
+
|
|
35
|
+
#### Emits
|
|
36
|
+
|
|
37
|
+
| Event | Payload | Description |
|
|
38
|
+
| ------------------- | -------- | ---------------------- |
|
|
39
|
+
| `update:modelValue` | `string` | Selected value changed |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### Card
|
|
44
|
+
|
|
45
|
+
Styled container card. Passes through all PrimeVue Card props and slots.
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<Card>
|
|
49
|
+
<template #content>
|
|
50
|
+
<p>Card body</p>
|
|
51
|
+
</template>
|
|
52
|
+
</Card>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Slots
|
|
56
|
+
|
|
57
|
+
All PrimeVue Card slots: `#header`, `#title`, `#subtitle`, `#content`, `#footer`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### ContextMenu
|
|
62
|
+
|
|
63
|
+
Right-click context menu with dark styling.
|
|
64
|
+
|
|
65
|
+
```vue
|
|
66
|
+
<div @contextmenu.prevent="ctxRef?.show($event)">
|
|
67
|
+
Right-click here
|
|
68
|
+
</div>
|
|
69
|
+
<ContextMenu ref="ctxRef" :items="menuItems" />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Props
|
|
73
|
+
|
|
74
|
+
| Name | Type | Default | Description |
|
|
75
|
+
| ------- | ------------ | -------- | ------------------------ |
|
|
76
|
+
| `items` | `MenuItem[]` | required | PrimeVue menu item array |
|
|
77
|
+
|
|
78
|
+
#### Exposed Methods
|
|
79
|
+
|
|
80
|
+
| Method | Signature | Description |
|
|
81
|
+
| -------- | ----------------------------- | ---------------------- |
|
|
82
|
+
| `show` | `(event: MouseEvent) => void` | Show at mouse position |
|
|
83
|
+
| `hide` | `() => void` | Hide menu |
|
|
84
|
+
| `toggle` | `(event: MouseEvent) => void` | Toggle visibility |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### DataTable
|
|
89
|
+
|
|
90
|
+
PrimeVue DataTable with virtual scrolling, compact styling, resizable columns, and scroll position memory.
|
|
91
|
+
|
|
92
|
+
```vue
|
|
93
|
+
<DataTable
|
|
94
|
+
v-model:selection="selected"
|
|
95
|
+
v-model:active-row="activeRow"
|
|
96
|
+
:items="rows"
|
|
97
|
+
:columns="columns"
|
|
98
|
+
selectable="multiple"
|
|
99
|
+
scroll-key="my-table"
|
|
100
|
+
data-key="id"
|
|
101
|
+
@row-select="onSelect"
|
|
102
|
+
/>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Props
|
|
106
|
+
|
|
107
|
+
| Name | Type | Default | Description |
|
|
108
|
+
| ------------ | --------------------------------------------------------- | -------- | ----------------------------------- |
|
|
109
|
+
| `items` | `Record<string, unknown>[]` | required | Row data |
|
|
110
|
+
| `columns` | `{ field: string; header: string; sortable?: boolean }[]` | required | Column definitions |
|
|
111
|
+
| `rowHeight` | `number` | `33` | Row height in pixels |
|
|
112
|
+
| `selectable` | `false \| "single" \| "multiple"` | `false` | Selection mode |
|
|
113
|
+
| `selection` | `Record<string, unknown>[]` | `[]` | Selected rows (v-model) |
|
|
114
|
+
| `activeRow` | `Record<string, unknown> \| null` | `null` | Highlighted row (v-model) |
|
|
115
|
+
| `scrollKey` | `string` | - | Key for scroll position persistence |
|
|
116
|
+
|
|
117
|
+
Additional PrimeVue DataTable props are forwarded via `$attrs` (e.g. `dataKey`).
|
|
118
|
+
|
|
119
|
+
#### Emits
|
|
120
|
+
|
|
121
|
+
| Event | Payload | Description |
|
|
122
|
+
| ------------------ | ----------------------------------- | ------------------ |
|
|
123
|
+
| `update:selection` | `Record<string, unknown>[]` | Selection changed |
|
|
124
|
+
| `update:activeRow` | `Record<string, unknown> \| null` | Active row changed |
|
|
125
|
+
| `row-select` | `{ data: Record<string, unknown> }` | Row selected |
|
|
126
|
+
| `row-unselect` | `{ data: Record<string, unknown> }` | Row unselected |
|
|
127
|
+
| `row-click` | `{ data: Record<string, unknown> }` | Row clicked |
|
|
128
|
+
|
|
129
|
+
#### Slots
|
|
130
|
+
|
|
131
|
+
| Slot | Scope | Description |
|
|
132
|
+
| -------------- | ----------------- | ------------------------------- |
|
|
133
|
+
| `cell-{field}` | `{ item, value }` | Custom cell renderer per column |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### Dialog
|
|
138
|
+
|
|
139
|
+
Modal dialog with header, optional footer with action/cancel buttons.
|
|
140
|
+
|
|
141
|
+
```vue
|
|
142
|
+
<Dialog
|
|
143
|
+
v-if="visible"
|
|
144
|
+
title="Confirm Action"
|
|
145
|
+
width="500px"
|
|
146
|
+
action-label="Save"
|
|
147
|
+
action-icon="fas fa-save"
|
|
148
|
+
@close="visible = false"
|
|
149
|
+
@action="handleSave"
|
|
150
|
+
>
|
|
151
|
+
<p>Dialog content here</p>
|
|
152
|
+
</Dialog>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Props
|
|
156
|
+
|
|
157
|
+
| Name | Type | Default | Description |
|
|
158
|
+
| ---------------- | --------- | ----------- | ------------------------------------ |
|
|
159
|
+
| `title` | `string` | required | Header text |
|
|
160
|
+
| `width` | `string` | required | CSS width (e.g. `"500px"`) |
|
|
161
|
+
| `showBack` | `boolean` | `false` | Show back arrow in header |
|
|
162
|
+
| `showFooter` | `boolean` | `true` | Show footer with buttons |
|
|
163
|
+
| `actionLabel` | `string` | `"Confirm"` | Primary button label |
|
|
164
|
+
| `actionIcon` | `string` | - | Font Awesome class for action button |
|
|
165
|
+
| `actionDisabled` | `boolean` | `false` | Disable action button |
|
|
166
|
+
| `actionLoading` | `boolean` | `false` | Show loading state on action button |
|
|
167
|
+
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label |
|
|
168
|
+
| `contentClass` | `string` | - | CSS class for dialog content area |
|
|
169
|
+
|
|
170
|
+
#### Emits
|
|
171
|
+
|
|
172
|
+
| Event | Description |
|
|
173
|
+
| -------- | ----------------------------- |
|
|
174
|
+
| `close` | X button or cancel clicked |
|
|
175
|
+
| `back` | Back arrow clicked |
|
|
176
|
+
| `action` | Primary action button clicked |
|
|
177
|
+
|
|
178
|
+
#### Slots
|
|
179
|
+
|
|
180
|
+
| Slot | Description |
|
|
181
|
+
| --------- | ----------------------------------- |
|
|
182
|
+
| `default` | Dialog body content |
|
|
183
|
+
| `footer` | Override the default footer buttons |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### HttpqlInput
|
|
188
|
+
|
|
189
|
+
HTTPQL query input with syntax highlighting and context-aware autocomplete.
|
|
190
|
+
|
|
191
|
+
```vue
|
|
192
|
+
<HttpqlInput
|
|
193
|
+
v-model="query"
|
|
194
|
+
:loading="isSearching"
|
|
195
|
+
placeholder="Filter requests..."
|
|
196
|
+
@submit="onSearch"
|
|
197
|
+
/>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Props
|
|
201
|
+
|
|
202
|
+
| Name | Type | Default | Description |
|
|
203
|
+
| ------------- | --------- | ---------------------------- | ----------------- |
|
|
204
|
+
| `modelValue` | `string` | required | Query string |
|
|
205
|
+
| `placeholder` | `string` | `"Enter an HTTPQL query..."` | Placeholder text |
|
|
206
|
+
| `loading` | `boolean` | `false` | Show spinner icon |
|
|
207
|
+
|
|
208
|
+
#### Emits
|
|
209
|
+
|
|
210
|
+
| Event | Description |
|
|
211
|
+
| ------------------- | ---------------------------------------------- |
|
|
212
|
+
| `update:modelValue` | Query text changed |
|
|
213
|
+
| `submit` | Enter pressed (when no suggestion is selected) |
|
|
214
|
+
|
|
215
|
+
#### Autocomplete
|
|
216
|
+
|
|
217
|
+
The suggestion engine provides context-aware completions:
|
|
218
|
+
|
|
219
|
+
| Context | Suggestions |
|
|
220
|
+
| ------------------------------------ | ------------------------------------------------- |
|
|
221
|
+
| Empty input / after logical operator | Namespaces: `req`, `resp`, `row`, `filter` |
|
|
222
|
+
| After namespace (e.g. `req.`) | Fields for that namespace |
|
|
223
|
+
| After field (e.g. `req.method.`) | Operators based on field type |
|
|
224
|
+
| After `filter:` | `inscope`, `recent`, `1hr`, `6hr`, `12hr`, `24hr` |
|
|
225
|
+
| After a completed value | `AND`, `OR` |
|
|
226
|
+
|
|
227
|
+
Syntax uses dot/colon notation: `req.method.eq:"GET"`
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### Menu
|
|
232
|
+
|
|
233
|
+
Popup tiered menu with dark styling.
|
|
234
|
+
|
|
235
|
+
```vue
|
|
236
|
+
<Menu ref="menuRef" :items="menuItems" />
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Props
|
|
240
|
+
|
|
241
|
+
| Name | Type | Default | Description |
|
|
242
|
+
| ------- | ------------ | -------- | ---------------------------------------------- |
|
|
243
|
+
| `items` | `MenuItem[]` | required | PrimeVue menu items (supports nested submenus) |
|
|
244
|
+
|
|
245
|
+
#### Exposed Methods
|
|
246
|
+
|
|
247
|
+
| Method | Signature | Description |
|
|
248
|
+
| -------- | ------------------------ | -------------------- |
|
|
249
|
+
| `show` | `(event: Event) => void` | Show at event target |
|
|
250
|
+
| `hide` | `() => void` | Hide menu |
|
|
251
|
+
| `toggle` | `(event: Event) => void` | Toggle visibility |
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### MenuButton
|
|
256
|
+
|
|
257
|
+
Button that opens a popup menu on click.
|
|
258
|
+
|
|
259
|
+
```vue
|
|
260
|
+
<MenuButton :items="menuItems" label="Actions" icon="fas fa-ellipsis-v" />
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Props
|
|
264
|
+
|
|
265
|
+
| Name | Type | Default | Description |
|
|
266
|
+
| ---------- | -------------------- | ------------ | ------------------------ |
|
|
267
|
+
| `items` | `MenuItem[]` | required | Menu items |
|
|
268
|
+
| `label` | `string` | - | Button label |
|
|
269
|
+
| `icon` | `string` | - | Font Awesome icon class |
|
|
270
|
+
| `severity` | `string` | `"contrast"` | PrimeVue button severity |
|
|
271
|
+
| `size` | `"small" \| "large"` | `"small"` | Button size |
|
|
272
|
+
| `disabled` | `boolean` | `false` | Disable button |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### MultiSelect
|
|
277
|
+
|
|
278
|
+
Compact multi-select dropdown with chip display.
|
|
279
|
+
|
|
280
|
+
```vue
|
|
281
|
+
<MultiSelect
|
|
282
|
+
v-model="selectedProfiles"
|
|
283
|
+
:options="profiles"
|
|
284
|
+
option-label="name"
|
|
285
|
+
option-value="id"
|
|
286
|
+
placeholder="Select profiles..."
|
|
287
|
+
/>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### Props
|
|
291
|
+
|
|
292
|
+
| Name | Type | Default | Description |
|
|
293
|
+
| --------------- | ------------------- | ------------- | -------------------------------- |
|
|
294
|
+
| `modelValue` | `unknown[]` | required | Selected values |
|
|
295
|
+
| `options` | `unknown[]` | required | Available options |
|
|
296
|
+
| `optionLabel` | `string` | `"label"` | Property name for display text |
|
|
297
|
+
| `optionValue` | `string` | `"value"` | Property name for value |
|
|
298
|
+
| `placeholder` | `string` | `"Select..."` | Placeholder text |
|
|
299
|
+
| `display` | `"comma" \| "chip"` | `"chip"` | How selected items are displayed |
|
|
300
|
+
| `filter` | `boolean` | `false` | Show search input |
|
|
301
|
+
| `showToggleAll` | `boolean` | `false` | Show select all checkbox |
|
|
302
|
+
| `disabled` | `boolean` | `false` | Disable component |
|
|
303
|
+
|
|
304
|
+
Additional PrimeVue MultiSelect props are forwarded via `$attrs`.
|
|
305
|
+
|
|
306
|
+
#### Emits
|
|
307
|
+
|
|
308
|
+
| Event | Payload | Description |
|
|
309
|
+
| ------------------- | ----------- | ----------------- |
|
|
310
|
+
| `update:modelValue` | `unknown[]` | Selection changed |
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
### RequestEditor
|
|
315
|
+
|
|
316
|
+
Caido's native HTTP request editor.
|
|
317
|
+
|
|
318
|
+
```vue
|
|
319
|
+
<RequestEditor :sdk="sdk" :content="rawRequest" />
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Props
|
|
323
|
+
|
|
324
|
+
| Name | Type | Default | Description |
|
|
325
|
+
| -------------- | --------------------- | ------------------------ | --------------------------------- |
|
|
326
|
+
| `sdk` | `any` | required | Caido frontend SDK instance |
|
|
327
|
+
| `content` | `string \| undefined` | required | Raw HTTP request text |
|
|
328
|
+
| `emptyMessage` | `string` | `"No request available"` | Message when content is undefined |
|
|
329
|
+
|
|
330
|
+
Use `request.getRaw().toText()` for the `content` prop.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### ResponseEditor
|
|
335
|
+
|
|
336
|
+
Caido's native HTTP response editor.
|
|
337
|
+
|
|
338
|
+
```vue
|
|
339
|
+
<ResponseEditor :sdk="sdk" :content="rawResponse" />
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### Props
|
|
343
|
+
|
|
344
|
+
| Name | Type | Default | Description |
|
|
345
|
+
| -------------- | --------------------- | ------------------------- | --------------------------------- |
|
|
346
|
+
| `sdk` | `any` | required | Caido frontend SDK instance |
|
|
347
|
+
| `content` | `string \| undefined` | required | Raw HTTP response text |
|
|
348
|
+
| `emptyMessage` | `string` | `"No response available"` | Message when content is undefined |
|
|
349
|
+
|
|
350
|
+
Use `response.getRaw().toText()` for the `content` prop.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### Table
|
|
355
|
+
|
|
356
|
+
High-performance sortable table with virtual scrolling (auto-enabled at 100+ rows), selection, context menu, and scroll position memory.
|
|
357
|
+
|
|
358
|
+
```vue
|
|
359
|
+
<Table
|
|
360
|
+
:items="rows"
|
|
361
|
+
:selected="selected"
|
|
362
|
+
:columns="columns"
|
|
363
|
+
:row-key="(r) => r.id"
|
|
364
|
+
:active-key="activeId"
|
|
365
|
+
selectable
|
|
366
|
+
scroll-key="my-table"
|
|
367
|
+
:context-menu="menuItems"
|
|
368
|
+
@update:selected="selected = $event"
|
|
369
|
+
@row-click="onRowClick"
|
|
370
|
+
>
|
|
371
|
+
<template #cell-status="{ value }">
|
|
372
|
+
<span :class="value === '200' ? 'text-green-400' : 'text-red-400'">{{ value }}</span>
|
|
373
|
+
</template>
|
|
374
|
+
</Table>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### Props
|
|
378
|
+
|
|
379
|
+
| Name | Type | Default | Description |
|
|
380
|
+
| -------------- | --------------------- | ----------- | ----------------------------------- |
|
|
381
|
+
| `items` | `T[]` | required | Row data |
|
|
382
|
+
| `selected` | `T[]` | required | Selected items |
|
|
383
|
+
| `columns` | `Column[]` | required | Column definitions (see below) |
|
|
384
|
+
| `rowKey` | `(item: T) => string` | required | Unique key extractor |
|
|
385
|
+
| `activeKey` | `string` | - | Row key of the highlighted row |
|
|
386
|
+
| `emptyMessage` | `string` | `"No data"` | Message when empty |
|
|
387
|
+
| `selectable` | `boolean` | `false` | Show checkbox column |
|
|
388
|
+
| `scrollKey` | `string` | - | Key for scroll position persistence |
|
|
389
|
+
| `contextMenu` | `MenuItem[]` | - | Right-click menu items |
|
|
390
|
+
| `resizable` | `boolean` | `false` | Enable column resizing |
|
|
391
|
+
|
|
392
|
+
#### Column Definition
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
type Column = {
|
|
396
|
+
key: string; // property name on row object
|
|
397
|
+
header: string; // column header text
|
|
398
|
+
width: string; // CSS width ("150px", "1fr", "auto")
|
|
399
|
+
sortable?: boolean;
|
|
400
|
+
sortType?: "string" | "number" | "date"; // default: "string"
|
|
401
|
+
};
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
#### Emits
|
|
405
|
+
|
|
406
|
+
| Event | Payload | Description |
|
|
407
|
+
| ----------------- | ----------------- | ----------------- |
|
|
408
|
+
| `update:selected` | `T[]` | Selection changed |
|
|
409
|
+
| `row-click` | `T` | Row clicked |
|
|
410
|
+
| `row-contextmenu` | `[T, MouseEvent]` | Row right-clicked |
|
|
411
|
+
|
|
412
|
+
#### Slots
|
|
413
|
+
|
|
414
|
+
| Slot | Scope | Description |
|
|
415
|
+
| ------------ | ----------------------------- | ------------------------------- |
|
|
416
|
+
| `cell-{key}` | `{ item: T, value: unknown }` | Custom cell renderer per column |
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { nextTick, ref, watch } from "vue";
|
|
2
|
+
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
3
3
|
|
|
4
|
+
import ButtonGroup from "../ButtonGroup/Container.vue";
|
|
4
5
|
import Card from "../Card/Container.vue";
|
|
6
|
+
import { formatHttpBody } from "../editors/format";
|
|
5
7
|
|
|
6
8
|
const props = withDefaults(
|
|
7
9
|
defineProps<{
|
|
@@ -14,54 +16,139 @@ const props = withDefaults(
|
|
|
14
16
|
},
|
|
15
17
|
);
|
|
16
18
|
|
|
19
|
+
const viewMode = ref("pretty");
|
|
20
|
+
const viewOptions = [
|
|
21
|
+
{ label: "Pretty", value: "pretty" },
|
|
22
|
+
{ label: "Raw", value: "raw" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const cardRef = ref<HTMLDivElement>();
|
|
17
26
|
const container = ref<HTMLDivElement>();
|
|
18
27
|
let editor: HTTPRequestEditor | undefined;
|
|
28
|
+
let intersectionObserver: IntersectionObserver | undefined;
|
|
29
|
+
let wasHidden = false;
|
|
19
30
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
if (
|
|
31
|
+
function injectStyles(el: HTMLElement) {
|
|
32
|
+
const shadow = el.shadowRoot;
|
|
33
|
+
if (!shadow) return;
|
|
34
|
+
const style = document.createElement("style");
|
|
35
|
+
style.textContent = `.c-card__header, .c-card__footer { display: none !important; }`;
|
|
36
|
+
shadow.appendChild(style);
|
|
37
|
+
}
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
const
|
|
39
|
+
function createEditor(): HTTPRequestEditor {
|
|
40
|
+
const instance = props.sdk.ui.httpRequestEditor();
|
|
41
|
+
const el = instance.getElement();
|
|
26
42
|
el.style.height = "100%";
|
|
27
43
|
el.style.width = "100%";
|
|
28
|
-
|
|
44
|
+
injectStyles(el);
|
|
45
|
+
return instance;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mountEditor() {
|
|
49
|
+
if (!container.value) return;
|
|
50
|
+
if (!editor) {
|
|
51
|
+
editor = createEditor();
|
|
52
|
+
}
|
|
53
|
+
const el = editor.getElement();
|
|
54
|
+
if (el.parentElement !== container.value) {
|
|
55
|
+
container.value.appendChild(el);
|
|
56
|
+
}
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
function updateEditor(content: string) {
|
|
32
|
-
if (editor
|
|
60
|
+
if (!editor) return;
|
|
33
61
|
const view = editor.getEditorView();
|
|
34
|
-
if (view
|
|
62
|
+
if (!view) return;
|
|
35
63
|
view.dispatch({
|
|
36
64
|
changes: { from: 0, to: view.state.doc.length, insert: content },
|
|
37
65
|
});
|
|
38
66
|
}
|
|
39
67
|
|
|
68
|
+
async function setContent(content: string) {
|
|
69
|
+
mountEditor();
|
|
70
|
+
if (viewMode.value === "pretty") {
|
|
71
|
+
const formatted = await formatHttpBody(content);
|
|
72
|
+
updateEditor(formatted);
|
|
73
|
+
} else {
|
|
74
|
+
updateEditor(content);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onVisibilityChange(entries: IntersectionObserverEntry[]) {
|
|
79
|
+
const entry = entries[0];
|
|
80
|
+
if (!entry) return;
|
|
81
|
+
|
|
82
|
+
if (!entry.isIntersecting) {
|
|
83
|
+
wasHidden = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (wasHidden && props.content !== undefined) {
|
|
88
|
+
wasHidden = false;
|
|
89
|
+
nextTick(() => setContent(props.content!));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onMounted(async () => {
|
|
94
|
+
await nextTick();
|
|
95
|
+
mountEditor();
|
|
96
|
+
if (props.content !== undefined) {
|
|
97
|
+
await setContent(props.content);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (cardRef.value) {
|
|
101
|
+
intersectionObserver = new IntersectionObserver(onVisibilityChange);
|
|
102
|
+
intersectionObserver.observe(cardRef.value);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
onBeforeUnmount(() => {
|
|
107
|
+
intersectionObserver?.disconnect();
|
|
108
|
+
if (editor) {
|
|
109
|
+
editor.getElement().remove();
|
|
110
|
+
editor = undefined;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
40
114
|
watch(
|
|
41
115
|
() => props.content,
|
|
42
116
|
async (content) => {
|
|
43
117
|
if (content === undefined) return;
|
|
44
118
|
await nextTick();
|
|
45
|
-
|
|
46
|
-
updateEditor(content);
|
|
119
|
+
await setContent(content);
|
|
47
120
|
},
|
|
48
121
|
);
|
|
122
|
+
|
|
123
|
+
watch(viewMode, async () => {
|
|
124
|
+
if (props.content !== undefined) {
|
|
125
|
+
await setContent(props.content);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
49
128
|
</script>
|
|
50
129
|
|
|
51
130
|
<template>
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
131
|
+
<div ref="cardRef" class="h-full">
|
|
132
|
+
<Card class="h-full">
|
|
133
|
+
<template #content>
|
|
134
|
+
<div
|
|
135
|
+
class="flex items-center justify-between px-3 py-1.5 border-b border-surface-700 shrink-0"
|
|
136
|
+
>
|
|
137
|
+
<span class="text-xs text-surface-400 font-medium">Request</span>
|
|
138
|
+
<ButtonGroup v-model="viewMode" :options="viewOptions" size="small" />
|
|
139
|
+
</div>
|
|
140
|
+
<div
|
|
141
|
+
v-show="content === undefined"
|
|
142
|
+
class="h-full flex items-center justify-center text-surface-500 text-xs"
|
|
143
|
+
>
|
|
144
|
+
{{ emptyMessage }}
|
|
145
|
+
</div>
|
|
146
|
+
<div
|
|
147
|
+
v-show="content !== undefined"
|
|
148
|
+
ref="container"
|
|
149
|
+
class="h-full w-full overflow-hidden"
|
|
150
|
+
/>
|
|
151
|
+
</template>
|
|
152
|
+
</Card>
|
|
153
|
+
</div>
|
|
67
154
|
</template>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { nextTick, ref, watch } from "vue";
|
|
2
|
+
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
3
3
|
|
|
4
|
+
import ButtonGroup from "../ButtonGroup/Container.vue";
|
|
4
5
|
import Card from "../Card/Container.vue";
|
|
6
|
+
import { formatHttpBody } from "../editors/format";
|
|
5
7
|
|
|
6
8
|
const props = withDefaults(
|
|
7
9
|
defineProps<{
|
|
8
10
|
sdk: any;
|
|
9
11
|
content: string | undefined;
|
|
12
|
+
render?: string;
|
|
10
13
|
emptyMessage?: string;
|
|
11
14
|
}>(),
|
|
12
15
|
{
|
|
@@ -14,54 +17,154 @@ const props = withDefaults(
|
|
|
14
17
|
},
|
|
15
18
|
);
|
|
16
19
|
|
|
20
|
+
const viewMode = ref("pretty");
|
|
21
|
+
const viewOptions = [
|
|
22
|
+
{ label: "Pretty", value: "pretty" },
|
|
23
|
+
{ label: "Raw", value: "raw" },
|
|
24
|
+
{ label: "Preview", value: "preview" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const cardRef = ref<HTMLDivElement>();
|
|
17
28
|
const container = ref<HTMLDivElement>();
|
|
18
29
|
let editor: HTTPResponseEditor | undefined;
|
|
30
|
+
let intersectionObserver: IntersectionObserver | undefined;
|
|
31
|
+
let wasHidden = false;
|
|
19
32
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
if (
|
|
33
|
+
function injectStyles(el: HTMLElement) {
|
|
34
|
+
const shadow = el.shadowRoot;
|
|
35
|
+
if (!shadow) return;
|
|
36
|
+
const style = document.createElement("style");
|
|
37
|
+
style.textContent = `.c-card__header, .c-card__footer { display: none !important; }`;
|
|
38
|
+
shadow.appendChild(style);
|
|
39
|
+
}
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
const
|
|
41
|
+
function createEditor(): HTTPResponseEditor {
|
|
42
|
+
const instance = props.sdk.ui.httpResponseEditor();
|
|
43
|
+
const el = instance.getElement();
|
|
26
44
|
el.style.height = "100%";
|
|
27
45
|
el.style.width = "100%";
|
|
28
|
-
|
|
46
|
+
injectStyles(el);
|
|
47
|
+
return instance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mountEditor() {
|
|
51
|
+
if (!container.value) return;
|
|
52
|
+
if (!editor) {
|
|
53
|
+
editor = createEditor();
|
|
54
|
+
}
|
|
55
|
+
const el = editor.getElement();
|
|
56
|
+
if (el.parentElement !== container.value) {
|
|
57
|
+
container.value.appendChild(el);
|
|
58
|
+
}
|
|
29
59
|
}
|
|
30
60
|
|
|
31
61
|
function updateEditor(content: string) {
|
|
32
|
-
if (editor
|
|
62
|
+
if (!editor) return;
|
|
33
63
|
const view = editor.getEditorView();
|
|
34
|
-
if (view
|
|
64
|
+
if (!view) return;
|
|
35
65
|
view.dispatch({
|
|
36
66
|
changes: { from: 0, to: view.state.doc.length, insert: content },
|
|
37
67
|
});
|
|
38
68
|
}
|
|
39
69
|
|
|
70
|
+
async function setContent(content: string) {
|
|
71
|
+
mountEditor();
|
|
72
|
+
if (viewMode.value === "pretty") {
|
|
73
|
+
const formatted = await formatHttpBody(content);
|
|
74
|
+
updateEditor(formatted);
|
|
75
|
+
} else {
|
|
76
|
+
updateEditor(content);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onVisibilityChange(entries: IntersectionObserverEntry[]) {
|
|
81
|
+
const entry = entries[0];
|
|
82
|
+
if (!entry) return;
|
|
83
|
+
|
|
84
|
+
if (!entry.isIntersecting) {
|
|
85
|
+
wasHidden = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (wasHidden && props.content !== undefined) {
|
|
90
|
+
wasHidden = false;
|
|
91
|
+
nextTick(() => setContent(props.content!));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onMounted(async () => {
|
|
96
|
+
await nextTick();
|
|
97
|
+
mountEditor();
|
|
98
|
+
if (props.content !== undefined) {
|
|
99
|
+
await setContent(props.content);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (cardRef.value) {
|
|
103
|
+
intersectionObserver = new IntersectionObserver(onVisibilityChange);
|
|
104
|
+
intersectionObserver.observe(cardRef.value);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
onBeforeUnmount(() => {
|
|
109
|
+
intersectionObserver?.disconnect();
|
|
110
|
+
if (editor) {
|
|
111
|
+
editor.getElement().remove();
|
|
112
|
+
editor = undefined;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
40
116
|
watch(
|
|
41
117
|
() => props.content,
|
|
42
118
|
async (content) => {
|
|
43
119
|
if (content === undefined) return;
|
|
44
120
|
await nextTick();
|
|
45
|
-
|
|
46
|
-
updateEditor(content);
|
|
121
|
+
await setContent(content);
|
|
47
122
|
},
|
|
48
123
|
);
|
|
124
|
+
|
|
125
|
+
watch(viewMode, async () => {
|
|
126
|
+
if (props.content !== undefined) {
|
|
127
|
+
await setContent(props.content);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
49
130
|
</script>
|
|
50
131
|
|
|
51
132
|
<template>
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
133
|
+
<div ref="cardRef" class="h-full">
|
|
134
|
+
<Card class="h-full">
|
|
135
|
+
<template #content>
|
|
136
|
+
<div
|
|
137
|
+
class="flex items-center justify-between px-3 py-1.5 border-b border-surface-700 shrink-0"
|
|
138
|
+
>
|
|
139
|
+
<span class="text-xs text-surface-400 font-medium">Response</span>
|
|
140
|
+
<ButtonGroup v-model="viewMode" :options="viewOptions" size="small" />
|
|
141
|
+
</div>
|
|
142
|
+
<div
|
|
143
|
+
v-show="content === undefined && viewMode !== 'preview'"
|
|
144
|
+
class="h-full flex items-center justify-center text-surface-500 text-xs"
|
|
145
|
+
>
|
|
146
|
+
{{ emptyMessage }}
|
|
147
|
+
</div>
|
|
148
|
+
<div
|
|
149
|
+
v-show="content !== undefined && viewMode !== 'preview'"
|
|
150
|
+
ref="container"
|
|
151
|
+
class="h-full w-full overflow-hidden"
|
|
152
|
+
/>
|
|
153
|
+
<div v-show="viewMode === 'preview'" class="flex-1 min-h-0 w-full">
|
|
154
|
+
<iframe
|
|
155
|
+
v-if="render"
|
|
156
|
+
:srcdoc="render"
|
|
157
|
+
sandbox=""
|
|
158
|
+
class="h-full w-full border-0 bg-white"
|
|
159
|
+
/>
|
|
160
|
+
<div
|
|
161
|
+
v-else
|
|
162
|
+
class="h-full flex items-center justify-center text-surface-500 text-xs"
|
|
163
|
+
>
|
|
164
|
+
No preview available
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
</Card>
|
|
169
|
+
</div>
|
|
67
170
|
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatHttpBody(raw: string): Promise<string>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { prettify } from "@caido-utils/frontend";
|
|
2
|
+
function splitHttpMessage(raw) {
|
|
3
|
+
let idx = raw.indexOf("\r\n\r\n");
|
|
4
|
+
let sepLen = 4;
|
|
5
|
+
if (idx === -1) {
|
|
6
|
+
idx = raw.indexOf("\n\n");
|
|
7
|
+
sepLen = 2;
|
|
8
|
+
}
|
|
9
|
+
if (idx === -1) return null;
|
|
10
|
+
return {
|
|
11
|
+
headers: raw.substring(0, idx + sepLen),
|
|
12
|
+
body: raw.substring(idx + sepLen)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function extractContentType(headers) {
|
|
16
|
+
const match = headers.match(/^content-type:\s*([^\r\n]+)/im);
|
|
17
|
+
return match?.[1]?.trim();
|
|
18
|
+
}
|
|
19
|
+
export async function formatHttpBody(raw) {
|
|
20
|
+
const parts = splitHttpMessage(raw);
|
|
21
|
+
if (!parts || !parts.body.trim()) return raw;
|
|
22
|
+
const contentType = extractContentType(parts.headers);
|
|
23
|
+
const formatted = await prettify(parts.body, contentType);
|
|
24
|
+
return parts.headers + formatted;
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caido-utils/components",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "unbuild"
|
|
16
16
|
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@caido-utils/frontend": "^0.19.0"
|
|
19
|
+
},
|
|
17
20
|
"peerDependencies": {
|
|
18
21
|
"@caido/sdk-frontend": ">=0.54.0",
|
|
19
|
-
"@codemirror/view": ">=6.0.0",
|
|
20
22
|
"primevue": ">=4.0.0",
|
|
21
23
|
"vue": ">=3.4.0"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
24
26
|
"@caido/sdk-frontend": "0.54.2-beta.15",
|
|
25
|
-
"@codemirror/view": "6.39.6",
|
|
26
27
|
"primevue": "4.1.0",
|
|
27
28
|
"typescript": "^5.5.4",
|
|
28
29
|
"unbuild": "^3.6.1",
|