@foxglove/extension 2.52.0 → 2.53.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxglove/extension",
3
- "version": "2.52.0",
3
+ "version": "2.53.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Foxglove Technologies",
@@ -5,6 +5,8 @@ import type {
5
5
  MessageEvent as BaseMessageEvent,
6
6
  PanelExtensionContext as BasePanelExtensionContext,
7
7
  RenderState as BaseRenderState,
8
+ Subscription as BaseSubscription,
9
+ Time,
8
10
  } from "./stable";
9
11
 
10
12
  /**
@@ -20,20 +22,35 @@ import type {
20
22
  export namespace Experimental {
21
23
  export type DataSourceId = string & { __brand: "DataSourceId" };
22
24
 
25
+ /**
26
+ * Requested start position for a source on the compare timeline.
27
+ * Manual values are in compare-time seconds and must be non-negative.
28
+ */
29
+ export type RequestedDataSourceTimeOffset = { mode: "auto" } | { mode: "manual"; offset: Time };
30
+
23
31
  export type DataSourceInfo = {
24
32
  /** ID of the data source (e.g. "A", "B", "C", ...) used to identify the data source. */
25
33
  id: DataSourceId;
26
34
  /** The name of the data source e.g. the name of the file or URL. */
27
35
  name: string;
36
+ /** User-requested compare-time start configuration. */
37
+ requestedTimeOffset: RequestedDataSourceTimeOffset;
38
+ /** Effective time offset applied to source timestamps. */
39
+ timeOffset: Time;
28
40
  };
29
41
 
30
42
  export type DataSourceMap = ReadonlyMap<DataSourceId, Immutable<DataSourceInfo>>;
31
43
 
