diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 0de434db4c1..9b785a3d3f0 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 563c7c5c022..8ffcae0e84e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -174,4 +174,5 @@ export interface FeatureToggles { onPremToCloudMigrations?: boolean; alertingSaveStatePeriodic?: boolean; promQLScope?: boolean; + slateAutocomplete?: boolean; } diff --git a/packages/grafana-ui/src/slate-plugins/suggestions.test.tsx b/packages/grafana-ui/src/slate-plugins/suggestions.test.tsx new file mode 100644 index 00000000000..8ce6c713d94 --- /dev/null +++ b/packages/grafana-ui/src/slate-plugins/suggestions.test.tsx @@ -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); + } + ); + }); + }); +}); diff --git a/packages/grafana-ui/src/slate-plugins/suggestions.tsx b/packages/grafana-ui/src/slate-plugins/suggestions.tsx index a6366e1a067..9ef22285578 100644 --- a/packages/grafana-ui/src/slate-plugins/suggestions.tsx +++ b/packages/grafana-ui/src/slate-plugins/suggestions.tsx @@ -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, + }; +} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 4d1708ccae9..44d5b325147 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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), + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ba6ec154042..18ac3654d23 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index d39f724b71c..150ec059934 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" ) diff --git a/pkg/services/ngalert/schedule/registry_test.go b/pkg/services/ngalert/schedule/registry_test.go index e92e8ea82a0..7aa080a7ce9 100644 --- a/pkg/services/ngalert/schedule/registry_test.go +++ b/pkg/services/ngalert/schedule/registry_test.go @@ -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()