Skip to content

fix(opcua): align UI with design system#744

Merged
JoaoGSP merged 5 commits into
developmentfrom
fix/ui-opcua-design-system
May 5, 2026
Merged

fix(opcua): align UI with design system#744
JoaoGSP merged 5 commits into
developmentfrom
fix/ui-opcua-design-system

Conversation

@marconetsf
Copy link
Copy Markdown
Contributor

@marconetsf marconetsf commented Apr 30, 2026

Summary

Editor-side mirror of openplc-web#377 (fix/ui-opcua-design-system). Keeps the shared frontend/ surface byte-identical with the web counterpart so the cross-repo Shared Surface Sync check can converge once the existing development drift backlog is drained.

Mirrored changes

Updates the OPC UA address-space tree, selected-variables list, and the certificate / security-profile / user tabs and modals:

  • Variable tree uses Radix Chevron icons instead of glyphs that were falling back to the color emoji font.
  • Type badges in the variable tree and selected-variables list collapse to a single neutral palette.
  • Delete/Remove buttons across the OPC UA tabs adopt the neutral box styling consistent with adjacent Edit actions.
  • Inline alert and validation-error containers drop the amber/red backgrounds in favour of a neutral border + background, with copy carrying the severity cue.

Files touched

All under src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/:

  • certificate-modal.tsx
  • certificates-tab.tsx
  • security-profile-modal.tsx
  • security-profiles-tab.tsx
  • selected-variables-list.tsx
  • user-modal.tsx
  • users-tab.tsx
  • variable-config-modal.tsx
  • variable-tree.tsx

Sync status

Test plan

  • Open the OPC UA server editor tab and verify the variable tree expand/collapse arrows render as Chevron icons.
  • Confirm the type badges (P/FB/G/S/[]/V) appear in the neutral palette.
  • Trigger a validation error in any OPC UA modal and confirm the inline alert uses the neutral styling.
  • Click a Delete/Remove button on Certificates / Security Profiles / Users tabs and confirm the neutral box styling.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Style

    • Unified validation, warning and action colors to neutral tones across OPC UA server screens.
    • Standardized icon/button hover styling with neutral base and red hover for destructive actions.
  • New Features

    • Replaced several card/list views with table layouts for Users, Security Profiles and Certificates.
    • Added icon-based Add/Edit/Delete controls and updated expand/collapse chevrons; improved empty-state messaging.
  • Refactor

    • Normalized node-type icons and simplified variable tree rendering.
    • Switched to per-field inline validation with nuanced password/certificate handling.

Mirror of openplc-web fix/ui-opcua-design-system to keep shared frontend
surfaces byte-identical. Updates the OPC UA address-space tree, selected
variables list, certificate/security/user tabs and modals so that:

- Variable tree uses Radix Chevron icons instead of glyphs that were
  falling back to the color emoji font.
- Type badges in the variable tree and selected-variables list are
  neutralized to a single palette.
- Delete/Remove buttons across the OPC UA tabs use the neutral box
  styling consistent with adjacent Edit actions.
- Inline alert and validation-error containers drop the amber/red
  backgrounds in favour of neutral border + background, with copy
  carrying the severity cue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Walkthrough

Updated UI/styling and layout across multiple OPC‑UA server editor components: validation/warning colors moved to neutral tones, several list/card views were refactored into table layouts, node-type icon rendering unified via a lookup, and form validation shifted to per-field inline errors.

Changes

OPC‑UA Editor UI & Layout Updates

