fix: duplicate name check - #829
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a shared Yup validator to enforce case/whitespace-insensitive uniqueness for resource names, and wires it into multiple create/edit forms by passing existing names via react-hook-form resolver context.
Changes:
- Introduces
uniqueNameTest(normalized duplicate check with optional “current name” exclusion). - Applies the duplicate-name test across workload/team/service/secret/netpol/code-repo/catalog schemas and supplies
existingNames/currentNamefrom relevant queries. - Adds/extends unit tests for the new validator and several schemas (teams, services, catalogs, code repositories, builds).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/uniqueName.validator.ts | New shared Yup test for normalized uniqueness checks. |
| src/utils/uniqueName.validator.test.ts | Unit tests for the shared uniqueness test behavior. |
| src/pages/workloads/create-edit/WorkloadsCreateEditPage.tsx | Fetches team workloads to supply existing names to form context. |
| src/pages/workloads/create-edit/create-edit-workloads.validator.ts | Adds uniqueness validation to workload name. |
| src/pages/teams/create-edit/TeamsCreateEditPage.tsx | Fetches teams to supply existing names to form context. |
| src/pages/teams/create-edit/create-edit-teams.validator.ts | Adds uniqueness validation to team name. |
| src/pages/teams/create-edit/create-edit-teams.validator.test.ts | New tests covering team name uniqueness behavior. |
| src/pages/services/create-edit/ServicesCreateEditPage.tsx | Fetches team services, filters K8s service options, supplies existing names to form context. |
| src/pages/services/create-edit/create-edit-services.validator.ts | Adds uniqueness validation to service name. |
| src/pages/services/create-edit/create-edit-services.validator.test.ts | New tests covering service name uniqueness behavior. |
| src/pages/secrets/team/create-edit/SecretCreateEditPage.tsx | Fetches sealed secrets to supply existing names to form context. |
| src/pages/secrets/team/create-edit/create-edit-secrets.validator.ts | Adds uniqueness validation to sealed secret name. |
| src/pages/network-policies/create-edit/NetworkPoliciesIngressCreateEditPage.tsx | Fetches team netpols to supply existing names to form context (ingress). |
| src/pages/network-policies/create-edit/NetworkPoliciesEgressCreateEditPage.tsx | Fetches team netpols to supply existing names to form context (egress). |
| src/pages/network-policies/create-edit/create-edit-networkPolicies.validator.ts | Adds uniqueness validation to network policy rule name. |
| src/pages/code-repositories/create-edit/create-edit-codeRepositories.validator.ts | Adds uniqueness validation to code repository name. |
| src/pages/code-repositories/create-edit/create-edit-codeRepositories.validator.test.ts | Adds tests for code repository name uniqueness behavior. |
| src/pages/code-repositories/create-edit/CodeRepositoriesCreateEditPage.tsx | Supplies existing code-repo names in form context. |
| src/pages/catalogs/platform/create-edit/create-edit-catalog.validator.ts | Adds uniqueness validation to catalog name. |
| src/pages/catalogs/platform/create-edit/create-edit-catalog.validator.test.ts | New tests covering catalog name uniqueness behavior. |
| src/pages/catalogs/platform/create-edit/CatalogsCreateEditPage.tsx | Fetches catalogs and supplies existing names to form context. |
| src/pages/builds/create-edit/create-edit-builds.validator.test.ts | New/expanded test coverage for build schema validation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| refetch: refetchWorkload, | ||
| } = useGetAplWorkloadQuery({ teamId, workloadName }, { skip: !workloadName }) | ||
|
|
||
| const { data: teamWorkloads, isLoading: isLoadingTeamWorkloads } = useGetTeamAplWorkloadsQuery( |
| const { | ||
| data: teams, | ||
| isLoading: isLoadingTeams, | ||
| isFetching: isFetchingTeams, | ||
| isError: isErrorTeams, | ||
| } = useGetAplTeamsQuery() |
| const { | ||
| data: teamServices, | ||
| isLoading: isLoadingTeamServices, | ||
| isFetching: isFetchingTeamServices, | ||
| refetch: refetchTeamServices, | ||
| } = useGetTeamAplServicesQuery({ teamId }, { skip: !teamId }) |
| const { | ||
| data: teamSealedSecrets, | ||
| isLoading: isLoadingTeamSealedSecrets, | ||
| isFetching: isFetchingTeamSealedSecrets, | ||
| isError: isErrorTeamSealedSecrets, | ||
| refetch: refetchTeamSealedSecrets, | ||
| } = useGetAplSealedSecretsQuery({ teamId }, { skip: !teamId }) |
| const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery( | ||
| { teamId }, | ||
| { skip: !teamId }, | ||
| ) |
| 'Invalid format, must start with a lowercase letter, contain only lowercase letters, numbers, or hyphens, and end with a letter or number.', | ||
| ), | ||
| ) | ||
| .test(uniqueNameTest('Secret Name already exists.')), |
| 'Invalid format, must start with a lowercase letter, contain only lowercase letters, numbers, or hyphens, and end with a letter or number.', | ||
| ), | ||
| ) | ||
| .test(uniqueNameTest('Code Repository Name already exists.')), |
| .matches(/^[a-z]([-a-z0-9]*[a-z0-9])?$/, 'Workload name cannot contain capital letters or underscores') | ||
| .test(uniqueNameTest('Workload name already exists.')), |
| .matches( | ||
| /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/, | ||
| 'Rule name must start with a lowercase letter, contain only lowercase letters, numbers, and hyphens, and end with a letter or number', | ||
| ), | ||
| ) | ||
| .test(uniqueNameTest('Rule name already exists.')), |
| domainSuffix: cluster.domainSuffix, | ||
| existingNames, | ||
| currentName: data?.metadata?.name, | ||
| validateOnSubmit: !serviceName, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (10)
src/pages/workloads/create-edit/WorkloadsCreateEditPage.tsx:106
isLoadingTeamWorkloadsis destructured fromuseGetTeamAplWorkloadsQuerybut never used in this file, which will trigger unused-var linting and adds noise.
const { data: teamWorkloads, isLoading: isLoadingTeamWorkloads } = useGetTeamAplWorkloadsQuery(
{ teamId },
{ skip: !teamId },
)
src/pages/teams/create-edit/TeamsCreateEditPage.tsx:60
isLoadingTeams,isFetchingTeams, andisErrorTeamsare destructured fromuseGetAplTeamsQuery()but not used, which is likely to fail unused-var linting.
data: teams,
isLoading: isLoadingTeams,
isFetching: isFetchingTeams,
isError: isErrorTeams,
} = useGetAplTeamsQuery()
src/pages/services/create-edit/ServicesCreateEditPage.tsx:117
isLoadingTeamServices,isFetchingTeamServices, andrefetchTeamServicesare destructured fromuseGetTeamAplServicesQuerybut never used in this file.
data: teamServices,
isLoading: isLoadingTeamServices,
isFetching: isFetchingTeamServices,
refetch: refetchTeamServices,
} = useGetTeamAplServicesQuery({ teamId }, { skip: !teamId })
src/pages/secrets/team/create-edit/SecretCreateEditPage.tsx:103
isLoadingTeamSealedSecrets,isFetchingTeamSealedSecrets,isErrorTeamSealedSecrets, andrefetchTeamSealedSecretsare destructured fromuseGetAplSealedSecretsQuerybut never used in this file.
data: teamSealedSecrets,
isLoading: isLoadingTeamSealedSecrets,
isFetching: isFetchingTeamSealedSecrets,
isError: isErrorTeamSealedSecrets,
refetch: refetchTeamSealedSecrets,
src/pages/network-policies/create-edit/NetworkPoliciesIngressCreateEditPage.tsx:89
isLoadingTeamNetworkPoliciesis destructured fromuseGetTeamAplNetpolsQuerybut not used.
const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery(
{ teamId },
{ skip: !teamId },
)
src/pages/network-policies/create-edit/NetworkPoliciesEgressCreateEditPage.tsx:50
isLoadingTeamNetworkPoliciesis destructured fromuseGetTeamAplNetpolsQuerybut not used.
const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery(
{ teamId },
{ skip: !teamId },
)
src/pages/services/create-edit/ServicesCreateEditPage.tsx:144
validateOnSubmitis set to!serviceName, which disables the new duplicate-name validation in edit flows. In this page, the service name can still be changed (admin text field or non-admin autocomplete), so duplicates can slip through when editing.
domainSuffix: cluster.domainSuffix,
existingNames,
currentName: data?.metadata?.name,
validateOnSubmit: !serviceName,
},
src/pages/secrets/team/create-edit/create-edit-secrets.validator.ts:25
- Error message capitalization is inconsistent with the field label and other validators ("Secret Name" vs "Secret name"). This looks user-facing and should be sentence-cased for consistency.
.test(uniqueNameTest('Secret Name already exists.')),
src/pages/code-repositories/create-edit/create-edit-codeRepositories.validator.ts:59
- Error message casing is inconsistent with the rest of the validator messages ("Code Repository Name" vs "Code repository name"). This appears user-facing and should use consistent sentence case.
.test(uniqueNameTest('Code Repository Name already exists.')),
src/utils/uniqueName.validator.ts:1
- The file header comment points to
validators/uniqueName.validator.ts, but the file lives insrc/utils. This is misleading when navigating/searching.
// validators/uniqueName.validator.ts
CasLubbers
left a comment
There was a problem hiding this comment.
Validated on cluster and is working.
For consistency you could also add the uniqueNameTest() function to create-edit-builds. In there is now a custom function to test if its unique.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (10)
src/utils/uniqueName.validator.ts:1
- The file header comment references a different path ("validators/...") than the actual location under
src/utils, which can be misleading when navigating/searching.
// validators/uniqueName.validator.ts
src/pages/workloads/create-edit/WorkloadsCreateEditPage.tsx:106
isLoadingTeamWorkloadsis declared but never used, which can fail builds whennoUnusedLocals/lint rules are enabled. Remove the unused binding.
const { data: teamWorkloads, isLoading: isLoadingTeamWorkloads } = useGetTeamAplWorkloadsQuery(
{ teamId },
{ skip: !teamId },
)
src/pages/teams/create-edit/TeamsCreateEditPage.tsx:60
isLoadingTeams,isFetchingTeams, andisErrorTeamsare declared but never used, which can fail builds whennoUnusedLocals/lint rules are enabled. Remove the unused bindings.
const {
data: teams,
isLoading: isLoadingTeams,
isFetching: isFetchingTeams,
isError: isErrorTeams,
} = useGetAplTeamsQuery()
src/pages/services/create-edit/ServicesCreateEditPage.tsx:117
isLoadingTeamServices,isFetchingTeamServices, andrefetchTeamServicesare declared but never used, which can fail builds whennoUnusedLocals/lint rules are enabled. Remove the unused bindings (or use them).
const {
data: teamServices,
isLoading: isLoadingTeamServices,
isFetching: isFetchingTeamServices,
refetch: refetchTeamServices,
} = useGetTeamAplServicesQuery({ teamId }, { skip: !teamId })
src/pages/services/create-edit/ServicesCreateEditPage.tsx:144
validateOnSubmitis set to!serviceName, which disables the duplicate-name check in edit mode. Since the Service Name field is still editable whenserviceNameis present, this can allow submitting a duplicate service name. Either disable renaming in edit mode, or keepvalidateOnSubmitenabled and rely oncurrentNameto allow the unchanged value.
context: {
domainSuffix: cluster.domainSuffix,
existingNames,
currentName: data?.metadata?.name,
validateOnSubmit: !serviceName,
},
src/pages/secrets/team/create-edit/SecretCreateEditPage.tsx:104
isLoadingTeamSealedSecrets,isFetchingTeamSealedSecrets,isErrorTeamSealedSecrets, andrefetchTeamSealedSecretsare declared but never used, which can fail builds whennoUnusedLocals/lint rules are enabled. Remove the unused bindings (or use them).
const {
data: teamSealedSecrets,
isLoading: isLoadingTeamSealedSecrets,
isFetching: isFetchingTeamSealedSecrets,
isError: isErrorTeamSealedSecrets,
refetch: refetchTeamSealedSecrets,
} = useGetAplSealedSecretsQuery({ teamId }, { skip: !teamId })
src/pages/network-policies/create-edit/NetworkPoliciesIngressCreateEditPage.tsx:89
isLoadingTeamNetworkPoliciesis declared but never used, which can fail builds whennoUnusedLocals/lint rules are enabled. Remove the unused binding (or use it).
const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery(
{ teamId },
{ skip: !teamId },
)
src/pages/network-policies/create-edit/NetworkPoliciesEgressCreateEditPage.tsx:50
isLoadingTeamNetworkPoliciesis declared but never used, which can fail builds whennoUnusedLocals/lint rules are enabled. Remove the unused binding (or use it).
const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery(
{ teamId },
{ skip: !teamId },
)
src/utils/uniqueName.validator.test.ts:62
- The unit tests cover duplicate detection and casing/whitespace normalization, but they don’t cover the edit/rename scenario where
currentNameshould be excluded from the duplicate check. Adding a test forcurrentNamewill protect the intended behavior inuniqueNameTest.
it('skips validation when validateOnSubmit is false', async () => {
await expect(
schema.validate('repo-name', {
context: {
validateOnSubmit: false,
existingNames: ['repo-name'],
},
}),
).resolves.toBe('repo-name')
})
})
src/pages/services/create-edit/ServicesCreateEditPage.tsx:125
existingNamesis a new array on every render; since it’s a dependency of thefilteredK8ServicesuseMemo, that memo will recompute every render anyway. MemoizingexistingNamesbyteamServicesmakes thefilteredK8Servicesmemoization effective.
const existingNames = (teamServices ?? [])
.map((item) => item?.metadata?.name)
.filter((name): name is string => Boolean(name))
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (10)
src/pages/workloads/create-edit/WorkloadsCreateEditPage.tsx:106
isLoadingTeamWorkloadsis destructured fromuseGetTeamAplWorkloadsQuerybut never used. WithnoUnusedLocals/ ESLint enabled, this will fail the build/lint and is also misleading since the loading state isn’t incorporated anywhere.
const { data: teamWorkloads, isLoading: isLoadingTeamWorkloads } = useGetTeamAplWorkloadsQuery(
{ teamId },
{ skip: !teamId },
)
src/pages/teams/create-edit/TeamsCreateEditPage.tsx:60
isLoadingTeams,isFetchingTeams, andisErrorTeamsare destructured fromuseGetAplTeamsQuery()but never used. This typically triggers TypeScript/ESLint unused-variable failures.
const {
data: teams,
isLoading: isLoadingTeams,
isFetching: isFetchingTeams,
isError: isErrorTeams,
} = useGetAplTeamsQuery()
src/pages/services/create-edit/ServicesCreateEditPage.tsx:117
isLoadingTeamServices,isFetchingTeamServices, andrefetchTeamServicesare destructured fromuseGetTeamAplServicesQuerybut never used. This will trip unused-variable checks and currently the query state isn’t reflected in the page’s loading/refetch logic.
const {
data: teamServices,
isLoading: isLoadingTeamServices,
isFetching: isFetchingTeamServices,
refetch: refetchTeamServices,
} = useGetTeamAplServicesQuery({ teamId }, { skip: !teamId })
src/pages/secrets/team/create-edit/SecretCreateEditPage.tsx:104
isLoadingTeamSealedSecrets,isFetchingTeamSealedSecrets,isErrorTeamSealedSecrets, andrefetchTeamSealedSecretsare destructured fromuseGetAplSealedSecretsQuerybut never used. This is likely to fail unused-variable checks.
const {
data: teamSealedSecrets,
isLoading: isLoadingTeamSealedSecrets,
isFetching: isFetchingTeamSealedSecrets,
isError: isErrorTeamSealedSecrets,
refetch: refetchTeamSealedSecrets,
} = useGetAplSealedSecretsQuery({ teamId }, { skip: !teamId })
src/pages/network-policies/create-edit/NetworkPoliciesIngressCreateEditPage.tsx:89
isLoadingTeamNetworkPoliciesis destructured fromuseGetTeamAplNetpolsQuerybut never used. This can fail unused-variable linting/TS checks.
const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery(
{ teamId },
{ skip: !teamId },
)
src/pages/network-policies/create-edit/NetworkPoliciesEgressCreateEditPage.tsx:50
isLoadingTeamNetworkPoliciesis destructured fromuseGetTeamAplNetpolsQuerybut never used. This is likely to trip unused-variable checks.
const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery(
{ teamId },
{ skip: !teamId },
)
src/utils/uniqueName.validator.ts:1
- The file header comment points to
validators/uniqueName.validator.ts, but the actual path issrc/utils/uniqueName.validator.ts. This can be confusing when searching or moving code.
// validators/uniqueName.validator.ts
src/pages/secrets/team/create-edit/create-edit-secrets.validator.ts:25
- The new duplicate-name error message uses inconsistent capitalization (
Secret Name...) compared to the other messages in this schema (sentence case). Aligning it improves consistency and readability.
.test(uniqueNameTest('Secret Name already exists.')),
src/pages/code-repositories/create-edit/create-edit-codeRepositories.validator.ts:59
- The new duplicate-name error message uses title casing (
Code Repository Name...) while the rest of the validator uses sentence case (e.g.,Code repository name ...). Consider standardizing the casing, and update the associated tests to match the chosen message.
.test(uniqueNameTest('Code Repository Name already exists.')),
src/utils/uniqueName.validator.test.ts:61
- The new helper supports
currentName(to allow keeping the existing name during edits), but there isn’t a unit test covering that branch. Adding a test for thecurrentNameexception helps prevent regressions in edit flows.
it('skips validation when validateOnSubmit is false', async () => {
await expect(
schema.validate('repo-name', {
context: {
validateOnSubmit: false,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (9)
src/pages/workloads/create-edit/WorkloadsCreateEditPage.tsx:103
isLoadingTeamWorkloadsis declared but never used, which will either fail lint/TSnoUnusedLocalschecks or leave dead code behind. If you don't need the loading state, destructure onlydata; otherwise, incorporate the loading flag into the page's loading/submit gating.
const { data: teamWorkloads, isLoading: isLoadingTeamWorkloads } = useGetTeamAplWorkloadsQuery(
src/pages/teams/create-edit/TeamsCreateEditPage.tsx:60
isLoadingTeams,isFetchingTeams, andisErrorTeamsare destructured but never used. This is dead code and may trigger unused-variable checks.
data: teams,
isLoading: isLoadingTeams,
isFetching: isFetchingTeams,
isError: isErrorTeams,
} = useGetAplTeamsQuery()
src/pages/services/create-edit/ServicesCreateEditPage.tsx:117
isLoadingTeamServices,isFetchingTeamServices, andrefetchTeamServicesare destructured but never used. Remove them or use them to gate UI/validation so the additional query isn’t just adding dead code.
data: teamServices,
isLoading: isLoadingTeamServices,
isFetching: isFetchingTeamServices,
refetch: refetchTeamServices,
} = useGetTeamAplServicesQuery({ teamId }, { skip: !teamId })
src/pages/services/create-edit/ServicesCreateEditPage.tsx:144
- In edit mode (
serviceNameis truthy), the service name field is still editable, butvalidateOnSubmitis set tofalse, which disables the duplicate-name validation entirely. This allows renaming to an existing service name without any client-side validation.
context: {
domainSuffix: cluster.domainSuffix,
existingNames,
currentName: data?.metadata?.name,
validateOnSubmit: !serviceName,
},
src/pages/secrets/team/create-edit/SecretCreateEditPage.tsx:103
isLoadingTeamSealedSecrets,isFetchingTeamSealedSecrets,isErrorTeamSealedSecrets, andrefetchTeamSealedSecretsare destructured but never used, which leaves dead code (and may violate unused-variable rules).
data: teamSealedSecrets,
isLoading: isLoadingTeamSealedSecrets,
isFetching: isFetchingTeamSealedSecrets,
isError: isErrorTeamSealedSecrets,
refetch: refetchTeamSealedSecrets,
src/pages/network-policies/create-edit/NetworkPoliciesIngressCreateEditPage.tsx:89
isLoadingTeamNetworkPoliciesis destructured but never used. Either remove it or use it to gate validation/UI so the uniqueness list is ready before submit.
const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery(
{ teamId },
{ skip: !teamId },
)
src/pages/network-policies/create-edit/NetworkPoliciesEgressCreateEditPage.tsx:50
isLoadingTeamNetworkPoliciesis destructured but never used. This is dead code and may trigger unused-variable checks.
const { data: teamNetworkPolicies, isLoading: isLoadingTeamNetworkPolicies } = useGetTeamAplNetpolsQuery(
{ teamId },
{ skip: !teamId },
)
src/utils/uniqueName.validator.ts:1
- The header comment references
validators/uniqueName.validator.ts, but this file lives undersrc/utils. This is misleading when navigating/searching the codebase.
// validators/uniqueName.validator.ts
src/utils/uniqueName.validator.test.ts:62
- The
uniqueNameTestbehavior forcurrentName(allowing the current resource name during edit) isn’t covered by tests. Adding a test for this prevents regressions in the duplicate-name logic.
it('skips validation when validateOnSubmit is false', async () => {
await expect(
schema.validate('repo-name', {
context: {
validateOnSubmit: false,
existingNames: ['repo-name'],
},
}),
).resolves.toBe('repo-name')
})
})
Considerations