32
- export type MessageEvent = BaseMessageEvent & {
44
+ export type MessageEvent<T = unknown> = BaseMessageEvent<T> & {
33
45
  /** The data source that the message stems from. */
34
46
  dataSourceId?: DataSourceId;
35
47
  };
36
48
 
49
+ export type Subscription = BaseSubscription & {
50
+ /** The data source that the subscription stems from. */
51
+ dataSourceId?: DataSourceId;
52
+ };
53
+
37
54
  /**
38
55
  * This type represents the arguments you pass to {@link ExtensionContext.registerDataLoader}
39
56
  * when you want to register a data loader.
@@ -287,11 +304,57 @@ export namespace Experimental {
287
304
  */
288
305
  export type PanelToolbarItem = PanelToolbarButton | PanelToolbarLink | PanelToolbarDivider;
289
306
 
307
+ /** A metadata record from an MCAP recording. @category Custom panels */
308
+ export type MetadataRecord = {
309
+ name: string;
310
+ metadata: Record<string, string>;
311
+ /**
312
+ * Which source this came from in multiplexed playback; `undefined` for single-source.
313
+ * In merge mode all records share the same `dataSourceId` (the default source's id) —
314
+ * use `dataSources` to detect compare mode.
315
+ */
316
+ dataSourceId?: DataSourceId;
317
+ };
318
+
319
+ /**
320
+ * Information about an attachment in an MCAP recording — name, size, and timing metadata,
321
+ * but not the bytes. Use `context.fetchAttachment(id)` to load the bytes on demand.
322
+ * @category Custom panels
323
+ */
324
+ export type AttachmentInfo = {
325
+ /** Session-scoped id; stable for the playback session but not persistent or globally unique. */
326
+ id: string;
327
+ name: string;
328
+ mediaType: string;
329
+ logTime: Time;
330
+ createTime: Time;
331
+ sizeBytes: number;
332
+ /**
333
+ * Which source this came from in multiplexed playback; `undefined` for single-source.
334
+ * In merge mode all attachments share the same `dataSourceId` (the default source's id),
335
+ * distinct from the attachment's own `id` above — use `dataSources` to detect compare mode.
336
+ */
337
+ dataSourceId?: DataSourceId;
338
+ };
339
+
290
340
  export type RenderState = BaseRenderState & {
291
341
  /**
292
342
  * Map of available individual data sources, keyed by their ID.
293
343
  */
294
344
  dataSources?: DataSourceMap;
345
+
346
+ /**
347
+ * MCAP metadata records from all active sources. In compare mode use `dataSourceId`
348
+ * to distinguish per-source records.
349
+ */
350
+ metadata?: MetadataRecord[];
351
+
352
+ /**
353
+ * MCAP attachment references from all active sources (name/size/timing only, no bytes).
354
+ * In compare mode use `dataSourceId` to distinguish per-source entries.
355
+ * Call `context.fetchAttachment(id)` to load bytes.
356
+ */
357
+ attachments?: AttachmentInfo[];
295
358
  };
296
359
 
297
360
  export interface PanelExtensionContext extends BasePanelExtensionContext {
@@ -300,21 +363,103 @@ export namespace Experimental {
300
363
  * this field changes.
301
364
  *
302
365
  * Use `context.watch` to indicate which fields in {@link RenderState} (e.g. `currentFrame`,
303
- * `currentTime`, `previewTime`, `parameters`, `topics`) should trigger panel re-renders when
304
- * their contained values change.
366
+ * `currentTime`, `previewTime`, `parameters`, `topics`, `metadata`, `attachments`) should
367
+ * trigger panel re-renders when their contained values change.
305
368
  *
306
369
  * ```ts
307
370
  * context.watch("topics");
308
371
  * context.watch("currentFrame");
309
372
  * context.watch("parameters");
310
373
  * context.watch("currentTime");
374
+ * context.watch("metadata");
375
+ * context.watch("attachments");
311
376
  * ```
312
377
  */
313
378
  watch(field: keyof RenderState): void;
314
379
 
380
+ /**
381
+ * Process render events for the panel. Each render event receives a render state and a done
382
+ * callback. This override surfaces the experimental {@link RenderState} fields
383
+ * (`dataSources`, `metadata`, `attachments`) so panels can read them after calling `watch`.
384
+ */
385
+ onRender?: (renderState: Immutable<RenderState>, done: () => void) => void;
386
+
387
+ /**
388
+ * Use `context.subscribe` to indicate the topics your panel wants to receive messages for. The
389
+ * messages are provided during render in {@link RenderState.currentFrame}.
390
+ *
391
+ * @remarks
392
+ *
393
+ * This method will update the current subscriptions to the new list of Subscriptions and
394
+ * unsubscribe from any previously subscribed topics no longer in the Subscription list. Passing
395
+ * an empty array will unsubscribe from all topics.
396
+ *
397
+ * ```ts
398
+ * context.subscribe([{ topic: "/some/topic" }, { topic: "/another/topic" }]);
399
+ * ```
400
+ *
401
+ * `context.subscribe([])` will unsubscribe from all topics, and is equivalent to
402
+ * `unsubscribeAll`.
403
+ *
404
+ * #### Range loading
405
+ *
406
+ * Most panels display data from the current frame; examples of built-in panels that display the
407
+ * current frame are 3D, Image, and Raw Message, however some panels can display data for multiple
408
+ * messages or even the entire dataset duration (Plot, Map, State Transitions).
409
+ *
410
+ * Subscriptions will provide only the messages for the current frame. If your panel would like to
411
+ * process all the available messages on a topic, use
412
+ * {@link @PanelExtensionContext.subscribeMessageRange | subscribeMessageRange} instead.
413
+ *
414
+ * > NOTE: Message range loading is done on a best-effort basis. If your range-loaded messages
415
+ * > exceed available memory limits for the browser or desktop app, then the data may not
416
+ * > represent the full dataset range. Range loading results in more data transfer and memory use
417
+ * > and is recommended only for panels which require access to the entire dataset.
418
+ *
419
+ * #### Message converters
420
+ *
421
+ * Message converters can convert messages from one schema to another – for example, a user might
422
+ * convert custom GPS message into
423
+ * [`foxglove.LocationFix`](https://docs.foxglove.dev/docs/visualization/message-schemas/location-fix)
424
+ * messages for visualization in the [Map
425
+ * panel](https://docs.foxglove.dev/docs/visualization/panels/map). Users may have one or more
426
+ * message converters registered.
427
+ *
428
+ * If your panel expects messages with specific schema names, you can leverage registered message
429
+ * converters to convert from one schema to another.
430
+ *
431
+ * Specify the `convertTo` option to enable message conversion on a topic. When conversion is
432
+ * enabled for a subscription, the {@link MessageEvent}s will contain `message` entries with the
433
+ * converted message rather than the original message on the topic. The original message is
434
+ * available in the `originalMessageEvent` field in the message event.
435
+ *
436
+ * ```ts
437
+ * context.subscribe([{ topic: "/some/topic", convertTo: "foxglove.LocationFix" }]);
438
+ * ```
439
+ *
440
+ * The {@link Topic.convertibleTo | convertibleTo} field within {@link RenderState.topics} will
441
+ * contain the names of schemas you can convert this topic into.
442
+ */
443
+ subscribe(subscriptions: Subscription[]): void;
444
+
445
+ /**
446
+ * @deprecated Use `subscribe` with an array of Subscription objects instead.
447
+ */
448
+ subscribe(topics: string[]): void;
449
+
315
450
  /**
316
451
  * Set the items in the panel toolbar.
317
452
  */
318
453
  setPanelToolbarItems: (items: Immutable<PanelToolbarItem[]>) => void;
454
+
455
+ /**
456
+ * Load raw bytes for an attachment by `id` (from `RenderState.attachments`). `undefined` when
457
+ * no attachments are available on the current recording — feature-detect with a truthy check.
458
+ *
459
+ * Concurrent calls for the same `id` are deduplicated to a single in-flight request, but
460
+ * resolved bytes are not cached: a subsequent call after the previous one settles will
461
+ * re-fetch. Panels that want to keep bytes across renders should memoize the result.
462
+ */
463
+ fetchAttachment?: (id: string) => Promise<Uint8Array>;
319
464
  }
320
465
  }
