From 7becccd628a2991feda2385f3f52b59fbc7813ab Mon Sep 17 00:00:00 2001 From: Zhihe Li Date: Thu, 13 Mar 2025 18:50:33 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Fix=20search=20field=20ra?= =?UTF-8?q?ce=20condition=20in=20DC=20targets=20(#2723)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 Fix search field race condition in DC targets * feat: 🎸 Keep track of last search to know when to debounce ✅ Closes: ICU-14775 --- .../scopes/scope/projects/targets/index.js | 2 - .../scopes/scope/projects/targets/index.js | 152 ++++++++++-------- 2 files changed, 88 insertions(+), 66 deletions(-) diff --git a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js index 70ccda175..c62c383d5 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js @@ -8,7 +8,6 @@ import { service } from '@ember/service'; import { action } from '@ember/object'; import { loading } from 'ember-loading'; import { tracked } from '@glimmer/tracking'; -import { debounce } from 'core/decorators/debounce'; import { notifySuccess, notifyError } from 'core/decorators/notify'; import orderBy from 'lodash/orderBy'; import { TYPES_TARGET } from 'api/models/target'; @@ -251,7 +250,6 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle * @param {object} event */ @action - @debounce(250) handleSearchInput(event) { const { value } = event.target; this.search = value; diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/index.js b/ui/desktop/app/routes/scopes/scope/projects/targets/index.js index 073a5b86f..bf35a93e6 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets/index.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets/index.js @@ -10,6 +10,7 @@ import { STATUS_SESSION_PENDING, } from 'api/models/session'; import { action } from '@ember/object'; +import { restartableTask, timeout } from 'ember-concurrency'; const { __electronLog } = globalThis; @@ -82,78 +83,101 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route { * @return {Promise<{totalItems: number, targets: [TargetModel], projects: [ScopeModel], * isCacheDaemonRunning: boolean, isLoadIncomplete: boolean, isCacheRefreshing: boolean}>} */ - async model({ search, scopes, availableSessions, types, page, pageSize }) { - const isCacheRunningPromise = this.ipc.invoke('isCacheDaemonRunning'); + async model(params) { + const useDebounce = + this.retrieveData?.lastPerformed?.args?.[0].search !== params.search; + return this.retrieveData.perform({ ...params, useDebounce }); + } - const orgScope = this.modelFor('scopes.scope'); - const projects = this.modelFor('scopes.scope.projects'); - - // orgFilter used to narrow down resources to only those under - // the current org scope if org is not global - const orgFilter = `"/item/scope/parent_scope_id" == "${orgScope.id}"`; - - const filters = { scope_id: [], id: { values: [] }, type: [] }; - scopes.forEach((scope) => { - filters.scope_id.push({ equals: scope }); - }); - types.forEach((type) => { - filters.type.push({ equals: type }); - }); - - const sessions = await this.getSessions(orgScope, scopes, orgFilter); - this.addActiveSessionFilters(filters, availableSessions, sessions); - - const query = { - recursive: true, - scope_id: orgScope.id, - query: { search, filters }, + retrieveData = restartableTask( + async ({ + search, + scopes, + availableSessions, + types, page, pageSize, - force_refresh: true, - }; - if (orgScope.isOrg && scopes.length === 0) { - query.filter = orgFilter; - } - let targets = await this.store.query('target', query); - const { totalItems, isLoadIncomplete, isCacheRefreshing } = targets.meta; - // Filter out targets to which users do not have the connect ability - targets = targets.filter((target) => - this.can.can('connect target', target), - ); + useDebounce, + }) => { + if (useDebounce) { + await timeout(250); + } - const aliasPromise = this.store.query('alias', { - scope_id: 'global', - force_refresh: true, - query: { - filters: { - destination_id: { - logicalOperator: 'or', - values: targets.map((target) => ({ - equals: target.id, - })), + const isCacheRunningPromise = this.ipc.invoke('isCacheDaemonRunning'); + + const orgScope = this.modelFor('scopes.scope'); + const projects = this.modelFor('scopes.scope.projects'); + + // orgFilter used to narrow down resources to only those under + // the current org scope if org is not global + const orgFilter = `"/item/scope/parent_scope_id" == "${orgScope.id}"`; + + const filters = { scope_id: [], id: { values: [] }, type: [] }; + scopes.forEach((scope) => { + filters.scope_id.push({ equals: scope }); + }); + types.forEach((type) => { + filters.type.push({ equals: type }); + }); + + const sessions = await this.getSessions(orgScope, scopes, orgFilter); + this.addActiveSessionFilters(filters, availableSessions, sessions); + + const query = { + recursive: true, + scope_id: orgScope.id, + query: { search, filters }, + page, + pageSize, + force_refresh: true, + }; + if (orgScope.isOrg && scopes.length === 0) { + query.filter = orgFilter; + } + let targets = await this.store.query('target', query); + const { totalItems, isLoadIncomplete, isCacheRefreshing } = targets.meta; + // Filter out targets to which users do not have the connect ability + targets = targets.filter((target) => + this.can.can('connect target', target), + ); + + const aliasPromise = this.store.query('alias', { + scope_id: 'global', + force_refresh: true, + query: { + filters: { + destination_id: { + logicalOperator: 'or', + values: targets.map((target) => ({ + equals: target.id, + })), + }, }, }, - }, - }); + }); - // Load the aliases for the targets on the current page - try { - await aliasPromise; - } catch (e) { - __electronLog?.warn('Could not retrieve aliases for targets', e.message); - // Separately await and catch the error here so we can continue loading - // the page in case the controller doesn't support aliases yet - } + // Load the aliases for the targets on the current page + try { + await aliasPromise; + } catch (e) { + __electronLog?.warn( + 'Could not retrieve aliases for targets', + e.message, + ); + // Separately await and catch the error here so we can continue loading + // the page in case the controller doesn't support aliases yet + } - return { - targets, - projects, - totalItems, - isLoadIncomplete, - isCacheRefreshing, - isCacheDaemonRunning: await isCacheRunningPromise, - }; - } + return { + targets, + projects, + totalItems, + isLoadIncomplete, + isCacheRefreshing, + isCacheDaemonRunning: await isCacheRunningPromise, + }; + }, + ); async getSessions(orgScope, scopes, orgFilter) { // Retrieve all sessions so that the session and activeSessions getters