@quazardous/qdadm 2.31.0 → 2.33.0

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.
Files changed (41) hide show
  1. package/dist/styles/qdadm.css +1 -1
  2. package/package.json +8 -2
  3. package/src/components/display/EmailPreview.vue +227 -0
  4. package/src/components/display/EntityTimeline.vue +408 -0
  5. package/src/components/display/LogStream.vue +364 -0
  6. package/src/components/display/PayloadDiff.vue +236 -0
  7. package/src/components/display/PayloadViewer.vue +246 -0
  8. package/src/components/index.ts +8 -0
  9. package/src/components/layout/AppLayout.vue +31 -11
  10. package/src/components/layout/defaults/DefaultMenu.vue +23 -11
  11. package/src/components/tasks/TaskDialog.vue +95 -0
  12. package/src/components/tasks/TaskHost.vue +45 -0
  13. package/src/components/tasks/TaskTray.vue +54 -0
  14. package/src/composables/index.ts +11 -0
  15. package/src/composables/useListPage.filters.ts +26 -1
  16. package/src/composables/useLogStream.ts +264 -0
  17. package/src/composables/useNavigation.ts +39 -3
  18. package/src/email/index.ts +46 -0
  19. package/src/entity/storage/SdkStorage.ts +53 -1
  20. package/src/entity/storage/index.ts +2 -0
  21. package/src/i18n/defaults/core.en.generated.ts +85 -0
  22. package/src/i18n/defaults/core.en.yml +85 -0
  23. package/src/i18n/defaults/core.fr.generated.ts +85 -0
  24. package/src/i18n/defaults/core.fr.yml +85 -0
  25. package/src/index.ts +21 -0
  26. package/src/kernel/Kernel.vue.ts +9 -2
  27. package/src/kernel/KernelContext.types.ts +11 -1
  28. package/src/module/moduleRegistry.ts +17 -1
  29. package/src/styles/_email.scss +188 -0
  30. package/src/styles/_log-stream.scss +167 -0
  31. package/src/styles/_payload.scss +312 -0
  32. package/src/styles/_sidebar.scss +8 -0
  33. package/src/styles/_tasks.scss +212 -0
  34. package/src/styles/_timeline.scss +229 -0
  35. package/src/styles/index.scss +5 -0
  36. package/src/tasks/taskStore.ts +460 -0
  37. package/src/tasks/useTaskRunner.ts +52 -0
  38. package/src/utils/ansi.ts +127 -0
  39. package/src/utils/email.ts +270 -0
  40. package/src/utils/index.ts +32 -0
  41. package/src/utils/payload.ts +480 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * useLogStream - the line buffer behind LogStream (#4110)
3
+ *
4
+ * Reads a backend log as it is written, from one of four sources the app
5
+ * picks, and keeps the lines parsed once (ANSI styles, error flag) so the
6
+ * view only slices them.
7
+ *
8
+ * ```ts
9
+ * // Offsets: GET /tasks/42/log?from=<n> → { lines, next, done }
10
+ * useLogStream({ poll: (from) => api.get(`/tasks/42/log`, { params: { from } }).then((r) => r.data) })
11
+ *
12
+ * // Server-sent events, one line (or several, newline-separated) per message
13
+ * useLogStream({ sse: '/tasks/42/log/stream', endEvent: 'end' })
14
+ *
15
+ * // Anything that pushes: an SSEBridge event, a WebSocket…
16
+ * useLogStream({ subscribe: ({ push, end }) => bridge.onEvent('task:log', (p) => push(p.data.lines)) })
17
+ *
18
+ * // A finished log
19
+ * useLogStream({ lines: text.split('\n') })
20
+ * ```
21
+ */
22
+
23
+ import { shallowRef, ref, onScopeDispose, getCurrentScope, type Ref, type ShallowRef } from 'vue'
24
+ import { parseAnsiLine, stripAnsi, type AnsiSegment, type AnsiStyle } from '../utils/ansi'
25
+
26
+ export interface LogPollResult {
27
+ lines: string[]
28
+ /** Offset to ask for next. Defaults to the previous one plus the lines received. */
29
+ next?: number
30
+ /** True once the log is complete: polling stops. */
31
+ done?: boolean
32
+ }
33
+
34
+ export interface LogPushApi {
35
+ push: (lines: string | string[]) => void
36
+ end: () => void
37
+ fail: (error: unknown) => void
38
+ }
39
+
40
+ export type LogSource =
41
+ | { poll: (from: number) => Promise<LogPollResult>; intervalMs?: number; from?: number }
42
+ | {
43
+ sse: string | (() => EventSource)
44
+ /** Event carrying lines. Default `message`. */
45
+ event?: string
46
+ /** Event that closes the log. Default `end`. */
47
+ endEvent?: string
48
+ /** Turn a message's data into lines. Default: JSON `{ lines, done }` or plain text split on newlines. */
49
+ parse?: (data: string) => string[] | LogPollResult
50
+ }
51
+ | { subscribe: (api: LogPushApi) => (() => void) | void }
52
+ | { lines: string[] }
53
+
54
+ export interface LogLine {
55
+ /** 1-based number in the whole log, kept when old lines are dropped. */
56
+ n: number
57
+ raw: string
58
+ /** The line without ANSI escapes: what search, errors and download read. */
59
+ text: string
60
+ segments: AnsiSegment[]
61
+ error: boolean
62
+ }
63
+
64
+ export type LogStreamStatus = 'idle' | 'streaming' | 'done' | 'error'
65
+
66
+ export interface UseLogStreamOptions {
67
+ /** Lines matching it are flagged as errors. */
68
+ errorPattern?: RegExp | string | null
69
+ /** Oldest lines are dropped past this count. Default 100 000. */
70
+ maxLines?: number
71
+ /** Start reading at once. Default true. */
72
+ immediate?: boolean
73
+ }
74
+
75
+ export interface UseLogStreamReturn {
76
+ lines: ShallowRef<LogLine[]>
77
+ status: Ref<LogStreamStatus>
78
+ error: Ref<string | null>
79
+ /** Lines dropped from the start because of maxLines. */
80
+ dropped: Ref<number>
81
+ start: () => void
82
+ stop: () => void
83
+ /** Empty the buffer and read the source again from its start. */
84
+ restart: () => void
85
+ /** Add lines by hand (used by the push sources, open to the app). */
86
+ append: (lines: string | string[]) => void
87
+ }
88
+
89
+ const MAX_BACKOFF_MS = 30_000
90
+
91
+ function toRegExp(pattern: RegExp | string | null | undefined): RegExp | null {
92
+ if (!pattern) return null
93
+ if (typeof pattern === 'string') return new RegExp(pattern, 'i')
94
+ // A global or sticky regex keeps lastIndex between test() calls and skips lines.
95
+ return new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ''))
96
+ }
97
+
98
+ function splitLines(chunk: string | string[]): string[] {
99
+ const list = Array.isArray(chunk) ? chunk : [chunk]
100
+ return list.flatMap((l) => String(l).replace(/\r\n?/g, '\n').split('\n'))
101
+ }
102
+
103
+ function defaultParse(data: string): string[] | LogPollResult {
104
+ const trimmed = data.trimStart()
105
+ if (trimmed.startsWith('{')) {
106
+ try {
107
+ const parsed = JSON.parse(data)
108
+ if (parsed && Array.isArray(parsed.lines)) return parsed as LogPollResult
109
+ } catch {
110
+ // not JSON: a line that happens to start with a brace
111
+ }
112
+ }
113
+ return splitLines(data)
114
+ }
115
+
116
+ export function useLogStream(source: LogSource, options: UseLogStreamOptions = {}): UseLogStreamReturn {
117
+ const { maxLines = 100_000, immediate = true } = options
118
+ const errorRe = toRegExp(options.errorPattern)
119
+
120
+ const lines = shallowRef<LogLine[]>([])
121
+ const status = ref<LogStreamStatus>('idle')
122
+ const error = ref<string | null>(null)
123
+ const dropped = ref(0)
124
+
125
+ let ansiState: AnsiStyle = {}
126
+ let counter = 0
127
+ let teardown: (() => void) | null = null
128
+ // A restart must not let a poll answer from the previous run append.
129
+ let run = 0
130
+
131
+ function append(chunk: string | string[]): void {
132
+ const incoming = splitLines(chunk)
133
+ if (incoming.length === 0) return
134
+ const parsed: LogLine[] = incoming.map((raw) => {
135
+ const { segments, state } = parseAnsiLine(raw, ansiState)
136
+ ansiState = state
137
+ const text = stripAnsi(raw)
138
+ return { n: ++counter, raw, text, segments, error: errorRe ? errorRe.test(text) : false }
139
+ })
140
+ let next = lines.value.concat(parsed)
141
+ if (next.length > maxLines) {
142
+ dropped.value += next.length - maxLines
143
+ next = next.slice(next.length - maxLines)
144
+ }
145
+ lines.value = next
146
+ }
147
+
148
+ function finish(): void {
149
+ status.value = 'done'
150
+ teardown?.()
151
+ teardown = null
152
+ }
153
+
154
+ function fail(err: unknown): void {
155
+ error.value = err instanceof Error ? err.message : String(err)
156
+ status.value = 'error'
157
+ }
158
+
159
+ function startPoll(src: Extract<LogSource, { poll: unknown }>): void {
160
+ const interval = src.intervalMs ?? 1000
161
+ const current = run
162
+ let offset = src.from ?? 0
163
+ let delay = interval
164
+ let timer: ReturnType<typeof setTimeout> | null = null
165
+ let stopped = false
166
+
167
+ const tick = async () => {
168
+ if (stopped || current !== run) return
169
+ try {
170
+ const result = await src.poll(offset)
171
+ if (stopped || current !== run) return
172
+ const received = result?.lines ?? []
173
+ append(received)
174
+ offset = result?.next ?? offset + received.length
175
+ status.value = 'streaming'
176
+ error.value = null
177
+ delay = interval
178
+ if (result?.done) return finish()
179
+ } catch (err) {
180
+ if (stopped || current !== run) return
181
+ fail(err)
182
+ delay = Math.min(delay * 2, MAX_BACKOFF_MS)
183
+ }
184
+ timer = setTimeout(tick, delay)
185
+ }
186
+ teardown = () => {
187
+ stopped = true
188
+ if (timer) clearTimeout(timer)
189
+ }
190
+ void tick()
191
+ }
192
+
193
+ function startSse(src: Extract<LogSource, { sse: unknown }>): void {
194
+ const es = typeof src.sse === 'string' ? new EventSource(src.sse) : src.sse()
195
+ const parse = src.parse ?? defaultParse
196
+ const onLines = (event: Event) => {
197
+ const result = parse(String((event as MessageEvent).data ?? ''))
198
+ if (Array.isArray(result)) append(result)
199
+ else {
200
+ append(result.lines ?? [])
201
+ if (result.done) finish()
202
+ }
203
+ }
204
+ const onEnd = () => finish()
205
+ const onError = () => {
206
+ // EventSource reconnects by itself; only a closed source is a failure.
207
+ if (es.readyState === 2) fail('The log stream closed')
208
+ }
209
+ es.addEventListener(src.event ?? 'message', onLines)
210
+ es.addEventListener(src.endEvent ?? 'end', onEnd)
211
+ es.addEventListener('error', onError)
212
+ teardown = () => es.close()
213
+ }
214
+
215
+ function startSubscribe(src: Extract<LogSource, { subscribe: unknown }>): void {
216
+ const current = run
217
+ const guard = <A extends unknown[]>(fn: (...args: A) => void) => (...args: A) => {
218
+ if (current === run && status.value === 'streaming') fn(...args)
219
+ }
220
+ const unsubscribe = src.subscribe({ push: guard(append), end: guard(finish), fail: guard(fail) })
221
+ if (status.value === 'streaming' && current === run) {
222
+ teardown = () => unsubscribe?.()
223
+ } else {
224
+ unsubscribe?.()
225
+ }
226
+ }
227
+
228
+ function start(): void {
229
+ if (teardown || status.value === 'streaming' || status.value === 'done') return
230
+ run++
231
+ error.value = null
232
+ if ('lines' in source) {
233
+ append(source.lines)
234
+ status.value = 'done'
235
+ return
236
+ }
237
+ status.value = 'streaming'
238
+ if ('poll' in source) startPoll(source)
239
+ else if ('sse' in source) startSse(source)
240
+ else startSubscribe(source)
241
+ }
242
+
243
+ function stop(): void {
244
+ run++
245
+ teardown?.()
246
+ teardown = null
247
+ if (status.value === 'streaming') status.value = 'idle'
248
+ }
249
+
250
+ function restart(): void {
251
+ stop()
252
+ lines.value = []
253
+ dropped.value = 0
254
+ counter = 0
255
+ ansiState = {}
256
+ status.value = 'idle'
257
+ start()
258
+ }
259
+
260
+ if (getCurrentScope()) onScopeDispose(stop)
261
+ if (immediate) start()
262
+
263
+ return { lines, status, error, dropped, start, stop, restart, append }
264
+ }
@@ -26,7 +26,19 @@ type Orchestrator = OrchestratorLike
26
26
  */
