Skip to content

Commit

Permalink
feat(compass-editor, compass-query-bar): show query history in autoco…
Browse files Browse the repository at this point in the history
…mplete COMPASS-8017 (#5995)
  • Loading branch information
VivianTNT authored Jul 10, 2024
1 parent 5c7c95f commit 00a8966
Show file tree
Hide file tree
Showing 14 changed files with 415 additions and 15 deletions.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/compass-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,4 @@ export {
useRequiredURLSearchParams,
} from './components/links/link';
export { ChevronCollapse } from './components/chevron-collapse-icon';
export { formatDate } from './utils/format-date';
1 change: 1 addition & 0 deletions packages/compass-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"@lezer/highlight": "^1.1.3",
"@mongodb-js/compass-components": "^1.27.0",
"@mongodb-js/mongodb-constants": "^0.10.0",
"mongodb-query-parser": "^4.1.2",
"polished": "^4.2.2",
"prettier": "^2.7.1",
"react": "^17.0.2"
Expand Down
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']);
});
});
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);
};
};
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);
}
},
};
}
12 changes: 12 additions & 0 deletions packages/compass-editor/src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
closeBrackets,
closeBracketsKeymap,
snippetCompletion,
startCompletion,
} from '@codemirror/autocomplete';
import type { IconGlyph } from '@mongodb-js/compass-components';
import {
Expand Down Expand Up @@ -344,6 +345,7 @@ function getStylesForTheme(theme: CodemirrorThemeType) {
},
'& .cm-tooltip .completion-info p': {
margin: 0,
marginRight: `${spacing[2]}px`,
marginTop: `${spacing[2]}px`,
marginBottom: `${spacing[2]}px`,
},
Expand Down Expand Up @@ -604,6 +606,7 @@ export type EditorRef = {
prettify: () => boolean;
applySnippet: (template: string) => boolean;
focus: () => boolean;
startCompletion: () => boolean;
readonly editorContents: string | null;
readonly editor: EditorView | null;
};
Expand Down Expand Up @@ -713,6 +716,12 @@ const BaseEditor = React.forwardRef<EditorRef, EditorProps>(function BaseEditor(
editorViewRef.current.focus();
return true;
},
startCompletion() {
if (!editorViewRef.current) {
return false;
}
return startCompletion(editorViewRef.current);
},
get editorContents() {
if (!editorViewRef.current) {
return null;
Expand Down Expand Up @@ -1353,6 +1362,9 @@ const MultilineEditor = React.forwardRef<EditorRef, MultilineEditorProps>(
applySnippet(template: string) {
return editorRef.current?.applySnippet(template) ?? false;
},
startCompletion() {
return editorRef.current?.startCompletion() ?? false;
},
get editorContents() {
return editorRef.current?.editorContents ?? null;
},
Expand Down
2 changes: 2 additions & 0 deletions packages/compass-editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export { createQueryAutocompleter } from './codemirror/query-autocompleter';
export { createStageAutocompleter } from './codemirror/stage-autocompleter';
export { createAggregationAutocompleter } from './codemirror/aggregation-autocompleter';
export { createSearchIndexAutocompleter } from './codemirror/search-index-autocompleter';
export { createQueryHistoryAutocompleter } from './codemirror/query-history-autocompleter';
export { createQueryWithHistoryAutocompleter } from './codemirror/query-autocompleter-with-history';
Loading

0 comments on commit 00a8966

Please sign in to comment.