fix: 🐛 Fix search field race condition in DC targets (#2723)

* fix: 🐛 Fix search field race condition in DC targets
* feat: 🎸 Keep track of last search to know when to debounce

✅ Closes: ICU-14775
This commit is contained in:
Zhihe Li authored and GitHub committed 2025-03-13 18:50:33 -04:00
1 parent dff5cac4fe
commit 7becccd628
2 files changed
+88 -66

No files matched your search

@@ -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;
@@ -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