Skip to content

Conversation

@ktx-vaidehi
Copy link
Collaborator

@ktx-vaidehi ktx-vaidehi commented Oct 17, 2025

PR Type

Bug fix


Description

  • Reset isPartialData on error paths

  • Prevent stale partial-data icon after failures

  • Align error handlers to clear partial state


Diagram Walkthrough

flowchart LR
  Start["Data loading flow"] -- "Error occurs" --> Err["Error handlers"]
  Err -- "Reset flags" --> Flags["loading/isCancelled/progress reset"]
  Err -- "Also reset" --> Partial["state.isPartialData = false"]
  Partial -- "Avoid stale UI" --> UI["No partial-data icon on error"]
Loading

File Walkthrough

Relevant files
Bug fix
usePanelDataLoader.ts
Reset partial-data flag across all error paths                     

web/src/composables/dashboard/usePanelDataLoader.ts

  • Set state.isPartialData = false in multiple error branches
  • Clear partial-data on API error responses and exceptions
  • Ensure processApiError always resets partial-data state
  • Keep other loading/reset flags unchanged
+7/-0     

@github-actions github-actions bot added ☢️ Bug Something isn't working Review effort 2/5 labels Oct 17, 2025
@github-actions
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

State Consistency

Ensure resetting state.isPartialData = false on all error paths does not mask legitimate partial-data cases (e.g., when partial results are intentionally surfaced alongside an error). Validate that UI logic depending on isPartialData still behaves correctly for mixed success/error scenarios.

    if (response.type === "error") {
      state.loading = false;
      state.loadingTotal = 0;
      state.loadingCompleted = 0;
      state.loadingProgressPercentage = 0;
      state.isOperationCancelled = false;
      state.isPartialData = false;
      processApiError(response?.content, "sql");
    }

    if (response.type === "end") {
      state.loading = false;
      state.loadingTotal = 0;
      state.loadingCompleted = 0;
      state.loadingProgressPercentage = 100; // Set to 100% when complete
      state.isOperationCancelled = false;
      state.isPartialData = false; // Explicitly set to false when complete
      saveCurrentStateToCache();
    }

    if (response.type === "event_progress") {
      state.loadingProgressPercentage = response?.content?.percent ?? 0;
      state.isPartialData = true;
      saveCurrentStateToCache();
    }
  } catch (error: any) {
    state.loading = false;
    state.isOperationCancelled = false;
    state.loadingTotal = 0;
    state.loadingCompleted = 0;
    state.loadingProgressPercentage = 0;
    state.isPartialData = false;
    state.errorDetail = {
      message: error?.message || "Unknown error in search response",
      code: error?.code ?? "",
    };
  }
};

const sendSearchMessage = async (payload: any) => {
  // check if query is already canceled, if it is, close the socket
  if (state.isOperationCancelled) {
    state.isOperationCancelled = false;

    // clean up the listeners
    cleanUpListeners(payload.traceId);

    return;
  }

  sendSearchMessageBasedOnRequestId({
    type: "search",
    content: {
      trace_id: payload.traceId,
      payload: {
        query: {
          ...(await getHistogramSearchRequest(
            payload.queryReq.query,
            payload.queryReq.it,
            payload.queryReq.startISOTimestamp,
            payload.queryReq.endISOTimestamp,
            null,
          )),
        },
        // pass encodig if enabled,
        // make sure that `encoding: null` is not being passed, that's why used object extraction logic
        ...(store.state.zoConfig.sql_base64_enabled
          ? { encoding: "base64" }
          : {}),
      },
      stream_type: payload.pageType,
      search_type: searchType.value ?? "dashboards",
      org_id: store?.state?.selectedOrganization?.identifier,
      use_cache: (window as any).use_cache ?? true,
      dashboard_id: dashboardId?.value,
      dashboard_name: dashboardName?.value,
      folder_id: folderId?.value,
      folder_name: folderName?.value,
      panel_id: panelSchema.value.id,
      panel_name: panelSchema.value.title,
      run_id: runId?.value,
      tab_id: tabId?.value,
      tab_name: tabName?.value,
      fallback_order_by_col: getFallbackOrderByCol(),
      is_ui_histogram: is_ui_histogram.value,
    },
  });
};

