diff --git a/.changeset/dull-banks-shake.md b/.changeset/dull-banks-shake.md new file mode 100644 index 0000000000..1882cf4f42 --- /dev/null +++ b/.changeset/dull-banks-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Added support for icons containing colons diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index 9fe041a839..1769503fe0 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -189,13 +189,15 @@ describe('FieldFormatEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*""/i); }); - it.each([['dashboard'], ['admin-dashboard'], ['foo_dashboard']])( - 'accepts valid link icon', - async icon => { - data.metadata.links = [{ url: 'http://foo', icon }]; - await expect(policy.enforce(data)).resolves.toBe(data); - }, - ); + it.each([ + ['dashboard'], + ['admin-dashboard'], + ['foo_dashboard'], + ['kind:api'], + ])('accepts valid link icon', async icon => { + data.metadata.links = [{ url: 'http://foo', icon }]; + await expect(policy.enforce(data)).resolves.toBe(data); + }); it.each([[123], [{}], [[]], ['abc xyz']])( 'rejects bad link icon value %s', diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 18e9523478..c3f2802258 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -97,6 +97,10 @@ export class FieldFormatEntityPolicy implements EntityPolicy { case 'isNonEmptyString': expectation = 'a non empty string'; break; + case 'isValidIcon': + expectation = + 'a string that is sequences of [a-zA-Z0-9] separated by any of [-_.:], at most 63 characters in total'; + break; default: expectation = undefined; break; @@ -161,7 +165,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { optional( `links.${i}.icon`, links[i]?.icon, - KubernetesValidatorFunctions.isValidObjectName, + CommonValidatorFunctions.isValidIcon, ); } diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 88b6e54113..66ab9f0932 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -243,4 +243,25 @@ describe('CommonValidatorFunctions', () => { ])(`isNonEmptyString %p ? %p`, (value, result) => { expect(CommonValidatorFunctions.isNonEmptyString(value)).toBe(result); }); + + it.each([ + [7, false], + [null, false], + ['', false], + ['a', true], + ['AZ09', true], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a/b', false], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', true], + ['a_b', true], + ['a.b', true], + ['a..b', true], + ['a:b', true], + ])(`isValidIcon %p ? %p`, (value, matches) => { + expect(CommonValidatorFunctions.isValidIcon(value)).toBe(matches); + }); }); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 295c5b8895..9a685094c8 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -147,4 +147,18 @@ export class CommonValidatorFunctions { static isNonEmptyString(value: unknown): value is string { return typeof value === 'string' && value?.trim()?.length >= 1; } + + /** + * Checks that the value is a valid icon. + * + * @param value - The value to check + */ + static isValidIcon(value: unknown): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^([A-Za-z0-9][-A-Za-z0-9_.:]*)?[A-Za-z0-9]$/.test(value) + ); + } }