@odigos/ui-kit 0.0.249 → 0.0.251

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 (140) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/docs/api-context.md +687 -0
  3. package/lib/chunks/connections-scope-CneIqjeg.js +1 -0
  4. package/lib/chunks/flow-CQi_KYfs.js +49 -0
  5. package/lib/chunks/helpers-K1tnyqhm.js +19 -0
  6. package/lib/chunks/{index-CRKgHzab.js → index-Dv0RvKAp.js} +8 -8
  7. package/lib/chunks/source-instrument-form-context-C79I_5cH.js +1 -0
  8. package/lib/chunks/{ui-components-DODwSdKu.js → ui-components-BKrLf2tx.js} +214 -214
  9. package/lib/chunks/use-odigos-api-CDHW7ekQ.js +5 -0
  10. package/lib/chunks/vendor-BFqT13Me.js +1 -0
  11. package/lib/components/_v2/segment/index.d.ts +4 -2
  12. package/lib/components/v2.js +1 -1
  13. package/lib/components.js +1 -1
  14. package/lib/constants/strings/index.d.ts +7 -0
  15. package/lib/constants.js +1 -1
  16. package/lib/containers/_v2/_drawers/add-action-drawer/index.d.ts +0 -2
  17. package/lib/containers/_v2/_drawers/add-destination-drawer/form-column/index.d.ts +1 -5
  18. package/lib/containers/_v2/_drawers/add-destination-drawer/index.d.ts +0 -6
  19. package/lib/containers/_v2/_drawers/add-destination-drawer/lists-filters/index.d.ts +2 -1
  20. package/lib/containers/_v2/_drawers/add-rule-drawer/index.d.ts +0 -2
  21. package/lib/containers/_v2/_drawers/add-source-drawer/index.d.ts +1 -4
  22. package/lib/containers/_v2/_drawers/edit-action-drawer/index.d.ts +0 -3
  23. package/lib/containers/_v2/_drawers/edit-config-drawer/index.d.ts +1 -3
  24. package/lib/containers/_v2/_drawers/edit-destination-drawer/index.d.ts +0 -5
  25. package/lib/containers/_v2/_drawers/edit-rule-drawer/index.d.ts +0 -3
  26. package/lib/containers/_v2/_drawers/edit-source-drawer/debug/index.d.ts +1 -2
  27. package/lib/containers/_v2/_drawers/edit-source-drawer/index.d.ts +3 -14
  28. package/lib/containers/_v2/_drawers/edit-source-drawer/overview/containers-card/index.d.ts +1 -2
  29. package/lib/containers/_v2/_drawers/edit-source-drawer/overview/index.d.ts +0 -2
  30. package/lib/containers/_v2/_drawers/edit-source-drawer/peers/index.d.ts +1 -6
  31. package/lib/containers/_v2/_drawers/edit-source-drawer/profiling/index.d.ts +1 -15
  32. package/lib/containers/_v2/_drawers/edit-source-drawer/sampling/index.d.ts +8 -0
  33. package/lib/containers/_v2/_drawers/edit-source-drawer/sampling/rule-row.d.ts +8 -0
  34. package/lib/containers/_v2/_drawers/edit-source-drawer/tabs.d.ts +1 -0
  35. package/lib/containers/_v2/_drawers/system-drawer/describe/index.d.ts +1 -8
  36. package/lib/containers/_v2/_drawers/system-drawer/diagnose/index.d.ts +1 -4
  37. package/lib/containers/_v2/_drawers/system-drawer/index.d.ts +1 -4
  38. package/lib/containers/_v2/_drawers/system-drawer/tokens/index.d.ts +1 -2
  39. package/lib/containers/_v2/_forms/add-destination-form/index.d.ts +1 -2
  40. package/lib/containers/_v2/_modals/add-data-stream-modal/index.d.ts +0 -3
  41. package/lib/containers/_v2/_modals/edit-data-stream-modal/index.d.ts +0 -2
  42. package/lib/containers/_v2/_modals/sampling-onboarding-modal/types.d.ts +1 -4
  43. package/lib/containers/_v2/central-connections/index.d.ts +1 -12
  44. package/lib/containers/_v2/onboarding/index.d.ts +0 -8
  45. package/lib/containers/_v2/overview/columns/column/head.d.ts +1 -1
  46. package/lib/containers/_v2/overview/columns/column/index.d.ts +2 -1
  47. package/lib/containers/_v2/overview/columns/index.d.ts +1 -8
  48. package/lib/containers/_v2/overview/data-stream-select/index.d.ts +1 -10
  49. package/lib/containers/_v2/overview/drawers/index.d.ts +2 -29
  50. package/lib/containers/_v2/overview/index.d.ts +2 -8
  51. package/lib/containers/_v2/overview/multi-source-control/index.d.ts +1 -5
  52. package/lib/containers/_v2/pipeline-collectors/index.d.ts +0 -7
  53. package/lib/containers/_v2/pipeline-collectors/pods-info/index.d.ts +1 -2
  54. package/lib/containers/_v2/pipeline-collectors/pods-info/pod-drawer/index.d.ts +1 -2
  55. package/lib/containers/_v2/sampling-rules/helpers.d.ts +2 -0
  56. package/lib/containers/_v2/sampling-rules/types.d.ts +2 -16
  57. package/lib/containers/_v2/settings/index.d.ts +0 -9
  58. package/lib/containers/service-map/index.d.ts +4 -2
  59. package/lib/containers/v2.js +197 -166
  60. package/lib/containers.js +7 -7
  61. package/lib/contexts/index.d.ts +1 -0
  62. package/lib/contexts/odigos-api/capabilities.d.ts +38 -0
  63. package/lib/contexts/odigos-api/client.d.ts +22 -0
  64. package/lib/contexts/odigos-api/connections-scope.d.ts +18 -0
  65. package/lib/contexts/odigos-api/hooks/use-actions-api.d.ts +26 -0
  66. package/lib/contexts/odigos-api/hooks/use-collectors-api.d.ts +25 -0
  67. package/lib/contexts/odigos-api/hooks/use-config-api.d.ts +24 -0
  68. package/lib/contexts/odigos-api/hooks/use-data-streams-api.d.ts +26 -0
  69. package/lib/contexts/odigos-api/hooks/use-describe-api.d.ts +33 -0
  70. package/lib/contexts/odigos-api/hooks/use-destinations-api.d.ts +50 -0
  71. package/lib/contexts/odigos-api/hooks/use-instrumentation-rules-api.d.ts +35 -0
  72. package/lib/contexts/odigos-api/hooks/use-k8s-manifest-api.d.ts +14 -0
  73. package/lib/contexts/odigos-api/hooks/use-metrics-api.d.ts +13 -0
  74. package/lib/contexts/odigos-api/hooks/use-namespace-api.d.ts +30 -0
  75. package/lib/contexts/odigos-api/hooks/use-profiling-api.d.ts +17 -0
  76. package/lib/contexts/odigos-api/hooks/use-sampling-api.d.ts +39 -0
  77. package/lib/contexts/odigos-api/hooks/use-service-map-api.d.ts +13 -0
  78. package/lib/contexts/odigos-api/hooks/use-snapshots-api.d.ts +13 -0
  79. package/lib/contexts/odigos-api/hooks/use-sources-api.d.ts +75 -0
  80. package/lib/contexts/odigos-api/hooks/use-tokens-api.d.ts +12 -0
  81. package/lib/contexts/odigos-api/index.d.ts +37 -0
  82. package/lib/contexts/odigos-api/internal/notify.d.ts +49 -0
  83. package/lib/contexts/odigos-api/internal/source-payloads.d.ts +53 -0
  84. package/lib/contexts/odigos-api/platform-helpers.d.ts +98 -0
  85. package/lib/contexts/odigos-api/provider.d.ts +28 -0
  86. package/lib/contexts/odigos-api/runner.d.ts +32 -0
  87. package/lib/contexts/odigos-api/types.d.ts +831 -0
  88. package/lib/contexts/odigos-api/use-api-for-connections.d.ts +43 -0
  89. package/lib/contexts/odigos-api/use-api-query.d.ts +130 -0
  90. package/lib/contexts/odigos-api/use-odigos-api.d.ts +103 -0
  91. package/lib/contexts/odigos-api.js +1 -0
  92. package/lib/contexts/odigos-context.d.ts +2 -2
  93. package/lib/contexts/storybook-context.d.ts +2 -2
  94. package/lib/contexts.js +1 -1
  95. package/lib/functions.js +1 -1
  96. package/lib/hooks.js +1 -1
  97. package/lib/icons/destinations/open-telemetry-logo/index.d.ts +2 -1
  98. package/lib/snippets/_v2/sampling-rule-form-sections/preset-with-custom-input.d.ts +1 -1
  99. package/lib/snippets/v2.js +1 -1
  100. package/lib/snippets.js +1 -49
  101. package/lib/store/index.d.ts +0 -1
  102. package/lib/store/useDrawerStore.d.ts +6 -1
  103. package/lib/store.js +1 -1
  104. package/lib/theme.js +1 -1
  105. package/lib/types/actions/index.d.ts +0 -5
  106. package/lib/types/common/index.d.ts +29 -4
  107. package/lib/types/destinations/index.d.ts +0 -10
  108. package/lib/types/index.d.ts +0 -1
  109. package/lib/types/instrumentation-rules/index.d.ts +0 -5
  110. package/lib/types/namespaces/index.d.ts +0 -5
  111. package/lib/types/pipeline-collectors/index.d.ts +0 -5
  112. package/lib/types/snapshot/index.d.ts +0 -2
  113. package/lib/types/sources/index.d.ts +0 -30
  114. package/lib/types/tokens/index.d.ts +0 -6
  115. package/lib/types/workload/index.d.ts +46 -22
  116. package/lib/types.js +1 -1
  117. package/lib/visuals.js +1 -1
  118. package/package.json +31 -22
  119. package/lib/chunks/helpers-IEFhGXc7.js +0 -19
  120. package/lib/chunks/source-instrument-form-context-DjIHPYQJ.js +0 -1
  121. package/lib/chunks/vendor-Be0cOokr.js +0 -1
  122. package/lib/mock-data/actions/index.d.ts +0 -2
  123. package/lib/mock-data/config/index.d.ts +0 -3
  124. package/lib/mock-data/connections/index.d.ts +0 -2
  125. package/lib/mock-data/describe/index.d.ts +0 -3
  126. package/lib/mock-data/destinations/index.d.ts +0 -4
  127. package/lib/mock-data/index.d.ts +0 -15
  128. package/lib/mock-data/instrumentation-instances/index.d.ts +0 -2
  129. package/lib/mock-data/instrumentation-rules/index.d.ts +0 -2
  130. package/lib/mock-data/namespaces/index.d.ts +0 -2
  131. package/lib/mock-data/pipeline-collectors/index.d.ts +0 -6
  132. package/lib/mock-data/profiling/index.d.ts +0 -8
  133. package/lib/mock-data/service-map/index.d.ts +0 -4
  134. package/lib/mock-data/snapshot/index.d.ts +0 -5
  135. package/lib/mock-data/sources/index.d.ts +0 -4
  136. package/lib/mock-data/sources/yamls.d.ts +0 -22
  137. package/lib/mock-data/tokens/index.d.ts +0 -2
  138. package/lib/mock-data/traces/index.d.ts +0 -2
  139. package/lib/store/useEntityStore.d.ts +0 -24
  140. package/lib/types/k8s-manifest/index.d.ts +0 -2
