openk8s 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +194 -40
  2. package/bin/openk8s.js +2 -0
  3. package/package.json +52 -6
  4. package/src/app/app-state.ts +461 -0
  5. package/src/app/app.tsx +708 -0
  6. package/src/app/components/inspector.tsx +449 -0
  7. package/src/app/components/kind-rows.tsx +66 -0
  8. package/src/app/components/notification-tray.tsx +59 -0
  9. package/src/app/components/overlays/delete-confirm-overlay.tsx +79 -0
  10. package/src/app/components/overlays/helm-rollback-overlay.tsx +86 -0
  11. package/src/app/components/overlays/index.ts +12 -0
  12. package/src/app/components/overlays/logs-dialog.tsx +303 -0
  13. package/src/app/components/overlays/port-forward-overlay.tsx +184 -0
  14. package/src/app/components/overlays/scale-dialog.tsx +96 -0
  15. package/src/app/components/overlays/select-overlay.tsx +68 -0
  16. package/src/app/components/overlays/shared.tsx +18 -0
  17. package/src/app/components/port-forwards-tray.tsx +57 -0
  18. package/src/app/components/resource-rows.tsx +120 -0
  19. package/src/app/hooks/use-app-keyboard.ts +723 -0
  20. package/src/app/hooks/use-app-side-effects.ts +39 -0
  21. package/src/app/hooks/use-clipboard.ts +54 -0
  22. package/src/app/hooks/use-data-fetching.ts +366 -0
  23. package/src/app/hooks/use-log-stream.ts +113 -0
  24. package/src/app/hooks/use-port-forward.ts +149 -0
  25. package/src/app/persistence.ts +44 -0
  26. package/src/app/theme.ts +95 -0
  27. package/src/app/use-polling-tick.ts +27 -0
  28. package/src/app/utils.ts +274 -0
  29. package/src/index.tsx +8 -0
  30. package/src/lib/k8s/k8s-format.ts +42 -0
  31. package/src/lib/k8s/resource-detail-builder.ts +545 -0
  32. package/src/lib/k8s/resource-parser.ts +308 -0
  33. package/src/lib/k8s/types.ts +164 -0
  34. package/src/lib/kubectl/kubectl-service.ts +1116 -0
  35. package/src/lib/kubectl/spawn-utils.ts +81 -0