const handleSearchClose = (payload: any, response: any) => {
  removeTraceId(payload?.traceId);

  if (response.type === "error") {
    processApiError(response?.content, "sql");
  }

  const errorCodes = [1001, 1006, 1010, 1011, 1012, 1013];

  if (errorCodes.includes(response.code)) {
    handleSearchError(payload, {
      content: {
        message:
          "WebSocket connection terminated unexpectedly. Please check your network and try again",
        trace_id: payload.traceId,
        code: response.code,
        error_detail: "",
      },
    });
  }

  // set loading to false
  state.loading = false;
  state.isOperationCancelled = false;
  state.isPartialData = false;
  // save current state to cache
  // this is async task, which will be executed in background(await is not required)
  saveCurrentStateToCache();
};

const handleSearchReset = (payload: any, traceId?: string) => {
  // Save current state to cache
  saveCurrentStateToCache();
  loadData();
};

const handleSearchError = (payload: any, response: any) => {
  removeTraceId(payload.traceId);

  // set loading to false
  state.loading = false;
  state.loadingTotal = 0;
  state.loadingCompleted = 0;
  state.loadingProgressPercentage = 0;
  state.isOperationCancelled = false;
  state.isPartialData = false;

  processApiError(response?.content, "sql");
};

const shouldSkipSearchDueToEmptyVariables = () => {
  // Retrieve all variables data
  const allVars = [
    ...(getDependentVariablesData() || []),
    ...(getDynamicVariablesData() || []),
  ];

  // Identify variables with empty values
  const variablesToSkip = allVars
    .filter(
      (v) =>
        v.value === null ||
        v.value === undefined ||
        (Array.isArray(v.value) && v.value.length === 0),
    )
    .map((v) => v.name);

  // Log variables for which the API will be skipped
  variablesToSkip.forEach((variableName) => {
    state.loading = false;
  });

  // Return true if there are any variables to skip, indicating loading should be continued
  return variablesToSkip.length > 0;
};

const getDataThroughWebSocket = async (
  query: string,
  it: any,
  startISOTimestamp: string,
  endISOTimestamp: string,
  pageType: string,
  currentQueryIndex: number,
) => {
  try {
    const { traceId } = generateTraceContext();
    addTraceId(traceId);

    const payload: {
      queryReq: any;
      type: "search" | "histogram" | "pageCount" | "values";
      isPagination: boolean;
      traceId: string;
      org_id: string;
      pageType: string;
      meta: any;
    } = {
      queryReq: {
        query,
        it,
        startISOTimestamp,
        endISOTimestamp,
        currentQueryIndex,
        // pass encodig if enabled,
        // make sure that encoding: null is not being passed, that's why used object extraction logic
        ...(store.state.zoConfig.sql_base64_enabled
          ? { encoding: "base64" }
          : {}),
      },
      type: "histogram",
      isPagination: false,
      traceId,
      org_id: store?.state?.selectedOrganization?.identifier,
      pageType,
      meta: {
        currentQueryIndex,
        panel_id: panelSchema.value.id,
        panel_name: panelSchema.value.title,
        run_id: runId?.value,
        tab_id: tabId?.value,
        tab_name: tabName?.value,
        dashboard_name: dashboardName?.value,
        folder_name: folderName?.value,
      },
    };

    // Add guard here
    if (shouldSkipSearchDueToEmptyVariables()) {
      return;
    }
    fetchQueryDataWithWebSocket(payload, {
      open: sendSearchMessage,
      close: handleSearchClose,
      error: handleSearchError,
      message: handleSearchResponse,
      reset: handleSearchReset,
    });

    addTraceId(traceId);
  } catch (e: any) {
    state.errorDetail = {
      message: e?.message || e,
      code: e?.code ?? "",
    };
    state.loading = false;
    state.isOperationCancelled = false;
    state.isPartialData = false;
  }
};

