mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
QueryField: Handle autocomplete better (#81484)
* extract out function + add unit tests * add feature toggle and default it to on
This commit is contained in:
@@ -57,6 +57,7 @@ Some features are enabled by default. You can disable these feature by setting t
|
||||
| `lokiQueryHints` | Enables query hints for Loki | Yes |
|
||||
| `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | |
|
||||
| `jitterAlertRules` | Distributes alert rule evaluations more evenly over time, by rule group | |
|
||||
| `slateAutocomplete` | Adjusts the behaviour of the slate editor to properly handle autocomplete. Feature toggled for safety. | Yes |
|
||||
|
||||
## Preview feature toggles
|
||||
|
||||
|
||||
@@ -174,4 +174,5 @@ export interface FeatureToggles {
|
||||
onPremToCloudMigrations?: boolean;
|
||||
alertingSaveStatePeriodic?: boolean;
|
||||
promQLScope?: boolean;
|
||||
slateAutocomplete?: boolean;
|
||||
}
|
||||
|
||||
53
packages/grafana-ui/src/slate-plugins/suggestions.test.tsx
Normal file
53
packages/grafana-ui/src/slate-plugins/suggestions.test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { getNumCharsToDelete } from './suggestions';
|
||||
|
||||
describe('suggestions', () => {
|
||||
describe('getNumCharsToDelete', () => {
|
||||
describe('when slateAutocomplete is enabled', () => {
|
||||
let originalBootData = window.grafanaBootData;
|
||||
|
||||
// hacky way to enable the feature toggle for this test
|
||||
beforeEach(() => {
|
||||
if (originalBootData) {
|
||||
window.grafanaBootData = {
|
||||
...originalBootData,
|
||||
settings: {
|
||||
...originalBootData.settings,
|
||||
featureToggles: {
|
||||
...originalBootData.settings.featureToggles,
|
||||
slateAutocomplete: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.grafanaBootData = originalBootData;
|
||||
});
|
||||
|
||||
const splunkCleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%:\\]/g, '').trim();
|
||||
it.each([
|
||||
// | represents the caret position
|
||||
['$query0 ', '', '', false, 0, undefined, { forward: 0, backward: 0 }], // "|" --> "$query0 |"
|
||||
['$query0 ', '$que', '$que', false, 0, undefined, { forward: 0, backward: 4 }], // "$que|" --> "$query0 |"
|
||||
['$query0 ', '$q', '$que', false, 0, undefined, { forward: 2, backward: 2 }], // "$q|ue" --> "$query0 |"
|
||||
['$query0 ', '$que', '($que)', false, 0, splunkCleanText, { forward: 0, backward: 4 }], // "($que|)" --> "($query0 |)"
|
||||
['$query0 ', '$que', 'esarvotionUsagePercent=$que', false, 0, undefined, { forward: 0, backward: 4 }], // "esarvotionUsagePercent=$que|" --> "esarvotionUsagePercent=$query0 |"
|
||||
])(
|
||||
'should calculate the correct number of characters to delete forwards and backwards',
|
||||
(suggestionText, typeaheadPrefix, typeaheadText, preserveSuffix, deleteBackwards, cleanText, expected) => {
|
||||
expect(
|
||||
getNumCharsToDelete(
|
||||
suggestionText,
|
||||
typeaheadPrefix,
|
||||
typeaheadText,
|
||||
preserveSuffix,
|
||||
deleteBackwards,
|
||||
cleanText
|
||||
)
|
||||
).toEqual(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,8 @@ import { debounce, sortBy } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Editor, Plugin as SlatePlugin } from 'slate-react';
|
||||
|
||||
import { BootData } from '@grafana/data';
|
||||
|
||||
import { Typeahead } from '../components/Typeahead/Typeahead';
|
||||
import { CompletionItem, SuggestionsState, TypeaheadInput, TypeaheadOutput } from '../types';
|
||||
import { makeFragment, SearchFunctionType } from '../utils';
|
||||
@@ -11,6 +13,12 @@ import TOKEN_MARK from './slate-prism/TOKEN_MARK';
|
||||
|
||||
export const TYPEAHEAD_DEBOUNCE = 250;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
grafanaBootData?: BootData;
|
||||
}
|
||||
}
|
||||
|
||||
// Commands added to the editor by this plugin.
|
||||
interface SuggestionsPluginCommands {
|
||||
selectSuggestion: (suggestion: CompletionItem) => Editor;
|
||||
@@ -157,13 +165,14 @@ export function SuggestionsPlugin({
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the current, incomplete text and replace it with the selected suggestion
|
||||
const backward = suggestion.deleteBackwards || typeaheadPrefix.length;
|
||||
const text = cleanText ? cleanText(typeaheadText) : typeaheadText;
|
||||
const suffixLength = text.length - typeaheadPrefix.length;
|
||||
const offset = typeaheadText.indexOf(typeaheadPrefix);
|
||||
const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
|
||||
const forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
|
||||
const { forward, backward } = getNumCharsToDelete(
|
||||
suggestionText,
|
||||
typeaheadPrefix,
|
||||
typeaheadText,
|
||||
preserveSuffix,
|
||||
suggestion.deleteBackwards,
|
||||
cleanText
|
||||
);
|
||||
|
||||
// If new-lines, apply suggestion as block
|
||||
if (suggestionText.match(/\n/)) {
|
||||
@@ -337,3 +346,35 @@ const handleTypeahead = async (
|
||||
// Bogus edit to force re-render
|
||||
editor.blur().focus();
|
||||
};
|
||||
|
||||
export function getNumCharsToDelete(
|
||||
suggestionText: string,
|
||||
typeaheadPrefix: string,
|
||||
typeaheadText: string,
|
||||
preserveSuffix: boolean,
|
||||
deleteBackwards?: number,
|
||||
cleanText?: (text: string) => string
|
||||
) {
|
||||
// remove the current, incomplete text and replace it with the selected suggestion
|
||||
const backward = deleteBackwards || typeaheadPrefix.length;
|
||||
const text = cleanText ? cleanText(typeaheadText) : typeaheadText;
|
||||
const offset = typeaheadText.indexOf(typeaheadPrefix);
|
||||
|
||||
let forward: number;
|
||||
|
||||
if (window.grafanaBootData?.settings.featureToggles['slateAutocomplete']) {
|
||||
const suffixLength =
|
||||
offset > -1 ? text.length - offset - typeaheadPrefix.length : text.length - typeaheadPrefix.length;
|
||||
const midWord = Boolean((typeaheadPrefix && suffixLength > 0) || suggestionText === typeaheadText);
|
||||
forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
|
||||
} else {
|
||||
const suffixLength = text.length - typeaheadPrefix.length;
|
||||
const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
|
||||
forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
|
||||
}
|
||||
|
||||
return {
|
||||
forward,
|
||||
backward,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1320,5 +1320,14 @@ var (
|
||||
Owner: grafanaObservabilityMetricsSquad,
|
||||
Created: time.Date(2024, time.January, 29, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Name: "slateAutocomplete",
|
||||
Description: "Adjusts the behaviour of the slate editor to properly handle autocomplete. Feature toggled for safety.",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Expression: "true", // enabled by default
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaFrontendPlatformSquad,
|
||||
Created: time.Date(2024, time.January, 29, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -155,3 +155,4 @@ jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,2024-01-17,false,tr
|
||||
onPremToCloudMigrations,experimental,@grafana/grafana-operator-experience-squad,2024-01-22,false,false,false
|
||||
alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,2024-01-22,false,false,false
|
||||
promQLScope,experimental,@grafana/observability-metrics,2024-01-29,false,false,false
|
||||
slateAutocomplete,GA,@grafana/grafana-frontend-platform,2024-01-29,false,false,true
|
||||
|
||||
|
@@ -630,4 +630,8 @@ const (
|
||||
// FlagPromQLScope
|
||||
// In-development feature that will allow injection of labels into prometheus queries.
|
||||
FlagPromQLScope = "promQLScope"
|
||||
|
||||
// FlagSlateAutocomplete
|
||||
// Adjusts the behaviour of the slate editor to properly handle autocomplete. Feature toggled for safety.
|
||||
FlagSlateAutocomplete = "slateAutocomplete"
|
||||
)
|
||||
|
||||
@@ -239,7 +239,7 @@ func TestSchedulableAlertRulesRegistry(t *testing.T) {
|
||||
assert.Len(t, rules, 0)
|
||||
assert.Len(t, folders, 0)
|
||||
|
||||
expectedFolders := map[models.FolderKey]string{models.FolderKey{OrgID: 1, UID: "test-uid"}: "test-title"}
|
||||
expectedFolders := map[models.FolderKey]string{{OrgID: 1, UID: "test-uid"}: "test-title"}
|
||||
// replace all rules in the registry with foo
|
||||
r.set([]*models.AlertRule{{OrgID: 1, UID: "foo", Version: 1}}, expectedFolders)
|
||||
rules, folders = r.all()
|
||||
|
||||
Reference in New Issue
Block a user