diff --git a/go.mod b/go.mod index cb798730c8b..26f97cd91c7 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,6 @@ require ( github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 github.com/gorilla/websocket v1.5.0 - github.com/gosimple/slug v1.12.0 github.com/grafana/cuetsy v0.1.1 github.com/grafana/grafana-aws-sdk v0.11.0 github.com/grafana/grafana-azure-sdk-go v1.3.1 @@ -288,7 +287,6 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa // indirect github.com/googleapis/go-type-adapters v1.0.0 // indirect - github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/memberlist v0.4.0 // indirect github.com/huandu/xstrings v1.3.1 // indirect diff --git a/go.sum b/go.sum index f475971f2e7..f25d7f604af 100644 --- a/go.sum +++ b/go.sum @@ -1353,10 +1353,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc= -github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= -github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= -github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cuetsy v0.1.1 h1:+1jaDDYCpvKlcOWJgBRbkc5+VZIClCEn5mbI+4PLZqM= diff --git a/pkg/infra/slugify/slugify.go b/pkg/infra/slugify/slugify.go new file mode 100644 index 00000000000..3011d55c1cf --- /dev/null +++ b/pkg/infra/slugify/slugify.go @@ -0,0 +1,370 @@ +/* + * This file evolved from the MIT licensed: https://github.com/machiel/slugify + */ + +/* + +The MIT License (MIT) + +Copyright (c) 2015 Machiel Molenaar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +package slugify + +import ( + "bytes" + "encoding/base64" + "strings" + "unicode/utf8" + + "github.com/gofrs/uuid" +) + +var ( + simpleSlugger = &slugger{ + isValidCharacter: validCharacter, + replaceCharacter: '-', + replacementMap: getDefaultReplacements(), + } +) + +// Slugify creates a URL safe latin slug for a given value +func Slugify(value string) string { + s := simpleSlugger.Slugify(value) + if s == "" { + s = base64.RawURLEncoding.EncodeToString([]byte(value)) + if len(s) > 50 || s == "" { + s = uuid.NewV5(uuid.NamespaceOID, value).String() + } + } + return s +} + +func validCharacter(c rune) bool { + if c >= 'a' && c <= 'z' { + return true + } + if c >= '0' && c <= '9' { + return true + } + return false +} + +// Slugifier based on settings +type slugger struct { + isValidCharacter func(c rune) bool + replaceCharacter rune + replacementMap map[rune]string +} + +// Slugify creates a slug for a string +func (s slugger) Slugify(value string) string { + value = strings.ToLower(value) + var buffer bytes.Buffer + lastCharacterWasInvalid := false + + for len(value) > 0 { + c, size := utf8.DecodeRuneInString(value) + value = value[size:] + + if newCharacter, ok := s.replacementMap[c]; ok { + buffer.WriteString(newCharacter) + lastCharacterWasInvalid = false + continue + } + + if s.isValidCharacter(c) { + buffer.WriteRune(c) + lastCharacterWasInvalid = false + } else if !lastCharacterWasInvalid { + buffer.WriteRune(s.replaceCharacter) + lastCharacterWasInvalid = true + } + } + + return strings.Trim(buffer.String(), string(s.replaceCharacter)) +} + +func getDefaultReplacements() map[rune]string { + return map[rune]string{ + '&': "and", + '@': "at", + '©': "c", + '®': "r", + 'Æ': "ae", + 'ß': "ss", + 'à': "a", + 'á': "a", + 'â': "a", + 'ä': "a", // or "ae" + 'å': "a", + 'æ': "ae", + 'ç': "c", + 'è': "e", + 'é': "e", + 'ê': "e", + 'ë': "e", + 'ì': "i", + 'í': "i", + 'î': "i", + 'ï': "i", + 'ò': "o", + 'ó': "o", + 'ô': "o", + 'õ': "o", + 'ö': "o", // or "oe"? + 'ø': "o", + 'ù': "u", + 'ú': "u", + 'û': "u", + 'ü': "ue", + 'ý': "y", + 'þ': "p", + 'ÿ': "y", + 'ā': "a", + 'ă': "a", + 'Ą': "a", + 'ą': "a", + 'ć': "c", + 'ĉ': "c", + 'ċ': "c", + 'č': "c", + 'ď': "d", + 'đ': "d", + 'ē': "e", + 'ĕ': "e", + 'ė': "e", + 'ę': "e", + 'ě': "e", + 'ĝ': "g", + 'ğ': "g", + 'ġ': "g", + 'ģ': "g", + 'ĥ': "h", + 'ħ': "h", + 'ĩ': "i", + 'ī': "i", + 'ĭ': "i", + 'į': "i", + 'ı': "i", + 'ij': "ij", + 'ĵ': "j", + 'ķ': "k", + 'ĸ': "k", + 'Ĺ': "l", + 'ĺ': "l", + 'ļ': "l", + 'ľ': "l", + 'ŀ': "l", + 'ł': "l", + 'ń': "n", + 'ņ': "n", + 'ň': "n", + 'ʼn': "n", + 'ŋ': "n", + 'ō': "o", + 'ŏ': "o", + 'ő': "o", + 'Œ': "oe", + 'œ': "oe", + 'ŕ': "r", + 'ŗ': "r", + 'ř': "r", + 'ś': "s", + 'ŝ': "s", + 'ş': "s", + 'š': "s", + 'ţ': "t", + 'ť': "t", + 'ŧ': "t", + 'ũ': "u", + 'ū': "u", + 'ŭ': "u", + 'ů': "u", + 'ű': "u", + 'ų': "u", + 'ŵ': "w", + 'ŷ': "y", + 'ź': "z", + 'ż': "z", + 'ž': "z", + 'ſ': "z", + 'Ə': "e", + 'ƒ': "f", + 'Ơ': "o", + 'ơ': "o", + 'Ư': "u", + 'ư': "u", + 'ǎ': "a", + 'ǐ': "i", + 'ǒ': "o", + 'ǔ': "u", + 'ǖ': "u", + 'ǘ': "u", + 'ǚ': "u", + 'ǜ': "u", + 'ǻ': "a", + 'Ǽ': "ae", + 'ǽ': "ae", + 'Ǿ': "o", + 'ǿ': "o", + 'ə': "e", + 'Є': "e", + 'Б': "b", + 'Г': "g", + 'Д': "d", + 'Ж': "zh", + 'З': "z", + 'У': "u", + 'Ф': "f", + 'Х': "h", + 'Ц': "c", + 'Ч': "ch", + 'Ш': "sh", + 'Щ': "sch", + 'Ъ': "-", + 'Ы': "y", + 'Ь': "-", + 'Э': "je", + 'Ю': "ju", + 'Я': "ja", + 'а': "a", + 'б': "b", + 'в': "v", + 'г': "g", + 'д': "d", + 'е': "e", + 'ж': "zh", + 'з': "z", + 'и': "i", + 'й': "j", + 'к': "k", + 'л': "l", + 'м': "m", + 'н': "n", + 'о': "o", + 'п': "p", + 'р': "r", + 'с': "s", + 'т': "t", + 'у': "u", + 'ф': "f", + 'х': "h", + 'ц': "c", + 'ч': "ch", + 'ш': "sh", + 'щ': "sch", + 'ъ': "-", + 'ы': "y", + 'ь': "-", + 'э': "je", + 'ю': "ju", + 'я': "ja", + 'ё': "jo", + 'є': "e", + 'і': "i", + 'ї': "i", + 'Ґ': "g", + 'ґ': "g", + 'א': "a", + 'ב': "b", + 'ג': "g", + 'ד': "d", + 'ה': "h", + 'ו': "v", + 'ז': "z", + 'ח': "h", + 'ט': "t", + 'י': "i", + 'ך': "k", + 'כ': "k", + 'ל': "l", + 'ם': "m", + 'מ': "m", + 'ן': "n", + 'נ': "n", + 'ס': "s", + 'ע': "e", + 'ף': "p", + 'פ': "p", + 'ץ': "C", + 'צ': "c", + 'ק': "q", + 'ר': "r", + 'ש': "w", + 'ת': "t", + '™': "tm", + 'ả': "a", + 'ã': "a", + 'ạ': "a", + + 'ắ': "a", + 'ằ': "a", + 'ẳ': "a", + 'ẵ': "a", + 'ặ': "a", + + 'ấ': "a", + 'ầ': "a", + 'ẩ': "a", + 'ẫ': "a", + 'ậ': "a", + + 'ẻ': "e", + 'ẽ': "e", + 'ẹ': "e", + 'ế': "e", + 'ề': "e", + 'ể': "e", + 'ễ': "e", + 'ệ': "e", + + 'ỉ': "i", + 'ị': "i", + + 'ỏ': "o", + 'ọ': "o", + 'ố': "o", + 'ồ': "o", + 'ổ': "o", + 'ỗ': "o", + 'ộ': "o", + 'ớ': "o", + 'ờ': "o", + 'ở': "o", + 'ỡ': "o", + 'ợ': "o", + + 'ủ': "u", + 'ụ': "u", + 'ứ': "u", + 'ừ': "u", + 'ử': "u", + 'ữ': "u", + 'ự': "u", + + 'ỳ': "y", + 'ỷ': "y", + 'ỹ': "y", + 'ỵ': "y", + } +} diff --git a/pkg/infra/slugify/slugify_test.go b/pkg/infra/slugify/slugify_test.go new file mode 100644 index 00000000000..7f88b7a962a --- /dev/null +++ b/pkg/infra/slugify/slugify_test.go @@ -0,0 +1,54 @@ +package slugify + +import ( + "testing" +) + +func TestSlugify(t *testing.T) { + results := make(map[string]string) + results["hello-playground"] = "Hello, playground" + results["hello-it-s-paradise"] = "😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise" + results["61db60b5-f1e7-5853-9b81-0f074fc268ea"] = "😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬" + results["8J-YoiAt"] = "😢 -" + results["a"] = "?,a . \n " + results["0a68eb57-c88a-5f34-9e9d-27f85e68af4f"] = "" // empty input has a slug! + results["hi-this-is-a-test"] = "方向盤後面 hi this is a test خلف المقو" + results["cong-hoa-xa-hoi-chu-nghia-viet-nam"] = "Cộng hòa xã hội chủ nghĩa Việt Nam" + results["noi-nang-canh-canh-ben-long-bieng-khuay"] = "Nỗi nàng canh cánh bên lòng biếng khuây" // This line in a poem called Truyen Kieu + + for slug, original := range results { + actual := Slugify(original) + + if actual != slug { + t.Errorf("Expected '%s', got: %s", slug, actual) + } + } +} + +func BenchmarkSlugify(b *testing.B) { + for i := 0; i < b.N; i++ { + Slugify("Hello, world!") + } +} + +func BenchmarkSlugifyLongString(b *testing.B) { + for i := 0; i < b.N; i++ { + Slugify(` + 😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise + 😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise + 😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise + 😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Aliquam sapien nisl, laoreet quis vestibulum ut, cursus + in turpis. Sed magna mi, blandit id nisi vel, imperdiet + mollis turpis. Fusce vel fringilla mauris. Donec cursus + rhoncus bibendum. Aliquam erat volutpat. Maecenas + faucibus turpis ex, quis lacinia ligula ultrices non. + Sed gravida justo augue. Nulla bibendum dignissim tellus + vitae lobortis. Suspendisse fermentum vel purus in pulvinar. + Vivamus eu fermentum purus, sit amet tempor orci. + Praesent congue convallis turpis, ac ullamcorper lorem + semper id. + `) + } +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index fc8f781909e..a89f451e007 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -1,14 +1,11 @@ package models import ( - "encoding/base64" "fmt" - "strings" "time" - "github.com/gosimple/slug" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/setting" ) @@ -147,22 +144,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { // UpdateSlug updates the slug func (d *Dashboard) UpdateSlug() { title := d.Data.Get("title").MustString() - d.Slug = SlugifyTitle(title) -} - -func SlugifyTitle(title string) string { - s := slug.Make(strings.ToLower(title)) - if s == "" { - // If the dashboard name is only characters outside of the - // sluggable characters, the slug creation will return an - // empty string which will mess up URLs. This failsafe picks - // that up and creates the slug as a base64 identifier instead. - s = base64.RawURLEncoding.EncodeToString([]byte(title)) - if slug.MaxLength != 0 && len(s) > slug.MaxLength { - s = s[:slug.MaxLength] - } - } - return s + d.Slug = slugify.Slugify(title) } // GetUrl return the html url for a folder if it's folder, otherwise for a dashboard diff --git a/pkg/models/dashboards_test.go b/pkg/models/dashboards_test.go index b8c5c78f1cc..2bebedcbb41 100644 --- a/pkg/models/dashboards_test.go +++ b/pkg/models/dashboards_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -72,14 +73,14 @@ func TestSlugifyTitle(t *testing.T) { testCases := map[string]string{ "Grafana Play Home": "grafana-play-home", "snöräv-över-ån": "snorav-over-an", - "漢字": "han-zi", // Hanzi for hanzi + "漢字": "5ryi5a2X", // "han-zi", // Hanzi for hanzi "🇦🇶": "8J-HpvCfh7Y", // flag of Antarctica-emoji, using fallback "𒆠": "8JKGoA", // cuneiform Ki, using fallback } for input, expected := range testCases { t.Run(input, func(t *testing.T) { - slug := SlugifyTitle(input) + slug := slugify.Slugify(input) assert.Equal(t, expected, slug) }) } diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 244a54e747d..4a42e941f92 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -12,11 +12,10 @@ import ( "runtime" "strings" - "github.com/gosimple/slug" - "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" @@ -343,7 +342,7 @@ func setDefaultNavURL(p *plugins.Plugin) { // slugify pages for _, include := range p.Includes { if include.Slug == "" { - include.Slug = slug.Make(include.Name) + include.Slug = slugify.Slugify(include.Name) } if !include.DefaultNav { diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 8eea579d2d6..7b5ab9518a6 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -10,6 +10,7 @@ import ( "github.com/go-sql-driver/mysql" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -170,7 +171,7 @@ func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.F } return nil }) - foldr.Url = models.GetFolderUrl(foldr.UID, models.SlugifyTitle(foldr.Title)) + foldr.Url = models.GetFolderUrl(foldr.UID, slugify.Slugify(foldr.Title)) return foldr, err } diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index d3de565aa81..12143abef99 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -3,6 +3,7 @@ package folder import ( "time" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/errutil" @@ -141,7 +142,7 @@ func (f *Folder) ToLegacyModel() *models.Folder { Id: f.ID, Uid: f.UID, Title: f.Title, - Url: models.GetFolderUrl(f.UID, models.SlugifyTitle(f.Title)), + Url: models.GetFolderUrl(f.UID, slugify.Slugify(f.Title)), Version: 0, Created: f.Created, Updated: f.Updated, diff --git a/pkg/services/provisioning/alerting/rules_provisioner.go b/pkg/services/provisioning/alerting/rules_provisioner.go index e3a44cf0af2..071c549edad 100644 --- a/pkg/services/provisioning/alerting/rules_provisioner.go +++ b/pkg/services/provisioning/alerting/rules_provisioner.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" alert_models "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -99,7 +100,7 @@ func (prov *defaultAlertRuleProvisioner) provisionRule( func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID( ctx context.Context, folderName string, orgID int64) (string, error) { cmd := &models.GetDashboardQuery{ - Slug: models.SlugifyTitle(folderName), + Slug: slugify.Slugify(folderName), OrgId: orgID, } err := prov.dashboardService.GetDashboard(ctx, cmd) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index b6c8fa73404..d87b4407e7c 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" @@ -299,7 +300,7 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv return 0, ErrFolderNameMissing } - cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(folderName), OrgId: cfg.OrgID} + cmd := &models.GetDashboardQuery{Slug: slugify.Slugify(folderName), OrgId: cfg.OrgID} err := fr.dashboardStore.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { diff --git a/pkg/services/sqlstore/migrations/ualert/dashboard.go b/pkg/services/sqlstore/migrations/ualert/dashboard.go index 917858ae788..aa6ff8be3ba 100644 --- a/pkg/services/sqlstore/migrations/ualert/dashboard.go +++ b/pkg/services/sqlstore/migrations/ualert/dashboard.go @@ -1,13 +1,10 @@ package ualert import ( - "encoding/base64" - "strings" "time" "github.com/grafana/grafana/pkg/components/simplejson" - - "github.com/gosimple/slug" + "github.com/grafana/grafana/pkg/infra/slugify" ) type dashboard struct { @@ -45,22 +42,7 @@ func (d *dashboard) setVersion(version int) { // UpdateSlug updates the slug func (d *dashboard) updateSlug() { title := d.Data.Get("title").MustString() - d.Slug = slugifyTitle(title) -} - -func slugifyTitle(title string) string { - s := slug.Make(strings.ToLower(title)) - if s == "" { - // If the dashboard name is only characters outside of the - // sluggable characters, the slug creation will return an - // empty string which will mess up URLs. This failsafe picks - // that up and creates the slug as a base64 identifier instead. - s = base64.RawURLEncoding.EncodeToString([]byte(title)) - if slug.MaxLength != 0 && len(s) > slug.MaxLength { - s = s[:slug.MaxLength] - } - } - return s + d.Slug = slugify.Slugify(title) } func newDashboardFromJson(data *simplejson.Json) *dashboard { diff --git a/pkg/services/store/kind/dashboard/summary.go b/pkg/services/store/kind/dashboard/summary.go index 4aa6780e86f..3137d5c3b58 100644 --- a/pkg/services/store/kind/dashboard/summary.go +++ b/pkg/services/store/kind/dashboard/summary.go @@ -7,6 +7,7 @@ import ( "fmt" "strconv" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ) @@ -57,7 +58,7 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) mo } dashboardRefs := NewReferenceAccumulator() - url := fmt.Sprintf("/d/%s/%s", uid, models.SlugifyTitle(dash.Title)) + url := fmt.Sprintf("/d/%s/%s", uid, slugify.Slugify(dash.Title)) summary.Name = dash.Title summary.Description = dash.Description summary.URL = url