-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(compass-editor, compass-query-bar): show query history in autoco…
…mplete COMPASS-8017 (#5995)
- Loading branch information
Showing
14 changed files
with
415 additions
and
15 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
packages/compass-editor/src/codemirror/query-autocompleter-with-history.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { expect } from 'chai'; | ||
import { createQueryWithHistoryAutocompleter } from './query-autocompleter-with-history'; | ||
import { setupCodemirrorCompleter } from '../../test/completer'; | ||
import type { SavedQuery } from '../../dist/codemirror/query-history-autocompleter'; | ||
|
||
describe('query history autocompleter', function () { | ||
const { getCompletions, cleanup } = setupCodemirrorCompleter( | ||
createQueryWithHistoryAutocompleter | ||
); | ||
|
||
const savedQueries: SavedQuery[] = [ | ||
{ | ||
lastExecuted: new Date('2023-06-01T12:00:00Z'), | ||
queryProperties: { | ||
filter: { status: 'active' }, | ||
}, | ||
}, | ||
{ | ||
lastExecuted: new Date('2023-06-02T14:00:00Z'), | ||
queryProperties: { | ||
filter: { age: { $gt: 30 } }, | ||
project: { name: 1, age: 1, address: 1 }, | ||
collation: { locale: 'en' }, | ||
sort: { age: 1 }, | ||
skip: 5, | ||
limit: 100, | ||
maxTimeMS: 3000, | ||
}, | ||
}, | ||
{ | ||
lastExecuted: new Date('2023-06-03T16:00:00Z'), | ||
queryProperties: { | ||
filter: { score: { $gte: 85 } }, | ||
project: { studentName: 1, score: 1 }, | ||
sort: { score: -1 }, | ||
hint: { indexName: 'score_1' }, | ||
limit: 20, | ||
maxTimeMS: 1000, | ||
}, | ||
}, | ||
{ | ||
lastExecuted: new Date('2023-06-04T18:00:00Z'), | ||
queryProperties: { | ||
filter: { isActive: true }, | ||
project: { userId: 1, isActive: 1 }, | ||
collation: { locale: 'simple' }, | ||
sort: { userId: 1 }, | ||
limit: 10, | ||
maxTimeMS: 500, | ||
}, | ||
}, | ||
{ | ||
lastExecuted: new Date('2023-06-05T20:00:00Z'), | ||
queryProperties: { | ||
filter: { category: 'electronics' }, | ||
project: { productId: 1, category: 1, price: 1 }, | ||
sort: { price: -1 }, | ||
limit: 30, | ||
maxTimeMS: 1500, | ||
}, | ||
}, | ||
]; | ||
|
||
after(cleanup); | ||
|
||
const mockOnApply: (query: SavedQuery['queryProperties']) => any = () => {}; | ||
|
||
it('returns all saved queries as completions on click', function () { | ||
expect( | ||
getCompletions('{}', savedQueries, undefined, mockOnApply) | ||
).to.have.lengthOf(5); | ||
}); | ||
|
||
it('returns normal autocompletion when user starts typing', function () { | ||
expect( | ||
getCompletions('foo', savedQueries, undefined, mockOnApply) | ||
).to.have.lengthOf(45); | ||
}); | ||
|
||
it('completes "any text" when inside a string', function () { | ||
expect( | ||
getCompletions( | ||
'{ bar: 1, buz: 2, foo: "b', | ||
savedQueries, | ||
undefined, | ||
mockOnApply | ||
).map((completion) => completion.label) | ||
).to.deep.eq(['bar', '1', 'buz', '2', 'foo']); | ||
}); | ||
}); |
30 changes: 30 additions & 0 deletions
30
packages/compass-editor/src/codemirror/query-autocompleter-with-history.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { | ||
type SavedQuery, | ||
createQueryHistoryAutocompleter, | ||
} from './query-history-autocompleter'; | ||
import { createQueryAutocompleter } from './query-autocompleter'; | ||
|
||
import type { | ||
CompletionSource, | ||
CompletionContext, | ||
} from '@codemirror/autocomplete'; | ||
import type { CompletionOptions } from '../autocompleter'; | ||
|
||
export const createQueryWithHistoryAutocompleter = ( | ||
recentQueries: SavedQuery[], | ||
options: Pick<CompletionOptions, 'fields' | 'serverVersion'> = {}, | ||
onApply: (query: SavedQuery['queryProperties']) => void | ||
): CompletionSource => { | ||
const queryHistoryAutocompleter = createQueryHistoryAutocompleter( | ||
recentQueries, | ||
onApply | ||
); | ||
|
||
const originalQueryAutocompleter = createQueryAutocompleter(options); | ||
|
||
return function fullCompletions(context: CompletionContext) { | ||
if (context.state.doc.toString() !== '{}') | ||
return originalQueryAutocompleter(context); | ||
return queryHistoryAutocompleter(context); | ||
}; | ||
}; |
94 changes: 94 additions & 0 deletions
94
packages/compass-editor/src/codemirror/query-history-autocompleter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import type { | ||
CompletionContext, | ||
CompletionSource, | ||
} from '@codemirror/autocomplete'; | ||
import { formatDate, spacing } from '@mongodb-js/compass-components'; | ||
import { toJSString } from 'mongodb-query-parser'; | ||
import { css } from '@mongodb-js/compass-components'; | ||
|
||
export type SavedQuery = { | ||
lastExecuted: Date; | ||
queryProperties: { | ||
[properyName: string]: any; | ||
}; | ||
}; | ||
|
||
export const createQueryHistoryAutocompleter = ( | ||
savedQueries: SavedQuery[], | ||
onApply: (query: SavedQuery['queryProperties']) => void | ||
): CompletionSource => { | ||
return function queryCompletions(context: CompletionContext) { | ||
if (savedQueries.length === 0) { | ||
return null; | ||
} | ||
|
||
const options = savedQueries.map((query) => ({ | ||
label: createQuery(query), | ||
type: 'text', | ||
detail: formatDate(query.lastExecuted.getTime()), | ||
info: () => createInfo(query).dom, | ||
apply: () => { | ||
onApply(query.queryProperties); | ||
}, | ||
boost: query.lastExecuted.getTime(), | ||
})); | ||
|
||
return { | ||
from: context.pos, | ||
options: options, | ||
}; | ||
}; | ||
}; | ||
|
||
const queryLabelStyles = css({ | ||
textTransform: 'capitalize', | ||
fontWeight: 'bold', | ||
margin: `${spacing[2]}px 0`, | ||
}); | ||
|
||
const queryCodeStyles = css({ | ||
maxHeight: '30vh', | ||
}); | ||
|
||
function createQuery(query: SavedQuery): string { | ||
let res = ''; | ||
Object.entries(query.queryProperties).forEach(([key, value]) => { | ||
const formattedQuery = toJSString(value); | ||
const noFilterKey = key === 'filter' ? '' : `${key}: `; | ||
res += formattedQuery ? `, ${noFilterKey}${formattedQuery}` : ''; | ||
}); | ||
const len = res.length; | ||
return len <= 100 ? res.slice(2, res.length) : res.slice(2, 100); | ||
} | ||
|
||
function createInfo(query: SavedQuery): { | ||
dom: Node; | ||
destroy?: () => void; | ||
} { | ||
const container = document.createElement('div'); | ||
Object.entries(query.queryProperties).forEach(([key, value]) => { | ||
const formattedQuery = toJSString(value); | ||
const codeDiv = document.createElement('div'); | ||
|
||
const label = document.createElement('label'); | ||
label.className = queryLabelStyles; | ||
label.textContent = key; | ||
|
||
const code = document.createElement('pre'); | ||
code.className = queryCodeStyles; | ||
if (formattedQuery) code.textContent = formattedQuery; | ||
|
||
codeDiv.append(label); | ||
codeDiv.appendChild(code); | ||
container.appendChild(codeDiv); | ||
}); | ||
|
||
return { | ||
dom: container, | ||
destroy: () => { | ||
while (container.firstChild) { | ||
container.removeChild(container.firstChild); | ||
} | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.