@@ -0,0 +1,39 @@
1
+ import { useEffect } from "react";
2
+ import type { ChildProcess } from "node:child_process";
3
+
4
+ import type { AppAction } from "../app-state";
5
+ import type { AppState } from "../../lib/k8s/types";
6
+ import type { ResourceRef } from "../../lib/k8s/types";
7
+ import { savePersistedState } from "../persistence";
8
+
9
+ export function useAppSideEffects(
10
+ state: AppState,
11
+ dispatch: React.Dispatch<AppAction>,
12
+ portForwardProcessesRef: React.MutableRefObject<Map<string, ChildProcess>>,
13
+ notificationTimersRef: React.MutableRefObject<Map<string, ReturnType<typeof setTimeout>>>,
14
+ activeResourceRef: ResourceRef | undefined,
15
+ ) {
16
+ // Kill all port-forward processes and cancel notification timers on unmount
17
+ useEffect(() => {
18
+ return () => {
19
+ for (const child of portForwardProcessesRef.current.values()) {
20
+ child.kill("SIGINT");
21
+ }
22
+ portForwardProcessesRef.current.clear();
23
+ for (const timer of notificationTimersRef.current.values()) {
24
+ clearTimeout(timer);
25
+ }
26
+ notificationTimersRef.current.clear();
27
+ };
28
+ }, []);
29
+
30
+ // Persist active context and namespace whenever they change
31
+ useEffect(() => {
32
+ savePersistedState({ activeContext: state.activeContext, activeNamespace: state.activeNamespace });
33
+ }, [state.activeContext, state.activeNamespace]);
34
+
35
+ // Clear selected container when the active resource changes
36
+ useEffect(() => {
37
+ dispatch({ type: "setLogsContainer", container: undefined });
38
+ }, [activeResourceRef?.kind, activeResourceRef?.name, activeResourceRef?.namespace]);
39
+ }
@@ -0,0 +1,54 @@
1
+ import { spawn } from "node:child_process";
2
+ import { useCallback } from "react";
3
+
4
+ import type { AppAction } from "../app-state";
5
+ import type { NotificationTone } from "../../lib/k8s/types";
6
+ import { loadError } from "../utils";
7
+
8
+ export function useClipboard(
9
+ dispatch: React.Dispatch<AppAction>,
10
+ toast: (tone: NotificationTone, message: string) => void,
11
+ ) {
12
+ const copyToClipboard = useCallback((text: string, label?: string): void => {
13
+ let cmd: string;
14
+ let args: string[];
15
+
16
+ if (process.env.WAYLAND_DISPLAY) {
17
+ cmd = "wl-copy";
18
+ args = [];
19
+ } else if (process.env.DISPLAY) {
20
+ cmd = "xclip";
21
+ args = ["-selection", "clipboard"];
22
+ } else if (process.platform === "darwin") {
23
+ cmd = "pbcopy";
24
+ args = [];
25
+ } else {
26
+ dispatch({ type: "setStatusMessage", message: "Clipboard not available on this platform" });
27
+ return;
28
+ }
29
+
30
+ const toastMessage = label ? `${label} copied to clipboard` : "Copied to clipboard";
31
+
32
+ try {
33
+ const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
34
+ child.stdin?.write(text);
35
+ child.stdin?.end();
36
+ child.on("error", () => {
37
+ dispatch({ type: "setStatusMessage", message: `Copy failed — is ${cmd} installed?` });
38
+ });
39
+ child.on("close", (code) => {
40
+ if (code === 0) {
41
+ toast("success", toastMessage);
42
+ }
43
+ });
44
+ } catch {
45
+ dispatch({ type: "setStatusMessage", message: `Copy failed — is ${cmd} installed?` });
46
+ }
47
+ }, [toast, dispatch]);
48
+
49
+ const toastError = useCallback((error: unknown): void => {
50
+ toast("danger", loadError(error).message);
51
+ }, [toast]);
52
+
53
+ return { copyToClipboard, toastError };
54
+ }
@@ -0,0 +1,366 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { Dispatch, SetStateAction } from "react";
3
+
4
+ import type { AppAction } from "../app-state";
5
+ import type { AppState } from "../../lib/k8s/types";
6
+ import type { ResourceKind, ResourceListItem, ResourceRef } from "../../lib/k8s/types";
7
+ import { defaultNamespace, loadError, nextVisibleResourceName } from "../utils";
8
+ import { KubectlService } from "../../lib/kubectl/kubectl-service";
9
+
10
+ export function useDataFetching(
11
+ state: AppState,
12
+ dispatch: React.Dispatch<AppAction>,
13
+ selectedKind: ResourceKind | undefined,
14
+ activeResourceRef: ResourceRef | undefined,
15
+ viewPollTick: number,
16
+ clusterPollTick: number,
17
+ metricsPollTick: number,
18
+ manualRefreshNonce: number,
19
+ filteredResources: ResourceListItem[],
20
+ kubectl: KubectlService,
21
+ setFilterMode: Dispatch<SetStateAction<boolean>>,
22
+ ) {
23
+ const resourcesLengthRef = useRef(state.resources.length);
24
+ resourcesLengthRef.current = state.resources.length;
25
+ const selectedResourceNameRef = useRef(state.selectedResourceName);
26
+ selectedResourceNameRef.current = state.selectedResourceName;
27
+ const selectedResourceDetailRef = useRef(state.selectedResourceDetail);
28
+ selectedResourceDetailRef.current = state.selectedResourceDetail;
29
+ const eventsLengthRef = useRef(state.events.length);
30
+ eventsLengthRef.current = state.events.length;
31
+ const lastLoadedResourceKeyRef = useRef<string | undefined>(undefined);
32
+
33
+ // Clamp visible selection to the filtered list when filter changes
34
+ useEffect(() => {
35
+ const nextSelection = nextVisibleResourceName({
36
+ resources: filteredResources,
37
+ selectedName: state.selectedResourceName,
38
+ });
39
+
40
+ if (nextSelection !== state.selectedResourceName) {
41
+ dispatch({ type: "setSelectedResourceName", name: nextSelection });
42
+ }
43
+ }, [filteredResources, state.selectedResourceName]);
44
+
45
+ // Exit filter mode when leaving the resources pane or opening an overlay
46
+ useEffect(() => {
47
+ if (state.activePane !== "resources" || state.overlay) {
48
+ setFilterMode(false);
49
+ }
50
+ }, [state.activePane, state.overlay]);
51
+
52
+ // Bootstrap: check kubectl availability and load contexts
53
+ useEffect(() => {
54
+ let cancelled = false;
55
+
56
+ async function bootstrap(): Promise<void> {
57
+ dispatch({ type: "setStatusMessage", message: "Checking kubectl" });
58
+ const available = await kubectl.isAvailable();
59
+
60
+ if (cancelled) {
61
+ return;
62
+ }
63
+
64
+ dispatch({ type: "setKubectlAvailable", available });
65
+
66
+ if (!available) {
67
+ dispatch({
68
+ type: "setError",
69
+ error: {
70
+ message: "kubectl is not installed or not on PATH",
71
+ detail: "Install kubectl to enable cluster discovery and resource browsing.",
72
+ },
73
+ });
74
+ dispatch({ type: "setStatusMessage", message: "kubectl unavailable" });
75
+ return;
76
+ }
77
+
78
+ dispatch({ type: "setContextsStatus", status: "loading" });
79
+
80
+ try {
81
+ const contexts = await kubectl.listContexts();
82
+
83
+ if (cancelled) {
84
+ return;
85
+ }
86
+
87
+ const activeContext =
88
+ contexts.find((context) => context.name === state.activeContext)?.name ??
89
+ contexts.find((context) => context.isCurrent)?.name ??
90
+ contexts[0]?.name;
91
+
92
+ dispatch({ type: "setContexts", contexts, activeContext });
93
+ dispatch({ type: "setContextsStatus", status: "ready" });
94
+ dispatch({ type: "setError", error: undefined });
95
+ dispatch({
96
+ type: "setStatusMessage",
97
+ message: activeContext ? `Connected to ${activeContext}` : "No contexts found",
98
+ });
99
+ } catch (error) {
100
+ if (cancelled) {
101
+ return;
102
+ }
103
+
104
+ dispatch({ type: "setContextsStatus", status: "error" });
105
+ dispatch({ type: "setError", error: loadError(error) });
106
+ dispatch({ type: "setStatusMessage", message: "Failed to load contexts" });
107
+ }
108
+ }
109
+
110
+ void bootstrap();
111
+ return () => {
112
+ cancelled = true;
113
+ };
114
+ }, [manualRefreshNonce, state.activeContext]);
115
+
116
+ // Load namespaces and resource kinds whenever the context changes or cluster poll ticks
117
+ useEffect(() => {
118
+ const context = state.activeContext;
119
+
120
+ if (!context || !state.kubectlAvailable) {
121
+ return;
122
+ }
123
+
124
+ const activeContext = context;
125
+ let cancelled = false;
126
+
127
+ async function loadClusterData(): Promise<void> {
128
+ dispatch({ type: "setNamespacesStatus", status: "loading" });
129
+ dispatch({ type: "setResourceKindsStatus", status: "loading" });
130
+
131
+ try {
132
+ const [namespaces, resourceKinds] = await Promise.all([
133
+ kubectl.listNamespaces({ context: activeContext }),
134
+ kubectl.listResourceKinds({ context: activeContext }),
135
+ ]);
136
+
137
+ if (cancelled) {
138
+ return;
139
+ }
140
+
141
+ const nextNamespace = defaultNamespace({ namespaces, currentNamespace: state.activeNamespace });
142
+ const nextSelectedKind =
143
+ resourceKinds.find((kind) => kind.name === state.selectedKind)?.name ?? resourceKinds[0]?.name ?? "pods";
144
+
145
+ dispatch({ type: "setNamespaces", namespaces, activeNamespace: nextNamespace });
146
+ dispatch({ type: "setNamespacesStatus", status: "ready" });
147
+ dispatch({ type: "setResourceKinds", resourceKinds, selectedKind: nextSelectedKind });
148
+ dispatch({ type: "setResourceKindsStatus", status: "ready" });
149
+ dispatch({ type: "setError", error: undefined });
150
+ } catch (error) {
151
+ if (cancelled) {
152
+ return;
153
+ }
154
+
155
+ dispatch({ type: "setNamespacesStatus", status: "error" });
156
+ dispatch({ type: "setResourceKindsStatus", status: "error" });
157
+ dispatch({ type: "setError", error: loadError(error) });
158
+ dispatch({ type: "setStatusMessage", message: "Failed to load cluster metadata" });
159
+ }
160
+ }
161
+
162
+ void loadClusterData();
163
+ return () => {
164
+ cancelled = true;
165
+ };
166
+ }, [
167
+ state.activeContext,
168
+ state.kubectlAvailable,
169
+ clusterPollTick,
170
+ manualRefreshNonce,
171
+ state.activeNamespace,
172
+ state.selectedKind,
173
+ ]);
174
+
175
+ // Load the resource list. Bug-fix: removed state.resources.length and state.selectedResourceName
176
+ // from deps (they caused re-fetches on every load/cursor-move). Read via refs instead.
177
+ useEffect(() => {
178
+ const context = state.activeContext;
179
+
180
+ if (!context || !selectedKind) {
181
+ return;
182
+ }
183
+
184
+ const activeContext = context;
185
+ const activeKind = selectedKind;
186
+ const activeNamespace = state.activeNamespace;
187
+ let cancelled = false;
188
+
189
+ async function loadResources(): Promise<void> {
190
+ const resourceKey = `${activeContext}:${activeNamespace}:${activeKind.name}`;
191
+ if (lastLoadedResourceKeyRef.current !== resourceKey && resourcesLengthRef.current > 0) {
192
+ dispatch({ type: "setResourcesStatus", status: "loading" });
193
+ }
194
+
195
+ try {
196
+ const resources = await kubectl.listResources({
197
+ context: activeContext,
198
+ namespace: activeNamespace,
199
+ resourceKind: activeKind,
200
+ });
201
+
202
+ if (cancelled) {
203
+ return;
204
+ }
205
+
206
+ lastLoadedResourceKeyRef.current = resourceKey;
207
+
208
+ const preservedSelection = resources.find(
209
+ (resource) => resource.ref.name === selectedResourceNameRef.current,
210
+ )?.ref.name;
211
+
212
+ dispatch({
213
+ type: "setResources",
214
+ resources,
215
+ selectedResourceName: preservedSelection ?? resources[0]?.ref.name,
216
+ });
217
+ dispatch({ type: "setResourcesStatus", status: "ready" });
218
+ dispatch({ type: "setError", error: undefined });
219
+ } catch (error) {
220
+ if (cancelled) {
221
+ return;
222
+ }
223
+
224
+ dispatch({ type: "setResourcesStatus", status: "error" });
225
+ dispatch({ type: "setError", error: loadError(error) });
226
+ dispatch({ type: "setStatusMessage", message: `Failed to load ${activeKind.name}` });
227
+ }
228
+ }
229
+
230
+ void loadResources();
231
+ return () => {
232
+ cancelled = true;
233
+ };
234
+ }, [selectedKind, state.activeContext, state.activeNamespace, viewPollTick, manualRefreshNonce]);
235
+
236
+ // Load detail for the selected resource. Bug-fix: removed state.selectedResourceDetail from deps
237
+ // (it caused an infinite re-fetch loop). Read via ref instead.
238
+ useEffect(() => {
239
+ const context = state.activeContext;
240
+ const selectedName = state.selectedResourceName;
241
+
242
+ if (!context || !selectedName || !selectedKind) {
243
+ return;
244
+ }
245
+
246
+ const activeContext = context;
247
+ const activeKind = selectedKind;
248
+ const activeName = selectedName;
249
+ const activeNamespace = state.activeNamespace;
250
+ let cancelled = false;
251
+
252
+ async function loadDetail(): Promise<void> {
253
+ const currentDetail = selectedResourceDetailRef.current;
254
+ const isSameDetail =
255
+ currentDetail?.ref.name === activeName &&
256
+ currentDetail?.ref.kind === activeKind.name &&
257
+ (currentDetail?.ref.namespace ?? "") === activeNamespace;
258
+
259
+ if (!isSameDetail) {
260
+ dispatch({ type: "setSelectedResourceDetailStatus", status: "loading" });
261
+ }
262
+
263
+ try {
264
+ const detail = await kubectl.getResourceDetail({
265
+ context: activeContext,
266
+ namespace: activeNamespace,
267
+ resourceKind: activeKind,
268
+ name: activeName,
269
+ });
270
+
271
+ if (cancelled) {
272
+ return;
273
+ }
274
+
275
+ dispatch({ type: "setSelectedResourceDetail", detail });
276
+ dispatch({ type: "setSelectedResourceDetailStatus", status: "ready" });
277
+ dispatch({ type: "setError", error: undefined });
278
+ } catch (error) {
279
+ if (cancelled) {
280
+ return;
281
+ }
282
+
283
+ dispatch({ type: "setSelectedResourceDetailStatus", status: "error" });
284
+ dispatch({ type: "setError", error: loadError(error) });
285
+ }
286
+ }
287
+
288
+ void loadDetail();
289
+ return () => {
290
+ cancelled = true;
291
+ };
292
+ }, [selectedKind, state.activeContext, state.activeNamespace, state.selectedResourceName, viewPollTick, manualRefreshNonce]);
293
+
294
+ // Load events for the active resource. Bug-fix: removed state.events.length from deps
295
+ // (it caused re-fetches after every successful load). Read via ref instead.
296
+ useEffect(() => {
297
+ const context = state.activeContext;
298
+
299
+ if (!context || !activeResourceRef || !selectedKind || state.inspectorTab !== "events") {
300
+ return;
301
+ }
302
+
303
+ const activeContext = context;
304
+ const activeResource = activeResourceRef;
305
+ const activeKind = selectedKind;
306
+ const activeNamespace = state.activeNamespace;
307
+ let cancelled = false;
308
+
309
+ async function loadEvents(): Promise<void> {
310
+ if (eventsLengthRef.current === 0) {
311
+ dispatch({ type: "setEventsStatus", status: "loading" });
312
+ }
313
+
314
+ try {
315
+ const events = await kubectl.listResourceEvents({
316
+ context: activeContext,
317
+ namespace: activeNamespace,
318
+ resourceRef: activeResource,
319
+ namespaced: activeKind.namespaced,
320
+ });
321
+
322
+ if (cancelled) {
323
+ return;
324
+ }
325
+
326
+ dispatch({ type: "setEvents", events });
327
+ dispatch({ type: "setEventsStatus", status: "ready" });
328
+ dispatch({ type: "setError", error: undefined });
329
+ } catch (error) {
330
+ if (cancelled) {
331
+ return;
332
+ }
333
+
334
+ dispatch({ type: "setEventsStatus", status: "error" });
335
+ dispatch({ type: "setError", error: loadError(error) });
336
+ }
337
+ }
338
+
339
+ void loadEvents();
340
+ return () => {
341
+ cancelled = true;
342
+ };
343
+ }, [activeResourceRef, selectedKind, state.activeContext, state.activeNamespace, state.inspectorTab, viewPollTick, manualRefreshNonce]);
344
+
345
+ // Poll pod metrics when viewing pods (silent degradation on error)
346
+ useEffect(() => {
347
+ const context = state.activeContext;
348
+ if (!context || state.selectedKind !== "pods") return;
349
+ void kubectl
350
+ .getPodMetrics({ context, namespace: state.activeNamespace })
351
+ .then((metrics) => dispatch({ type: "setPodMetrics", metrics }))
352
+ .catch(() => {/* metrics-server absent — silent degradation */});
353
+ }, [state.activeContext, state.activeNamespace, state.selectedKind, metricsPollTick]);
354
+
355
+ // Poll node metrics when viewing nodes (silent degradation on error)
356
+ useEffect(() => {
357
+ const context = state.activeContext;
358
+ if (!context || state.selectedKind !== "nodes") return;
359
+ void kubectl
360
+ .getNodeMetrics({ context })
361
+ .then((metrics) => dispatch({ type: "setNodeMetrics", metrics }))
362
+ .catch(() => {/* metrics-server absent — silent degradation */});
363
+ }, [state.activeContext, state.selectedKind, metricsPollTick]);
364
+
365
+ return { resourcesLengthRef, selectedResourceNameRef, selectedResourceDetailRef, eventsLengthRef };
366
+ }
@@ -0,0 +1,113 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { ChildProcess } from "node:child_process";
3
+
4
+ import type { AppAction } from "../app-state";
5
+ import type { AppState, LoadStatus } from "../../lib/k8s/types";
6
+ import { loadError } from "../utils";
7
+ import { KubectlService } from "../../lib/kubectl/kubectl-service";
8
+
9
+ export function useLogStream(
10
+ dispatch: React.Dispatch<AppAction>,
11
+ state: Pick<AppState, "overlay" | "activeContext" | "activeNamespace" | "logsContainer" | "logsOptions">,
12
+ activeLogsRef: { kind: string; name: string; namespaced: boolean } | undefined,
13
+ activeLogsTarget: string | undefined,
14
+ activeLogsKey: string | undefined,
15
+ kubectl: KubectlService,
16
+ ) {
17
+ const logStreamRef = useRef<ChildProcess | undefined>(undefined);
18
+
19
+ useEffect(() => {
20
+ const context = state.activeContext;
21
+
22
+ if (state.overlay !== "logs" || !context || !activeLogsTarget || !activeLogsRef || !activeLogsKey) {
23
+ if (logStreamRef.current) {
24
+ logStreamRef.current.kill();
25
+ logStreamRef.current = undefined;
26
+ }
27
+ return;
28
+ }
29
+
30
+ let cancelled = false;
31
+ let flushTimer: ReturnType<typeof setTimeout> | undefined;
32
+
33
+ try {
34
+ const { target, child } = kubectl.streamLogs({
35
+ context,
36
+ namespace: state.activeNamespace,
37
+ resourceRef: activeLogsRef,
38
+ namespaced: activeLogsRef.namespaced,
39
+ container: state.logsContainer,
40
+ logOptions: state.logsOptions,
41
+ });
42
+
43
+ if (logStreamRef.current) {
44
+ logStreamRef.current.kill();
45
+ }
46
+
47
+ logStreamRef.current = child;
48
+
49
+ let buffer = "";
50
+
51
+ const flush = (status: LoadStatus): void => {
52
+ if (cancelled) {
53
+ return;
54
+ }
55
+ dispatch({ type: "setLogsData", logsTarget: target, logsText: buffer, logsStatus: status });
56
+ };
57
+
58
+ const scheduleFlush = (status: LoadStatus): void => {
59
+ if (flushTimer) {
60
+ return;
61
+ }
62
+ flushTimer = setTimeout(() => {
63
+ flushTimer = undefined;
64
+ flush(status);
65
+ }, 80);
66
+ };
67
+
68
+ dispatch({ type: "setLogsData", logsTarget: target, logsText: "", logsStatus: "loading" });
69
+
70
+ child.stdout?.on("data", (chunk: Buffer | string) => {
71
+ if (cancelled) return;
72
+ buffer += chunk.toString();
73
+ scheduleFlush("ready");
74
+ });
75
+
76
+ child.stderr?.on("data", (chunk: Buffer | string) => {
77
+ if (cancelled) return;
78
+ buffer += chunk.toString();
79
+ scheduleFlush("error");
80
+ });
81
+
82
+ child.on("close", () => {
83
+ if (cancelled) return;
84
+ if (flushTimer) {
85
+ clearTimeout(flushTimer);
86
+ flushTimer = undefined;
87
+ }
88
+ flush("ready");
89
+ });
90
+ } catch (error) {
91
+ const parsed = loadError(error);
92
+ dispatch({
93
+ type: "setLogsData",
94
+ logsTarget: `${activeLogsRef.kind}/${activeLogsRef.name}`,
95
+ logsText: parsed.detail ? `${parsed.message}\n${parsed.detail}` : parsed.message,
96
+ logsStatus: "error",
97
+ });
98
+ }
99
+
100
+ return () => {
101
+ cancelled = true;
102
+ if (flushTimer) {
103
+ clearTimeout(flushTimer);
104
+ }
105
+ if (logStreamRef.current) {
106
+ logStreamRef.current.kill();
107
+ logStreamRef.current = undefined;
108
+ }
109
+ };
110
+ }, [state.overlay, activeLogsKey, state.activeContext, state.activeNamespace, activeLogsRef, activeLogsTarget]);
111
+
112
+ return logStreamRef;
113
+ }