From 2384e4ea1f31a4bfe543b268fcf73589dc3e0740 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Tue, 29 Oct 2024 10:18:35 -0500 Subject: [PATCH 01/45] WIP --- src/pkg/api/handlers.go | 33 ++++++ src/pkg/api/rest/filter.go | 1 + src/pkg/api/start.go | 1 + ui/src/app.postcss | 2 +- .../components/k8s/DataTable/component.svelte | 6 +- .../components/k8s/Drawer/component.svelte | 16 ++- .../workloads/pods/logs/+page.svelte | 112 ++++++++++++++++++ ui/src/routes/+layout.svelte | 2 +- ui/src/routes/+layout.ts | 5 +- ui/src/routes/auth.test.ts | 15 +++ 10 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 ui/src/routes/(resources)/workloads/pods/logs/+page.svelte diff --git a/src/pkg/api/handlers.go b/src/pkg/api/handlers.go index 58709b86d..d12c3f967 100644 --- a/src/pkg/api/handlers.go +++ b/src/pkg/api/handlers.go @@ -136,6 +136,39 @@ func getPod(cache *resources.Cache) func(w http.ResponseWriter, r *http.Request) return rest.Bind(cache.Pods) } +// @Description Get Pod logs +// @Tags workloads +// @Accept html +// @Produce json +// @Success 200 +// @Router /api/v1/resources/workloads/pods/logs [get] +// @Param pod query string true "Pod name" +// @Param namespace query string true "Namespace" +func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // todo: guard against accessing logs for pods that shouldn't be accessed? + // todo: test back/forward button, etc + pod := r.URL.Query().Get("pod") + namespace := r.URL.Query().Get("namespace") + if pod == "" || namespace == "" { + http.Error(w, "Pod and Namespace are required", http.StatusBadRequest) + return + } + + // return fake logs for now + logs := []string{ + "2024-01-01T00:00:00Z [INFO] This is a log message", + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(logs) + if err != nil { + return + } + } +} + // @Description Get Deployments // @Tags workloads // @Accept html diff --git a/src/pkg/api/rest/filter.go b/src/pkg/api/rest/filter.go index d12c2f6ce..25fdc0304 100644 --- a/src/pkg/api/rest/filter.go +++ b/src/pkg/api/rest/filter.go @@ -28,6 +28,7 @@ func jsonMarshal(payload any, fieldsList []string) ([]byte, error) { // Handle multiple resources case []unstructured.Unstructured: filteredItems := filterItemsByFields(payload, fieldsList) + // todo: this line fails sometimes with: fatal error: concurrent map iteration and map write data, err = json.Marshal(filteredItems) default: diff --git a/src/pkg/api/start.go b/src/pkg/api/start.go index 72999f00d..18444a6cc 100644 --- a/src/pkg/api/start.go +++ b/src/pkg/api/start.go @@ -93,6 +93,7 @@ func Setup(assets *embed.FS) (*chi.Mux, bool, error) { r.Route("/workloads", func(r chi.Router) { r.Get("/pods", withLatestCache(k8sSession, getPods)) r.Get("/pods/{uid}", withLatestCache(k8sSession, getPod)) + r.Get("/pods/logs", getPodLogs(k8sSession)) r.Get("/deployments", withLatestCache(k8sSession, getDeployments)) r.Get("/deployments/{uid}", withLatestCache(k8sSession, getDeployment)) diff --git a/ui/src/app.postcss b/ui/src/app.postcss index b29389486..50f712429 100644 --- a/ui/src/app.postcss +++ b/ui/src/app.postcss @@ -58,7 +58,7 @@ } .table-section { - @apply flex h-full flex-col bg-gray-50 dark:bg-gray-900; + @apply flex flex-grow h-[65%] flex-shrink-0 flex-col bg-gray-50 dark:bg-gray-900; .table-container { @apply flex h-full w-full flex-col; diff --git a/ui/src/lib/components/k8s/DataTable/component.svelte b/ui/src/lib/components/k8s/DataTable/component.svelte index 2f57e210e..0cc9f95ed 100644 --- a/ui/src/lib/components/k8s/DataTable/component.svelte +++ b/ui/src/lib/components/k8s/DataTable/component.svelte @@ -271,7 +271,11 @@ - !disableRowClick && row.resource.metadata?.uid && goto(`${baseURL}/${row.resource.metadata?.uid}`)} + !disableRowClick && + row.resource.metadata?.uid && + goto( + `${baseURL.includes('/logs') ? baseURL.replace('/logs', '') : baseURL}/${row.resource.metadata?.uid}`, + )} class:active={row.resource.metadata?.uid && pathName.includes(row.resource.metadata?.uid ?? '')} class:cursor-pointer={!disableRowClick} > diff --git a/ui/src/lib/components/k8s/Drawer/component.svelte b/ui/src/lib/components/k8s/Drawer/component.svelte index 8d9d159e4..f63242ca5 100644 --- a/ui/src/lib/components/k8s/Drawer/component.svelte +++ b/ui/src/lib/components/k8s/Drawer/component.svelte @@ -18,7 +18,7 @@ export let resource: KubernetesObject export let baseURL: string - type Tab = 'metadata' | 'yaml' | 'events' + type Tab = 'metadata' | 'yaml' | 'events' | 'logs' let events: CoreV1Event[] = [] @@ -128,6 +128,9 @@
  • +
  • + +
  • @@ -184,6 +187,17 @@ {@html DOMPurify.sanitize(hljs.highlight(YAML.stringify(resource), { language: 'yaml' }).value)} + {:else if activeTab === 'logs'} + + {/if} diff --git a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte new file mode 100644 index 000000000..9cf7acbc6 --- /dev/null +++ b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte @@ -0,0 +1,112 @@ + + + + + +
    + + +
    +
    +
    + +
    +
    +
    + + Pod Logs +
    + + + + + +
    + + +
    + +
    + +
    + + + + +
    +
    + +
    + +
    +
    +
    +
    +
    diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index d1c0f500e..51ebf88e8 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -56,7 +56,7 @@ ? 'md:ml-64' : 'md:ml-16'}" > -
    +
    diff --git a/ui/src/routes/+layout.ts b/ui/src/routes/+layout.ts index d8699d6fa..20523d975 100644 --- a/ui/src/routes/+layout.ts +++ b/ui/src/routes/+layout.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial import { authenticated } from '$features/auth/store' -import { createStore } from '$features/k8s/namespaces/store' +import { createStore as createNamespaceStore } from '$features/k8s/namespaces/store' import type { UserData } from '$features/navigation/types' export const ssr = false @@ -55,7 +55,8 @@ async function auth(token: string): Promise { // load namespace and auth data before rendering the app export const load = async () => { - const namespaces = createStore() + const namespaces = createNamespaceStore() + const url = new URL(window.location.href) const localAuthToken = url.searchParams.get('token') || '' let userData: UserData = { diff --git a/ui/src/routes/auth.test.ts b/ui/src/routes/auth.test.ts index 53e4bcf12..11b325429 100644 --- a/ui/src/routes/auth.test.ts +++ b/ui/src/routes/auth.test.ts @@ -12,6 +12,11 @@ vi.mock('$features/k8s/namespaces/store', () => ({ start: vi.fn(), })), })) +vi.mock('$features/k8s/workloads/pods/store', () => ({ + createStore: vi.fn(() => ({ + start: vi.fn(), + })), +})) vi.mock('$features/auth/store', () => ({ authenticated: { set: vi.fn(), @@ -71,11 +76,13 @@ describe('load function', () => { // Verify store operations expect(result.namespaces.start).toHaveBeenCalled() + expect(result.pods.start).toHaveBeenCalled() expect(authenticated.set).toHaveBeenCalledWith(true) // Verify return value expect(result).toEqual({ namespaces: expect.any(Object), + pods: expect.any(Object), userData: { name: '', preferredUsername: '', @@ -111,11 +118,14 @@ describe('load function', () => { // Verify namespaces was called expect(result.namespaces.start).toHaveBeenCalled() + expect(result.pods.start).toHaveBeenCalled() + expect(authenticated.set).toHaveBeenCalledWith(true) // Verify return value expect(result).toEqual({ namespaces: expect.any(Object), + pods: expect.any(Object), userData: { name: 'Doug Unicorn', preferredUsername: 'doug@defenseunicorns.com', @@ -136,11 +146,13 @@ describe('load function', () => { // Verify namespaces wasn't started and authenticated state was set to false expect(result.namespaces.start).not.toHaveBeenCalled() + expect(result.pods.start).not.toHaveBeenCalled() expect(authenticated.set).toHaveBeenCalledWith(false) // Verify return value expect(result).toEqual({ namespaces: expect.any(Object), + pods: expect.any(Object), userData: { name: '', preferredUsername: '', @@ -163,11 +175,13 @@ describe('load function', () => { // Verify namespaces wasn't started authenticated state was set to false expect(result.namespaces.start).not.toHaveBeenCalled() + expect(result.pods.start).not.toHaveBeenCalled() expect(authenticated.set).toHaveBeenCalledWith(false) // Verify return value expect(result).toEqual({ namespaces: expect.any(Object), + pods: expect.any(Object), userData: { name: '', preferredUsername: '', @@ -193,6 +207,7 @@ describe('load function', () => { // Verify return value expect(result).toEqual({ namespaces: expect.any(Object), + pods: expect.any(Object), userData: { name: '', preferredUsername: '', From b2502bed3e462c9c512b03874b7212d8d335e578 Mon Sep 17 00:00:00 2001 From: UncleGedd <42304551+UncleGedd@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:46:57 -0500 Subject: [PATCH 02/45] Update src/pkg/api/handlers.go Co-authored-by: Clint --- src/pkg/api/handlers.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pkg/api/handlers.go b/src/pkg/api/handlers.go index d12c3f967..d80f54704 100644 --- a/src/pkg/api/handlers.go +++ b/src/pkg/api/handlers.go @@ -161,9 +161,10 @@ func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *h } w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) err := json.NewEncoder(w).Encode(logs) if err != nil { + slog.Error("failed to encode getPodLog response", "error", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } } From 92ca73ce5dda9d3f28cf397859b7b9d1c1008627 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Tue, 29 Oct 2024 17:12:03 -0500 Subject: [PATCH 03/45] WIP: making progress on dropdown --- .../workloads/pods/logs/+page.svelte | 120 ++++++++++++++---- 1 file changed, 92 insertions(+), 28 deletions(-) diff --git a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte index 9cf7acbc6..4bb1f92e8 100644 --- a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte +++ b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte @@ -2,47 +2,100 @@ @@ -54,21 +107,33 @@
    -
    +
    - Pod Logs + Logs
    + + + +
    - -
    - -
    - -
    - - - + +
    +
    - -
    - + + +
    + + +
    +
    + + +
    + + p.metadata.name)} + id="pod-dropdown" + width="w-48" + /> + c.name)} + id="container-dropdown" + width="w-48" + /> +
    + +
    +
    + {#each Array.from({ length: 100 }) as _, i} +
    [2021-10-01T12:00:00Z] Log line {i}
    + {/each}
    From 03263ad5a40f0347b3105afc927d69b8eb998350 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Thu, 31 Oct 2024 15:46:15 -0500 Subject: [PATCH 07/45] WIP: ugly code but working pretty well --- src/pkg/api/handlers.go | 112 ++++++++++++++++-- src/pkg/api/rest/filter.go | 6 +- .../components/AnsiDisplay/component.svelte | 53 +++++++-- ui/src/lib/components/index.ts | 1 + .../workloads/pods/logs/+page.svelte | 70 ++++++++--- 5 files changed, 201 insertions(+), 41 deletions(-) diff --git a/src/pkg/api/handlers.go b/src/pkg/api/handlers.go index d80f54704..72a735aab 100644 --- a/src/pkg/api/handlers.go +++ b/src/pkg/api/handlers.go @@ -4,9 +4,15 @@ package api import ( + "bufio" + "context" "encoding/json" + "errors" + "fmt" + "io" "log/slog" "net/http" + "strings" "time" "github.com/defenseunicorns/uds-runtime/src/pkg/api/auth" @@ -14,6 +20,8 @@ import ( "github.com/defenseunicorns/uds-runtime/src/pkg/api/resources" "github.com/defenseunicorns/uds-runtime/src/pkg/api/rest" "github.com/defenseunicorns/uds-runtime/src/pkg/k8s/session" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // @Description Get Nodes @@ -144,29 +152,113 @@ func getPod(cache *resources.Cache) func(w http.ResponseWriter, r *http.Request) // @Router /api/v1/resources/workloads/pods/logs [get] // @Param pod query string true "Pod name" // @Param namespace query string true "Namespace" +// todo: guard against accessing logs for pods that shouldn't be accessed? +// todo: test back/forward button, etc +// todo: rate limiting? +// todo: error retry logic? func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - // todo: guard against accessing logs for pods that shouldn't be accessed? - // todo: test back/forward button, etc + // Get query parameters pod := r.URL.Query().Get("pod") namespace := r.URL.Query().Get("namespace") - if pod == "" || namespace == "" { + container := r.URL.Query().Get("container") + if pod == "" || namespace == "" || container == "" { http.Error(w, "Pod and Namespace are required", http.StatusBadRequest) return } - // return fake logs for now - logs := []string{ - "2024-01-01T00:00:00Z [INFO] This is a log message", + // Set headers for SSE + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + ctx := r.Context() + + flusher, ok := w.(http.Flusher) + + // Get existing logs first without following + numLines := int64(100) + existingLogs := k8sSession.Clients.Clientset.CoreV1().Pods(namespace).GetLogs(pod, &v1.PodLogOptions{ + Container: container, + TailLines: &numLines, // limit the lines so we don't send too much data to the client + }) + + // Read existing logs + lastReadTime := time.Now() + existingLogsStream, err := existingLogs.Stream(ctx) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to get existing logs: %v", err), http.StatusInternalServerError) + return + } + + defer existingLogsStream.Close() + + // Send existing logs to client + scanner := bufio.NewScanner(existingLogsStream) + for scanner.Scan() { + fmt.Fprintf(w, "data: %s\n\n", scanner.Text()) } + flusher.Flush() - w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(logs) + if err := scanner.Err(); err != nil { + http.Error(w, fmt.Sprintf("Error reading existing logs: %v", err), http.StatusInternalServerError) + return + } + + // after sending existing logs, continue to stream new logs + podLogs := k8sSession.Clients.Clientset.CoreV1().Pods(namespace).GetLogs(pod, &v1.PodLogOptions{ + Container: container, + Follow: true, + SinceTime: &metav1.Time{Time: lastReadTime}, + }) + + logStream, err := podLogs.Stream(ctx) if err != nil { - slog.Error("failed to encode getPodLog response", "error", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("Failed to open log stream: %v", err), http.StatusInternalServerError) return } + defer logStream.Close() + + reader := bufio.NewReader(logStream) + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } + + // Buffer for batching log lines + var buffer strings.Builder + ticker := time.NewTicker(500 * time.Millisecond) // Batch window + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + slog.Debug("Log stream closed") + return + case <-ticker.C: + // check for data + line, err := reader.ReadBytes('\n') + if err != nil { + if err != io.EOF && !errors.Is(err, context.Canceled) { + // todo: named error event? + fmt.Fprintf(w, "data: %v\n\n", err) + flusher.Flush() + } + return + } + + // Add to batch + buffer.Write(line) + + // Flush batch if we have any data + if buffer.Len() > 0 { + fmt.Fprintf(w, "data: %s\n\n", buffer.String()) + buffer.Reset() + flusher.Flush() + } + } + } } } diff --git a/src/pkg/api/rest/filter.go b/src/pkg/api/rest/filter.go index 25fdc0304..a2a688552 100644 --- a/src/pkg/api/rest/filter.go +++ b/src/pkg/api/rest/filter.go @@ -97,7 +97,11 @@ func filterItemsByFields(items []unstructured.Unstructured, fieldPaths []string) // value, found := getNestedValueFromUnstructured(data, []string{"user", "addresses[]", "city"}) // // value will be []interface{}{"New York", "Los Angeles"}, found will be true func getNestedValueFromUnstructured(obj map[string]interface{}, keys []string) (interface{}, bool) { - current := obj + // make copy of obj + current := make(map[string]interface{}, len(obj)) + for k, v := range obj { + current[k] = v + } // Iterate over each key in the keys slice for i, key := range keys { diff --git a/ui/src/lib/components/AnsiDisplay/component.svelte b/ui/src/lib/components/AnsiDisplay/component.svelte index 5266691b7..9532ab8fa 100644 --- a/ui/src/lib/components/AnsiDisplay/component.svelte +++ b/ui/src/lib/components/AnsiDisplay/component.svelte @@ -9,23 +9,54 @@ const convert = new Convert({ newline: true, stream: true, + colors: { + 0: '#374151', + 1: '#F3F4F6', + 2: '#D1D5DB', + }, }) - let termElement: HTMLElement | null - let scrollAnchor: Element | null | undefined - // exported in parent component to handle incoming SSE messages + let container: HTMLDivElement + let scrollAnchor: HTMLDivElement + + // Debounce scroll updates to prevent performance issues + let scrollTimeout: number + const debouncedScroll = () => { + if (scrollTimeout) { + window.clearTimeout(scrollTimeout) + } + scrollTimeout = window.setTimeout(() => { + scrollAnchor?.scrollIntoView({ behavior: 'auto' }) + }, 100) + } + export const addMessage = (message: string) => { - let html = convert.toHtml(message) - // Print the html or a non-breaking space if the message is empty to preserve line breaks - html = `
    ${html || ' '}
    ` - scrollAnchor?.insertAdjacentHTML('beforebegin', html) - scrollAnchor?.scrollIntoView() + if (!message || !scrollAnchor?.parentElement) return + + console.log('message:', message) + + const html = convert.toHtml(message) + const lineElement = document.createElement('div') + lineElement.className = 'log-line text-gray-300 text-sm font-mono py-0.5' + lineElement.innerHTML = html + + // Use requestAnimationFrame for DOM updates + window.requestAnimationFrame(() => { + scrollAnchor.parentElement?.insertBefore(lineElement, scrollAnchor) + debouncedScroll() + }) } + // Clean up on destroy onMount(() => { - termElement = document.getElementById('terminal') - scrollAnchor = termElement?.lastElementChild + return () => { + if (scrollTimeout) { + window.clearTimeout(scrollTimeout) + } + } }) -
    +
    +
    +
    diff --git a/ui/src/lib/components/index.ts b/ui/src/lib/components/index.ts index 5142a39f6..2925994fa 100644 --- a/ui/src/lib/components/index.ts +++ b/ui/src/lib/components/index.ts @@ -1,6 +1,7 @@ // Copyright 2024 Defense Unicorns // SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial +export { default as AnsiDisplay } from './AnsiDisplay/component.svelte' export { default as CoreServicesWidget } from './CoreServicesWidget/component.svelte' export { default as Card } from './k8s/Card/component.svelte' export { default as DataTable } from './k8s/DataTable/component.svelte' diff --git a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte index 1e4b25805..5abe5c55d 100644 --- a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte +++ b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte @@ -4,13 +4,15 @@ @@ -185,9 +219,7 @@
    - {#each Array.from({ length: 100 }) as _, i} -
    [2021-10-01T12:00:00Z] Log line {i}
    - {/each} +
    From 7d3b68df5c9434a6c662ce8f2dca2a0b04b2a06d Mon Sep 17 00:00:00 2001 From: unclegedd Date: Thu, 31 Oct 2024 16:08:03 -0500 Subject: [PATCH 08/45] WIP: dont miss logs --- src/pkg/api/handlers.go | 57 ++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/pkg/api/handlers.go b/src/pkg/api/handlers.go index 72a735aab..d73c8ccdf 100644 --- a/src/pkg/api/handlers.go +++ b/src/pkg/api/handlers.go @@ -174,7 +174,6 @@ func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *h w.Header().Set("Access-Control-Allow-Origin", "*") ctx := r.Context() - flusher, ok := w.(http.Flusher) // Get existing logs first without following @@ -200,7 +199,6 @@ func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *h fmt.Fprintf(w, "data: %s\n\n", scanner.Text()) } flusher.Flush() - if err := scanner.Err(); err != nil { http.Error(w, fmt.Sprintf("Error reading existing logs: %v", err), http.StatusInternalServerError) return @@ -231,32 +229,55 @@ func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *h ticker := time.NewTicker(500 * time.Millisecond) // Batch window defer ticker.Stop() - for { - select { - case <-ctx.Done(): - slog.Debug("Log stream closed") - return - case <-ticker.C: - // check for data + // Create a channel to signal when we should flush + shouldFlush := make(chan struct{}, 1) + + // Start a goroutine to continuously read logs + go func() { + for { + // block until a complete log line is read line, err := reader.ReadBytes('\n') if err != nil { if err != io.EOF && !errors.Is(err, context.Canceled) { - // todo: named error event? - fmt.Fprintf(w, "data: %v\n\n", err) - flusher.Flush() + buffer.WriteString(fmt.Sprintf("ERROR: %v\n", err)) } + close(shouldFlush) return } - // Add to batch buffer.Write(line) - // Flush batch if we have any data - if buffer.Len() > 0 { - fmt.Fprintf(w, "data: %s\n\n", buffer.String()) - buffer.Reset() - flusher.Flush() + // non-blocking channel send to shouldFlush + select { + case shouldFlush <- struct{}{}: + default: + } + } + }() + + // Main loop to handle flushes + for { + var shouldTryFlush bool + + select { + case <-ctx.Done(): + slog.Debug("Log stream closed") + return + + case _, ok := <-shouldFlush: + if !ok { + return } + shouldTryFlush = true + + case <-ticker.C: + shouldTryFlush = true + } + + if shouldTryFlush && buffer.Len() > 0 { + fmt.Fprintf(w, "data: %s\n\n", buffer.String()) + buffer.Reset() + flusher.Flush() } } } From 5d145b18dcde608bbb0e84409062754e2c45523c Mon Sep 17 00:00:00 2001 From: unclegedd Date: Thu, 31 Oct 2024 16:11:38 -0500 Subject: [PATCH 09/45] WIP: remove ticker --- src/pkg/api/handlers.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pkg/api/handlers.go b/src/pkg/api/handlers.go index d73c8ccdf..edf2f82fe 100644 --- a/src/pkg/api/handlers.go +++ b/src/pkg/api/handlers.go @@ -226,8 +226,6 @@ func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *h // Buffer for batching log lines var buffer strings.Builder - ticker := time.NewTicker(500 * time.Millisecond) // Batch window - defer ticker.Stop() // Create a channel to signal when we should flush shouldFlush := make(chan struct{}, 1) @@ -269,9 +267,6 @@ func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *h return } shouldTryFlush = true - - case <-ticker.C: - shouldTryFlush = true } if shouldTryFlush && buffer.Len() > 0 { From f57b039b7c266383d350d2f697e9040e0ee8554d Mon Sep 17 00:00:00 2001 From: unclegedd Date: Thu, 31 Oct 2024 21:07:52 -0500 Subject: [PATCH 10/45] WIP: tiny issue with container dropdown --- src/pkg/api/handlers.go | 3 +- .../lib/components/Dropdown/component.svelte | 13 ++- .../workloads/pods/logs/+page.svelte | 104 ++++++++++-------- 3 files changed, 69 insertions(+), 51 deletions(-) diff --git a/src/pkg/api/handlers.go b/src/pkg/api/handlers.go index edf2f82fe..e4a24e75b 100644 --- a/src/pkg/api/handlers.go +++ b/src/pkg/api/handlers.go @@ -152,8 +152,6 @@ func getPod(cache *resources.Cache) func(w http.ResponseWriter, r *http.Request) // @Router /api/v1/resources/workloads/pods/logs [get] // @Param pod query string true "Pod name" // @Param namespace query string true "Namespace" -// todo: guard against accessing logs for pods that shouldn't be accessed? -// todo: test back/forward button, etc // todo: rate limiting? // todo: error retry logic? func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *http.Request) { @@ -191,6 +189,7 @@ func getPodLogs(k8sSession *session.K8sSession) func(w http.ResponseWriter, r *h return } + // close existing logs stream when done defer existingLogsStream.Close() // Send existing logs to client diff --git a/ui/src/lib/components/Dropdown/component.svelte b/ui/src/lib/components/Dropdown/component.svelte index 4025c2258..a54556189 100644 --- a/ui/src/lib/components/Dropdown/component.svelte +++ b/ui/src/lib/components/Dropdown/component.svelte @@ -5,10 +5,12 @@ import { Dropdown } from 'flowbite' export let items: string[] - export let selectedValue: string + export let selectedValue: string | null = null export let width: string = 'w-32' // must be a tailwind class export let maxHeight: string = 'max-h-32' // must be a tailwind class export let id: string + export let placeholder: string = 'Select an option' // new placeholder prop + export let disabled: boolean = false function handleSelect(item: string) { selectedValue = item @@ -19,6 +21,10 @@ const dropdown = new Dropdown(dropDownMenu, dropdownButton) dropdown.hide() } + + // Helper to determine what text to display + $: displayText = selectedValue || placeholder + $: isPlaceholderShown = !selectedValue
    @@ -27,8 +33,11 @@ data-dropdown-toggle="{id}-menu" class="{width} justify-between truncate bg-gray-800 text-gray-300 hover:bg-gray-700 border border-gray-700 text-sm px-2 py-1 rounded-md inline-flex items-center h-7" type="button" + {disabled} > - {selectedValue || items[0]} + + {displayText} +
    - + p.metadata.name)} id="pod-dropdown" width="w-48" + placeholder="Select pod" + disabled={!selectedNamespace} /> c.name)} id="container-dropdown" width="w-48" + placeholder="Select container" + disabled={!selectedPodName} />
    From 795577c48c2ef5acb0631a020e965809b58ed53a Mon Sep 17 00:00:00 2001 From: unclegedd Date: Thu, 31 Oct 2024 21:25:21 -0500 Subject: [PATCH 11/45] WIP: swap reactive statements for stores --- .../components/AnsiDisplay/component.svelte | 2 - .../workloads/pods/logs/+page.svelte | 140 +++++++++--------- 2 files changed, 67 insertions(+), 75 deletions(-) diff --git a/ui/src/lib/components/AnsiDisplay/component.svelte b/ui/src/lib/components/AnsiDisplay/component.svelte index 9532ab8fa..5735fabdb 100644 --- a/ui/src/lib/components/AnsiDisplay/component.svelte +++ b/ui/src/lib/components/AnsiDisplay/component.svelte @@ -33,8 +33,6 @@ export const addMessage = (message: string) => { if (!message || !scrollAnchor?.parentElement) return - console.log('message:', message) - const html = convert.toHtml(message) const lineElement = document.createElement('div') lineElement.className = 'log-line text-gray-300 text-sm font-mono py-0.5' diff --git a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte index dcfefc388..dac434cd9 100644 --- a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte +++ b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte @@ -3,6 +3,7 @@ + +
    +
    + +
    + +
    + +
    + + Logs +
    + + +
    +
    + +
    + +
    + + +
    + +
    +
    + + +
    + + +
    +
    + + +
    + + p.metadata.name)} + id="pod-dropdown" + width="w-48" + placeholder="Select pod" + disabled={!$namespace} + /> + c.name)} + id="container-dropdown" + width="w-48" + placeholder="Select container" + disabled={!$podName} + /> +
    + +
    +
    + +
    +
    +
    +
    diff --git a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte index dac434cd9..639138cf3 100644 --- a/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte +++ b/ui/src/routes/(resources)/workloads/pods/logs/+page.svelte @@ -2,230 +2,11 @@
    - -
    -
    - -
    - -
    - -
    - - Logs -
    - - -
    -
    - -
    - -
    - - -
    - -
    -
    - - -
    - - -
    -
    - - -
    - - p.metadata.name)} - id="pod-dropdown" - width="w-48" - placeholder="Select pod" - disabled={!$namespace} - /> - c.name)} - id="container-dropdown" - width="w-48" - placeholder="Select container" - disabled={!$podName} - /> -
    - -
    -
    - -
    -
    -
    -
    +
    From 3ac79763c3db71b248277cbce5f640997a02e56a Mon Sep 17 00:00:00 2001 From: unclegedd Date: Thu, 31 Oct 2024 22:21:21 -0500 Subject: [PATCH 13/45] autoscroll working --- .../components/AnsiDisplay/component.svelte | 29 +++++++-- .../lib/components/k8s/Logs/component.svelte | 59 ++++++------------- ui/src/lib/types.ts | 14 +++++ ui/src/routes/auth.test.ts | 5 -- 4 files changed, 57 insertions(+), 50 deletions(-) diff --git a/ui/src/lib/components/AnsiDisplay/component.svelte b/ui/src/lib/components/AnsiDisplay/component.svelte index 5735fabdb..32ad21271 100644 --- a/ui/src/lib/components/AnsiDisplay/component.svelte +++ b/ui/src/lib/components/AnsiDisplay/component.svelte @@ -18,10 +18,15 @@ let container: HTMLDivElement let scrollAnchor: HTMLDivElement - - // Debounce scroll updates to prevent performance issues let scrollTimeout: number + + // Two-way binding for autoScroll + export let autoScroll = true + let lastScrollTop = 0 + const debouncedScroll = () => { + if (!autoScroll) return + if (scrollTimeout) { window.clearTimeout(scrollTimeout) } @@ -30,6 +35,22 @@ }, 100) } + // Handle manual scrolling + const handleScroll = (event: Event) => { + const target = event.target as HTMLDivElement + const isScrollingUp = target.scrollTop < lastScrollTop + const isAtBottom = Math.abs(target.scrollHeight - target.clientHeight - target.scrollTop) < 50 + + // Update autoScroll based on scroll behavior + autoScroll = !isScrollingUp && isAtBottom + lastScrollTop = target.scrollTop + } + + // Watch autoScroll changes + $: if (autoScroll) { + debouncedScroll() + } + export const addMessage = (message: string) => { if (!message || !scrollAnchor?.parentElement) return @@ -38,14 +59,12 @@ lineElement.className = 'log-line text-gray-300 text-sm font-mono py-0.5' lineElement.innerHTML = html - // Use requestAnimationFrame for DOM updates window.requestAnimationFrame(() => { scrollAnchor.parentElement?.insertBefore(lineElement, scrollAnchor) debouncedScroll() }) } - // Clean up on destroy onMount(() => { return () => { if (scrollTimeout) { @@ -55,6 +74,6 @@ }) -
    +
    diff --git a/ui/src/lib/components/k8s/Logs/component.svelte b/ui/src/lib/components/k8s/Logs/component.svelte index 08b9be19c..647e2518b 100644 --- a/ui/src/lib/components/k8s/Logs/component.svelte +++ b/ui/src/lib/components/k8s/Logs/component.svelte @@ -6,39 +6,21 @@ import { derived, writable } from 'svelte/store' import { AnsiDisplay, Dropdown } from '$components' + import type { Pod } from '$lib/types' import { Copy, Download, Search, Terminal } from 'carbon-icons-svelte' // todo: esc exits log view (change url) // todo: memory leak? time out after 30 minutes or so? what about large data volume? + // track auto scroll state let autoScroll = true - let addMessage: (message: string) => void - - // Toggle auto-scroll - function toggleAutoScroll() { - autoScroll = !autoScroll - } - // interfaces for pod data - interface Container { - name: string - } - - interface Pod { - metadata: { - name: string - namespace: string - } - spec: { - containers: Container[] - } - } + // used in AnsiDisplay + let addMessage: (message: string) => void let logEventSource: EventSource let search = '' - // todo: test back/forward button, etc - function cleanupLogs() { if (logEventSource) { logEventSource.close() @@ -48,22 +30,21 @@ } // Create base stores - const pods = writable([]) - const namespaces = writable>(new Set()) - const namespace = writable(null) + const pods = writable([]) // raw pod data + const selectedNamespace = writable(null) + const namespaces = writable>(new Set()) // derived from pods const podName = writable(null) + const selectedPod = writable(null) // used to derive containers const containerName = writable(null) - const selectedPod = writable(null) - // Derive filtered lists - const filteredPods = derived([namespace], () => - $namespace ? Array.from($pods).filter((p) => p.metadata.namespace === $namespace) : [], + // Derive filtered lists for dropdowns + const filteredPods = derived([selectedNamespace], () => + $selectedNamespace ? Array.from($pods).filter((p) => p.metadata.namespace === $selectedNamespace) : [], ) - const filteredContainers = derived([selectedPod], () => $selectedPod?.spec.containers ?? []) - // Subscribe to changes - namespace.subscribe((newNamespace) => { + // Subscribe to dropdown changes + selectedNamespace.subscribe((newNamespace) => { if (newNamespace) { cleanupLogs() podName.set(null) @@ -71,7 +52,6 @@ containerName.set(null) } }) - podName.subscribe((newPodName) => { if (newPodName) { cleanupLogs() @@ -80,13 +60,12 @@ selectedPod.set(pod ?? null) } }) - containerName.subscribe((newContainerName) => { if (newContainerName) { cleanupLogs() // create new SSE stream - const logsURL = `/api/v1/resources/workloads/pods/logs?namespace=${$namespace}&pod=${$podName}&container=${newContainerName}` + const logsURL = `/api/v1/resources/workloads/pods/logs?namespace=${$selectedNamespace}&pod=${$podName}&container=${newContainerName}` logEventSource = new EventSource(logsURL) logEventSource.onmessage = (event) => { @@ -125,7 +104,7 @@ // Set from URL params after data is loaded if (podParam && nsParam) { - namespace.set(nsParam) + selectedNamespace.set(nsParam) podName.set(podParam) } } @@ -177,7 +156,7 @@
    + /> Auto-scroll
    @@ -197,7 +176,7 @@
    - +
    diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index e4fcd78c3..bf20a94ec 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -39,3 +39,17 @@ export type CoreServiceType = { phase: UDSPackageStatus } } + +// types for pod data loaded for the logs view +type Container = { + name: string +} +export type Pod = { + metadata: { + name: string + namespace: string + } + spec: { + containers: Container[] + } +} diff --git a/ui/src/routes/auth.test.ts b/ui/src/routes/auth.test.ts index 11b325429..bc8f740a7 100644 --- a/ui/src/routes/auth.test.ts +++ b/ui/src/routes/auth.test.ts @@ -76,7 +76,6 @@ describe('load function', () => { // Verify store operations expect(result.namespaces.start).toHaveBeenCalled() - expect(result.pods.start).toHaveBeenCalled() expect(authenticated.set).toHaveBeenCalledWith(true) // Verify return value @@ -118,8 +117,6 @@ describe('load function', () => { // Verify namespaces was called expect(result.namespaces.start).toHaveBeenCalled() - expect(result.pods.start).toHaveBeenCalled() - expect(authenticated.set).toHaveBeenCalledWith(true) // Verify return value @@ -146,7 +143,6 @@ describe('load function', () => { // Verify namespaces wasn't started and authenticated state was set to false expect(result.namespaces.start).not.toHaveBeenCalled() - expect(result.pods.start).not.toHaveBeenCalled() expect(authenticated.set).toHaveBeenCalledWith(false) // Verify return value @@ -175,7 +171,6 @@ describe('load function', () => { // Verify namespaces wasn't started authenticated state was set to false expect(result.namespaces.start).not.toHaveBeenCalled() - expect(result.pods.start).not.toHaveBeenCalled() expect(authenticated.set).toHaveBeenCalledWith(false) // Verify return value From 33f819dd8e16d356e5f6b9472023410e2d02a246 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Thu, 31 Oct 2024 22:47:48 -0500 Subject: [PATCH 14/45] copy working --- .../components/AnsiDisplay/component.svelte | 29 +++++++++- .../lib/components/k8s/Logs/component.svelte | 57 +++++++++++++++++-- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/ui/src/lib/components/AnsiDisplay/component.svelte b/ui/src/lib/components/AnsiDisplay/component.svelte index 32ad21271..54176359b 100644 --- a/ui/src/lib/components/AnsiDisplay/component.svelte +++ b/ui/src/lib/components/AnsiDisplay/component.svelte @@ -16,17 +16,24 @@ }, }) + // refs to rendered elements let container: HTMLDivElement let scrollAnchor: HTMLDivElement + let scrollTimeout: number - // Two-way binding for autoScroll + // Track log elements + let logElements: HTMLDivElement[] = [] + export let autoScroll = true let lastScrollTop = 0 + // Debounce auto-scrolling, if many logs come in at once, this creates a smooth scroll UX const debouncedScroll = () => { if (!autoScroll) return + // if a new log arrives during the timeout, clear it and start over + // this prevents the scroll from jumping around if (scrollTimeout) { window.clearTimeout(scrollTimeout) } @@ -41,7 +48,7 @@ const isScrollingUp = target.scrollTop < lastScrollTop const isAtBottom = Math.abs(target.scrollHeight - target.clientHeight - target.scrollTop) < 50 - // Update autoScroll based on scroll behavior + // enable autoscroll if user is at the bottom, else disable it autoScroll = !isScrollingUp && isAtBottom lastScrollTop = target.scrollTop } @@ -51,6 +58,7 @@ debouncedScroll() } + // used by parent component to add messages export const addMessage = (message: string) => { if (!message || !scrollAnchor?.parentElement) return @@ -61,10 +69,27 @@ window.requestAnimationFrame(() => { scrollAnchor.parentElement?.insertBefore(lineElement, scrollAnchor) + logElements.push(lineElement) + // Keep only last 1000 lines in memory to prevent memory leaks + if (logElements.length > 1000) { + logElements.shift() + } debouncedScroll() }) } + // Function to get last N lines of logs + export const getLastNLines = (n: number): string => { + const lastN = logElements.slice(-n) + return lastN + .map((el) => { + // Convert HTML back to plain text and remove ANSI codes + const text = el.textContent || '' + return text.replace(/\n$/, '') // Remove trailing newline if present + }) + .join('\n') + } + onMount(() => { return () => { if (scrollTimeout) { diff --git a/ui/src/lib/components/k8s/Logs/component.svelte b/ui/src/lib/components/k8s/Logs/component.svelte index 647e2518b..fcecf0281 100644 --- a/ui/src/lib/components/k8s/Logs/component.svelte +++ b/ui/src/lib/components/k8s/Logs/component.svelte @@ -2,12 +2,13 @@ -
    +
    From 5b7b09bc7d11643206298867a2bba3a4cd481f6d Mon Sep 17 00:00:00 2001 From: unclegedd Date: Sat, 2 Nov 2024 18:00:03 -0500 Subject: [PATCH 18/45] labeled dropdown --- ui/src/lib/components/k8s/Logs/Dropdown/component.svelte | 8 ++++++-- ui/src/lib/components/k8s/Logs/component.svelte | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ui/src/lib/components/k8s/Logs/Dropdown/component.svelte b/ui/src/lib/components/k8s/Logs/Dropdown/component.svelte index 174574c29..4a82d5685 100644 --- a/ui/src/lib/components/k8s/Logs/Dropdown/component.svelte +++ b/ui/src/lib/components/k8s/Logs/Dropdown/component.svelte @@ -14,6 +14,7 @@ export let id: string export let placeholder: string = 'Select an option' // new placeholder prop export let disabled: boolean = false + export let label: string = '' function handleSelect(item: string) { selectedValue = item @@ -30,11 +31,14 @@ $: isPlaceholderShown = !selectedValue -
    +
    + + {label} + + + {#if resource.kind === 'Pod'} + + {/if}
    {#each details as { label, value }}
    diff --git a/ui/src/lib/components/k8s/Logs/component.svelte b/ui/src/lib/components/k8s/Logs/component.svelte index 4de94fa46..203ed9cf3 100644 --- a/ui/src/lib/components/k8s/Logs/component.svelte +++ b/ui/src/lib/components/k8s/Logs/component.svelte @@ -27,7 +27,7 @@ searchTerm, selectedNamespace, } from '$components/k8s/Logs/store' - import type { Pod } from '$lib/types' + import type { Pod } from '$features/k8s/types' import { CheckmarkOutline, Copy, Download, Search, Terminal } from 'carbon-icons-svelte' let podEventSource: EventSource diff --git a/ui/src/lib/components/k8s/Logs/store.ts b/ui/src/lib/components/k8s/Logs/store.ts index c5afdeb64..efd3d3a77 100644 --- a/ui/src/lib/components/k8s/Logs/store.ts +++ b/ui/src/lib/components/k8s/Logs/store.ts @@ -4,7 +4,7 @@ import { derived, get, writable } from 'svelte/store' import { addLog, cleanupLogs, debounceScroll, highlightMatches, resetHighlights } from '$components/k8s/Logs/helpers' -import type { Pod } from '$lib/types' +import type { Pod } from '$features/k8s/types' // stores for pod data export const pods = writable([]) // raw pod data diff --git a/ui/src/lib/features/k8s/types.ts b/ui/src/lib/features/k8s/types.ts index c6494a449..6d4a62474 100644 --- a/ui/src/lib/features/k8s/types.ts +++ b/ui/src/lib/features/k8s/types.ts @@ -121,3 +121,17 @@ export type ClusterData = { } }[] } + +// types for pod data loaded for the logs view +type Container = { + name: string +} +export type Pod = { + metadata: { + name: string + namespace: string + } + spec: { + containers: Container[] + } +} diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index e72be6f24..3dcaf3f0e 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -39,17 +39,3 @@ export type ClusterOverviewUDSPackageType = { endpoints: string[] } } - -// types for pod data loaded for the logs view -type Container = { - name: string -} -export type Pod = { - metadata: { - name: string - namespace: string - } - spec: { - containers: Container[] - } -} diff --git a/ui/tests/drawer.spec.ts b/ui/tests/drawer.spec.ts index e20949dcf..ac3eb2425 100644 --- a/ui/tests/drawer.spec.ts +++ b/ui/tests/drawer.spec.ts @@ -11,7 +11,7 @@ test.describe('Drawer', async () => { }) test.describe('is opened when clicking on a table row and', async () => { - test('will display Metadata details', async ({ page }) => { + test('will display Details', async ({ page }) => { const drawerEl = page.getByTestId('drawer') await expect(drawerEl).toBeVisible() @@ -19,6 +19,7 @@ test.describe('Drawer', async () => { await expect(drawerEl.getByText('Name', { exact: true })).toBeVisible() await expect(drawerEl.getByText('Annotations')).toBeVisible() await expect(drawerEl.getByText('podinfo', { exact: true })).toBeVisible() + await expect(drawerEl.getByText('View Logs')).toBeVisible() }) test('will display Events details', async ({ page }) => { @@ -43,5 +44,14 @@ test.describe('Drawer', async () => { // Ensure metadata:uid matches url pod:id await expect(drawerEl.locator(`:text("uid: ${podID}")`)).toBeVisible() }) + + test("don't display logs button for non-pods", async ({ page }) => { + await page.goto('/workloads/deployments') + await page.getByRole('cell', { name: 'podinfo-' }).click() + + const drawerEl = page.getByTestId('drawer') + await expect(drawerEl).toBeVisible() + await expect(drawerEl.getByText('View Logs')).not.toBeVisible() + }) }) }) From ee3c4afb7797b83c778c1d01992a3a6a83c7cf64 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Wed, 6 Nov 2024 14:25:24 -0600 Subject: [PATCH 40/45] add space to bottom of logs view --- ui/src/lib/components/k8s/Logs/component.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/lib/components/k8s/Logs/component.svelte b/ui/src/lib/components/k8s/Logs/component.svelte index 203ed9cf3..b3279e24d 100644 --- a/ui/src/lib/components/k8s/Logs/component.svelte +++ b/ui/src/lib/components/k8s/Logs/component.svelte @@ -173,7 +173,7 @@ } -
    +
    From bf99260471668925ec7393f6fa907ff60cbe3e31 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Wed, 6 Nov 2024 14:34:44 -0600 Subject: [PATCH 41/45] fix --- ui/src/lib/components/k8s/Logs/component.svelte | 6 +++--- ui/src/routes/+layout.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/lib/components/k8s/Logs/component.svelte b/ui/src/lib/components/k8s/Logs/component.svelte index b3279e24d..fbe94c744 100644 --- a/ui/src/lib/components/k8s/Logs/component.svelte +++ b/ui/src/lib/components/k8s/Logs/component.svelte @@ -252,7 +252,7 @@ bind:selectedValue={$selectedNamespace} items={Array.from($namespaces)} id="ns-dropdown" - width="w-48" + width="w-56" placeholder="Select namespace" label="Namespace" /> @@ -260,7 +260,7 @@ bind:selectedValue={$podName} items={$filteredPods.map((p) => p.metadata.name)} id="pod-dropdown" - width="w-48" + width="w-56" placeholder="Select pod" disabled={!$selectedNamespace} label="Pod" @@ -269,7 +269,7 @@ bind:selectedValue={$containerName} items={$filteredContainers.map((c) => c.name)} id="container-dropdown" - width="w-48" + width="w-56" placeholder="Select container" disabled={!$podName} label="Container" diff --git a/ui/src/routes/+layout.ts b/ui/src/routes/+layout.ts index e72d35dcd..8f3e802c4 100644 --- a/ui/src/routes/+layout.ts +++ b/ui/src/routes/+layout.ts @@ -56,7 +56,7 @@ async function auth(token: string): Promise { // load namespace and auth data before rendering the app export const load = async () => { - const namespaces = createNamespaceStore() + const namespaces = createStore() const url = new URL(window.location.href) const localAuthToken = url.searchParams.get('token') || '' From b342efcd1321c2d68f89990ec479b74d80aa4c48 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Wed, 6 Nov 2024 14:43:29 -0600 Subject: [PATCH 42/45] fix api test --- src/test/e2e/api_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/e2e/api_test.go b/src/test/e2e/api_test.go index fb4dd08e0..be8ab3d89 100644 --- a/src/test/e2e/api_test.go +++ b/src/test/e2e/api_test.go @@ -21,6 +21,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" ) type TestRoute struct { @@ -265,7 +266,7 @@ func TestLogs(t *testing.T) { require.NoError(t, err) // get podinfo pod name using k8s client - k8sClient, err := client.NewClient() + k8sClient, err := client.New(&clientcmd.ConfigOverrides{}) require.NoError(t, err) pods, err := k8sClient.Clientset.CoreV1().Pods("podinfo").List(context.TODO(), metav1.ListOptions{}) require.NoError(t, err) From 5f0723d5ec826519014f2d22f52dd7c65791e5a1 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Wed, 6 Nov 2024 15:03:15 -0600 Subject: [PATCH 43/45] fix test and refactor cluster dropdown --- .../navbar/clustermenu/component.svelte | 29 ++++++++++++------- ui/tests/drawer.spec.ts | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/ui/src/lib/features/navigation/navbar/clustermenu/component.svelte b/ui/src/lib/features/navigation/navbar/clustermenu/component.svelte index 71f64b619..5d34fe3d0 100644 --- a/ui/src/lib/features/navigation/navbar/clustermenu/component.svelte +++ b/ui/src/lib/features/navigation/navbar/clustermenu/component.svelte @@ -67,7 +67,7 @@ $: availableClusters = $clusters.filter((cluster) => !cluster.selected).sort((a, b) => a.name.localeCompare(b.name)) -
    +
    +