Layer / File(s) Summary
Data / Labels
.../variable-tree.tsx, .../selected-variables-list.tsx
Introduces NODE_TYPE_LABEL lookup and removes per-type color logic; node-type badges now use a unified neutral label/icon.
Form Validation
.../user-modal.tsx
Replaces bottom aggregated validationErrors with memoized per-field errors (usernameError, passwordError, certificateError); updates derived flags for password setting/mismatch and isValid.
Core UI Structure
.../certificates-tab.tsx, .../security-profiles-tab.tsx, .../users-tab.tsx
Refactors multiple card/list views into table layouts (Certificates, Security Profiles, Users); adds explicit empty-state message for empty security profiles.
Action & Button Styling
.../certificates-tab.tsx, .../security-profiles-tab.tsx, .../users-tab.tsx
Replaces text/bordered action controls with brand or icon buttons (adds PlusIcon, TrashIcon); delete/remove actions become icon-only with neutral base and red hover.
Validation / Warning Styling
.../certificate-modal.tsx, .../security-profile-modal.tsx, .../variable-config-modal.tsx, .../users-tab.tsx
Converts validation/error and warning containers and list-item texts from red/amber Tailwind classes to neutral equivalents (light & dark).
Presentation polish
.../variable-tree.tsx, .../selected-variables-list.tsx
Replaces text arrow glyphs with Radix chevrons for expand/collapse; simplifies per-node visuals to neutral badges.
Tests / Documentation
no test or doc files changed
No tests or docs were modified in this PR.

Sequence Diagram(s)