const getDataThroughStreaming = async (
  query: string,
  it: any,
  startISOTimestamp: string,
  endISOTimestamp: string,
  pageType: string,
  currentQueryIndex: number,
  abortControllerRef: any,
) => {
  try {
    const { traceId } = generateTraceContext();

    const payload: {
      queryReq: any;
      type: "search" | "histogram" | "pageCount";
      isPagination: boolean;
      traceId: string;
      org_id: string;
      pageType: string;
      searchType: string;
      meta: any;
    } = {
      queryReq: {
        query: {
          ...(await getHistogramSearchRequest(
            query,
            it,
            startISOTimestamp,
            endISOTimestamp,
            null,
          )),
        },
      },
      type: "histogram",
      isPagination: false,
      traceId,
      org_id: store?.state?.selectedOrganization?.identifier,
      pageType,
      searchType: searchType.value ?? "dashboards",
      meta: {
        currentQueryIndex,
        dashboard_id: dashboardId?.value,
        dashboard_name: dashboardName?.value,
        folder_id: folderId?.value,
        folder_name: folderName?.value,
        panel_id: panelSchema.value.id,
        panel_name: panelSchema.value.title,
        run_id: runId?.value,
        tab_id: tabId?.value,
        tab_name: tabName?.value,
        fallback_order_by_col: getFallbackOrderByCol(),
        is_ui_histogram: is_ui_histogram.value,
      },
    };

    // type: "search",
    // content: {
    //   trace_id: payload.traceId,
    //   payload: {
    //     query: await getHistogramSearchRequest(
    //       payload.queryReq.query,
    //       payload.queryReq.it,
    //       payload.queryReq.startISOTimestamp,
    //       payload.queryReq.endISOTimestamp,
    //       null,
    //     ),
    //   },
    //   stream_type: payload.pageType,
    //   search_type: searchType.value ?? "dashboards",
    //   org_id: store?.state?.selectedOrganization?.identifier,
    //   use_cache: (window as any).use_cache ?? true,
    //   dashboard_id: dashboardId?.value,
    //   folder_id: folderId?.value,
    //   fallback_order_by_col: getFallbackOrderByCol(),
    // },

    // if aborted, return
    if (abortControllerRef?.signal?.aborted) {
      // Set partial data flag on abort
      state.isPartialData = true;
      // Save current state to cache
      saveCurrentStateToCache();
      return;
    }

    // Add guard here
    if (shouldSkipSearchDueToEmptyVariables()) {
      return;
    }

    fetchQueryDataWithHttpStream(payload, {
      data: handleSearchResponse,
      error: handleSearchError,
      complete: handleSearchClose,
      reset: handleSearchReset,
    });

    addTraceId(traceId);
  } catch (e: any) {
    state.errorDetail = {
      message: e?.message || e,
      code: e?.code ?? "",
    };
    state.loading = false;
    state.isOperationCancelled = false;
    state.isPartialData = false;
  }
};
Centralization

Multiple error handlers set the same flags. Consider consolidating common reset logic (loading, totals, cancel, partial) into a single helper to avoid drift and ensure future changes remain consistent.

    if (response.type === "error") {
      state.loading = false;
      state.loadingTotal = 0;
      state.loadingCompleted = 0;
      state.loadingProgressPercentage = 0;
      state.isOperationCancelled = false;
      state.isPartialData = false;
      processApiError(response?.content, "sql");
    }

    if (response.type === "end") {
      state.loading = false;
      state.loadingTotal = 0;
      state.loadingCompleted = 0;
      state.loadingProgressPercentage = 100; // Set to 100% when complete
      state.isOperationCancelled = false;
      state.isPartialData = false; // Explicitly set to false when complete
      saveCurrentStateToCache();
    }

    if (response.type === "event_progress") {
      state.loadingProgressPercentage = response?.content?.percent ?? 0;
      state.isPartialData = true;
      saveCurrentStateToCache();
    }
  } catch (error: any) {
    state.loading = false;
    state.isOperationCancelled = false;
    state.loadingTotal = 0;
    state.loadingCompleted = 0;
    state.loadingProgressPercentage = 0;
    state.isPartialData = false;
    state.errorDetail = {
      message: error?.message || "Unknown error in search response",
      code: error?.code ?? "",
    };
  }
};