27
27
  export interface NavItem {
28
28
  label: string
29
- route: string
29
+ route?: string
30
+ /**
31
+ * A link out of the app instead of a route (#3675) — exactly one of `route`
32
+ * and `href`. An absolute URL opens in a new tab unless `target` says otherwise.
33
+ */
34
+ href?: string
35
+ /** Where an `href` entry opens: default `_blank` for an absolute URL, same tab otherwise. */
36
+ target?: string
37
+ /** An `href` entry is shown only to whoever is granted this attribute. */
38
+ permission?: string
39
+ /** Set on `href` entries by `navSections`: opens outside the app in another tab. */
40
+ external?: boolean
41
+ rel?: string
30
42
  icon?: string
31
43
  entity?: string
32
44
  exact?: boolean
@@ -79,6 +91,8 @@ export function useNavigation(): UseNavigationReturn {
79
91
  const router = useRouter()
80
92
  const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
81
93
  const hooks = inject<HookRegistry | null>('qdadmHooks', null)
94
+ // null default: an app without security shows every link (#3675).
95
+ const securityChecker = inject<{ isGranted(attribute: string, subject?: unknown): boolean } | null>('qdadmSecurityChecker', null)
82
96
 
83
97
  // i18n integration — labels resolve through nav.sections.* / nav.routes.*
84
98
  const { i18n: kernelI18n, locale: i18nLocale } = useI18n()
@@ -96,13 +110,17 @@ export function useNavigation(): UseNavigationReturn {
96
110
  * Check if user can access a nav item: canList() when it opens a list route, canRead() otherwise
97
111
  */
98
112
  function canAccessNavItem(item: NavItem): boolean {
113
+ // A link out has no page to guard: only its own permission, if any (#3675).
114
+ if (item.href) {
115
+ return !item.permission || !securityChecker || securityChecker.isGranted(item.permission)
116
+ }
99
117
  if (!item.entity || !orchestrator) return true
100
118
  const manager = orchestrator.get(item.entity)
101
119
  if (!manager) return true
102
120
  // A menu entry is shown to whoever may do what its page does (#2497): a
103
121
  // ctx.crud() entry opens a list, so it follows `list`. A route that
104
122
  // declares no action, or is not registered, keeps `read`.
105
- if (routeAction(item.route) === 'list' && manager.canList) return manager.canList()
123
+ if (item.route && routeAction(item.route) === 'list' && manager.canList) return manager.canList()
106
124
  return manager.canRead()
107
125
  }
108
126
 
@@ -126,6 +144,21 @@ export function useNavigation(): UseNavigationReturn {
126
144
  }
127
145
  })
128
146
 
147
+ /**
148
+ * A link out of the app as the renderers draw it (#3675): an absolute URL
149
+ * opens in a new tab, without handing it this window.
150
+ */
151
+ function linkItem(item: NavItem): NavItem {
152
+ const absolute = /^([a-z][a-z\d+.-]*:|\/\/)/i.test(item.href!)
153
+ const target = item.target ?? (absolute ? '_blank' : undefined)
154
+ return {
155
+ ...item,
156
+ ...(target ? { target } : {}),
157
+ external: target === '_blank',
158
+ ...(target === '_blank' ? { rel: 'noopener noreferrer' } : {}),
159
+ }
160
+ }
161
+
129
162
  /**
130
163
  * Resolve a label through i18n with the given convention key, falling back
131
164
  * to the inline label when the bundle has no matching translation.
@@ -153,7 +186,7 @@ export function useNavigation(): UseNavigationReturn {
153
186
  title: resolveNavLabel(`nav.sections.${section.title}`, section.title),
154
187
  items: section.items
155
188
  .filter((item) => canAccessNavItem(item))
156
- .map((item) => ({
189
+ .map((item) => (item.href ? linkItem(item) : {
157
190
  ...item,
158
191
  label: resolveNavLabel(`nav.routes.${item.route}`, item.label),
159
192
  })),
@@ -166,6 +199,8 @@ export function useNavigation(): UseNavigationReturn {
166
199
  * Uses semantic breadcrumb only - no route segment deduction
167
200
  */
168
201
  function isNavActive(item: NavItem): boolean {
202
+ // A link out never stands for the page on screen (#3675).
203
+ if (item.href) return false
169
204
  // Exact match mode
170
205
  if (item.exact) {
171
206
  return route.name === item.route
@@ -199,6 +234,7 @@ export function useNavigation(): UseNavigationReturn {
199
234
  * Handle nav click - force navigation if on same route with query params
200
235
  */
201
236
  function handleNavClick(event: Event, item: NavItem): void {
237
+ if (item.href) return
202
238
  if (route.name === item.route && Object.keys(route.query).length > 0) {
203
239
  event.preventDefault()
204
240
  router.push({ name: item.route })
@@ -0,0 +1,46 @@
1
+ /**
2
+ * qdadm/email - raw MIME parsing for EmailPreview (#4113), through postal-mime
3
+ *
4
+ * Import from '@quazardous/qdadm/email' only if the app shows raw MIME
5
+ * messages (an .eml, a sink that returns the source). postal-mime is an
6
+ * optional peer dependency: install it to use this entry point. A parsed
7
+ * email ({ headers, html, text, attachments }) needs neither.
8
+ *
9
+ * Usage:
10
+ * import { parseMime } from '@quazardous/qdadm/email'
11
+ * <EmailPreview :raw="capture.source" :parser="parseMime" />
12
+ */
13
+ import PostalMime from 'postal-mime'
14
+ import type { EmailAddress, ParsedEmail } from '../utils/email'
15
+
16
+ type PostalAddress = { name: string; address?: string; group?: Array<{ name: string; address: string }> }
17
+
18
+ function flatten(list: PostalAddress[] | PostalAddress | undefined): EmailAddress[] {
19
+ const items = !list ? [] : Array.isArray(list) ? list : [list]
20
+ return items.flatMap((a): Array<{ name: string; address?: string }> => (a.group ? a.group : [a])).map((a) => ({ name: a.name || null, address: a.address ?? null }))
21
+ }
22
+
23
+ /** Parse a raw MIME message. The source is kept for the Source tab. */
24
+ export async function parseMime(raw: string | ArrayBuffer | Uint8Array): Promise<ParsedEmail> {
25
+ const email = await new PostalMime({ attachmentEncoding: 'base64' }).parse(raw)
26
+ return {
27
+ from: flatten(email.from as PostalAddress | undefined)[0] ?? null,
28
+ to: flatten(email.to as PostalAddress[] | undefined),
29
+ cc: flatten(email.cc as PostalAddress[] | undefined),
30
+ replyTo: flatten(email.replyTo as PostalAddress[] | undefined),
31
+ subject: email.subject ?? null,
32
+ date: email.date ?? null,
33
+ messageId: email.messageId ?? null,
34
+ headers: email.headers.map((h) => ({ key: h.originalKey ?? h.key, value: h.value })),
35
+ html: email.html ?? null,
36
+ text: email.text ?? null,
37
+ raw: typeof raw === 'string' ? raw : new TextDecoder().decode(raw),
38
+ attachments: email.attachments.map((a) => ({
39
+ filename: a.filename,
40
+ contentType: a.mimeType,
41
+ content: a.content as string,
42
+ contentId: a.contentId ?? null,
43
+ disposition: a.disposition,
44
+ })),
45
+ }
46
+ }
@@ -62,6 +62,29 @@ export interface SdkMethods<T extends EntityRecord = EntityRecord> {
62
62
  [key: string]: SdkMethodConfig<T> | undefined
63
63
  }
64
64
 
65
+ /** The options of a path request, as `EntityManager.request` passes them. */
66
+ export interface SdkPathRequestOptions {
67
+ data?: unknown
68
+ params?: Record<string, unknown>
69
+ headers?: Record<string, string>
70
+ }
71
+
72
+ /**
73
+ * How a path request (`'GET', 'distinct/status'`) reaches the backend (#4069).
74
+ *
75
+ * A generated SDK has no method for an arbitrary path, and no reliable way
76
+ * to tell its base URL or this entity's endpoint, so the app supplies it —
77
+ * usually through the SDK's own client.
78
+ */
79
+ export type SdkPathRequest = (
80
+ method: string,
81
+ path: string,
82
+ options: SdkPathRequestOptions,
83
+ sdk: SdkInstance
84
+ ) => Promise<unknown>
85
+
86
+ const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'])
87
+
65
88
  /**
66
89
  * SdkStorage options
67
90
  */
@@ -74,6 +97,8 @@ export interface SdkStorageOptions<T extends EntityRecord = EntityRecord> {
74
97
  transforms?: Partial<Record<string, SdkMethodTransforms<T>>>
75
98
  responseFormat?: SdkResponseFormat | null
76
99
  clientSidePagination?: boolean
100
+ /** Runs `request(method, path, options)` calls — `optionsEndpoint` filters among them. */
101
+ request?: SdkPathRequest | null
77
102
  }
78
103
 
79
104
  /**
@@ -101,6 +126,7 @@ export class SdkStorage<T extends EntityRecord = EntityRecord> extends IStorage<
101
126
  protected _transformResponse: SdkGlobalResponseTransform<T> | null
102
127
  protected _transforms: Partial<Record<string, SdkMethodTransforms<T>>>
103
128
  protected _responseFormat: SdkResponseFormat | null
129
+ protected _pathRequest: SdkPathRequest | null
104
130
 
105
131
  constructor(options: SdkStorageOptions<T> = {}) {
106
132
  super()
@@ -113,6 +139,7 @@ export class SdkStorage<T extends EntityRecord = EntityRecord> extends IStorage<
113
139
  transforms = {},
114
140
  responseFormat = null,
115
141
  clientSidePagination = false,
142
+ request = null,
116
143
  } = options
117
144
 
118
145
  this._sdk = sdk
@@ -123,6 +150,12 @@ export class SdkStorage<T extends EntityRecord = EntityRecord> extends IStorage<
123
150
  this._transforms = transforms
124
151
  this._responseFormat = responseFormat
125
152
  this.clientSidePagination = clientSidePagination
153
+ this._pathRequest = request
154
+ }
155
+
156
+ /** Whether `request(method, path)` can run, i.e. a `request` option was given. */
157
+ get supportsPathRequests(): boolean {
158
+ return this._pathRequest !== null
126
159
  }
127
160
 
128
161
  get sdk(): SdkInstance {
@@ -335,7 +368,26 @@ export class SdkStorage<T extends EntityRecord = EntityRecord> extends IStorage<
335
368
  await this._execute('delete', pathParams)
336
369
  }
337
370
 
338
- async request<R = unknown>(methodName: string, params: unknown = {}): Promise<R> {
371
+ /**
372
+ * Two calls share this name. `request('GET', 'distinct/status', options)` is
373
+ * the path request every storage answers, the one `EntityManager.request`
374
+ * makes; it goes through the `request` option. `request('getStats', params)`
375
+ * calls a configured method or an SDK method by name.
376
+ *
377
+ * Told apart by the verb: no SDK method is called `GET`, and before #4069
378
+ * the path form looked one up and failed with "SDK method 'GET' not found".
379
+ */
380
+ async request<R = unknown>(methodName: string, params: unknown = {}, options: SdkPathRequestOptions = {}): Promise<R> {
381
+ if (HTTP_METHODS.has(methodName) && typeof params === 'string') {
382
+ if (!this._pathRequest) {
383
+ throw new Error(
384
+ `SdkStorage cannot run ${methodName} '${params}': a generated SDK has no method for a path. ` +
385
+ `Pass a request option, e.g. request: (method, url, o, sdk) => sdk.client.request({ method, url, query: o.params, body: o.data })`
386
+ )
387
+ }
388
+ return (await this._pathRequest(methodName, params, options, this.sdk)) as R
389
+ }
390
+
339
391
  if (this._methods[methodName]) {
340
392
  return (await this._execute(methodName, params)) as R
341
393
  }
@@ -72,4 +72,6 @@ export type {
72
72
  SdkResponseFormat,
73
73
  SdkMethods,
74
74
  SdkStorageOptions,
75
+ SdkPathRequest,
76
+ SdkPathRequestOptions,
75
77
  } from './SdkStorage'
@@ -71,6 +71,91 @@ export default `core:
71
71
  notRegistered: not registered
72
72
  remove: Remove
73
73
  via: "via {origin}"
74
+ # LogStream (#4110)
75
+ log:
76
+ streaming: Streaming
77
+ done: Complete
78
+ stopped: Stopped
79
+ failed: "Failed: {error}"
80
+ lines: "{count} lines"
81
+ search: Search the log
82
+ previous: Previous match
83
+ next: Next match
84
+ onlyMatches: Only matching lines
85
+ errors: "{count} errors"
86
+ firstError: Go to the next error line
87
+ wrap: Wrap long lines
88
+ follow: Follow the end of the log
89
+ download: Download the log
90
+ newLines: "{count} new lines, resume"
91
+ empty: No output yet
92
+ # EntityTimeline (#4111)
93
+ timeline:
94
+ actors: Filter by actor
95
+ loading: Loading…
96
+ empty: Nothing happened yet
97
+ newEvents: "{count} new events"
98
+ oldestFirst: Oldest first
99
+ newestFirst: Newest first
100
+ # PayloadViewer / PayloadDiff (#4112)
101
+ payload:
102
+ headers: "Headers ({count})"
103
+ headerRow: First row is a header
104
+ copy: Copy
105
+ copied: Copied to the clipboard
106
+ copyFailed: Could not copy
107
+ download: Download
108
+ showAll: "Show all {count} rows"
109
+ binary: "Binary content, {size}. First bytes:"
110
+ malformed: "Not valid {format} ({error}), shown as text"
111
+ expected: Expected
112
+ captured: Captured
113
+ identical: Identical
114
+ unchanged: "{count} unchanged lines"
115
+ showSame: "Show {count} unchanged rows"
116
+ inline: Inline
117
+ split: Side by side
118
+ # EmailPreview (#4113)
119
+ email:
120
+ from: From
121
+ to: To
122
+ cc: Cc
123
+ replyTo: Reply to
124
+ date: Date
125
+ noSubject: (no subject)
126
+ allHeaders: "All headers ({count})"
127
+ noParser: "A raw message needs a parser: pass parseMime from @quazardous/qdadm/email. Showing the source."
128
+ tab:
129
+ html: HTML
130
+ text: Text
131
+ source: Source
132
+ imagesBlocked: "{count} remote images blocked"
133
+ loadImages: Load images
134
+ blockImages: Block images
135
+ empty: This email has no body
136
+ unnamed: (unnamed)
137
+ inline: inline
138
+ download: Download
139
+ # TaskRunner (#4114)
140
+ task:
141
+ status:
142
+ starting: Starting
143
+ queued: Queued
144
+ running: Running
145
+ succeeded: Done
146
+ failed: Failed
147
+ cancelled: Cancelled
148
+ cancel: Cancel the task
149
+ close: Close
150
+ background: Run in the background
151
+ open: Open the task
152
+ readError: "Could not read the task ({error}), still trying"
153
+ start: Start
154
+ dontStart: Do not start
155
+ typeToConfirm: "Type {value} to confirm"
156
+ tray: "{count} running tasks"
157
+ trayDone: Recent tasks
158
+ clear: Clear finished
74
159
  # The breadcrumb's View↔Edit toggle (useNavContext).
75
160
  breadcrumb:
76
161
  view: View
@@ -69,6 +69,91 @@ core:
69
69
  notRegistered: not registered
70
70
  remove: Remove
71
71
  via: "via {origin}"
72
+ # LogStream (#4110)
73
+ log:
74
+ streaming: Streaming
75
+ done: Complete
76
+ stopped: Stopped
77
+ failed: "Failed: {error}"
78
+ lines: "{count} lines"
79
+ search: Search the log
80
+ previous: Previous match
81
+ next: Next match
82
+ onlyMatches: Only matching lines
83
+ errors: "{count} errors"
84
+ firstError: Go to the next error line
85
+ wrap: Wrap long lines
86
+ follow: Follow the end of the log
87
+ download: Download the log
88
+ newLines: "{count} new lines, resume"
89
+ empty: No output yet
90
+ # EntityTimeline (#4111)
91
+ timeline:
92
+ actors: Filter by actor
93
+ loading: Loading…
94
+ empty: Nothing happened yet
95
+ newEvents: "{count} new events"
96
+ oldestFirst: Oldest first
97
+ newestFirst: Newest first
98
+ # PayloadViewer / PayloadDiff (#4112)
99
+ payload:
100
+ headers: "Headers ({count})"
101
+ headerRow: First row is a header
102
+ copy: Copy
103
+ copied: Copied to the clipboard
104
+ copyFailed: Could not copy
105
+ download: Download
106
+ showAll: "Show all {count} rows"
107
+ binary: "Binary content, {size}. First bytes:"
108
+ malformed: "Not valid {format} ({error}), shown as text"
109
+ expected: Expected
110
+ captured: Captured
111
+ identical: Identical
112
+ unchanged: "{count} unchanged lines"
113
+ showSame: "Show {count} unchanged rows"
114
+ inline: Inline
115
+ split: Side by side
116
+ # EmailPreview (#4113)
117
+ email:
118
+ from: From
119
+ to: To
120
+ cc: Cc
121
+ replyTo: Reply to
122
+ date: Date
123
+ noSubject: (no subject)
124
+ allHeaders: "All headers ({count})"
125
+ noParser: "A raw message needs a parser: pass parseMime from @quazardous/qdadm/email. Showing the source."
126
+ tab:
127
+ html: HTML
128
+ text: Text
129
+ source: Source
130
+ imagesBlocked: "{count} remote images blocked"
131
+ loadImages: Load images
132
+ blockImages: Block images
133
+ empty: This email has no body
134
+ unnamed: (unnamed)
135
+ inline: inline
136
+ download: Download
137
+ # TaskRunner (#4114)
138
+ task:
139
+ status:
140
+ starting: Starting
141
+ queued: Queued
142
+ running: Running
143
+ succeeded: Done
144
+ failed: Failed
145
+ cancelled: Cancelled
146
+ cancel: Cancel the task
147
+ close: Close
148
+ background: Run in the background
149
+ open: Open the task
150
+ readError: "Could not read the task ({error}), still trying"
151
+ start: Start
152
+ dontStart: Do not start
153
+ typeToConfirm: "Type {value} to confirm"
154
+ tray: "{count} running tasks"
155
+ trayDone: Recent tasks
156
+ clear: Clear finished
72
157
  # The breadcrumb's View↔Edit toggle (useNavContext).
73
158
  breadcrumb:
74
159
  view: View