@@ -0,0 +1,687 @@
1
+ # API Context Refactor — PLAT-1062
2
+
3
+ > **Linear**: [PLAT-1062](https://linear.app/odigos/issue/PLAT-1062/introduce-odigosapicontext-in-ui-kit-to-replace-per-container-api-prop)
4
+ > **Scope**: `@odigos/ui-kit`, `odigos/frontend/webapp`, `odigos-enterprise/central-ui`
5
+
6
+ ## TL;DR
7
+
8
+ - **Apollo moved into the kit.** The two host apps no longer construct an `ApolloClient` — the kit's `<OdigosApiProvider>` owns it and is configured by the host via a small `ApolloConfig` shape.
9
+ - **Containers fetch their own data.** Every `_v2` container that previously took a forest of API-function props now consumes `useOdigosApi()` / `useApiQuery(key)` from the kit's API context. Pages collapse to one-liners.
10
+ - **Hosts ship a thin adapter** (~250 lines) mapping GraphQL documents to typed `Operation` slots. No more 16 CRUD hooks per host (`useSourceCRUD`, `useDestinationCRUD`, …).
11
+ - **Apollo cache is the entity store.** The kit's `useEntityStore` Zustand store is gone — sources/destinations/actions/rules/namespaces all flow through Apollo's normalized cache via `useApiQuery('GET_X', …, { fetchPolicy: 'cache-first' })`. Mutations call `runMutation` then `await fetchAll()`; cache writes broadcast to every active subscriber.
12
+ - **Uniform API surface.** `useOdigosApi()` returns `{ sourcesApi, destinationsApi, actionsApi, instrumentationRulesApi, …, capabilities, resetCache }` — every entity-list namespace exposes `{ items, loading, fetchAll, … }`. No aliasing on destructure, no stuttery `sourcesApi.sources`.
13
+ - **Strict types end-to-end.** ~80 named `OdigosApiOperations` slots, each typed as `Operation<TData, TVars>`. Zero `any`, zero envelope/bare unions, zero `unknown` data fields at the public hook surface.
14
+ - **Storybook is production-unaware.** The kit's runtime has no mock context, no interceptor hooks. The Storybook mock provider uses a custom Apollo `ApolloLink` that intercepts stub queries with predefined data.
15
+ - **Mock data lives in `.storybook/`.** `src/mock-data/` was relocated; production builds never touch it.
16
+
17
+ ---
18
+
19
+ ## Why
20
+
21
+ Before this refactor, the same shape repeated everywhere:
22
+
23
+ 1. **Each host app declared its own Apollo client and graphql operations.** Two near-duplicate Apollo configs, two near-duplicate sets of typed `gql` documents.
24
+ 2. **Each host app owned 16 CRUD hooks** (`useSourceCRUD`, `useDestinationCRUD`, `useActionCRUD`, `useInstrumentationRuleCRUD`, etc.). Each hook re-implemented optimistic updates, notifications, and progress tracking against the kit's `useEntityStore`.
25
+ 3. **A separate Zustand entity store mirrored Apollo.** `useEntityStore` held sources/destinations/actions/rules/namespaces with bespoke `setEntities` / `addEntities` / `removeEntities` / `setEntitiesLoading` mutators. Domain hooks pushed query results into it; SSE handlers patched per-id; consumers subscribed via selectors. Two layers of caching, two sets of write paths to keep in sync.
26
+ 4. **Each `_v2` container received API functions as props.** A typical drawer took 5–15 callbacks (`fetchById`, `update`, `restartWorkloads`, `fetchPeerSources`, …). Pages hand-wired dozens of props per container.
27
+ 5. **API functions were drilled through nested components.** `<EditSourceDrawer>` → `<Profiling>` → `<FlameGraphCanvas>` carried `fetchProfilingSlots` / `enableProfiling` / `fetchSourceProfiling` purely to satisfy the leaf.
28
+ 6. **Types diverged.** Each host had its own `Workload` / `Destination` / `Action` types; the kit's mock types didn't match wire types; envelope vs bare result shapes leaked into containers as runtime narrowing.
29
+
30
+ The user-facing symptom: **adding a new operation required touching 5 places** — host's gql doc + host's CRUD hook + host's page + kit's container prop + kit's container body. Container changes routinely required correlated host changes. Two host apps duplicated the same plumbing 80% verbatim.
31
+
32
+ ---
33
+
34
+ ## Architecture: Before / After
35
+
36
+ ### Before — duplicated host-side plumbing, deep prop drilling, two caches
37
+
38
+ ```mermaid
39
+ graph TB
40
+ subgraph "Each host app"
41
+ AC[ApolloClient]
42
+ H1[useSourceCRUD]
43
+ H2[useDestinationCRUD]
44
+ H3[useActionCRUD]
45
+ H4[13 more CRUD hooks]
46
+ end
47
+
48
+ subgraph "Kit"
49
+ ENTITY[useEntityStore<br/>Zustand mirror]
50
+ end
51
+
52
+ AC --> H1
53
+ AC --> H2
54
+ AC --> H3
55
+ AC --> H4
56
+ H1 -->|setEntities| ENTITY
57
+ H2 -->|setEntities| ENTITY
58
+ H3 -->|setEntities| ENTITY
59
+ H4 -->|setEntities| ENTITY
60
+
61
+ subgraph "Page"
62
+ H1 --> P[Page wires<br/>40+ props]
63
+ H2 --> P
64
+ H3 --> P
65
+ H4 --> P
66
+ ENTITY -->|selector| P
67
+ end
68
+
69
+ P -->|"fetchAll, persist,<br/>update, refresh, …"| C1[Overview container]
70
+ P -->|"fetchById, update,<br/>fetchPeerSources, …"| C2[EditSourceDrawer]
71
+ C2 -->|"fetchK8sManifest"| L1[Debug tab]
72
+ C2 -->|"fetchProfilingSlots,<br/>enableProfiling, …"| L2[Profiling tab]
73
+ C2 -->|"fetchPeerSources"| L3[Peers tab]
74
+ ```
75
+
76
+ ### After — single source of truth in the kit, one cache
77
+
78
+ ```mermaid
79
+ graph TB
80
+ subgraph "Host adapter (one file, ~250 LOC)"
81
+ ADAPTER[OdigosApiAdapter]
82
+ OPS[Operations map]
83
+ APOLLO_CONFIG[ApolloConfig]
84
+ ADAPTER --> OPS
85
+ ADAPTER --> APOLLO_CONFIG
86
+ end
87
+
88
+ subgraph "Kit"
89
+ ADAPTER --> PROVIDER[OdigosApiProvider]
90
+ PROVIDER --> APOLLO["ApolloClient<br/>(normalized cache)"]
91
+ PROVIDER --> CTX[OdigosApiContext]
92
+ CTX --> HOOK1["useOdigosApi()<br/>sourcesApi.items, fetchAll, …"]
93
+ CTX --> HOOK2["useApiQuery('GET_X')"]
94
+ CTX --> HOOK3["useApiMutation('CREATE_X')"]
95
+ HOOK1 -.reads / writes.-> APOLLO
96
+ HOOK2 -.reads / writes.-> APOLLO
97
+ HOOK3 -.writes.-> APOLLO
98
+ end
99
+
100
+ subgraph "Pages"
101
+ PAGE1[Overview page<br/>1 line]
102
+ PAGE2[Settings page<br/>1 line]
103
+ PAGE3[Sampling page<br/>1 line]
104
+ end
105
+
106
+ HOOK2 --> CONTAINER[Containers]
107
+ HOOK1 --> CONTAINER
108
+ HOOK3 --> CONTAINER
109
+ CONTAINER --> PAGE1
110
+ CONTAINER --> PAGE2
111
+ CONTAINER --> PAGE3
112
+ ```
113
+
114
+ Pages went from `<Overview metrics={…} effectiveConfig={…} workloads={…} {/* ~12 more props */} />` to `<Overview />`. Every fetch lives inside the container that consumes the data, and every consumer reads through Apollo's cache — there's no second store to keep in sync.
115
+
116
+ ---
117
+
118
+ ## Key concepts
119
+
120
+ ### `Operation<TData, TVars>`
121
+
122
+ The atomic unit of data exchange. A host adapter declares one `Operation` per logical query/mutation:
123
+
124
+ ```typescript
125
+ export interface Operation<TData = unknown, TVars = unknown> {
126
+ /** Apollo document. May be a function of context for versioned-query maps. */
127
+ document: DocumentNode | ((ctx: OperationContext) => DocumentNode | undefined);
128
+
129
+ /** Massage variables before they hit the wire. */
130
+ transformVariables?(vars: TVars, ctx: OperationContext): unknown;
131
+
132
+ /** Massage the raw Apollo response before it reaches the consumer. */
133
+ transformResult?(raw: unknown, ctx: OperationContext): TData | undefined;
134
+
135
+ /** Per-op ApolloClient override (multi-endpoint use cases). */
136
+ client?: ApolloClient<unknown>;
137
+
138
+ // …multi-fetch variants for fan-out…
139
+ }
140
+ ```
141
+
142
+ The function-shaped `document` is the lever that lets central-ui swap GraphQL strings per platform/version (`PlatformType.K8s` v1.20 vs `PlatformType.Vm` v0.6.0) without the kit knowing how versioning works. The kit's `pickByPlatform()` and `versionedDocument()` helpers ([platform-helpers.ts](../src/contexts/odigos-api/platform-helpers.ts)) cover the common shapes.
143
+
144
+ ### `OdigosApiOperations` — typed slot map
145
+
146
+ The aggregate. Every operation has an explicit `<TData, TVars>`:
147
+
148
+ ```typescript
149
+ export interface OdigosApiOperations {
150
+ // sources
151
+ GET_WORKLOADS: Operation<GetWorkloadsData, GetWorkloadsVars | undefined>;
152
+ GET_WORKLOADS_BY_IDS: Operation<GetWorkloadsByIdsData, GetWorkloadsByIdsVars>;
153
+ PERSIST_SOURCES: Operation<unknown, PersistSourcesVars>;
154
+ UPDATE_SOURCE: Operation<{ updateK8sActualSource: boolean }, UpdateSourceVars>;
155
+
156
+ // destinations
157
+ TEST_DESTINATION_CONNECTION: Operation<TestConnectionResponse, TestDestinationConnectionVars>;
158
+
159
+ // …~80 slots total…
160
+
161
+ // tokens — bare slot, adapter flattens the wire envelope via transformResult
162
+ GET_TOKENS?: Operation<TokenPayload[], undefined>;
163
+
164
+ // config — bare FetchedConfig, slot type lives in the kit
165
+ GET_CONFIG?: Operation<FetchedConfig, undefined>;
166
+
167
+ // Mutation vars are wire-shaped — they match the GraphQL mutation's
168
+ // declared `($variable: Type)` exactly so consumers don't have to
169
+ // shape-shift in calling code.
170
+ DELETE_ACTION: Operation<unknown, DeleteActionVars>; // { id, actionType }
171
+ UPDATE_LOCAL_UI_CONFIG?: Operation<unknown, UpdateLocalUiConfigVars>; // { config: LocalUiConfigInput }
172
+ UPDATE_DATA_STREAM: Operation<unknown, UpdateDataStreamVars>; // { id, dataStream }
173
+ }
174
+ ```
175
+
176
+ Operations marked `?` are optional. Host adapters that don't support an op simply omit it; the kit's [capabilities deriver](../src/contexts/odigos-api/capabilities.ts) reads slot presence to decide which UI affordances to show (`canCreateAction`, `canFetchSnapshots`, `canTestDestinationConnection`, etc.).
177
+
178
+ #### Variance trick — method shorthand
179
+
180
+ `Operation`'s callbacks are declared with method-shorthand (`fn(args): R`) rather than property syntax (`fn: (args) => R`). TypeScript checks method shorthand **bivariantly**, so a host adapter's tightly-typed `Operation<MyData, MyVars>` can be assigned into the aggregate slot without strict-contravariance friction on `TVars`. Without this, the slot map would force every operation to use `any` for variables.
181
+
182
+ ### `<OdigosApiAdapter>` — host's only integration point
183
+
184
+ A host's full integration shrinks to one file:
185
+
186
+ ```typescript
187
+ // odigos/frontend/webapp/lib/odigos-api-adapter.tsx (sketch)
188
+ const operations: OdigosApiOperations = {
189
+ GET_WORKLOADS: { document: GET_WORKLOADS },
190
+ CREATE_DESTINATION: { document: CREATE_DESTINATION },
191
+ GET_K8S_MANIFEST: {
192
+ document: GET_K8S_MANIFEST,
193
+ // The wire returns `{ k8sManifest: string }` (or the legacy
194
+ // `k8sActualManifest`/`manifest` shapes); the kit's slot is bare
195
+ // `string`. Walk every accepted name so the consumer never has
196
+ // to runtime-narrow the envelope.
197
+ transformResult: (raw) => {
198
+ const obj = raw as { k8sManifest?: string; k8sActualManifest?: string; manifest?: string };
199
+ return obj?.k8sManifest ?? obj?.k8sActualManifest ?? obj?.manifest;
200
+ },
201
+ },
202
+ // …
203
+ };
204
+
205
+ export default function OdigosApiAdapter({ children, context }) {
206
+ const { token, isLoading } = useCSRF();
207
+ const ready = IS_LOCAL || !isLoading;
208
+
209
+ const apolloConfig = useMemo(() => ({
210
+ httpUrl: API.GRAPHQL,
211
+ csrfHeader: () => token ? { 'X-CSRF-Token': token } : {},
212
+ addTypename: false,
213
+ }), [token]);
214
+
215
+ // CRITICAL: gate the provider behind the CSRF bootstrap. Without
216
+ // the gate, the kit's domain hooks fire `useApiQuery(…cache-first)`
217
+ // on mount and the request goes out before `csrfHeader()` has a
218
+ // token to inject. Use `useState(!IS_LOCAL)` for `isLoading` in
219
+ // the CSRF hook so the very first render reports the loading
220
+ // state and the gate doesn't briefly mount the provider with a
221
+ // null token.
222
+ if (!ready) return <Loader />;
223
+
224
+ return (
225
+ <OdigosApiProvider apolloConfig={apolloConfig} operations={operations} context={context}>
226
+ {children}
227
+ </OdigosApiProvider>
228
+ );
229
+ }
230
+ ```
231
+
232
+ Central-ui's adapter is similar but uses a `wrapRemote(versionedQuery, extract?)` helper to wrap each slot in a `REMOTE_FETCH` proxy call, with versioned-query resolution driven off the active proxy's reported version. The `extract` callback is the same idea as `transformResult` above — it normalizes per-op envelopes to the kit's bare slot shape.
233
+
234
+ #### Provider-mounting gotcha — every page that calls `useApiQuery` / `useOdigosApi` must be inside `<OdigosApiAdapter>`
235
+
236
+ The kit's `useOdigosApiInternal()` hook _throws_ if it can't find an `<OdigosApiProvider>` above it. That includes anything that goes through it transitively: `useOdigosApi`, `useApiQuery`, `useApiLazyQuery`, `useApiMutation`. The error is `[ui-kit] useOdigosApi() / useXxxApi() called outside of <OdigosApiProvider>`.
237
+
238
+ In Next.js app-router setups, this is easy to miss. If your app routes look like:
239
+
240
+ ```
241
+ app/
242
+ layout.tsx ← root layout (no adapter)
243
+ page.tsx ← root page (calls useConfig() to redirect)
244
+ (v2)/
245
+ layout.tsx ← mounts <OdigosApiAdapter>
246
+ overview/page.tsx
247
+
248
+ (setup)/
249
+ layout.tsx ← mounts <OdigosApiAdapter>
250
+ onboarding/page.tsx
251
+ ```
252
+
253
+ The `app/page.tsx` ("root page") is rendered with only `app/layout.tsx` above it — neither of the route-group layouts apply. If that page calls `useConfig` (or any kit hook), it crashes the moment it's rendered. Wrap it inline:
254
+
255
+ ```tsx
256
+ // app/page.tsx
257
+ function Redirect() {
258
+ const { config } = useConfig();
259
+ // …
260
+ }
261
+
262
+ export default function App() {
263
+ return (
264
+ <OdigosApiAdapter>
265
+ <Redirect />
266
+ </OdigosApiAdapter>
267
+ );
268
+ }
269
+ ```
270
+
271
+ Pre-refactor this worked because the host kept its own `<ApolloProvider>` in `app/layout.tsx`; after the kit took over Apollo, the kit's provider only mounts where the host explicitly puts it.
272
+
273
+ #### Adapter checklist: matching wire shapes to kit slots
274
+
275
+ The most common bugs in adapters come from mismatched wire-shape ↔ kit-slot envelopes. Use this as a sanity sweep when adding or auditing slots:
276
+
277
+ | Slot | Kit-slot shape | Wire shape (current backend) | Adapter must |
278
+ | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
279
+ | `GET_WORKLOADS` | `{ workloads: Workload[] }` | `{ workloads: [...] }` | pass through |
280
+ | `GET_DESTINATIONS` / `GET_ACTIONS` / `GET_INSTRUMENTATION_RULES` / `GET_DATA_STREAMS` | `{ computePlatform?: { x }, x? }` | `{ computePlatform: { x: [...] } }` | pass through |
281
+ | `GET_NAMESPACES_WITH_WORKLOADS` | `{ namespaces?: Namespace[] }` | `{ namespaces: [...] }` (v1.20+) | pass through |
282
+ | `GET_TOKENS` | bare `TokenPayload[]` | `{ computePlatform: { apiTokens: [...] } }` | extract `apiTokens` (note: NOT `tokens`) |
283
+ | `GET_CONFIG` | bare `FetchedConfig` | `{ config: { … } }` | extract `config` |
284
+ | `GET_EFFECTIVE_CONFIG` | bare `EffectiveConfig` | `{ effectiveConfig: { … } }` | extract `effectiveConfig` |
285
+ | `GET_K8S_MANIFEST` | bare `string` | `{ k8sManifest: "…" }` | extract `k8sManifest` |
286
+ | `GET_GATEWAY_INFO` | bare `GatewayInfo` | `{ gatewayDeploymentInfo: { … } }` | extract `gatewayDeploymentInfo` |
287
+ | `GET_NODE_COLLECTOR_INFO` | bare `NodeCollectoInfo` | `{ odigletDaemonSetInfo: { … } }` | extract `odigletDaemonSetInfo` |
288
+ | `GET_NODE_COLLECTOR_PODS` | bare `PodInfo[]` | `{ odigletPods: [...] }` | extract `odigletPods` |
289
+ | `GET_COLLECTOR_POD_INFO` | bare `ExtendedPodInfo` | `{ collectorPod: { … } }` | extract `collectorPod` |
290
+ | `GET_DATA_STREAMS` | `{ dataStreams? } \| { computePlatform: { dataStreams } }` | `{ computePlatform: { dataStreams: [...] } }` | pass through (kit hook reads either path); the `useDataStreamsApi` hook auto-fetches via `useApiQuery` and bridges into `useDataStreamStore` for legacy consumers |
291
+ | `GET_SOURCE_PROFILING` | bare `SourceProfilingResult` | `{ computePlatform: { source: { profiling: {…} } } }` | walk `computePlatform.source.profiling` |
292
+ | `GET_SAMPLING_RULES` | `{ samplingRules, k8sHealthProbesConfig }` | `{ sampling: { rules, configs.effective.k8sHealthProbesSampling } }` | flatten `sampling.rules` and `sampling.configs.effective.k8sHealthProbesSampling` |
293
+ | `GET_SERVICE_MAP` | `{ serviceMap?: ServiceMapSources }` | `{ getServiceMap: { services: [...] } }` | flatten to `{ serviceMap: services }` |
294
+ | `GET_DIAGNOSE` | `DiagnoseResult` (bare) | `{ diagnose: { stats, … } }` | extract `diagnose` |
295
+
296
+ Mutation variables follow the same logic — declare `Vars` shapes that match what the wire's `mutation(...)` declaration expects, NOT the prettier shape the consumer code wants:
297
+
298
+ | Slot | Kit-slot vars | Wire variables | Notes |
299
+ | ------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
300
+ | `DELETE_ACTION` | `{ id: string; actionType: string }` | `($id: ID!, $actionType: String!)` | Pass the `Action.type` enum value as `actionType` |
301
+ | `UPDATE_LOCAL_UI_CONFIG` | `{ config: LocalUiConfigInput }` | `($config: LocalUiConfigInput!)` | Wrap, don't spread |
302
+ | `UPDATE_LOCAL_UI_SAMPLING_CONFIG` | `{ config: SamplingK8sHealthConfigInput }` | `($config: SamplingConfigInput)` | Wrap, don't spread |
303
+ | `UPDATE_DATA_STREAM` | `{ id: string; dataStream: DataStream }` | `($id: ID!, $dataStream: DataStreamInput!)` | The data stream's `name` doubles as wire `id` |
304
+ | `DELETE_DATA_STREAM` | `{ id: string }` | `($id: ID!)` | Same — name is id |
305
+ | `CREATE_INSTRUMENTATION_RULE` / `UPDATE_INSTRUMENTATION_RULE` | input includes `sourceScopes` (kit spec shape) | `InstrumentationRuleInput` declares `sourcesScopes` (row array) | Kit's `useInstrumentationRulesApi` runs `adaptInstrumentationRuleInputForWire` for both directions; adapters don't need extra plumbing here. |
306
+ | `GET_DIAGNOSE` | `{ input: DiagnoseFormData; dryRun?: boolean }` | `($input: DiagnoseInput!, $dryRun: Boolean)` | `useDescribeApi.downloadDiagnose(formData)` wraps internally — adapters don't need to do anything beyond `transformResult` for the response. |
307
+
308
+ When in doubt, copy a working query from the host's existing GraphQL files and `console.log` the raw `apolloClient.query(...)` response — what comes back is exactly what `transformResult(raw, ctx)` receives.
309
+
310
+ #### Why we don't add identity `transformResult` to _every_ slot
311
+
312
+ A reasonable defensive instinct is: "let's add a `transformResult` to every operation, even when the wire shape already matches the kit slot, so future shape drifts surface in one place." We've considered this and chose not to. Identity transforms add noise without catching anything — if the wire silently switches `gatewayInfo` to `gatewayDeploymentInfo`, the identity transform `(raw) => raw` still passes through and the consumer still breaks downstream. The bug surfaces in the same place either way: in the consumer.
313
+
314
+ The actual safety nets are:
315
+
316
+ 1. **Strict slot types — no `unknown`, no `any`.** Every `OdigosApiOperations[K]` declares `Operation<TData, TVars>` with concrete `TData` / `TVars` types — including mutation result types like `CreateActionResult = { createAction: Action }`, `DeleteActionResult = { deleteAction: boolean }`, and so on. Adapters that produce the wrong shape get a TypeScript error at the slot assignment. The few `unknown`s that remain in the kit's internals are inherent type-erasure boundaries and are documented inline:
317
+ - `transformResult?(raw: unknown, …)` — Apollo's response is opaque from the kit's perspective; the adapter narrows it.
318
+ - `messageFromError(e: unknown)` — error catching.
319
+ - `ApolloClient<unknown>` — Apollo's cache-shape generic; `unknown` is the safe pass-through.
320
+ - `Operation['document']` and the kit's K-distributed-union collapse (`AnyOp = Operation<unknown, unknown>`) — type-level inference helpers, never observed by adapter authors or container consumers.
321
+ 2. **Mutation `Vars` types match wire variables exactly.** `DeleteActionVars` is `{ id, actionType }` because the wire is `mutation($id: ID!, $actionType: String!)`. `UpdateLocalUiConfigVars` is `{ config: LocalUiConfigInput }` because the wire wraps the input under `$config`. No "spread the form data flat" — every mutation's `TVars` is a literal mirror of its `mutation(…)` declaration.
322
+ 3. **The wire-shape table above** — when adding a new slot, scan it for the matching row. If your wire returns something different, write a `transformResult`.
323
+ 4. **Domain-hook guards** — for the rare slot whose wire shape varies across versions (e.g. `GET_DESTINATIONS` returning `{ computePlatform: { destinations } }` on most backends and a flat shape on some), the kit's domain hook reads `data.computePlatform?.destinations ?? data.destinations ?? []` so either shape works.
324
+
325
+ Add a `transformResult` _only_ when the wire shape doesn't already match the slot's `TData`. Otherwise leave the slot as `{ document }` and trust the type system.
326
+
327
+ ---
328
+
329
+ ## Hooks — declarative + imperative
330
+
331
+ The kit ships two access patterns; containers pick whichever fits.
332
+
333
+ ### Declarative — `useApiQuery` / `useApiLazyQuery` / `useApiMutation`
334
+
335
+ Apollo-backed hooks layered on top of `useQuery` / `useLazyQuery` / `useMutation`. Slot types drive everything — no `<TData, TVars>` generics at the call site:
336
+
337
+ ```typescript
338
+ // Inside any kit container
339
+ const {
340
+ data: gatewayInfo,
341
+ loading,
342
+ error,
343
+ refetch,
344
+ } = useApiQuery('GET_GATEWAY_INFO', undefined, {
345
+ pollInterval: 3_000,
346
+ skip: !visible,
347
+ });
348
+
349
+ const [updateToken, { loading: saving }] = useApiMutation('UPDATE_TOKEN', {
350
+ refetchQueries: ['GET_TOKENS'],
351
+ });
352
+ ```
353
+
354
+ `useApiQuery` consults the operations map, applies `transformVariables` / `transformResult` automatically, and returns `{ data: SlotData<K>, loading, error, refetch, unsupported }`. The `unsupported` flag is `true` when the host adapter omitted the slot — containers either render an "operation not supported" UI or hide the affordance via `capabilities.canX`.
355
+
356
+ ### Imperative — `useOdigosApi()` + per-domain APIs
357
+
358
+ For places that need both a cached list and dispatchable methods (event handlers, side-effect chains, drawers):
359
+
360
+ ```typescript
361
+ // Subscribe to the entity lists this view actually reads.
362
+ const { sourcesApi } = useOdigosApi({ subscribe: ['sources'] });
363
+
364
+ // Cached list — read-only, kept fresh by Apollo's cache + SSE-driven refetch.
365
+ const { items: sources, loading } = sourcesApi;
366
+
367
+ const onClick = async () => {
368
+ await sourcesApi.update(sourceId, formData);
369
+ // sourcesApi.update internally awaits fetchAll() → cache write
370
+ // → broadcast → every active `useApiQuery('GET_WORKLOADS')` re-renders.
371
+ };
372
+ ```
373
+
374
+ Every entity-list namespace (`sourcesApi`, `destinationsApi`, `actionsApi`, `instrumentationRulesApi`, `namespacesApi`, `dataStreamsApi`) follows the same `{ items, loading, fetchAll, … }` shape. The `*Api` suffix on the top-level keys means destructuring stays rename-free even when the consumer has a local `sources` / `destinations` variable.
375
+
376
+ #### `subscribe` opt-in — the six entity-list domains
377
+
378
+ The six entity-list domain APIs (`sourcesApi`, `destinationsApi`, `actionsApi`, `instrumentationRulesApi`, `namespacesApi`, `dataStreamsApi`) own a `useApiQuery('GET_…')` subscription that auto-fetches and broadcasts to every consumer. **Without an explicit opt-in those subscriptions stay `skip: true`** — calling `useOdigosApi()` from a page that only needs `configApi` (e.g. `/settings`) does NOT trigger every list query. Containers that read `.items` / `.loading` MUST list the domains they need:
379
+
380
+ ```typescript
381
+ // Module-level constant so the array reference is stable across renders.
382
+ const SUBSCRIBE_ENTITIES: SubscribableDomain[] = ['sources', 'destinations', 'actions', 'rules'];
383
+
384
+ export const Columns: FC = () => {
385
+ const { sourcesApi, destinationsApi, actionsApi, instrumentationRulesApi } = useOdigosApi({
386
+ subscribe: SUBSCRIBE_ENTITIES,
387
+ });
388
+ const { items: sources, loading: sourcesLoading } = sourcesApi;
389
+ // …
390
+ };
391
+ ```
392
+
393
+ Symptoms of forgetting the opt-in: empty lists in the UI, `loading` permanently `false`, no auto-refresh on SSE. The fix is always to add the missing domain key. Containers that only call imperative methods (create / update / delete / fetchAll) leave the option off entirely — the domain still exposes those methods, just without the auto-fetch.
394
+
395
+ | Container | Subscribes to |
396
+ | --------------------------------------- | ------------------------------------------------- |
397
+ | `_v2/overview/columns` | `['sources', 'destinations', 'actions', 'rules']` |
398
+ | `_v2/overview/drawers` | `['sources']` |
399
+ | `_v2/overview/filters` | `['sources', 'namespaces']` |
400
+ | `_v2/overview/search` | `['sources', 'destinations', 'actions', 'rules']` |
401
+ | `_v2/overview/data-stream-select` | `['dataStreams']` |
402
+ | `_v2/sampling-rules` | `['sources']` |
403
+ | `service-map` | `['sources']` |
404
+ | `_v2/_drawers/edit-source-drawer/peers` | `['sources']` |
405
+ | `_v2/_drawers/system-drawer/diagnose` | `['namespaces']` |
406
+ | `contexts/destination-form-context` | `['destinations']` |
407
+
408
+ The full surface returned by `useOdigosApi()`:
409
+
410
+ ```typescript
411
+ const {
412
+ sourcesApi, // { items, loading, fetchAll, fetchByTargets, fetchById, persist, update, … }
413
+ destinationsApi, // { items, loading, fetchAll, create, update, remove, testConnection, … }
414
+ actionsApi, // { items, loading, fetchAll, create, update, remove }
415
+ instrumentationRulesApi, // { items, loading, fetchAll, create, update, remove }
416
+ namespacesApi, // { items, loading, fetchAll, persist }
417
+ dataStreamsApi, // { items, loading, fetchAll, create, update, remove }
418
+ k8sManifestApi,
419
+ configApi,
420
+ describeApi,
421
+ tokensApi,
422
+ metricsApi,
423
+ serviceMapApi,
424
+ profilingApi,
425
+ collectorsApi,
426
+ samplingApi,
427
+ snapshotsApi,
428
+ capabilities, // canCreateAction, canFetchSnapshots, canTestDestinationConnection, …
429
+ resetCache, // wipes Apollo's cache (used on proxy switch in central-ui)
430
+ } = useOdigosApi();
431
+ ```
432
+
433
+ Both declarative and imperative paths share the same `runner.ts` dispatcher under the hood — same `transformVariables` / `transformResult` / `client` flow, same graceful "operation not configured" fallback when a slot is absent, same Apollo cache.
434
+
435
+ ---
436
+
437
+ ## Data flow — Apollo cache as the entity store
438
+
439
+ The deleted `useEntityStore` Zustand store used to be the kit's mirror of every entity list. It's gone. Apollo's normalized cache is now the single source of truth, and the kit's domain hooks read/write through it directly.
440
+
441
+ ```mermaid
442
+ graph LR
443
+ subgraph "Kit domain hook (e.g. useSourcesApi)"
444
+ SUB["useApiQuery('GET_WORKLOADS')<br/>fetchPolicy: cache-first"]
445
+ FETCH["fetchAll() / fetchByTargets() / fetchById()"]
446
+ MUT["update / persist / restartWorkloads / …"]
447
+ end
448
+
449
+ SUB -->|"items, loading"| CONSUMER1[Container A]
450
+ SUB -.shared cache.-> SUB2["useApiQuery('GET_WORKLOADS')<br/>(in another consumer)"]
451
+ SUB2 -->|"items, loading"| CONSUMER2[Container B]
452
+
453
+ CACHE[("Apollo<br/>normalized<br/>cache")]
454
+ SUB <-->|reads| CACHE
455
+ FETCH -->|"runQuery<br/>(network-only)"| CACHE
456
+ MUT -->|"runMutation<br/>then await fetchAll()"| CACHE
457
+ SSE[Host SSE handler] -->|"sourcesApi.fetchAll()"| CACHE
458
+ PROXY[Proxy switch<br/>central-ui] -->|"resetCache()"| CACHE
459
+
460
+ CACHE -.broadcast.-> SUB
461
+ CACHE -.broadcast.-> SUB2
462
+ ```
463
+
464
+ **Reads.** Every consumer of `useOdigosApi().sourcesApi` registers an Apollo observer of `GET_WORKLOADS` via `useApiQuery`. The first observer fetches over the network, every subsequent one hits the shared cache (`fetchPolicy: 'cache-first'`). Cache writes broadcast — when _any_ refetch lands, every observer re-renders with the new list.
465
+
466
+ **Writes.** Mutations call `runMutation` and `await fetchAll()` immediately after success. The runner's `client.query({ fetchPolicy: 'network-only' })` writes the response into the cache, which broadcasts. Every container that reads `sourcesApi.items` updates within one render — no Zustand selector hop, no manual `setEntities`. Apollo's request dedup keeps the cost of "fan-out from one mutation" at a single network round-trip.
467
+
468
+ **SSE.** Both host SSE handlers (`useSSE.ts`) drop their old `useEntityStore` imports and refresh through the same `sourcesApi.fetchAll()` / `destinationsApi.fetchAll()` entry points. The deleted-target eviction primitive is gone — for an envelope-aware kit (central-ui's `REMOTE_FETCH` makes generic `cache.modify` impractical), refetching the canonical list is simpler and the result is identical.
469
+
470
+ **Reset.** Central-ui's `useProxy.resetStores()` calls `useOdigosApi().resetCache()` when switching between proxies (and on every out-of-proxy navigation). `resetCache()` delegates to `client.cache.reset()` — **not** `client.clearStore()`. This matters: `clearStore()` aborts every in-flight query, rejecting it with Apollo error #42 ("Store reset while query was in flight"). Because central-ui resets on out-of-proxy navigation, that abort raced with the legitimate out-of-proxy queries still loading on the same render (`GetSystemConfig`, `GetHealth`, `GetComputePlatforms`) and surfaced them as confusing network/CORS-looking errors plus an error toast. `cache.reset()` wipes the cache and broadcasts to active watchers (they re-render empty and re-fetch on next access) but leaves in-flight requests alone — they write into the freshly-reset cache when they resolve.
471
+
472
+ **Precondition guards (`Operation.canRun`).** An `Operation` may declare `canRun?(ctx): boolean`. When it returns `false`, `useApiQuery` / `useApiLazyQuery` / `useApiMutation` skip the dispatch (no network request, no Apollo observer) and the imperative `runQuery` / `runMutation` short-circuit to `{ data: undefined }` with no error (so domain hooks don't toast). central-ui sets `canRun: (ctx) => !!ctx.proxyID && !!getVersionedQuery(...)` on every `REMOTE_FETCH`-backed op: out-of-proxy pages have no selected proxy, so without the guard the kit would fire an empty `RemoteFetch($proxyID, $query, $variables)` with both required vars missing. The multi-cluster fan-out path (`runMulti` / `transformVariablesMulti`) is scoped by explicit `proxyIDs` and intentionally bypasses `canRun`, so connections-scope bulk drawers keep working even though `ctx.proxyID` is empty there.
473
+
474
+ > **Optimistic instrument-on-form-submit lag.** The previous version of `prepareSourcePayloads` accepted Zustand mutators and patched the entity store optimistically before the mutation resolved. That path is gone — the mutation now fires, then `await fetchAll()` refreshes (~200–500ms total). Per-adapter `cache.modify` plumbing would reinstate optimism, but central-ui's opaque `REMOTE_FETCH` envelope makes it more code than it's worth. The 5s SSE debounce that previously masked the optimistic pattern is no longer the critical path either — the post-mutation refetch arrives well before SSE.
475
+
476
+ ---
477
+
478
+ ## Migration patterns
479
+
480
+ ### Container migration — 10 props → 1 hook
481
+
482
+ **Before:**
483
+
484
+ ```typescript
485
+ interface OverviewProps {
486
+ metrics: Metrics;
487
+ effectiveConfig?: EffectiveConfig;
488
+ fetchAll: () => Promise<void>;
489
+ fetchPeerSources: (s: string) => Promise<...>;
490
+ updateSource: (id, payload) => Promise<...>;
491
+ // …12 more callbacks
492
+ }
493
+
494
+ export const Overview: FC<OverviewProps> = (props) => { … };
495
+ ```
496
+
497
+ **After:**
498
+
499
+ ```typescript
500
+ export interface OverviewProps {} // Empty
501
+ export const Overview: FC<OverviewProps> = () => {
502
+ const { sourcesApi, destinationsApi, capabilities } = useOdigosApi();
503
+ const { items: sources, loading: sourcesLoading } = sourcesApi;
504
+ const { items: destinations } = destinationsApi;
505
+
506
+ const { data: metrics } = useApiQuery('GET_METRICS', undefined, { pollInterval: 10_000 });
507
+ // …
508
+ };
509
+ ```
510
+
511
+ ### Page shrink — data plumbing → one-liner
512
+
513
+ **Before** (`app/(v2)/overview/page.tsx`):
514
+
515
+ ```typescript
516
+ 'use client';
517
+ export default function Page() {
518
+ const { metrics } = useMetrics();
519
+ const { effectiveConfig } = useEffectiveConfig();
520
+ const sources = useSources();
521
+ const namespaces = useNamespaces();
522
+ // …+ 12 hook calls + correlated state
523
+ return <Overview metrics={metrics} effectiveConfig={effectiveConfig} workloads={…} {/* …more */} />;
524
+ }
525
+ ```
526
+
527
+ **After:**
528
+
529
+ ```typescript
530
+ 'use client';
531
+ export default function Page() {
532
+ return <Overview />;
533
+ }
534
+ ```
535
+
536
+ The 4 webapp pages (`/overview`, `/settings`, `/sampling`, `/service-map`) collapsed to one-liners. Same on central-ui.
537
+
538
+ ### Storybook — mocks via `parameters.api`
539
+
540
+ Stories pass mock data through Storybook's `parameters` (NOT `args` — args spread into the rendered component as unknown props):
541
+
542
+ ```typescript
543
+ export const Default = Template.bind({});
544
+ Default.parameters = {
545
+ api: {
546
+ queries: {
547
+ GET_GATEWAY_INFO: MOCK_GATEWAY_INFO,
548
+ GET_GATEWAY_PODS: MOCK_GATEWAY_PODS,
549
+ },
550
+ },
551
+ };
552
+
553
+ export const Loading = Template.bind({});
554
+ Loading.parameters = { api: { loading: ['GET_METRICS'] } };
555
+
556
+ export const Error = Template.bind({});
557
+ Error.parameters = { api: { errors: { GET_DESTINATIONS: 'Forbidden' } } };
558
+ ```
559
+
560
+ The Storybook decorator translates `parameters.api` into a custom Apollo `ApolloLink` that resolves stub queries with the predefined data. **No production code knows mocks exist.**
561
+
562
+ ---
563
+
564
+ ## Mock architecture
565
+
566
+ ### Production — kit hooks talk to real Apollo
567
+
568
+ ```mermaid
569
+ graph LR
570
+ Container -->|useApiQuery| Hook[useApiQuery]
571
+ Hook -->|operations map| Slot[Operation slot]
572
+ Slot -->|document, transforms| Apollo[Apollo useQuery]
573
+ Apollo -->|HTTP| Backend[GraphQL backend]
574
+ ```
575
+
576
+ The kit's `use-api-query.ts` has zero knowledge of mocks. No interceptor context, no `if (mockState)` branches.
577
+
578
+ ### Storybook — Apollo Link intercepts at the network boundary
579
+
580
+ ```mermaid
581
+ graph LR
582
+ Container -->|useApiQuery| Hook[useApiQuery]
583
+ Hook -->|operations: stub ops| Slot[Stub Operation]
584
+ Slot -->|stub gql doc| Apollo[Apollo useQuery]
585
+ Apollo -->|operationName: GET_X| Link[Custom ApolloLink]
586
+ Link -->|matches mock| Resolve["Observable.of #123 data: MOCK_X #125"]
587
+ ```
588
+
589
+ The Storybook mock provider builds a real `ApolloClient` whose only `Link` is a custom one that reads `operation.operationName` and returns:
590
+
591
+ - a never-completing `Observable` (loading)
592
+ - an error `Observable` (errors)
593
+ - `Observable.of({ data: { mockData: <data> } })` (queries)
594
+
595
+ It also builds stub `OdigosApiOperations` whose document operation-names match the slot keys, so when `useApiQuery('GET_GATEWAY_INFO')` fires, the link sees `operationName: 'GET_GATEWAY_INFO'` and routes to the right mock.
596
+
597
+ The kit's hooks therefore go through their **real** Apollo path even in Storybook — same `useQuery` calls, same `transformResult`. Only the network is replaced.
598
+
599
+ ---
600
+
601
+ ## VM / cloud flexibility
602
+
603
+ The `Operation` shape is platform-aware enough to absorb divergent backend schemas without "weird mappings" in the kit:
604
+
605
+ ```typescript
606
+ import { dialects } from '@odigos/vmagent-api-apollo-client/registry';
607
+ import { versionedDocument, vmDialectMap, pickByPlatform } from '@odigos/ui-kit/contexts/odigos-api';
608
+
609
+ const operations: OdigosApiOperations = {
610
+ GET_WORKLOADS: {
611
+ document: versionedDocument({
612
+ [PlatformType.K8s]: { 'v1.20': K8S_GET_WORKLOADS_V1_20, 'v1.16': K8S_GET_WORKLOADS_V1_16 },
613
+ [PlatformType.Vm]: vmDialectMap(dialects, (d) => d.getSourcesV2),
614
+ }),
615
+ transformResult: (raw, ctx) =>
616
+ pickByPlatform(ctx, {
617
+ K8s: () => raw as GetWorkloadsData,
618
+ Vm: () => ({ workloads: mapVmSourceV2ToWorkload(raw) }),
619
+ })?.(),
620
+ },
621
+ };
622
+ ```
623
+
624
+ For multi-client topologies (e.g. a future `vmagent-api-apollo-client` that ships a constructed client), each `Operation` can carry an optional `client?: ApolloClient<unknown>` override. The runner's `pickClient(op, defaultClient)` honors it on every query, mutation, and multi-fetch fan-out.
625
+
626
+ ---
627
+
628
+ ## Diagnose download — kit-owned 2-step flow
629
+
630
+ Some flows need a second, non-GraphQL round-trip after the Apollo step succeeds. Today only the **diagnose archive download** uses this — the GraphQL `diagnose` mutation prepares a tarball in a temp directory, then a REST endpoint streams it back to the browser. Both steps live entirely inside the kit; the host adapter doesn't wire anything beyond the standard `apolloConfig`.
631
+
632
+ `useDescribeApi.downloadDiagnose(formData)` runs as a 2-step async chain:
633
+
634
+ 1. `runQuery(GET_DIAGNOSE, { input: formData, dryRun: false })` — backend builds the bundle and emits SSE progress events to the notification store.
635
+ 2. After step 1 resolves with `stats.fileCount > 0` and `dryRun` is false, the kit GETs `<apolloConfig.httpUrl-origin>/diagnose/download`, re-using the host's existing transport configuration:
636
+ - URL derived via `new URL('/diagnose/download', apolloConfig.httpUrl)` so the GraphQL endpoint can sit at any path (`/graphql`, `/api/graphql`, …) and the diagnose URL still resolves to `<origin>/diagnose/download`.
637
+ - `apolloConfig.credentials` controls cookies (`'same-origin'` vs `'include'`).
638
+ - `apolloConfig.authHeader()` and `apolloConfig.csrfHeader()` are awaited and merged into the request headers — same auth as Apollo's auth link, so rotating tokens stay in sync between the prep and download steps.
639
+ 3. The returned `Blob` is wrapped in an object URL and triggered as a browser download via a hidden `<a download>` element.
640
+
641
+ Step 2 is skipped on dry runs and when the bundle is empty. Step 2 errors are logged but not surfaced — step 1 has already succeeded by then, so a step-2 failure shouldn't roll back progress or fire a confusing toast.
642
+
643
+ Central-ui doesn't map `GET_DIAGNOSE` at all (the SystemDrawer hides the diagnose tab when `describeApi.downloadDiagnose` is `undefined`), so step 2 never fires there. If a future cloud platform needs a different diagnose URL, the kit can grow an `apolloConfig.diagnoseDownloadUrl?: string` override — but until that's a real requirement, the convention "same origin, `/diagnose/download` path" is the contract.
644
+
645
+ ---
646
+
647
+ ## Numbers
648
+
649
+ | | Before | After |
650
+ | ----------------------------------------- | ----------------------------: | ----------------------------: |
651
+ | Apollo clients constructed | 2 (one per host) | 1 (in the kit) |
652
+ | Caching layers for entity lists | 2 (Apollo + `useEntityStore`) | 1 (Apollo's normalized cache) |
653
+ | Per-host CRUD hooks | ~16 each | 0 |
654
+ | `_v2` containers taking API props | 37 | 0 |
655
+ | Webapp pages | 4 with 50+ lines of plumbing | 4 one-liner files |
656
+ | Central-ui pages | 4 with 50+ lines of plumbing | 4 one-liner files |
657
+ | `any` types in kit's API surface | many | 0 |
658
+ | Envelope/bare unions in slot types | 12 | 0 |
659
+ | `eslint-disable` in production code paths | 6+ | 0 |
660
+ | Mock files in `lib/` (production) | n/a | 0 |
661
+ | Mock-data location | `src/mock-data/` | `.storybook/mock-data/` |
662
+ | `useEntityStore` consumers (kit + hosts) | 30+ files | 0 |
663
+ | Lines deleted from host hooks | ~3,000 | — |
664
+
665
+ ---
666
+
667
+ ## Open work / follow-ups
668
+
669
+ - **VM API integration** ([odigos-enterprise/2881](https://github.com/odigos-io/odigos-enterprise/pull/2881), [odigos-enterprise/2836](https://github.com/odigos-io/odigos-enterprise/pull/2836)) — the kit is ready; the central-ui adapter just needs to consume `@odigos/vmagent-api-apollo-client/registry` via `vmDialectMap`.
670
+ - **Workload shape platform-agnosticism** — the kit's `Workload.id.namespace` is required, so VM mappers stuff `'default'` as a placeholder. If/when a third platform lands, this should grow into a discriminated union.
671
+ - **Mixed-version multi-fetch fan-out** — the runner currently groups all proxies under the active `ctx.version`. The legacy `useRemoteFetch.fetchMulti` supported per-proxy-version groups; preserved as a follow-up.
672
+ - **Optimistic instrument-on-form-submit** — `prepareSourcePayloads` no longer patches the cache before the mutation resolves. The user sees the new sources after the post-mutation refetch (~200–500ms instead of instant). If the lag becomes noticeable, reinstate optimism via `client.cache.modify` on `GET_WORKLOADS` — but that requires a per-adapter "modify the canonical list" hook, since central-ui's `REMOTE_FETCH` envelope is opaque to the kit.
673
+ - **Test coverage** — add unit tests for the runner's transform pipeline + the storybook mock link. The current test surface is the two host build pipelines.
674
+
675
+ ---
676
+
677
+ ## Reference
678
+
679
+ - [`src/contexts/odigos-api/types.ts`](../src/contexts/odigos-api/types.ts) — `Operation`, `OdigosApiOperations`, `OperationContext`, `ApolloConfig`
680
+ - [`src/contexts/odigos-api/runner.ts`](../src/contexts/odigos-api/runner.ts) — imperative `runQuery` / `runMutation` / `runMulti`
681
+ - [`src/contexts/odigos-api/use-api-query.ts`](../src/contexts/odigos-api/use-api-query.ts) — declarative hooks
682
+ - [`src/contexts/odigos-api/use-odigos-api.ts`](../src/contexts/odigos-api/use-odigos-api.ts) — namespaced API surface
683
+ - [`src/contexts/odigos-api/platform-helpers.ts`](../src/contexts/odigos-api/platform-helpers.ts) — `pickByPlatform`, `versionedDocument`, `vmDialectMap`
684
+ - [`.storybook/odigos-api-mock-provider.tsx`](../.storybook/odigos-api-mock-provider.tsx) — Apollo-Link-based mock provider
685
+ - [`odigos/frontend/webapp/lib/odigos-api-adapter.tsx`](https://github.com/odigos-io/odigos/blob/main/frontend/webapp/lib/odigos-api-adapter.tsx) — webapp adapter (canonical pattern)
686
+ - [`odigos-enterprise/central-ui/src/lib/odigos-api-adapter.tsx`](https://github.com/odigos-io/odigos-enterprise/blob/main/central-ui/src/lib/odigos-api-adapter.tsx) — central-ui adapter (REMOTE_FETCH wrapped)
687
+ - [`CHANGELOG.md`](../CHANGELOG.md) — `1.0.0` entry with the breaking-change migration guide