const sendSearchMessage = async (payload: any) => {
  // check if query is already canceled, if it is, close the socket
  if (state.isOperationCancelled) {
    state.isOperationCancelled = false;

    // clean up the listeners
    cleanUpListeners(payload.traceId);

    return;
  }

  sendSearchMessageBasedOnRequestId({
    type: "search",
    content: {
      trace_id: payload.traceId,
      payload: {
        query: {
          ...(await getHistogramSearchRequest(
            payload.queryReq.query,
            payload.queryReq.it,
            payload.queryReq.startISOTimestamp,
            payload.queryReq.endISOTimestamp,
            null,
          )),
        },
        // pass encodig if enabled,
        // make sure that `encoding: null` is not being passed, that's why used object extraction logic
        ...(store.state.zoConfig.sql_base64_enabled
          ? { encoding: "base64" }
          : {}),
      },
      stream_type: payload.pageType,
      search_type: searchType.value ?? "dashboards",
      org_id: store?.state?.selectedOrganization?.identifier,
      use_cache: (window as any).use_cache ?? true,
      dashboard_id: dashboardId?.value,
      dashboard_name: dashboardName?.value,
      folder_id: folderId?.value,
      folder_name: folderName?.value,
      panel_id: panelSchema.value.id,
      panel_name: panelSchema.value.title,
      run_id: runId?.value,
      tab_id: tabId?.value,
      tab_name: tabName?.value,
      fallback_order_by_col: getFallbackOrderByCol(),
      is_ui_histogram: is_ui_histogram.value,
    },
  });
};

const handleSearchClose = (payload: any, response: any) => {
  removeTraceId(payload?.traceId);

  if (response.type === "error") {
    processApiError(response?.content, "sql");
  }

  const errorCodes = [1001, 1006, 1010, 1011, 1012, 1013];

  if (errorCodes.includes(response.code)) {
    handleSearchError(payload, {
      content: {
        message:
          "WebSocket connection terminated unexpectedly. Please check your network and try again",
        trace_id: payload.traceId,
        code: response.code,
        error_detail: "",
      },
    });
  }

  // set loading to false
  state.loading = false;
  state.isOperationCancelled = false;
  state.isPartialData = false;
  // save current state to cache
  // this is async task, which will be executed in background(await is not required)
  saveCurrentStateToCache();
};

const handleSearchReset = (payload: any, traceId?: string) => {
  // Save current state to cache
  saveCurrentStateToCache();
  loadData();
};

const handleSearchError = (payload: any, response: any) => {
  removeTraceId(payload.traceId);

  // set loading to false
  state.loading = false;
  state.loadingTotal = 0;
  state.loadingCompleted = 0;
  state.loadingProgressPercentage = 0;
  state.isOperationCancelled = false;
  state.isPartialData = false;

  processApiError(response?.content, "sql");
};

const shouldSkipSearchDueToEmptyVariables = () => {
  // Retrieve all variables data
  const allVars = [
    ...(getDependentVariablesData() || []),
    ...(getDynamicVariablesData() || []),
  ];

  // Identify variables with empty values
  const variablesToSkip = allVars
    .filter(
      (v) =>
        v.value === null ||
        v.value === undefined ||
        (Array.isArray(v.value) && v.value.length === 0),
    )
    .map((v) => v.name);

  // Log variables for which the API will be skipped
  variablesToSkip.forEach((variableName) => {
    state.loading = false;
  });

  // Return true if there are any variables to skip, indicating loading should be continued
  return variablesToSkip.length > 0;
};