package/src/stable.ts CHANGED
@@ -770,6 +770,12 @@ export type PanelExtensionContext = {
770
770
  * case "perform-node-action":
771
771
  * // Handle user-defined actions for nodes in the settings tree
772
772
  * break;
773
+ * case "reorder-children":
774
+ * if (action.payload.path[0] === "rules") {
775
+ * // Move the child identified by action.payload.fromKey before or after
776
+ * // action.payload.toKey within the parent node at action.payload.path.
777
+ * }
778
+ * break;
773
779
  * case "update":
774
780
  * if (action.payload.path[0] === "general" && action.payload.path[1] === "title") {
775
781
  * // Read action.payload.value for the new panel title value
@@ -787,14 +793,20 @@ export type PanelExtensionContext = {
787
793
  *
788
794
  * #### `SettingsTreeAction`
789
795
  *
790
- * A {@link SettingsTreeAction} describes how the settings UI should update when a user interacts
791
- * with its fields.
796
+ * A {@link SettingsTreeAction} is a discriminated union describing how the settings UI should
797
+ * update when a user interacts with it.
792
798
  *
793
- * Each `SettingsTreeAction` has a `payload` with a `path` to the settings field to update (e.g.
794
- * `["general", "title"]`).
799
+ * All actions include a `payload.path` that identifies the target node or field.
795
800
  *
796
- * The `update` action corresponds to a user setting a new value for a field (e.g. "My new
797
- * title").
801
+ * - `update` corresponds to a user setting a new field value. Its path points to the field, for
802
+ * example `["general", "title"]`.
803
+ * - `perform-node-action` corresponds to a user invoking a custom node action from `actions`.
804
+ * - `reorder-children` corresponds to a user reordering the children of a node whose
805
+ * `childrenReorderable` property is enabled. Its path points to the parent node, and the payload
806
+ * identifies the moved child and destination child by key.
807
+ *
808
+ * This union is extended additively over time. To stay forward-compatible, action handlers should
809
+ * ignore unknown `action` values at runtime instead of throwing from a `default` branch.
798
810
  *
799
811
  * #### Special node properties
800
812
  *
@@ -807,6 +819,10 @@ export type PanelExtensionContext = {
807
819
  * editor will provide a button to toggle the visibility of the node and you will receive an
808
820
  * `update` action with `visibility` as the final element in the path.
809
821
  *
822
+ * If you set `childrenReorderable` on a node with at least two children, the settings editor lets
823
+ * users drag and drop that node's children and dispatches `reorder-children` actions when the
824
+ * user changes their order.
825
+ *
810
826
  * For an example of how to use these special properties, check out the [panel settings example
811
827
  * extension](https://github.com/foxglove/create-foxglove-extension/tree/main/examples/panel-settings).
812
828
  *
@@ -1635,6 +1651,12 @@ export type SettingsTreeNode = {
1635
1651
  */
1636
1652
  renamable?: boolean;
1637
1653
 
1654
+ /**
1655
+ * Set to `false` on a child of a `childrenReorderable` node to keep this node rendered in place
1656
+ * without drag-and-drop controls or default "Move up" / "Move down" action menu items.
1657
+ */
1658
+ reorderable?: boolean;
1659
+
1638
1660
  /**
1639
1661
  * Optional sort order to override natural object ordering. All nodes
1640
1662
  * with a sort order will be rendered before nodes all with no sort order.
@@ -1646,6 +1668,23 @@ export type SettingsTreeNode = {
1646
1668
  */
1647
1669
  order?: number | string;
1648
1670
 
1671
+ /**
1672
+ * When set to `true`, the renderer treats this node's reorderable children as a
1673
+ * user-reorderable list with drag-and-drop controls and default "Move up" / "Move down" action
1674
+ * menu items when at least two reorderable children are present. Both reorder methods dispatch
1675
+ * "reorder-children" actions whose `path` is the path of this parent node.
1676
+ *
1677
+ * When a visibility filter or tree-wide text filter hides any of this node's children,
1678
+ * drag-and-drop affordances are suppressed until the filter is cleared so that the user is never
1679
+ * dragging against a subset of the list. The action menu items remain available because they
1680
+ * resolve against sibling keys in the complete list.
1681
+ *
1682
+ * When this option is enabled, child render order is driven by the order of entries in
1683
+ * `children`. Explicit `order` values on children are ignored and should not be used for
1684
+ * reorderable children.
1685
+ */
1686
+ childrenReorderable?: boolean;
1687
+
1649
1688
  /**
1650
1689
  * An optional visibility status. If this is not undefined, the node
1651
1690
  * editor will display a visibility toggle button and send update actions
@@ -1684,6 +1723,25 @@ export type SettingsTreeAction =
1684
1723
  "input" | "value"
1685
1724
  >;
1686
1725
  }
1726
+ | {
1727
+ action: "reorder-children";
1728
+ payload: {
1729
+ /** Path to the parent node whose children are being reordered. */
1730
+ path: readonly string[];
1731
+ /**
1732
+ * Key of the child being moved. If this key no longer exists under `path` (for example
1733
+ * because the list changed since the drag started), handlers should ignore the action.
1734
+ */
1735
+ fromKey: string;
1736
+ /**
1737
+ * Key of the child currently occupying the destination slot. If this key no longer
1738
+ * exists under `path`, handlers should ignore the action.
1739
+ */
1740
+ toKey: string;
1741
+ /** Whether the moved child should land before or after `toKey`. */
1742
+ position: "before" | "after";
1743
+ };
1744
+ }
1687
1745
  | {
1688
1746
  action: "perform-node-action";
1689
1747
  payload: { id: string; path: readonly string[] };