Dashboard Tags: Validate max length (#116047)

This commit is contained in:
Stephanie Hingtgen
2026-01-09 03:57:39 -07:00
committed by GitHub
parent 5f8668b3aa
commit b0785e506f
8 changed files with 121 additions and 4 deletions
@@ -186,7 +186,7 @@ For the JSON and field usage notes, refer to the [links schema documentation](ht
### `tags`
The tags associated with the dashboard:
Tags associated with the dashboard. Each tag can be up to 50 characters long.
` [...string]`
@@ -54,6 +54,7 @@ export const TagsInput = forwardRef<HTMLInputElement, Props>(
const [newTagName, setNewTagName] = useState('');
const styles = useStyles2(getStyles);
const theme = useTheme2();
const isTagTooLong = newTagName.length > 50;
const onNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setNewTagName(event.target.value);
@@ -65,6 +66,9 @@ export const TagsInput = forwardRef<HTMLInputElement, Props>(
const onAdd = (event?: React.MouseEvent | React.KeyboardEvent) => {
event?.preventDefault();
if (newTagName.length > 50) {
return;
}
if (!tags.includes(newTagName)) {
onChange(tags.concat(newTagName));
}
@@ -94,14 +98,17 @@ export const TagsInput = forwardRef<HTMLInputElement, Props>(
value={newTagName}
onKeyDown={onKeyboardAdd}
onBlur={onBlur}
invalid={invalid}
invalid={invalid || isTagTooLong}
suffix={
<Button
fill="text"
className={styles.addButtonStyle}
onClick={onAdd}
size="md"
disabled={newTagName.length <= 0}
disabled={newTagName.length <= 0 || isTagTooLong}
title={
isTagTooLong ? t('grafana-ui.tags-input.tag-too-long', 'Tag too long, max 50 characters') : undefined
}
>
<Trans i18nKey="grafana-ui.tags-input.add">Add</Trans>
</Button>
+36
View File
@@ -389,6 +389,11 @@ func (b *DashboardsAPIBuilder) validateCreate(ctx context.Context, a admission.A
return apierrors.NewBadRequest(err.Error())
}
// Validate tags
if err := validateDashboardTags(dashObj); err != nil {
return apierrors.NewBadRequest(err.Error())
}
id, err := identity.GetRequester(ctx)
if err != nil {
return fmt.Errorf("error getting requester: %w", err)
@@ -459,6 +464,11 @@ func (b *DashboardsAPIBuilder) validateUpdate(ctx context.Context, a admission.A
return apierrors.NewBadRequest(err.Error())
}
// Validate tags
if err := validateDashboardTags(newDashObj); err != nil {
return apierrors.NewBadRequest(err.Error())
}
// Validate folder existence if specified and changed
if !a.IsDryRun() && newAccessor.GetFolder() != oldAccessor.GetFolder() && newAccessor.GetFolder() != "" {
id, err := identity.GetRequester(ctx)
@@ -556,6 +566,32 @@ func getDashboardProperties(obj runtime.Object) (string, string, error) {
return title, refresh, nil
}
// validateDashboardTags validates that all dashboard tags are within the maximum length
func validateDashboardTags(obj runtime.Object) error {
var tags []string
switch d := obj.(type) {
case *dashv0.Dashboard:
tags = d.Spec.GetNestedStringSlice("tags")
case *dashv1.Dashboard:
tags = d.Spec.GetNestedStringSlice("tags")
case *dashv2alpha1.Dashboard:
tags = d.Spec.Tags
case *dashv2beta1.Dashboard:
tags = d.Spec.Tags
default:
return fmt.Errorf("unsupported dashboard version: %T", obj)
}
for _, tag := range tags {
if len(tag) > 50 {
return dashboards.ErrDashboardTagTooLong
}
}
return nil
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
storageOpts := apistore.StorageOptions{
EnableFolderSupport: true,
@@ -542,6 +542,9 @@ func (d *dashboardStore) saveDashboard(ctx context.Context, sess *db.Session, cm
tags := dash.GetTags()
if len(tags) > 0 {
for _, tag := range tags {
if len(tag) > 50 {
return nil, dashboards.ErrDashboardTagTooLong
}
if _, err := sess.Insert(dashboardTag{DashboardId: dash.ID, Term: tag, OrgID: dash.OrgID, DashboardUID: dash.UID}); err != nil {
return nil, err
}
+5
View File
@@ -79,6 +79,11 @@ var (
Reason: "message too long, max 500 characters",
StatusCode: 400,
}
ErrDashboardTagTooLong = dashboardaccess.DashboardErr{
Reason: "dashboard tag too long, max 50 characters",
StatusCode: 400,
Status: "tag-too-long",
}
ErrDashboardCannotSaveProvisionedDashboard = dashboardaccess.DashboardErr{
Reason: "Cannot save provisioned dashboard",
StatusCode: 400,
@@ -393,6 +393,66 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
})
})
t.Run("Dashboard tag validations", func(t *testing.T) {
t.Run("reject dashboard with tag over 50 characters on creation", func(t *testing.T) {
dashObj := createDashboardObject(t, "Dashboard with Long Tag", "", 0)
meta, _ := utils.MetaAccessor(dashObj)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["tags"] = []string{"this-is-a-very-long-tag-that-exceeds-fifty-characters-limit"}
_ = meta.SetSpec(specMap)
_, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "tag too long")
})
t.Run("reject dashboard update with tag over 50 characters", func(t *testing.T) {
dash, err := createDashboard(t, adminClient, "Valid Dashboard", nil, nil, ctx.Helper)
require.NoError(t, err)
require.NotNil(t, dash)
meta, _ := utils.MetaAccessor(dash)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["tags"] = []string{"this-is-a-very-long-tag-that-exceeds-fifty-characters-limit"}
_ = meta.SetSpec(specMap)
_, err = adminClient.Resource.Update(context.Background(), dash, v1.UpdateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "tag too long")
err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
})
t.Run("accept dashboard with tag at 50 characters", func(t *testing.T) {
dashObj := createDashboardObject(t, "Dashboard with Valid Tag", "", 0)
meta, _ := utils.MetaAccessor(dashObj)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["tags"] = []string{"this-tag-is-exactly-fifty-characters-long-12345"}
_ = meta.SetSpec(specMap)
createdDash, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, createdDash)
err = adminClient.Resource.Delete(context.Background(), createdDash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
})
t.Run("reject dashboard with multiple tags where one exceeds limit", func(t *testing.T) {
dashObj := createDashboardObject(t, "Dashboard with Mixed Tags", "", 0)
meta, _ := utils.MetaAccessor(dashObj)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["tags"] = []string{
"valid-tag",
"another-valid-tag",
"this-is-a-very-long-tag-that-exceeds-fifty-characters-limit",
}
_ = meta.SetSpec(specMap)
_, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "tag too long")
})
})
t.Run("Dashboard folder validations", func(t *testing.T) {
// Test non-existent folder UID
t.Run("reject dashboard with non-existent folder UID", func(t *testing.T) {
@@ -18,6 +18,10 @@ export const validateDashboardJson = (json: string) => {
if (hasInvalidTag) {
return t('dashboard.validation.tags-expected-strings', 'tags expected array of strings');
}
const hasTooLongTag = dashboard.tags.some((tag: string) => tag.length > 50);
if (hasTooLongTag) {
return t('dashboard.validation.tag-too-long', 'Dashboard tag too long, max 50 characters');
}
} else {
return t('dashboard.validation.tags-expected-array', 'tags expected array');
}
+3 -1
View File
@@ -5696,6 +5696,7 @@
"validation": {
"invalid-dashboard-id": "Could not find a valid Grafana.com ID",
"invalid-json": "Not valid JSON",
"tag-too-long": "Dashboard tag too long, max 50 characters",
"tags-expected-array": "tags expected array",
"tags-expected-strings": "tags expected array of strings"
},
@@ -9254,7 +9255,8 @@
"tags-input": {
"add": "Add",
"placeholder-new-tag": "New tag (enter key to add)",
"remove": "Remove tag: {{name}}"
"remove": "Remove tag: {{name}}",
"tag-too-long": "Tag too long, max 50 characters"
},
"time-sync-button": {
"aria-label-sync": "Sync times",