const getDataThroughWebSocket = async (
  query: string,
  it: any,
  startISOTimestamp: string,
  endISOTimestamp: string,
  pageType: string,
  currentQueryIndex: number,
) => {
  try {
    const { traceId } = generateTraceContext();
    addTraceId(traceId);

    const payload: {
      queryReq: any;
      type: "search" | "histogram" | "pageCount" | "values";
      isPagination: boolean;
      traceId: string;
      org_id: string;
      pageType: string;
      meta: any;
    } = {
      queryReq: {
        query,
        it,
        startISOTimestamp,
        endISOTimestamp,
        currentQueryIndex,
        // pass encodig if enabled,
        // make sure that encoding: null is not being passed, that's why used object extraction logic
        ...(store.state.zoConfig.sql_base64_enabled
          ? { encoding: "base64" }
          : {}),
      },
      type: "histogram",
      isPagination: false,
      traceId,
      org_id: store?.state?.selectedOrganization?.identifier,
      pageType,
      meta: {
        currentQueryIndex,
        panel_id: panelSchema.value.id,
        panel_name: panelSchema.value.title,
        run_id: runId?.value,
        tab_id: tabId?.value,
        tab_name: tabName?.value,
        dashboard_name: dashboardName?.value,
        folder_name: folderName?.value,
      },
    };

    // Add guard here
    if (shouldSkipSearchDueToEmptyVariables()) {
      return;
    }
    fetchQueryDataWithWebSocket(payload, {
      open: sendSearchMessage,
      close: handleSearchClose,
      error: handleSearchError,
      message: handleSearchResponse,
      reset: handleSearchReset,
    });

    addTraceId(traceId);
  } catch (e: any) {
    state.errorDetail = {
      message: e?.message || e,
      code: e?.code ?? "",
    };
    state.loading = false;
    state.isOperationCancelled = false;
    state.isPartialData = false;
  }
};

