Skip to content

Commit

Permalink
feat(Queries): inline suggestions [YTFRONT-4612]
Browse files Browse the repository at this point in the history
  • Loading branch information
SimbiozizV committed Feb 6, 2025
1 parent a6972a1 commit 4639d41
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 3 deletions.
5 changes: 5 additions & 0 deletions packages/ui/src/server/ServerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import renderLayout, {AppLayoutConfig} from './render-layout';
import {isLocalModeByEnvironment} from './utils';
import {VcsApi} from '../shared/vcs';
import {CustomVCSType, VCSSettings} from '../shared/ui-settings';
import {SuggestApi} from '../shared/suggestApi';

export interface ServerFactory {
getExtraRootPages(): Array<string>;
Expand All @@ -18,6 +19,7 @@ export interface ServerFactory {
vcs: Omit<VCSSettings, 'type'>,
token?: string,
): VcsApi | undefined;
createQuerySuggestApi(): SuggestApi | undefined;
}

let app: ExpressKit;
Expand Down Expand Up @@ -52,6 +54,9 @@ const serverFactory: ServerFactory = {
createCustomVcsApi() {
return undefined;
},
createQuerySuggestApi() {
return undefined;
},
};

function configureServerFactoryItem<K extends keyof ServerFactory>(
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/server/components/layout-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {isUserColumnPresetsEnabled} from '../controllers/table-column-preset';
import {getInterfaceVersion, isProductionEnv} from '../utils';
import {getAuthWay} from '../utils/authorization';
import {getOAuthSettings, isOAuthAllowed} from './oauth';
import ServerFactory from '../ServerFactory';

interface Params {
name?: string;
Expand Down Expand Up @@ -70,6 +71,7 @@ export async function getLayoutConfig(req: Request, params: Params): Promise<App
allowUserColumnPresets: isUserColumnPresetsEnabled(req),
odinPageEnabled: Boolean(odinBaseUrl),
allowTabletErrorsAPI: Boolean(tabletErrorsBaseUrl),
querySuggestions: Boolean(ServerFactory.createQuerySuggestApi()),
},
pluginsOptions: {
yandexMetrika: {
Expand Down
49 changes: 49 additions & 0 deletions packages/ui/src/server/controllers/query-suggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {Request, Response} from '@gravity-ui/expresskit';
import ServerFactory from '../ServerFactory';
import {ErrorWithCode, sendAndLogError} from '../utils';
import {QuerySuggestTelemetryData, QuerySuggestionsData} from '../../shared/suggestApi';

const getQuerySuggestApi = () => {
const suggestApi = ServerFactory.createQuerySuggestApi();

if (!suggestApi) {
throw new ErrorWithCode(400, 'Query suggest api is not configured');
}

return suggestApi;
};

export const getQuerySuggestions = async (req: Request, res: Response) => {
try {
const suggestApi = getQuerySuggestApi();
const requestId = req.ctx.getMetadata()['x-request-id'];
const {contextId, query, line, column, engine} = req.query as Omit<
QuerySuggestionsData,
'requestId'
>;

const data = await suggestApi.getQuerySuggestions(req, {
requestId,
contextId,
query,
line,
column,
engine,
});
res.status(200).json(data);
} catch (e) {
sendAndLogError(req.ctx, res, 500, e as Error);
}
};

export const sendTelemetry = async (req: Request, res: Response) => {
try {
const suggestApi = getQuerySuggestApi();
const telemetry: QuerySuggestTelemetryData = req.body.telemetry;

await suggestApi.sendTelemetry(req, telemetry);
res.status(200).json({success: true});
} catch (e) {
sendAndLogError(req.ctx, res, 500, e as Error);
}
};
4 changes: 4 additions & 0 deletions packages/ui/src/server/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
removeToken,
} from './controllers/vcs';
import {ytTabletErrorsApi} from './controllers/yt-tablet-errors-api';
import {getQuerySuggestions, sendTelemetry} from './controllers/query-suggestions';

const HOME_INDEX_TARGET: AppRouteDescription = {handler: homeIndexFactory(), ui: true};

Expand Down Expand Up @@ -62,6 +63,9 @@ const routes: AppRoutes = {
'GET /api/vcs/branches': {handler: getBranches},
'GET /api/vcs/tokens-availability': {handler: getVcsTokensAvailability},

'GET /api/query-suggestions/suggest': {handler: getQuerySuggestions},
'POST /api/query-suggestions/telemetry': {handler: sendTelemetry},

'POST /api/yt/:ytAuthCluster/change-password': {handler: handleChangePassword, ui: true},
'POST /api/remote-copy': {handler: handleRemoteCopy},

Expand Down
45 changes: 45 additions & 0 deletions packages/ui/src/shared/suggestApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {Request} from '@gravity-ui/expresskit';

export interface SuggestApi {
getQuerySuggestions(
req: Request,
queryData: QuerySuggestionsData,
): Promise<{items: string[]; requestId: string}>;
sendTelemetry(req: Request, telemetryData: QuerySuggestTelemetryData): Promise<void>;
}

export type QuerySuggestionsData = {
requestId: string;
contextId: string;
query: string;
line: string;
column: string;
engine: string;
};

export type TelemetryData = {
requestId: string;
timestamp: number;
};

export type AcceptedTelemetryData = TelemetryData & {
type: 'accepted';
acceptedText: string;
convertedText: string;
};

export type DiscardedTelemetryData = TelemetryData & {
type: 'discarded';
reason: 'OnCancel';
discardedText: string;
};

export type IgnoredTelemetryData = TelemetryData & {
type: 'ignored';
ignoredText: string;
};

export type QuerySuggestTelemetryData =
| AcceptedTelemetryData
| DiscardedTelemetryData
| IgnoredTelemetryData;
12 changes: 12 additions & 0 deletions packages/ui/src/ui/libs/monaco-yql-languages/_.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ interface ILangImpl {
) =>
| {suggestions: languages.CompletionItem[]}
| Promise<{suggestions: languages.CompletionItem[]}>;
provideInlineSuggestionsFunction?: (
model: editor.ITextModel,
monacoCursorPosition: Position,
_context: languages.InlineCompletionContext,
_token: CancellationToken,
) => Promise<{items: languages.InlineCompletion[]}>;
}

const languageDefinitions: {[languageId: string]: ILang} = {};
Expand Down Expand Up @@ -82,6 +88,12 @@ export function registerLanguage(def: ILang): void {
provideCompletionItems: mod.provideSuggestionsFunction,
});
}
if (mod.provideInlineSuggestionsFunction) {
languages.registerInlineCompletionsProvider(languageId, {
provideInlineCompletions: mod.provideInlineSuggestionsFunction,
freeInlineCompletions: () => {},
});
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {createProvideSuggestionsFunction} from '../helpers/createProvideSuggesti
import {generateClickhouseOldSafariSuggestions} from './clickhouse.keywords';
import {MonacoLanguage} from '../../../constants/monaco';
import {QueryEngine} from '../../../pages/query-tracker/module/engines';
import {createInlineSuggestions} from '../../../pages/query-tracker/querySuggestionsModule/createInlineSuggestions';

registerLanguage({
id: MonacoLanguage.CHYT,
Expand All @@ -29,6 +30,7 @@ registerLanguage({
QueryEngine.CHYT,
)
: generateClickhouseOldSafariSuggestions,
provideInlineSuggestionsFunction: createInlineSuggestions(QueryEngine.CHYT),
};
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {createProvideSuggestionsFunction} from '../helpers/createProvideSuggesti
import {MonacoLanguage} from '../../../constants/monaco';
import {generateYqlOldSafariSuggestion} from './yql.keywords';
import {QueryEngine} from '../../../pages/query-tracker/module/engines';
import {createInlineSuggestions} from '../../../pages/query-tracker/querySuggestionsModule/createInlineSuggestions';

registerLanguage({
id: MonacoLanguage.YQL,
Expand All @@ -19,6 +20,7 @@ registerLanguage({
provideSuggestionsFunction: autocomplete
? createProvideSuggestionsFunction(autocomplete.parseYqlQuery, QueryEngine.YQL)
: generateYqlOldSafariSuggestion,
provideInlineSuggestionsFunction: createInlineSuggestions(QueryEngine.YQL),
};
},
});
14 changes: 11 additions & 3 deletions packages/ui/src/ui/pages/query-tracker/QueryEditor/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {Button, Flex, Icon, Loader} from '@gravity-ui/uikit';
import playIcon from '../../../assets/img/svg/play.svg';
import {useDispatch, useSelector} from 'react-redux';
import {
getQuery,
getQueryEditorErrors,
getQueryEngine,
getQueryId,
getQueryText,
isQueryExecuted,
isQueryLoading,
Expand Down Expand Up @@ -41,6 +41,7 @@ import {WaitForFont} from '../../../containers/WaitForFont/WaitForFont';
import {getHashLineNumber} from './helpers/getHashLineNumber';
import {makeHighlightedLineDecorator} from './helpers/makeHighlightedLineDecorator';
import {getDecorationsWithoutHighlight} from './helpers/getDecorationsWithoutHighlight';
import {useMonacoQuerySuggestions} from '../querySuggestionsModule/useMonacoQuerySuggestions';

const b = block('query-container');

Expand All @@ -65,7 +66,7 @@ const QueryEditorView = React.memo(function QueryEditorView({
const [changed, setChanged] = useState(false);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const {setEditor} = useMonaco();
const activeQuery = useSelector(getQuery);
const id = useSelector(getQueryId);
const text = useSelector(getQueryText);
const engine = useSelector(getQueryEngine);
const editorErrors = useSelector(getQueryEditorErrors);
Expand All @@ -76,6 +77,7 @@ const QueryEditorView = React.memo(function QueryEditorView({
undefined,
);
const model = editorRef.current?.getModel();
useMonacoQuerySuggestions(editorRef.current);

const runQueryCallback = useCallback(() => {
dispatch(runQuery(onStartQuery));
Expand All @@ -84,7 +86,7 @@ const QueryEditorView = React.memo(function QueryEditorView({
useEffect(() => {
editorRef.current?.focus();
editorRef.current?.setScrollTop(0);
}, [activeQuery?.id]);
}, [id]);

useEffect(() => {
if (editorRef.current) {
Expand Down Expand Up @@ -167,6 +169,12 @@ const QueryEditorView = React.memo(function QueryEditorView({
minimap: {
enabled: true,
},
inlineSuggest: {
enabled: true,
showToolbar: 'always',
mode: 'subword',
keepOnBlur: true,
},
};
}, [engine]);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {QueryEngine} from '../module/engines';
import axios from 'axios';
import {QuerySuggestTelemetryData} from '../../../../shared/suggestApi';

export type QuerySuggestionProps = {
contextId: string;
query: string;
line: number;
column: number;
engine: QueryEngine;
};
const BASE_PATH = '/api/query-suggestions';

export const getQuerySuggestions = async (data: QuerySuggestionProps) => {
const response = await axios.get<{items: string[]; requestId: string}>(`${BASE_PATH}/suggest`, {
params: data,
});
return response.data;
};

export const sendQuerySuggestionsTelemetry = async (data: QuerySuggestTelemetryData) => {
const response = await axios.post(`${BASE_PATH}/telemetry`, {
telemetry: data,
});
return response.data;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {CancellationToken, Position, editor, languages} from 'monaco-editor';
import {getRangeToInsertSuggestion} from '../../../libs/monaco-yql-languages/helpers/getRangeToInsertSuggestion';
import {QueryEngine} from '../module/engines';
import debounce_ from 'lodash/debounce';
import {getWindowStore} from '../../../store/window-store';
import {getQuerySuggestions, sendQuerySuggestionsTelemetry} from './api';
import {
PrevAction,
setPrevAction,
setRequestId,
setSuggestions,
} from '../module/querySuggestions/querySuggestionsSlice';
import {
selectPrevAction,
selectQuerySuggestionsContextId,
} from '../module/querySuggestions/selectors';
import {AcceptedTelemetryData} from '../../../../shared/suggestApi';
import {getQuerySuggestionsEnabled} from '../../../store/selectors/settings/settings-ts';

const debouncedGetSuggestions = debounce_(getQuerySuggestions, 200);
const store = getWindowStore();

export const createInlineSuggestions =
(engine: QueryEngine) =>
async (
model: editor.ITextModel,
monacoCursorPosition: Position,
_context: languages.InlineCompletionContext,
_token: CancellationToken,
): Promise<{items: languages.InlineCompletion[]}> => {
const state = store.getState();
const contextId = selectQuerySuggestionsContextId(state);
const prevAction = selectPrevAction(state);
const enabled = getQuerySuggestionsEnabled(state);

if (!enabled) {
return {
items: [],
};
}

const response = await debouncedGetSuggestions({
contextId,
query: model.getValue(),
line: monacoCursorPosition.lineNumber,
column: monacoCursorPosition.column,
engine,
});

if (!response) {
return {
items: [],
};
}

let action: PrevAction = response.items.length > 0 ? 'received' : 'empty';
if (prevAction === 'received') {
action = 'ignored';
await sendQuerySuggestionsTelemetry({
requestId: response.requestId,
timestamp: Date.now(),
type: 'ignored',
ignoredText: response.items[0],
});
}

store.dispatch(setPrevAction(action));
store.dispatch(setSuggestions(response.items));
store.dispatch(setRequestId(response.requestId));

const range = getRangeToInsertSuggestion(model, monacoCursorPosition);
return {
items: response.items.map((item) => {
const data: AcceptedTelemetryData = {
requestId: response.requestId,
timestamp: Date.now(),
type: 'accepted',
acceptedText: item,
convertedText: item,
};

return {
insertText: item,
range,
command: {
id: 'querySuggestionsTelemetry',
title: 'string',
arguments: [data],
},
};
}),
};
};
Loading

0 comments on commit 4639d41

Please sign in to comment.