@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.
- package/CHANGELOG.md +14 -0
- package/docs/api-context.md +687 -0
- package/lib/chunks/connections-scope-CneIqjeg.js +1 -0
- package/lib/chunks/flow-CQi_KYfs.js +49 -0
- package/lib/chunks/helpers-K1tnyqhm.js +19 -0
- package/lib/chunks/{index-CRKgHzab.js → index-Dv0RvKAp.js} +8 -8
- package/lib/chunks/source-instrument-form-context-C79I_5cH.js +1 -0
- package/lib/chunks/{ui-components-DODwSdKu.js → ui-components-BKrLf2tx.js} +214 -214
- package/lib/chunks/use-odigos-api-CDHW7ekQ.js +5 -0
- package/lib/chunks/vendor-BFqT13Me.js +1 -0
- package/lib/components/_v2/segment/index.d.ts +4 -2
- package/lib/components/v2.js +1 -1
- package/lib/components.js +1 -1
- package/lib/constants/strings/index.d.ts +7 -0
- package/lib/constants.js +1 -1
- package/lib/containers/_v2/_drawers/add-action-drawer/index.d.ts +0 -2
- package/lib/containers/_v2/_drawers/add-destination-drawer/form-column/index.d.ts +1 -5
- package/lib/containers/_v2/_drawers/add-destination-drawer/index.d.ts +0 -6
- package/lib/containers/_v2/_drawers/add-destination-drawer/lists-filters/index.d.ts +2 -1
- package/lib/containers/_v2/_drawers/add-rule-drawer/index.d.ts +0 -2
- package/lib/containers/_v2/_drawers/add-source-drawer/index.d.ts +1 -4
- package/lib/containers/_v2/_drawers/edit-action-drawer/index.d.ts +0 -3
- package/lib/containers/_v2/_drawers/edit-config-drawer/index.d.ts +1 -3
- package/lib/containers/_v2/_drawers/edit-destination-drawer/index.d.ts +0 -5
- package/lib/containers/_v2/_drawers/edit-rule-drawer/index.d.ts +0 -3
- package/lib/containers/_v2/_drawers/edit-source-drawer/debug/index.d.ts +1 -2
- package/lib/containers/_v2/_drawers/edit-source-drawer/index.d.ts +3 -14
- package/lib/containers/_v2/_drawers/edit-source-drawer/overview/containers-card/index.d.ts +1 -2
- package/lib/containers/_v2/_drawers/edit-source-drawer/overview/index.d.ts +0 -2
- package/lib/containers/_v2/_drawers/edit-source-drawer/peers/index.d.ts +1 -6
- package/lib/containers/_v2/_drawers/edit-source-drawer/profiling/index.d.ts +1 -15
- package/lib/containers/_v2/_drawers/edit-source-drawer/sampling/index.d.ts +8 -0
- package/lib/containers/_v2/_drawers/edit-source-drawer/sampling/rule-row.d.ts +8 -0
- package/lib/containers/_v2/_drawers/edit-source-drawer/tabs.d.ts +1 -0
- package/lib/containers/_v2/_drawers/system-drawer/describe/index.d.ts +1 -8
- package/lib/containers/_v2/_drawers/system-drawer/diagnose/index.d.ts +1 -4
- package/lib/containers/_v2/_drawers/system-drawer/index.d.ts +1 -4
- package/lib/containers/_v2/_drawers/system-drawer/tokens/index.d.ts +1 -2
- package/lib/containers/_v2/_forms/add-destination-form/index.d.ts +1 -2
- package/lib/containers/_v2/_modals/add-data-stream-modal/index.d.ts +0 -3
- package/lib/containers/_v2/_modals/edit-data-stream-modal/index.d.ts +0 -2
- package/lib/containers/_v2/_modals/sampling-onboarding-modal/types.d.ts +1 -4
- package/lib/containers/_v2/central-connections/index.d.ts +1 -12
- package/lib/containers/_v2/onboarding/index.d.ts +0 -8
- package/lib/containers/_v2/overview/columns/column/head.d.ts +1 -1
- package/lib/containers/_v2/overview/columns/column/index.d.ts +2 -1
- package/lib/containers/_v2/overview/columns/index.d.ts +1 -8
- package/lib/containers/_v2/overview/data-stream-select/index.d.ts +1 -10
- package/lib/containers/_v2/overview/drawers/index.d.ts +2 -29
- package/lib/containers/_v2/overview/index.d.ts +2 -8
- package/lib/containers/_v2/overview/multi-source-control/index.d.ts +1 -5
- package/lib/containers/_v2/pipeline-collectors/index.d.ts +0 -7
- package/lib/containers/_v2/pipeline-collectors/pods-info/index.d.ts +1 -2
- package/lib/containers/_v2/pipeline-collectors/pods-info/pod-drawer/index.d.ts +1 -2
- package/lib/containers/_v2/sampling-rules/helpers.d.ts +2 -0
- package/lib/containers/_v2/sampling-rules/types.d.ts +2 -16
- package/lib/containers/_v2/settings/index.d.ts +0 -9
- package/lib/containers/service-map/index.d.ts +4 -2
- package/lib/containers/v2.js +197 -166
- package/lib/containers.js +7 -7
- package/lib/contexts/index.d.ts +1 -0
- package/lib/contexts/odigos-api/capabilities.d.ts +38 -0
- package/lib/contexts/odigos-api/client.d.ts +22 -0
- package/lib/contexts/odigos-api/connections-scope.d.ts +18 -0
- package/lib/contexts/odigos-api/hooks/use-actions-api.d.ts +26 -0
- package/lib/contexts/odigos-api/hooks/use-collectors-api.d.ts +25 -0
- package/lib/contexts/odigos-api/hooks/use-config-api.d.ts +24 -0
- package/lib/contexts/odigos-api/hooks/use-data-streams-api.d.ts +26 -0
- package/lib/contexts/odigos-api/hooks/use-describe-api.d.ts +33 -0
- package/lib/contexts/odigos-api/hooks/use-destinations-api.d.ts +50 -0
- package/lib/contexts/odigos-api/hooks/use-instrumentation-rules-api.d.ts +35 -0
- package/lib/contexts/odigos-api/hooks/use-k8s-manifest-api.d.ts +14 -0
- package/lib/contexts/odigos-api/hooks/use-metrics-api.d.ts +13 -0
- package/lib/contexts/odigos-api/hooks/use-namespace-api.d.ts +30 -0
- package/lib/contexts/odigos-api/hooks/use-profiling-api.d.ts +17 -0
- package/lib/contexts/odigos-api/hooks/use-sampling-api.d.ts +39 -0
- package/lib/contexts/odigos-api/hooks/use-service-map-api.d.ts +13 -0
- package/lib/contexts/odigos-api/hooks/use-snapshots-api.d.ts +13 -0
- package/lib/contexts/odigos-api/hooks/use-sources-api.d.ts +75 -0
- package/lib/contexts/odigos-api/hooks/use-tokens-api.d.ts +12 -0
- package/lib/contexts/odigos-api/index.d.ts +37 -0
- package/lib/contexts/odigos-api/internal/notify.d.ts +49 -0
- package/lib/contexts/odigos-api/internal/source-payloads.d.ts +53 -0
- package/lib/contexts/odigos-api/platform-helpers.d.ts +98 -0
- package/lib/contexts/odigos-api/provider.d.ts +28 -0
- package/lib/contexts/odigos-api/runner.d.ts +32 -0
- package/lib/contexts/odigos-api/types.d.ts +831 -0
- package/lib/contexts/odigos-api/use-api-for-connections.d.ts +43 -0
- package/lib/contexts/odigos-api/use-api-query.d.ts +130 -0
- package/lib/contexts/odigos-api/use-odigos-api.d.ts +103 -0
- package/lib/contexts/odigos-api.js +1 -0
- package/lib/contexts/odigos-context.d.ts +2 -2
- package/lib/contexts/storybook-context.d.ts +2 -2
- package/lib/contexts.js +1 -1
- package/lib/functions.js +1 -1
- package/lib/hooks.js +1 -1
- package/lib/icons/destinations/open-telemetry-logo/index.d.ts +2 -1
- package/lib/snippets/_v2/sampling-rule-form-sections/preset-with-custom-input.d.ts +1 -1
- package/lib/snippets/v2.js +1 -1
- package/lib/snippets.js +1 -49
- package/lib/store/index.d.ts +0 -1
- package/lib/store/useDrawerStore.d.ts +6 -1
- package/lib/store.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/types/actions/index.d.ts +0 -5
- package/lib/types/common/index.d.ts +29 -4
- package/lib/types/destinations/index.d.ts +0 -10
- package/lib/types/index.d.ts +0 -1
- package/lib/types/instrumentation-rules/index.d.ts +0 -5
- package/lib/types/namespaces/index.d.ts +0 -5
- package/lib/types/pipeline-collectors/index.d.ts +0 -5
- package/lib/types/snapshot/index.d.ts +0 -2
- package/lib/types/sources/index.d.ts +0 -30
- package/lib/types/tokens/index.d.ts +0 -6
- package/lib/types/workload/index.d.ts +46 -22
- package/lib/types.js +1 -1
- package/lib/visuals.js +1 -1
- package/package.json +31 -22
- package/lib/chunks/helpers-IEFhGXc7.js +0 -19
- package/lib/chunks/source-instrument-form-context-DjIHPYQJ.js +0 -1
- package/lib/chunks/vendor-Be0cOokr.js +0 -1
- package/lib/mock-data/actions/index.d.ts +0 -2
- package/lib/mock-data/config/index.d.ts +0 -3
- package/lib/mock-data/connections/index.d.ts +0 -2
- package/lib/mock-data/describe/index.d.ts +0 -3
- package/lib/mock-data/destinations/index.d.ts +0 -4
- package/lib/mock-data/index.d.ts +0 -15
- package/lib/mock-data/instrumentation-instances/index.d.ts +0 -2
- package/lib/mock-data/instrumentation-rules/index.d.ts +0 -2
- package/lib/mock-data/namespaces/index.d.ts +0 -2
- package/lib/mock-data/pipeline-collectors/index.d.ts +0 -6
- package/lib/mock-data/profiling/index.d.ts +0 -8
- package/lib/mock-data/service-map/index.d.ts +0 -4
- package/lib/mock-data/snapshot/index.d.ts +0 -5
- package/lib/mock-data/sources/index.d.ts +0 -4
- package/lib/mock-data/sources/yamls.d.ts +0 -22
- package/lib/mock-data/tokens/index.d.ts +0 -2
- package/lib/mock-data/traces/index.d.ts +0 -2
- package/lib/store/useEntityStore.d.ts +0 -24
- 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
|