const getDataThroughStreaming = async (
  query: string,
  it: any,
  startISOTimestamp: string,
  endISOTimestamp: string,
  pageType: string,
  currentQueryIndex: number,
  abortControllerRef: any,
) => {
  try {
    const { traceId } = generateTraceContext();

    const payload: {
      queryReq: any;
      type: "search" | "histogram" | "pageCount";
      isPagination: boolean;
      traceId: string;
      org_id: string;
      pageType: string;
      searchType: string;
      meta: any;
    } = {
      queryReq: {
        query: {
          ...(await getHistogramSearchRequest(
            query,
            it,
            startISOTimestamp,
            endISOTimestamp,
            null,
          )),
        },
      },
      type: "histogram",
      isPagination: false,
      traceId,
      org_id: store?.state?.selectedOrganization?.identifier,
      pageType,
      searchType: searchType.value ?? "dashboards",
      meta: {
        currentQueryIndex,
        dashboard_id: dashboardId?.value,
        dashboard_name: dashboardName?.value,
        folder_id: folderId?.value,
        folder_name: folderName?.value,
        panel_id: panelSchema.value.id,
        panel_name: panelSchema.value.title,
        run_id: runId?.value,
        tab_id: tabId?.value,
        tab_name: tabName?.value,
        fallback_order_by_col: getFallbackOrderByCol(),
        is_ui_histogram: is_ui_histogram.value,
      },
    };

    // type: "search",
    // content: {
    //   trace_id: payload.traceId,
    //   payload: {
    //     query: await getHistogramSearchRequest(
    //       payload.queryReq.query,
    //       payload.queryReq.it,
    //       payload.queryReq.startISOTimestamp,
    //       payload.queryReq.endISOTimestamp,
    //       null,
    //     ),
    //   },
    //   stream_type: payload.pageType,
    //   search_type: searchType.value ?? "dashboards",
    //   org_id: store?.state?.selectedOrganization?.identifier,
    //   use_cache: (window as any).use_cache ?? true,
    //   dashboard_id: dashboardId?.value,
    //   folder_id: folderId?.value,
    //   fallback_order_by_col: getFallbackOrderByCol(),
    // },

    // if aborted, return
    if (abortControllerRef?.signal?.aborted) {
      // Set partial data flag on abort
      state.isPartialData = true;
      // Save current state to cache
      saveCurrentStateToCache();
      return;
    }

    // Add guard here
    if (shouldSkipSearchDueToEmptyVariables()) {
      return;
    }

    fetchQueryDataWithHttpStream(payload, {
      data: handleSearchResponse,
      error: handleSearchError,
      complete: handleSearchClose,
      reset: handleSearchReset,
    });

    addTraceId(traceId);
  } catch (e: any) {
    state.errorDetail = {
      message: e?.message || e,
      code: e?.code ?? "",
    };
    state.loading = false;
    state.isOperationCancelled = false;
    state.isPartialData = false;
  }
Async Race

If concurrent requests can update shared state, resetting isPartialData in late-arriving error handlers might clear a valid partial state from a newer request. Verify request-scoping/traceId guards prevent stale handlers from mutating current state.

const handleSearchError = (payload: any, response: any) => {
  removeTraceId(payload.traceId);

  // set loading to false
  state.loading = false;
  state.loadingTotal = 0;
  state.loadingCompleted = 0;
  state.loadingProgressPercentage = 0;
  state.isOperationCancelled = false;
  state.isPartialData = false;

  processApiError(response?.content, "sql");
};

@github-actions
Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Conditionally clear partial data

Guard the global isPartialData reset to only clear when the error is terminal for
the current request. If type can be non-terminal (e.g., partial stream chunk
errors), conditionally reset based on type or error code to avoid wiping
legitimately displayed partial results.

web/src/composables/dashboard/usePanelDataLoader.ts [2086-2092]

 const processApiError = async (error: any, type: any) => {
-  state.isPartialData = false;
+  const isTerminal = type === "sql" || type === "promql" || type === "traces";
+  if (isTerminal) {
+    state.isPartialData = false;
+  }
 
   switch (type) {
     case "promql": {
       const errorDetailValue = error?.response?.data?.error || error?.message;
       const trimmedErrorMessage =
Suggestion importance[1-10]: 7

__

Why: Making the isPartialData reset conditional can preserve valid partial results during non-terminal errors, aligning with streaming scenarios. The idea is contextually sound, though specifics depend on how type maps to terminal errors.

Medium
General
Avoid double state resets

Ensure isPartialData is not reset before error classification if some handlers rely
on it for user messaging. Move the reset into processApiError (already added) or
after it to avoid briefly clearing it and then re-clearing inside. This prevents
race conditions between different error paths.

web/src/composables/dashboard/usePanelDataLoader.ts [864-872]

 if (response.type === "error") {
   state.loading = false;
   state.loadingTotal = 0;
   state.loadingCompleted = 0;
   state.loadingProgressPercentage = 0;
   state.isOperationCancelled = false;
-  state.isPartialData = false;
   processApiError(response?.content, "sql");
 }
Suggestion importance[1-10]: 6

__

Why: The existing code resets state.isPartialData here and again inside processApiError, causing redundant updates; moving it avoids duplicate state churn. Impact is moderate as it cleans up state management but doesn't fix a critical bug.

Low
Order state updates safely

Preserve errorDetail assignment ordering so UI can read consistent state even if
reactive watchers fire immediately. Set errorDetail before toggling flags like
loading and isPartialData to avoid transient UI states that clear partial data
indicators without the error visible.

web/src/composables/dashboard/usePanelDataLoader.ts [889-899]

 } catch (error: any) {
+  state.errorDetail = {
+    message: error?.message || "Unknown error in search response",
+    code: error?.code ?? "",
+  };
   state.loading = false;
   state.isOperationCancelled = false;
   state.loadingTotal = 0;
   state.loadingCompleted = 0;
   state.loadingProgressPercentage = 0;
   state.isPartialData = false;
-  state.errorDetail = {
-    message: error?.message || "Unknown error in search response",
-    code: error?.code ?? "",
-  };
Suggestion importance[1-10]: 5

__

Why: Reordering to set errorDetail first can reduce transient UI inconsistencies in reactive contexts; it's plausible and harmless. It’s a minor robustness improvement without clear evidence of a current bug.

Low

@testdino-playwright-reporter
Copy link

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
All tests passed 364 345 0 19 0 95% 4m 38s

View Detailed Results

@ktx-vaidehi ktx-vaidehi marked this pull request as ready for review October 27, 2025 09:10
@ktx-vaidehi ktx-vaidehi force-pushed the fix/dashboard/partial-data-icon-on-error branch from f065579 to 2dd1b4e Compare October 27, 2025 09:11
@github-actions
Copy link
Contributor

Failed to generate code suggestions for PR

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Overview

Greptile Summary

Resets state.isPartialData to false across all error handlers to prevent stale partial-data UI indicators from persisting after query failures.

Changes

  • Added state.isPartialData = false in 7 error handling paths:
    • handleSearchResponse error type handler (line 870)
    • handleSearchResponse exception catch block (line 895)
    • handleSearchError function (line 998)
    • handleSearchClose function (line 977)
    • getDataThroughWebSocket catch block (line 1099)
    • getDataThroughStreaming catch block (line 1208)
    • processApiError function (line 2087)

Context

The isPartialData flag controls a UI indicator showing when data is incomplete. Previously set to true on cancellations and partial responses (lines 350, 471, 886, 1182, 2550), it was not consistently cleared on errors, causing the partial-data icon to stick around even after failures.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are simple, focused, and follow existing patterns. Each addition of state.isPartialData = false is placed in error handling paths alongside existing state resets (loading = false, isOperationCancelled = false). The logic is sound: errors represent definitive completion states (not partial data), so the partial-data flag should be cleared. No complex logic changes or side effects introduced.
  • No files require special attention

Important Files Changed

File Analysis

Filename Score Overview
web/src/composables/dashboard/usePanelDataLoader.ts 5/5 Added state.isPartialData = false resets across 7 error handling paths to prevent stale partial-data UI state after failures

Sequence Diagram

sequenceDiagram
    participant User
    participant DataLoader as usePanelDataLoader
    participant API as Query API
    participant State as UI State
    
    User->>DataLoader: Trigger data load
    DataLoader->>API: Send query request
    
    alt Query succeeds
        API-->>DataLoader: Success response
        DataLoader->>State: isPartialData = false
        DataLoader->>State: loading = false
        State-->>User: Show complete data
    else Query fails (Error)
        API-->>DataLoader: Error response
        DataLoader->>State: isPartialData = false ✅
        DataLoader->>State: loading = false
        DataLoader->>State: Set error details
        State-->>User: Show error (no stale partial icon)
    else Query cancelled
        User->>DataLoader: Cancel operation
        DataLoader->>API: Cancel request
        DataLoader->>State: isPartialData = true
        DataLoader->>State: isOperationCancelled = true
        State-->>User: Show partial data indicator
    else Exception thrown
        API-->>DataLoader: Exception
        DataLoader->>State: isPartialData = false ✅
        DataLoader->>State: loading = false
        DataLoader->>State: Set error details
        State-->>User: Show error (no stale partial icon)
    end
Loading

1 file reviewed, no comments

Edit Code Review Agent Settings | Greptile

@ktx-vaidehi ktx-vaidehi force-pushed the fix/dashboard/partial-data-icon-on-error branch from 2dd1b4e to 9bd6af9 Compare October 27, 2025 14:22
@ktx-vaidehi ktx-vaidehi merged commit bc945de into main Oct 27, 2025
31 of 32 checks passed
@ktx-vaidehi ktx-vaidehi deleted the fix/dashboard/partial-data-icon-on-error branch October 27, 2025 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

☢️ Bug Something isn't working Review effort 2/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants