Skip to content

Commit 24a3df0

Browse files
committed
feat: 🎸 Refactor session recording filters
1 parent 25f015b commit 24a3df0

File tree

11 files changed

+261
-131
lines changed

11 files changed

+261
-131
lines changed

‎addons/api/addon/handlers/sqlite-handler.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default class SqliteHandler {
3535
pushToStore = true,
3636
peekDb = false,
3737
storeToken = true,
38+
returnRawData = false,
3839
} = {},
3940
} = data;
4041
const supportedModels = Object.keys(modelMapping);
@@ -49,7 +50,13 @@ export default class SqliteHandler {
4950
const schema = store.modelFor(type);
5051
const serializer = store.serializerFor(type);
5152

52-
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
53+
let {
54+
page,
55+
pageSize,
56+
select,
57+
query: queryObj,
58+
...remainingQuery
59+
} = query;
5360
let payload,
5461
listToken,
5562
writeToDbPromise,
@@ -119,17 +126,21 @@ export default class SqliteHandler {
119126
const { sql, parameters } = generateSQLExpressions(type, queryObj, {
120127
page,
121128
pageSize,
122-
select: ['data'],
129+
select: select ?? [{ field: 'data' }],
123130
});
124131

125132
const rows = await this.sqlite.fetchResource({
126133
sql,
127134
parameters,
128135
});
129136

137+
if (returnRawData) {
138+
return rows;
139+
}
140+
130141
const { sql: countSql, parameters: countParams } =
131142
generateSQLExpressions(type, queryObj, {
132-
select: ['count(*) as total'],
143+
select: [{ field: '*', isCount: true, alias: 'total' }],
133144
});
134145
const count = await this.sqlite.fetchResource({
135146
sql: countSql,

‎addons/api/addon/services/sqlite.js

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ export const modelMapping = {
100100
target_name: 'create_time_values.target.name',
101101
target_scope_id: 'create_time_values.target.scope.id',
102102
target_scope_name: 'create_time_values.target.scope.name',
103+
target_scope_parent_scope_id:
104+
'create_time_values.target.scope.parent_scope_id',
103105
created_time: 'created_time',
104106
},
105107
session: {
@@ -114,21 +116,6 @@ export const modelMapping = {
114116
},
115117
};
116118

117-
// A list of tables that we support searching using FTS5 in SQLite.
118-
export const searchTables = new Set([
119-
'target',
120-
'alias',
121-
'group',
122-
'role',
123-
'user',
124-
'credential-store',
125-
'scope',
126-
'auth-method',
127-
'host-catalog',
128-
'session-recording',
129-
'session',
130-
]);
131-
132119
export default class SqliteDbService extends Service {
133120
// =attributes
134121

‎addons/api/addon/utils/sqlite-query.js

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55

66
import { modelMapping } from 'api/services/sqlite';
7-
import { searchTables } from 'api/services/sqlite';
87
import { typeOf } from '@ember/utils';
98
import { underscore } from '@ember/string';
109

@@ -35,17 +34,15 @@ export function generateSQLExpressions(
3534
addFilterConditions({ filters, parameters, conditions });
3635
addSearchConditions({ search, resource, tableName, parameters, conditions });
3736

37+
const selectClause = constructSelectClause(select, tableName);
3838
const orderByClause = constructOrderByClause(resource, sort);
39-
4039
const whereClause = conditions.length ? `WHERE ${and(conditions)}` : '';
4140

4241
const paginationClause = page && pageSize ? `LIMIT ? OFFSET ?` : '';
4342
if (paginationClause) {
4443
parameters.push(pageSize, (page - 1) * pageSize);
4544
}
4645

47-
const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM "${tableName}"`;
48-
4946
return {
5047
// Replace any empty newlines or leading whitespace on each line to be consistent with formatting
5148
// This is mainly to help us read and test the generated SQL as it has no effect on the actual SQL execution.
@@ -114,32 +111,57 @@ function addSearchConditions({
114111
parameters,
115112
conditions,
116113
}) {
117-
if (!search) {
114+
if (!search || !modelMapping[resource]) {
118115
return;
119116
}
120117

121-
if (searchTables.has(resource)) {
122-
// Use the special prefix indicator "*" for full-text search
123-
parameters.push(`"${search}"*`);
124-
// Use a subquery to match against the FTS table with rowids as SQLite is
125-
// much more efficient with FTS queries when using rowids or MATCH (or both).
126-
// We could have also used a join here but a subquery is simpler.
127-
conditions.push(
128-
`rowid IN (SELECT rowid FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?)`,
118+
// Use the special prefix indicator "*" for full-text search
119+
if (typeOf(search) === 'object') {
120+
if (!search?.value) {
121+
return;
122+
}
123+
124+
parameters.push(
125+
or(search.fields.map((field) => `${field}:"${search.value}"*`)),
129126
);
130-
return;
127+
} else {
128+
parameters.push(`"${search}"*`);
131129
}
132130

133-
const fields = Object.keys(modelMapping[resource]);
134-
const searchConditions = parenthetical(
135-
or(
136-
fields.map((field) => {
137-
parameters.push(`%${search}%`);
138-
return `${field}${OPERATORS['contains']}`;
139-
}),
140-
),
131+
// Use a subquery to match against the FTS table with rowids as SQLite is
132+
// much more efficient with FTS queries when using rowids or MATCH (or both).
133+
// We could have also used a join here but a subquery is simpler.
134+
conditions.push(
135+
`rowid IN (SELECT rowid FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?)`,
141136
);
142-
conditions.push(searchConditions);
137+
}
138+
function constructSelectClause(select = [{ field: '*' }], tableName) {
139+
const distinctColumns = select.filter(({ isDistinct }) => isDistinct);
140+
let selectColumns;
141+
142+
// Special case for distinct columns as they must be grouped together.
143+
// We're only handling simple use cases as anything more complicated
144+
// like windows/CTEs can be custom SQL.
145+
if (distinctColumns.length > 0) {
146+
selectColumns = `DISTINCT ${distinctColumns.map(({ field }) => field).join(', ')}`;
147+
} else {
148+
selectColumns = select
149+
.map(({ field, isCount, alias }) => {
150+
let column = field;
151+
152+
if (isCount) {
153+
column = `count(${column})`;
154+
}
155+
if (alias) {
156+
column = `${column} as ${alias}`;
157+
}
158+
159+
return column;
160+
})
161+
.join(', ');
162+
}
163+
164+
return `SELECT ${selectColumns} FROM "${tableName}"`;
143165
}
144166

145167
function constructOrderByClause(resource, sort) {

‎addons/api/addon/workers/utils/schema.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ CREATE TABLE IF NOT EXISTS session_recording (
352352
target_name TEXT,
353353
target_scope_id TEXT,
354354
target_scope_name TEXT,
355+
target_scope_parent_scope_id TEXT,
355356
created_time TEXT NOT NULL,
356357
data TEXT NOT NULL
357358
);
@@ -371,21 +372,22 @@ CREATE VIRTUAL TABLE IF NOT EXISTS session_recording_fts USING fts5(
371372
target_name,
372373
target_scope_id,
373374
target_scope_name,
375+
target_scope_parent_scope_id,
374376
created_time,
375377
content='',
376378
);
377379
378380
CREATE TRIGGER IF NOT EXISTS session_recording_ai AFTER INSERT ON session_recording BEGIN
379381
INSERT INTO session_recording_fts(
380-
id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, created_time
382+
id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, target_scope_parent_scope_id, created_time
381383
) VALUES (
382-
new.id, new.type, new.state, new.start_time, new.end_time, new.duration, new.scope_id, new.user_id, new.user_name, new.target_id, new.target_name, new.target_scope_id, new.target_scope_name, new.created_time
384+
new.id, new.type, new.state, new.start_time, new.end_time, new.duration, new.scope_id, new.user_id, new.user_name, new.target_id, new.target_name, new.target_scope_id, new.target_scope_name, new.target_scope_parent_scope_id, new.created_time
383385
);
384386
END;
385387
386388
CREATE TRIGGER IF NOT EXISTS session_recording_ad AFTER DELETE ON session_recording BEGIN
387-
INSERT INTO session_recording_fts(session_recording_fts, rowid, id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, created_time)
388-
VALUES('delete', old.rowid, old.id, old.type, old.state, old.start_time, old.end_time, old.duration, old.scope_id, old.user_id, old.user_name, old.target_id, old.target_name, old.target_scope_id, old.target_scope_name, old.created_time);
389+
INSERT INTO session_recording_fts(session_recording_fts, rowid, id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, target_scope_parent_scope_id, created_time)
390+
VALUES('delete', old.rowid, old.id, old.type, old.state, old.start_time, old.end_time, old.duration, old.scope_id, old.user_id, old.user_name, old.target_id, old.target_name, old.target_scope_id, old.target_scope_name, old.target_scope_parent_scope_id, old.created_time);
389391
END;`;
390392

391393
const createSessionTables = `

‎addons/api/tests/unit/services/sqlite-test.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
import { module, test } from 'qunit';
77
import { setupTest } from 'dummy/tests/helpers';
88
import { setupSqlite } from 'api/test-support/helpers/sqlite';
9-
import { modelMapping, searchTables } from 'api/services/sqlite';
9+
import { modelMapping } from 'api/services/sqlite';
1010
import { underscore } from '@ember/string';
1111

1212
const supportedModels = Object.keys(modelMapping);
13-
const supportedFtsTables = [...searchTables];
1413

1514
module('Unit | Service | sqlite', function (hooks) {
1615
setupTest(hooks);
@@ -38,7 +37,7 @@ module('Unit | Service | sqlite', function (hooks) {
3837

3938
test.each(
4039
'Mapping matches fts table columns',
41-
supportedFtsTables,
40+
supportedModels,
4241
async function (assert, resource) {
4342
const service = this.owner.lookup('service:sqlite');
4443

‎addons/core/addon/components/dropdown/index.hbs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,26 @@
2020
<DD.Header @hasDivider={{true}}>
2121
<Hds::Form::TextInput::Base
2222
@type='search'
23-
@value={{this.searchTerm}}
23+
@value={{or this.searchTerm @searchTerm}}
2424
placeholder={{t 'actions.narrow-results'}}
2525
aria-label={{t 'actions.narrow-results'}}
2626
{{on 'input' this.filterItems}}
2727
data-test-dropdown-search
2828
/>
2929
</DD.Header>
3030
{{/if}}
31-
{{#unless this.itemOptions}}
31+
32+
{{#unless (or this.itemOptions @isLoading)}}
3233
<DD.Description @text={{t 'titles.no-results'}} />
3334
{{/unless}}
3435

35-
{{#if (has-block)}}
36+
{{#if @isLoading}}
37+
<DD.Generic>
38+
<Hds::Layout::Flex @align='center' @justify='center'>
39+
<Hds::Icon @name='loading' @size='24' class='margin-y-xs' />
40+
</Hds::Layout::Flex>
41+
</DD.Generic>
42+
{{else if (has-block)}}
3643
{{yield DD this.selectItem this.itemOptions}}
3744
{{else}}
3845
{{#each this.itemOptions as |itemOption|}}
@@ -47,13 +54,15 @@
4754
{{/each}}
4855
{{/if}}
4956

50-
<DD.Footer @hasDivider={{true}}>
51-
<Hds::Button
52-
@text='Apply'
53-
@isFullWidth={{true}}
54-
@size='small'
55-
{{on 'click' (fn this.applyFilter DD.close)}}
56-
data-test-dropdown-apply-button
57-
/>
58-
</DD.Footer>
57+
{{#unless @isLoading}}
58+
<DD.Footer @hasDivider={{true}}>
59+
<Hds::Button
60+
@text='Apply'
61+
@isFullWidth={{true}}
62+
@size='small'
63+
{{on 'click' (fn this.applyFilter DD.close)}}
64+
data-test-dropdown-apply-button
65+
/>
66+
</DD.Footer>
67+
{{/unless}}
5968
</Hds::Dropdown>

‎addons/core/addon/components/dropdown/index.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export default class DropdownComponent extends Component {
2121
*/
2222
get itemOptions() {
2323
let items = this.args.itemOptions;
24-
if (this.searchTerm) {
24+
25+
if (this.searchTerm && !this.args.updateSearchTerm) {
2526
const searchTerm = this.searchTerm.toLowerCase();
2627
items = this.args.itemOptions.filter((item) => {
2728
const isNameMatch = item.name?.toLowerCase().includes(searchTerm);
@@ -39,10 +40,15 @@ export default class DropdownComponent extends Component {
3940
* @param {object} event
4041
*/
4142
@action
42-
@debounce(150)
43-
filterItems(event) {
43+
@debounce(250)
44+
async filterItems(event) {
4445
const { value } = event.target;
45-
this.searchTerm = value;
46+
47+
if (this.args.updateSearchTerm) {
48+
this.args.updateSearchTerm(value);
49+
} else {
50+
this.searchTerm = value;
51+
}
4652
}
4753

4854
/**

‎addons/core/addon/styles/addon.scss

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* SPDX-License-Identifier: BUSL-1.1
44
*/
55

6+
@use 'rose/app/styles/rose/variables/sizing' as sizing;
7+
68
.filters-applied {
79
display: flex;
810
flex-wrap: wrap;
@@ -72,3 +74,34 @@
7274
margin-bottom: 0.5rem;
7375
}
7476
}
77+
78+
$margin-directions: (
79+
'': 'margin',
80+
'-top': 'margin-top',
81+
'-right': 'margin-right',
82+
'-bottom': 'margin-bottom',
83+
'-left': 'margin-left',
84+
'-x': (
85+
'margin-left',
86+
'margin-right',
87+
),
88+
'-y': (
89+
'margin-top',
90+
'margin-bottom',
91+
),
92+
);
93+
94+
// Generate margin utility classes with sizes and directions
95+
@each $size-name, $size-coefficient in sizing.$size-coefficients {
96+
@each $direction-suffix, $properties in $margin-directions {
97+
.margin#{$direction-suffix}-#{$size-name} {
98+
@if type-of($properties) == 'list' {
99+
@each $property in $properties {
100+
#{$property}: #{sizing.rems($size-name)};
101+
}
102+
} @else {
103+
#{$properties}: #{sizing.rems($size-name)};
104+
}
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)