(Skipped — changes are UI/layout and do not introduce new multi-component control flow.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Autonomy-Logic/openplc-editor#546: Modifies the same OPC‑UA editor components (certificate-modal, certificates-tab, security-profile-modal, security-profiles-tab, selected-variables-list, user-modal, users-tab, variable-config-modal, variable-tree).
  • Autonomy-Logic/openplc-editor#547: Also touches the same OPC‑UA editor component set and appears related to prior UI/layout work.

Suggested labels

enhancement

Suggested reviewers

  • JoaoGSP

Poem

🐰 I hopped through tables, neutral and bright,
Swapped arrows for chevrons, trimmed reds to light,
Inline errors whisper where lists once stood,
Icons click softly — tidy, calm, and good,
A rabbit's nibble on UI made right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(opcua): align UI with design system' clearly and concisely describes the main change: aligning OPC UA UI components with the design system through color and icon updates.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering summary, mirrored changes, files touched, and sync status. However, the DOD checklist template sections (issue references, Jira link, test coverage %, and acceptance criteria checkboxes) are not filled out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ui-opcua-design-system

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/variable-tree.tsx:
- Around line 33-44: The ExpandIcon component renders an icon-only button
without accessible name/state; update ExpandIcon to import and use i18next
(e.g., useTranslation) and add aria-expanded={expanded} and a localized
aria-label such as aria-label={t(expanded ? 'opcua.collapse' : 'opcua.expand')}
(or similar keys) so the button exposes both state and a translated name while
keeping the existing onClick and Icon selection
(ChevronDownIcon/ChevronRightIcon).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9bff4e36-1802-41e3-8756-6f078a2c2681

📥 Commits

Reviewing files that changed from the base of the PR and between ab29bf0 and c41bd98.

📒 Files selected for processing (9)
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/certificate-modal.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/certificates-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/security-profile-modal.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/selected-variables-list.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/user-modal.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/users-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/variable-config-modal.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/variable-tree.tsx

Comment on lines +33 to +44
const ExpandIcon = ({ expanded, onClick }: { expanded: boolean; onClick: (e: React.MouseEvent) => void }) => {
const Icon = expanded ? ChevronDownIcon : ChevronRightIcon
return (
<button
type='button'
onClick={onClick}
className='flex h-4 w-4 items-center justify-center text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200'
>
<Icon className='h-3.5 w-3.5' />
</button>
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add accessible name/state to the expand button.

At Line 36, this is now an icon-only control, but it has no accessible name and no exposed expanded state. Please add aria-label + aria-expanded (with localized labels).

♿ Suggested fix
-import { useCallback, useState } from 'react'
+import { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
...
 const ExpandIcon = ({ expanded, onClick }: { expanded: boolean; onClick: (e: React.MouseEvent) => void }) => {
+  const { t } = useTranslation()
   const Icon = expanded ? ChevronDownIcon : ChevronRightIcon
   return (
     <button
       type='button'
       onClick={onClick}
+      aria-label={expanded ? t('opcua.variableTree.collapseNode') : t('opcua.variableTree.expandNode')}
+      aria-expanded={expanded}
       className='flex h-4 w-4 items-center justify-center text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200'
     >
       <Icon className='h-3.5 w-3.5' />
     </button>
   )
 }

As per coding guidelines "Use i18next for internationalization in frontend components".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ExpandIcon = ({ expanded, onClick }: { expanded: boolean; onClick: (e: React.MouseEvent) => void }) => {
const Icon = expanded ? ChevronDownIcon : ChevronRightIcon
return (
<button
type='button'
onClick={onClick}
className='flex h-4 w-4 items-center justify-center text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200'
>
<Icon className='h-3.5 w-3.5' />
</button>
)
}
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
const ExpandIcon = ({ expanded, onClick }: { expanded: boolean; onClick: (e: React.MouseEvent) => void }) => {
const { t } = useTranslation()
const Icon = expanded ? ChevronDownIcon : ChevronRightIcon
return (
<button
type='button'
onClick={onClick}
aria-label={expanded ? t('opcua.variableTree.collapseNode') : t('opcua.variableTree.expandNode')}
aria-expanded={expanded}
className='flex h-4 w-4 items-center justify-center text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200'
>
<Icon className='h-3.5 w-3.5' />
</button>
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/variable-tree.tsx
around lines 33 - 44, The ExpandIcon component renders an icon-only button
without accessible name/state; update ExpandIcon to import and use i18next
(e.g., useTranslation) and add aria-expanded={expanded} and a localized
aria-label such as aria-label={t(expanded ? 'opcua.collapse' : 'opcua.expand')}
(or similar keys) so the button exposes both state and a translated name while
keeping the existing onClick and Icon selection
(ChevronDownIcon/ChevronRightIcon).

marconetsf and others added 2 commits May 1, 2026 11:26
Convert Security Profiles, Users, and Trusted Client Certificates lists
to tables matching the S7Comm Data Blocks pattern, with the brand-blue
"Add" action button and edit/delete icon buttons. In the user modal,
move per-field validation errors inline beneath each input and drop
the bottom validation card. On the Users tab, drop the colored role
badges in favor of plain text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/users-tab.tsx (1)

102-135: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Move hardcoded UI strings to i18next translation keys.

This component contains multiple user-facing strings that should be localized: the warning message about missing password users, "Add User" button text, empty state message, table headers ("Name", "Type", "Role", "Actions"), role descriptions, and the header description. The i18next infrastructure is already configured in the project; use useTranslation() hook to consume these strings from translation files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/users-tab.tsx
around lines 102 - 135, The UI strings in users-tab.tsx (including the warning
block text, the "Add User" button label, the empty-state message, table headers
"Name"/"Type"/"Role"/"Actions", role descriptions and header description) should
be replaced with i18next translation keys via the useTranslation() hook; import
and call const { t } = useTranslation() at the top of the UsersTab component,
swap each hardcoded literal passed to elements (e.g. the warning paragraph, the
button label used by handleAddUser and PlusIcon button, the empty-state <p>, and
the <th> headers) to t('opcua.users.<key>') or similar keys, and add
corresponding keys to the translation files so the same keys are available in
locales.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/certificates-tab.tsx:
- Around line 298-305: The delete button using <TrashIcon /> lacks an accessible
name; update the button element that calls handleDeleteCertificate(cert.id) to
include an aria-label (e.g., aria-label={`Delete certificate ${cert.id}`} or
include cert.name if available) so assistive tech can announce the control; keep
the existing title and visual markup but add the aria-label attribute to the
same button element to provide a clear, unique accessible name.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx:
- Around line 137-140: The delete path currently only checks total row count and
can remove the last enabled profile; update the deletion check and any delete
handlers to consider enabled count: compute const enabledCount =
config.securityProfiles.filter(p => p.enabled).length and before allowing delete
(or enabling the delete button) disallow removing a profile when profile.enabled
&& enabledCount <= 1; apply the same check where you render the delete action
and in the actual delete handler used by the map over config.securityProfiles
(the block using isLastEnabled and the separate delete code at the later section
referenced) so deletion is prevented consistently.
- Around line 143-149: The checkbox and icon-only buttons in the
SecurityProfilesTab component lack accessible names; update the input and the
Edit/Delete icon buttons to include descriptive aria-labels that use the profile
context (e.g., for the checkbox: aria-label={`Toggle ${profile.name} enabled`}
or similar, and for the Edit/Delete buttons: aria-label={`Edit ${profile.name}
profile`} and aria-label={`Delete ${profile.name} profile`}); locate the
controls around handleToggleEnabled/profile.enabled and the icon buttons (the
Edit/Delete handlers) and add the aria-label props so screen readers announce
each control clearly.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/user-modal.tsx:
- Around line 156-183: certificateError must only represent the "please select a
certificate" validation message, not the "no trusted certs" UI banner; change
the logic so certificateError === 'Please select a certificate' when authType
=== 'certificate' and certificateId is falsy, and create a separate boolean
(e.g. hasNoTrustedCerts or missingTrustedCerts) computed from authType ===
'certificate' && availableCertificates.length === 0 &&
!existingUser?.certificateId; include that boolean in the isValid check (isValid
= ... && !missingTrustedCerts) so Save is disabled for new users when no certs
exist, and keep the JSX guard that only renders the certificateError span when
availableCertificates.length > 0 so the banner and the inline error don't
duplicate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/users-tab.tsx:
- Around line 152-167: The Edit and Delete icon-only buttons lack reliable
accessible names; update the two button elements that call handleEditUser(user)
and handleDeleteUser(user.id) to include explicit aria-label attributes (e.g.,
aria-label={`Edit user ${user.username ?? user.id}`} and aria-label={`Delete
user ${user.username ?? user.id}`}) so screen readers get a proper name while
keeping the existing title and icons (Pencil1Icon and TrashIcon).

---

Outside diff comments:
In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/users-tab.tsx:
- Around line 102-135: The UI strings in users-tab.tsx (including the warning
block text, the "Add User" button label, the empty-state message, table headers
"Name"/"Type"/"Role"/"Actions", role descriptions and header description) should
be replaced with i18next translation keys via the useTranslation() hook; import
and call const { t } = useTranslation() at the top of the UsersTab component,
swap each hardcoded literal passed to elements (e.g. the warning paragraph, the
button label used by handleAddUser and PlusIcon button, the empty-state <p>, and
the <th> headers) to t('opcua.users.<key>') or similar keys, and add
corresponding keys to the translation files so the same keys are available in
locales.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f8310d88-4e31-40cd-b58c-4fb38d283f52

📥 Commits

Reviewing files that changed from the base of the PR and between c41bd98 and deabccb.

📒 Files selected for processing (4)
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/certificates-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/user-modal.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/users-tab.tsx

Comment on lines +298 to +305
<button
type='button'
onClick={() => handleDeleteCertificate(cert.id)}
className='rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400'
title='Delete'
>
<TrashIcon className='h-4 w-4' />
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Provide an accessible name for the icon-only delete control.

Please add aria-label; relying on title alone is insufficient for consistent assistive-tech support.

Proposed fix
                         <button
                           type='button'
                           onClick={() => handleDeleteCertificate(cert.id)}
                           className='rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400'
+                          aria-label={`Delete trusted certificate ${cert.id}`}
                           title='Delete'
                         >
                           <TrashIcon className='h-4 w-4' />
                         </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type='button'
onClick={() => handleDeleteCertificate(cert.id)}
className='rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400'
title='Delete'
>
<TrashIcon className='h-4 w-4' />
</button>
<button
type='button'
onClick={() => handleDeleteCertificate(cert.id)}
className='rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400'
aria-label={`Delete trusted certificate ${cert.id}`}
title='Delete'
>
<TrashIcon className='h-4 w-4' />
</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/certificates-tab.tsx
around lines 298 - 305, The delete button using <TrashIcon /> lacks an
accessible name; update the button element that calls
handleDeleteCertificate(cert.id) to include an aria-label (e.g.,
aria-label={`Delete certificate ${cert.id}`} or include cert.name if available)
so assistive tech can announce the control; keep the existing title and visual
markup but add the aria-label attribute to the same button element to provide a
clear, unique accessible name.

Comment on lines +137 to +140
{config.securityProfiles.map((profile) => {
const isLastEnabled =
profile.enabled && config.securityProfiles.filter((p) => p.enabled).length <= 1
return (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Deleting an enabled profile can bypass the “at least one enabled” rule.

The toggle path blocks disabling the last enabled profile, but delete only checks total row count. If one profile is enabled and others are disabled, deleting the enabled one leaves zero enabled profiles.

Proposed fix
               {config.securityProfiles.map((profile) => {
-                const isLastEnabled =
-                  profile.enabled && config.securityProfiles.filter((p) => p.enabled).length <= 1
+                const enabledCount = config.securityProfiles.filter((p) => p.enabled).length
+                const isLastEnabled = profile.enabled && enabledCount <= 1
+                const disableDelete = config.securityProfiles.length <= 1 || isLastEnabled
                 return (
@@
                         <button
                           type='button'
                           onClick={() => handleDeleteProfile(profile.id)}
-                          disabled={config.securityProfiles.length <= 1}
+                          disabled={disableDelete}
                           className={cn(
                             'rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400',
-                            config.securityProfiles.length <= 1 && 'cursor-not-allowed opacity-50',
+                            disableDelete && 'cursor-not-allowed opacity-50',
                           )}
                           title='Delete'
                         >

Also applies to: 179-186

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx
around lines 137 - 140, The delete path currently only checks total row count
and can remove the last enabled profile; update the deletion check and any
delete handlers to consider enabled count: compute const enabledCount =
config.securityProfiles.filter(p => p.enabled).length and before allowing delete
(or enabling the delete button) disallow removing a profile when profile.enabled
&& enabledCount <= 1; apply the same check where you render the delete action
and in the actual delete handler used by the map over config.securityProfiles
(the block using isLastEnabled and the separate delete code at the later section
referenced) so deletion is prevented consistently.

Comment on lines +143 to +149
<label className='relative inline-flex cursor-pointer items-center'>
<input
type='checkbox'
checked={profile.enabled}
onChange={() => handleToggleEnabled(profile)}
className='peer sr-only'
disabled={isLastEnabled}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add ARIA labels to unnamed toggle/action controls.

The enabled checkbox has no text label, and Edit/Delete are icon-only buttons. Please add explicit aria-labels so controls are announced clearly by assistive technologies.

Proposed fix
                         <input
                           type='checkbox'
                           checked={profile.enabled}
                           onChange={() => handleToggleEnabled(profile)}
                           className='peer sr-only'
                           disabled={isLastEnabled}
+                          aria-label={`Enable security profile ${profile.name}`}
                         />
@@
                         <button
                           type='button'
                           onClick={() => handleEditProfile(profile)}
                           className='rounded p-1 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200'
+                          aria-label={`Edit security profile ${profile.name}`}
                           title='Edit'
                         >
@@
                         <button
                           type='button'
                           onClick={() => handleDeleteProfile(profile.id)}
                           disabled={config.securityProfiles.length <= 1}
                           className={cn(
                             'rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400',
                             config.securityProfiles.length <= 1 && 'cursor-not-allowed opacity-50',
                           )}
+                          aria-label={`Delete security profile ${profile.name}`}
                           title='Delete'
                         >

Also applies to: 171-190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx
around lines 143 - 149, The checkbox and icon-only buttons in the
SecurityProfilesTab component lack accessible names; update the input and the
Edit/Delete icon buttons to include descriptive aria-labels that use the profile
context (e.g., for the checkbox: aria-label={`Toggle ${profile.name} enabled`}
or similar, and for the Edit/Delete buttons: aria-label={`Edit ${profile.name}
profile`} and aria-label={`Delete ${profile.name} profile`}); locate the
controls around handleToggleEnabled/profile.enabled and the icon buttons (the
Edit/Delete handlers) and add the aria-label props so screen readers announce
each control clearly.

Comment on lines +156 to +183
const certificateError = useMemo<string | null>(() => {
if (authType !== 'certificate') return null
if (availableCertificates.length === 0 && !existingUser?.certificateId) {
// The certificate-auth section already renders this banner inline
// when there are no trusted certs. Skip surfacing it as a duplicate
// field error.
return null
}

return errors
}, [
authType,
username,
password,
confirmPassword,
certificateId,
existingUsernames,
existingUser,
isEditing,
availableCertificates,
])

const isValid = validationErrors.length === 0
if (!certificateId) return 'Please select a certificate'
return null
}, [authType, certificateId, availableCertificates, existingUser])

// Password mismatch handling has two flavours:
// - `passwordsMismatchVisible`: drives the inline red label under the
// Confirm Password input. Only true once the user has typed something
// in confirmPassword, so the error doesn't flicker while they're still
// filling out the form.
// - `passwordsOk`: gates the Save button. Requires both fields to be
// filled AND match when a password is being set (new user, or editing
// user with non-empty password). Empty confirmPassword keeps Save
// disabled silently — no error is shown until the user types into the
// confirm field.
const isSettingPassword = authType === 'password' && (!isEditing || password.length > 0)
const passwordsMismatchVisible =
isSettingPassword && confirmPassword.length > 0 && password !== confirmPassword
const passwordsOk = !isSettingPassword || (confirmPassword.length > 0 && password === confirmPassword)

const isValid = !usernameError && !passwordError && !certificateError && passwordsOk
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

certificateError returns null when no certs exist, making isValid = true and allowing a malformed user to be saved

When authType === 'certificate', availableCertificates.length === 0, and existingUser is undefined (new user):

  • usernameErrornull (password branch skipped)
  • passwordErrornull (password branch skipped)
  • certificateError → hits the early return null on lines 158-163
  • passwordsOktrue (isSettingPassword is false)
  • isValidtrue

The Save button becomes enabled; handleSave runs and persists:

{ type: 'certificate', certificateId: null, ... }

The comment's intent (avoid duplicating the existing "no trusted certs" banner as a second error label) is sound UX, but the current implementation conflates the display decision with the validity decision. These need to be separated.

🐛 Proposed fix
  const certificateError = useMemo<string | null>(() => {
    if (authType !== 'certificate') return null
    if (availableCertificates.length === 0 && !existingUser?.certificateId) {
-     // The certificate-auth section already renders this banner inline
-     // when there are no trusted certs. Skip surfacing it as a duplicate
-     // field error.
-     return null
+     // No trusted certificates exist — still an error for isValid purposes,
+     // but we return a sentinel so the render layer knows NOT to show a
+     // duplicate label (the "no trusted certs" banner already conveys this).
+     return 'NO_CERTS' // not shown as a label; just blocks Save
    }
    if (!certificateId) return 'Please select a certificate'
    return null
  }, [authType, certificateId, availableCertificates, existingUser])

Then in the JSX, keep the existing guard that only renders the error span when there are available certificates (the banner already handles the empty case):

- {certificateError && (
+ {certificateError && certificateError !== 'NO_CERTS' && (
    <span className='font-caption text-cp-xs font-normal text-red-500'>
      {certificateError}
    </span>
  )}

Alternatively, extract a separate hasNoCertsError boolean and keep certificateError purely for the "please select" message, then isValid uses both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/user-modal.tsx
around lines 156 - 183, certificateError must only represent the "please select
a certificate" validation message, not the "no trusted certs" UI banner; change
the logic so certificateError === 'Please select a certificate' when authType
=== 'certificate' and certificateId is falsy, and create a separate boolean
(e.g. hasNoTrustedCerts or missingTrustedCerts) computed from authType ===
'certificate' && availableCertificates.length === 0 &&
!existingUser?.certificateId; include that boolean in the isValid check (isValid
= ... && !missingTrustedCerts) so Save is disabled for new users when no certs
exist, and keep the JSX guard that only renders the certificateError span when
availableCertificates.length > 0 so the banner and the inline error don't
duplicate.

Comment on lines +152 to +167
<button
type='button'
onClick={() => handleEditUser(user)}
className='rounded p-1 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200'
title='Edit'
>
<Pencil1Icon className='h-4 w-4' />
</button>
<button
type='button'
onClick={() => handleDeleteUser(user.id)}
className='rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400'
title='Delete'
>
<TrashIcon className='h-4 w-4' />
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add explicit accessible names to icon-only action buttons.

title is not a reliable accessible name for screen readers. Please add aria-label on Edit/Delete buttons.

Proposed fix
                       <button
                         type='button'
                         onClick={() => handleEditUser(user)}
                         className='rounded p-1 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200'
+                        aria-label={`Edit user ${getUserDisplayName(user)}`}
                         title='Edit'
                       >
                         <Pencil1Icon className='h-4 w-4' />
                       </button>
                       <button
                         type='button'
                         onClick={() => handleDeleteUser(user.id)}
                         className='rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400'
+                        aria-label={`Delete user ${getUserDisplayName(user)}`}
                         title='Delete'
                       >
                         <TrashIcon className='h-4 w-4' />
                       </button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/users-tab.tsx
around lines 152 - 167, The Edit and Delete icon-only buttons lack reliable
accessible names; update the two button elements that call handleEditUser(user)
and handleDeleteUser(user.id) to include explicit aria-label attributes (e.g.,
aria-label={`Edit user ${user.username ?? user.id}`} and aria-label={`Delete
user ${user.username ?? user.id}`}) so screen readers get a proper name while
keeping the existing title and icons (Pencil1Icon and TrashIcon).

marconetsf and others added 2 commits May 5, 2026 10:26
Collapses short multi-line expressions in security-profiles-tab,
user-modal and users-tab so prettier --check passes. Mirrored across
both repos to keep ci-sync byte-identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx (2)

142-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Icon-only controls still lack aria-label.

The toggle checkbox (line 143), the Edit button (lines 170-177), and the Delete button (lines 178-189) are all visually unlabeled. title attributes are not a reliable accessible name for screen readers.

♿ Proposed fix
                        <input
                          type='checkbox'
                          checked={profile.enabled}
                          onChange={() => handleToggleEnabled(profile)}
                          className='peer sr-only'
                          disabled={isLastEnabled}
+                         aria-label={`Enable security profile ${profile.name}`}
                        />
                        <button
                          type='button'
                          onClick={() => handleEditProfile(profile)}
                          className='rounded p-1 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200'
                          title='Edit'
+                         aria-label={`Edit security profile ${profile.name}`}
                        >
                        <button
                          type='button'
                          onClick={() => handleDeleteProfile(profile.id)}
-                         disabled={config.securityProfiles.length <= 1}
+                         disabled={disableDelete}
                          className={...}
                          title='Delete'
+                         aria-label={`Delete security profile ${profile.name}`}
                        >

Also applies to: 170-190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx
around lines 142 - 149, The icon-only interactive controls (the checkbox and the
Edit/Delete buttons) lack accessible names; update the input checkbox in the
profile row and the Edit/Delete button elements to include explicit aria-label
attributes (not relying on title) that uniquely describe the action and target
(e.g., aria-label={`Toggle enable ${profile.name}`} for the checkbox and
aria-label={`Edit ${profile.name}`} / aria-label={`Delete ${profile.name}`} for
the buttons). Locate the checkbox using handleToggleEnabled/profile.enabled and
the Edit/Delete buttons in the same component where those handlers are invoked,
and ensure disabled/visibility states remain unchanged while adding these
aria-labels.

178-186: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Delete button still permits removing the last enabled profile.

isLastEnabled (line 138) is computed and used to disable the toggle, but the delete disabled guard on line 181 only checks total count (config.securityProfiles.length <= 1). A user can still delete the sole enabled profile as long as there are ≥ 2 profiles total, leaving zero enabled profiles and violating the invariant enforced by handleToggleEnabled.

🐛 Proposed fix
              {config.securityProfiles.map((profile) => {
-               const isLastEnabled = profile.enabled && config.securityProfiles.filter((p) => p.enabled).length <= 1
+               const enabledCount = config.securityProfiles.filter((p) => p.enabled).length
+               const isLastEnabled = profile.enabled && enabledCount <= 1
+               const disableDelete = config.securityProfiles.length <= 1 || isLastEnabled
                return (
                        <button
                          type='button'
                          onClick={() => handleDeleteProfile(profile.id)}
-                         disabled={config.securityProfiles.length <= 1}
+                         disabled={disableDelete}
                          className={cn(
                            'rounded p-1 text-neutral-500 hover:bg-red-100 hover:text-red-600 dark:text-neutral-400 dark:hover:bg-red-900/30 dark:hover:text-red-400',
-                           config.securityProfiles.length <= 1 && 'cursor-not-allowed opacity-50',
+                           disableDelete && 'cursor-not-allowed opacity-50',
                          )}
                          title='Delete'
                        >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx
around lines 178 - 186, The Delete button currently only checks
config.securityProfiles.length and can remove the last enabled profile; update
the disabled guard on the delete button (the element using handleDeleteProfile)
to also prevent deleting a profile when it is the last enabled one by using the
existing isLastEnabled check for that profile (or equivalent enabled-count
logic) — i.e., disable the Delete button if config.securityProfiles.length <= 1
OR isLastEnabled(profile.id) is true so deletion cannot leave zero enabled
profiles (consistent with handleToggleEnabled invariant).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@src/frontend/components/_features/`[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx:
- Around line 142-149: The icon-only interactive controls (the checkbox and the
Edit/Delete buttons) lack accessible names; update the input checkbox in the
profile row and the Edit/Delete button elements to include explicit aria-label
attributes (not relying on title) that uniquely describe the action and target
(e.g., aria-label={`Toggle enable ${profile.name}`} for the checkbox and
aria-label={`Edit ${profile.name}`} / aria-label={`Delete ${profile.name}`} for
the buttons). Locate the checkbox using handleToggleEnabled/profile.enabled and
the Edit/Delete buttons in the same component where those handlers are invoked,
and ensure disabled/visibility states remain unchanged while adding these
aria-labels.
- Around line 178-186: The Delete button currently only checks
config.securityProfiles.length and can remove the last enabled profile; update
the disabled guard on the delete button (the element using handleDeleteProfile)
to also prevent deleting a profile when it is the last enabled one by using the
existing isLastEnabled check for that profile (or equivalent enabled-count
logic) — i.e., disable the Delete button if config.securityProfiles.length <= 1
OR isLastEnabled(profile.id) is true so deletion cannot leave zero enabled
profiles (consistent with handleToggleEnabled invariant).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: aa875938-02db-41ee-a42f-0b86e50ba0b5

📥 Commits

Reviewing files that changed from the base of the PR and between deabccb and 3b0a2d8.

📒 Files selected for processing (3)
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/security-profiles-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/user-modal.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/users-tab.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/users-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/server/opcua-server/components/user-modal.tsx

@marconetsf marconetsf requested a review from JoaoGSP May 5, 2026 12:02
@JoaoGSP JoaoGSP merged commit b6bc81d into development May 5, 2026
13 checks passed
@JoaoGSP JoaoGSP deleted the fix/ui-opcua-design-system branch May 5, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants