refactor: migrate signal-based services from HttpClient to ApiService#1608
Conversation
Replace direct HttpClient (_http) usage with centralized ApiService (_api) in DashboardsService and SavedQueriesService. Convert Observable-based methods to async/await Promises and update all callers accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates DashboardsService and SavedQueriesService off direct HttpClient usage onto the centralized ApiService, and updates affected components/tests to use Promise-based async/await instead of Observables.
Changes:
- Refactor
SavedQueriesServiceandDashboardsServiceto useApiService(includinghttpResource-backed resources). - Convert saved-query call sites to async/await (
DashboardWidgetComponent,ChartEditComponent,ChartDeleteDialogComponent). - Update unit tests to mock Promise-returning service methods and
awaitasync component methods.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/app/services/saved-queries.service.ts | Replace HttpClient/RxJS resource + Observable APIs with ApiService.resource and Promise-based CRUD/execute/test methods |
| frontend/src/app/services/dashboards.service.ts | Replace HttpClient + resource loaders with ApiService.resource and Promise-based CRUD/widget methods |
| frontend/src/app/components/dashboards/widget-renderers/dashboard-widget/dashboard-widget.component.ts | Update saved query fetch/execute to async/await |
| frontend/src/app/components/charts/chart-edit/chart-edit.component.ts | Convert query load/test/save flows from Observable subscriptions to async/await with finally cleanup |
| frontend/src/app/components/charts/chart-edit/chart-edit.component.spec.ts | Update mocks/tests for Promise-based service methods and async component methods |
| frontend/src/app/components/charts/chart-delete-dialog/chart-delete-dialog.component.ts | Convert delete flow from subscription to async/await |
| frontend/src/app/components/charts/chart-delete-dialog/chart-delete-dialog.component.spec.ts | Update mocks/tests for Promise-based delete and async handler |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const [query, result] = await Promise.all([ | ||
| firstValueFrom(this._savedQueries.fetchSavedQuery(this.connectionId, this.widget.query_id)), | ||
| firstValueFrom(this._savedQueries.executeSavedQuery(this.connectionId, this.widget.query_id)), | ||
| this._savedQueries.fetchSavedQuery(this.connectionId, this.widget.query_id), | ||
| this._savedQueries.executeSavedQuery(this.connectionId, this.widget.query_id), | ||
| ]); | ||
|
|
||
| this.savedQuery.set(query); | ||
| this.queryData.set(result.data); | ||
| if (result) { |
There was a problem hiding this comment.
fetchSavedQuery/executeSavedQuery now resolve null on error (via ApiService) instead of rejecting. In this flow, a null result leaves error unset and the widget can render with empty data/name and no failure state. Add an explicit if (!query || !result) branch (set an error message and return) so errors don't become silent successes.
| @@ -169,17 +126,7 @@ export class DashboardsService { | |||
| widgetId: string, | |||
| payload: UpdateWidgetPayload, | |||
| ): Promise<DashboardWidget | null> { | |||
| try { | |||
| const widget = await firstValueFrom( | |||
| this._http.put<DashboardWidget>(`/dashboard/${dashboardId}/widget/${widgetId}/${connectionId}`, payload), | |||
| ); | |||
| return widget; | |||
| } catch (err: unknown) { | |||
| const error = err as { error?: { message?: string } }; | |||
| console.log(err); | |||
| this._notifications.showErrorSnackbar(error?.error?.message || 'Failed to update widget'); | |||
| return null; | |||
| } | |||
| return this._api.put<DashboardWidget>(`/dashboard/${dashboardId}/widget/${widgetId}/${connectionId}`, payload); | |||
| } | |||
There was a problem hiding this comment.
These _api.put(...) calls don't provide an errorMessage, so failures can degrade to a generic "Unknown error" snackbar compared to the previous explicit "Failed to update widget" fallback. Pass an errorMessage option to _api.put to preserve the prior behavior and make errors actionable.
| this._notifications.showErrorSnackbar(error?.error?.message || 'Failed to update widget position'); | ||
| return null; | ||
| } | ||
| return this._api.put<DashboardWidget>(`/dashboard/${dashboardId}/widget/${widgetId}/${connectionId}`, payload); |
There was a problem hiding this comment.
Same as updateWidget: consider providing an errorMessage option (e.g., "Failed to update widget position") so user-facing errors remain specific when the backend doesn't return error.message.
| return this._api.put<DashboardWidget>(`/dashboard/${dashboardId}/widget/${widgetId}/${connectionId}`, payload); | |
| return this._api.put<DashboardWidget>( | |
| `/dashboard/${dashboardId}/widget/${widgetId}/${connectionId}`, | |
| payload, | |
| { | |
| errorMessage: 'Failed to update widget position', | |
| }, | |
| ); |
| it('should set submitting to true during delete', () => { | ||
| component.onDelete(); | ||
| // The deleteSavedQuery returns synchronously in the mock, so submitting would be false after | ||
| // In real usage, submitting would be true while the request is in flight | ||
| expect(mockSavedQueriesService.deleteSavedQuery).toHaveBeenCalled(); | ||
| // The deleteSavedQuery returns asynchronously, so submitting should be true while in flight | ||
| const testable = component as ChartDeleteDialogComponentTestable; | ||
| expect(testable.submitting()).toBe(true); | ||
| }); |
There was a problem hiding this comment.
This test calls the async onDelete() without awaiting the returned Promise. That leaves a pending async task that can leak into subsequent tests and cause flakiness. Capture the Promise, assert submitting() is true, then await the Promise before the test completes.
| }), | ||
| ); | ||
| async fetchSavedQuery(connectionId: string, queryId: string): Promise<SavedQuery | null> { | ||
| return this._api.get<SavedQuery>(`/connection/${connectionId}/saved-query/${queryId}`); |
There was a problem hiding this comment.
fetchSavedQuery no longer supplies a fallback errorMessage. Since ApiService will otherwise default to the server-provided message (or "Unknown error" when missing), this is a UX regression vs the previous "Failed to fetch saved query" behavior. Pass { errorMessage: 'Failed to fetch saved query' } to _api.get (and keep the server message as details if desired).
| return this._api.get<SavedQuery>(`/connection/${connectionId}/saved-query/${queryId}`); | |
| return this._api.get<SavedQuery>(`/connection/${connectionId}/saved-query/${queryId}`, { | |
| errorMessage: 'Failed to fetch saved query', | |
| }); |
| async createSavedQuery(connectionId: string, payload: CreateSavedQueryPayload): Promise<SavedQuery | null> { | ||
| const query = await this._api.post<SavedQuery>(`/connection/${connectionId}/saved-query`, payload, { | ||
| successMessage: 'Saved query created successfully', | ||
| }); |
There was a problem hiding this comment.
The create call only sets successMessage; on failures, ApiService may surface "Unknown error" if the backend doesn't provide error.message. To preserve the previous explicit fallback, provide an errorMessage (e.g., "Failed to create saved query") in the ApiRequestOptions here (and similarly for update/delete).
| return this._api.post<QueryExecutionResult>( | ||
| `/connection/${connectionId}/saved-query/${queryId}/execute`, | ||
| {}, | ||
| tableName ? { params: { tableName } } : undefined, |
There was a problem hiding this comment.
executeSavedQuery no longer provides a fallback error message. Because ApiService.post resolves to null on error (rather than throwing), callers may not enter catch blocks and you can end up with silent failures unless an errorMessage is provided and/or the caller checks for null. Consider passing { errorMessage: 'Failed to execute query' } and ensuring callers handle a null result.
| return this._api.post<QueryExecutionResult>( | |
| `/connection/${connectionId}/saved-query/${queryId}/execute`, | |
| {}, | |
| tableName ? { params: { tableName } } : undefined, | |
| const options = tableName | |
| ? { params: { tableName }, errorMessage: 'Failed to execute query' } | |
| : { errorMessage: 'Failed to execute query' }; | |
| return this._api.post<QueryExecutionResult>( | |
| `/connection/${connectionId}/saved-query/${queryId}/execute`, | |
| {}, | |
| options, |
| }), | ||
| ); | ||
| async testQuery(connectionId: string, payload: TestQueryPayload): Promise<TestQueryResult | null> { | ||
| return this._api.post<TestQueryResult>(`/connection/${connectionId}/query/test`, payload); |
There was a problem hiding this comment.
testQuery no longer passes an errorMessage, so failures can degrade to a generic "Unknown error" snackbar depending on backend response shape. Add an explicit errorMessage option (e.g., "Failed to test query") to preserve the previous fallback behavior.
| return this._api.post<TestQueryResult>(`/connection/${connectionId}/query/test`, payload); | |
| return this._api.post<TestQueryResult>(`/connection/${connectionId}/query/test`, payload, { | |
| errorMessage: 'Failed to test query', | |
| }); |
Resolve merge conflicts in chart-edit component: keep async/await pattern from signal migration, adopt /panels/ route rename from main. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace direct HttpClient (_http) usage with centralized ApiService (_api) in DashboardsService and SavedQueriesService. Convert Observable-based methods to async/await Promises and update all callers accordingly.