Skip to content

OU-1195 [DashboardListPage] List OCP namespaces in Create Dashboard ProjectSelector #765

Open
zhuje wants to merge 2 commits intoopenshift:mainfrom
zhuje:ou1195-list-all-projects-main-pr
Open

OU-1195 [DashboardListPage] List OCP namespaces in Create Dashboard ProjectSelector #765
zhuje wants to merge 2 commits intoopenshift:mainfrom
zhuje:ou1195-list-all-projects-main-pr

Conversation

@zhuje
Copy link
Contributor

@zhuje zhuje commented Feb 12, 2026

JIRA
https://issues.redhat.com/browse/OU-1195

Kubeadmin Demo
https://github.com/user-attachments/assets/29d72d16-3d8a-424e-94c6-a1cebb3317d9

User Demo
https://github.com/user-attachments/assets/a026d4d4-8f48-4643-af96-7d781873ffaa

Perses Image (containing API updates to K8s auth)
quay.io/jezhu/perses:feb16-1231-amd64

Monitoring-Console-Plugin Image
quay.io/jezhu/monitoring-console-plugin:ou1195-1770874341

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 12, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: zhuje

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Feb 12, 2026
@zhuje zhuje force-pushed the ou1195-list-all-projects-main-pr branch from c781f9b to c191b22 Compare February 12, 2026 05:30
@zhuje zhuje force-pushed the ou1195-list-all-projects-main-pr branch from 6fa563f to d6f3b6d Compare February 12, 2026 05:52
): string[] => {
const persesProjectNames = Object.keys(persesUserPermissions).filter((name) => name !== '*');
const allAvailableProjects = new Set<string>([...persesProjectNames]);
ocpProjects?.forEach((project) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to be required.

Suggested change
ocpProjects?.forEach((project) => {
ocpProjects.forEach((project) => {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Included in the latest commit

return (
<>
{disabled ? (
<Tooltip content={t('Create button is disabled because you do not have permission')}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<Tooltip content={t('Create button is disabled because you do not have permission')}>
<Tooltip content={t('You don't have permissions to create dashboards')}>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Included in the latest commit

key={selectedProject || 'no-selection'}
initialOptions={projectOptions}
placeholder={t('Select a project')}
noOptionsFoundMessage={(filter) => t(`No project found for "${filter}"`)}
Copy link
Contributor

Choose a reason for hiding this comment

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

This won't work for the translation as it does not use interpolation. Either we use interpolation inside the translation or we just translate the static part

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in the latest commit and run make i18n-frontend to update translations list in plugins__monitoring-plugin.json

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

This PR extends the Perses monitoring plugin with comprehensive dashboard management features, including localization for dashboard CRUD operations, project-based permissions evaluation, OCP project integration, and component refactoring replacing PatternFly Select with TypeaheadSelect for improved UX.

Changes

Cohort / File(s) Summary
Localization
web/locales/en/plugin__monitoring-plugin.json
Added 50+ new translation keys for dashboard lifecycle management (create, rename, duplicate, delete), project selection, permissions messaging, validation errors, and empty/not-found states. Removed legacy "Dashboard Variables" key.
Dashboard Component Refactoring
web/src/components/dashboards/perses/dashboard-action-modals.tsx, web/src/components/dashboards/perses/dashboard-create-dialog.tsx
Replaced PatternFly Select dropdowns with TypeaheadSelect for project/namespace selection. Integrated permissions-aware project lists and form validation. Added support for project creation inline with dashboard operations. Updated loading/error states to reflect permissions evaluation results.
Permissions & Project APIs
web/src/components/dashboards/perses/perses-client.ts, web/src/components/dashboards/perses/dashboard-api.ts
Introduced PersesPermission model and fetchPersesUserPermissions function to retrieve user's scoped dashboard actions per project. Added createPersesProject mutation for creating new projects via Perses API with cache invalidation.
Permissions & Project Hooks
web/src/components/dashboards/perses/hooks/useEditableProjects.ts, web/src/components/dashboards/perses/hooks/useOcpProjects.ts
New useEditableProjects hook combines OCP and Perses project lists, filters by user permissions (create/update/delete on Dashboard scope), and exposes editable/all projects with permission loading states. New useOcpProjects hook watches OCP Project resources with memoization for stable references.
Hook Maintenance
web/src/components/dashboards/perses/hooks/usePerses.ts
Switched localization from direct i18next import to useTranslation hook to align with React patterns.
Removed
web/src/components/dashboards/perses/dashboard-permissions.ts
Deleted file containing checkProjectPermissions and useProjectPermissions logic; functionality consolidated into new useEditableProjects hook with expanded OCP project support.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Component
    participant PermAPI as Perses Permissions API
    participant ProjAPI as Perses Project API
    participant Cache as Query Cache

    User->>UI: Open create dashboard dialog
    UI->>UI: useEditableProjects hook initialized
    UI->>PermAPI: Fetch user permissions for Dashboard scope
    PermAPI-->>UI: Return permission list by project
    UI->>UI: Evaluate editable vs. all projects
    UI-->>User: Display available projects in TypeaheadSelect

    User->>UI: Select project or enter new project name
    alt Project doesn't exist
        User->>UI: Trigger project creation
        UI->>ProjAPI: POST new ProjectResource
        ProjAPI-->>UI: ProjectResource created
        UI->>Cache: Invalidate 'projects' & 'dashboards' queries
        UI-->>User: Project added to selection list
    end

    User->>UI: Fill dashboard name & submit
    UI->>ProjAPI: Create dashboard with selected project
    ProjAPI-->>UI: Dashboard created
    UI->>Cache: Invalidate cache
    UI-->>User: Navigate to new dashboard
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Dashboards dance in the dashboard dawn,
With projects galore and permissions drawn,
TypeaheadSelect hops swift and clean,
Creating new worlds in the Perses scene,
Five dozen strings for every task—
A rabbit's joy, no need to ask! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (18 files):

⚔️ web/cypress/e2e/perses/01.coo_list_perses_admin.cy.ts (content)
⚔️ web/cypress/fixtures/perses/constants.ts (content)
⚔️ web/cypress/support/perses/00.coo_bvt_perses_admin_1.cy.ts (content)
⚔️ web/cypress/support/perses/01.coo_list_perses_admin.cy.ts (content)
⚔️ web/cypress/support/perses/01.coo_list_perses_admin_namespace.cy.ts (content)
⚔️ web/cypress/support/perses/02.coo_edit_perses_admin.cy.ts (content)
⚔️ web/cypress/support/perses/02.coo_edit_perses_admin_1.cy.ts (content)
⚔️ web/cypress/support/perses/03.coo_create_perses_admin.cy.ts (content)
⚔️ web/cypress/support/perses/99.coo_rbac_perses_user1.cy.ts (content)
⚔️ web/cypress/support/perses/99.coo_rbac_perses_user2.cy.ts (content)
⚔️ web/cypress/views/perses-dashboards.ts (content)
⚔️ web/locales/en/plugin__monitoring-plugin.json (content)
⚔️ web/src/components/dashboards/perses/dashboard-action-modals.tsx (content)
⚔️ web/src/components/dashboards/perses/dashboard-api.ts (content)
⚔️ web/src/components/dashboards/perses/dashboard-create-dialog.tsx (content)
⚔️ web/src/components/dashboards/perses/hooks/usePerses.ts (content)
⚔️ web/src/components/dashboards/perses/perses-client.ts (content)
⚔️ web/src/components/data-test.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding OCP namespaces to the Create Dashboard ProjectSelector, which aligns with the primary objective documented in the PR description and the substantial changes made to dashboard creation and project selection flows.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch ou1195-list-all-projects-main-pr
  • Post resolved changes as copyable diffs in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

@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: 9

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/dashboards/perses/dashboard-action-modals.tsx (1)

197-205: ⚠️ Potential issue | 🔴 Critical

Runtime crash: editableProjects and allProjects can be undefined.

When permissions are loading or errored, useEditableProjects returns undefined for both editableProjects and allProjects. Line 200 calls editableProjects.includes(...) and line 204 accesses allProjects[0], both of which will throw TypeError on undefined.

Suggested fix — add optional chaining / fallbacks
   const defaultProject = useMemo(() => {
     if (!dashboard) return '';
 
-    if (dashboard.metadata.project && editableProjects.includes(dashboard.metadata.project)) {
+    if (dashboard.metadata.project && editableProjects?.includes(dashboard.metadata.project)) {
       return dashboard.metadata.project;
     }
 
-    return allProjects[0] || '';
+    return allProjects?.[0] || '';
   }, [dashboard, editableProjects, allProjects]);
🤖 Fix all issues with AI agents
In `@web/locales/en/plugin__monitoring-plugin.json`:
- Around line 169-229: The code in dashboard-action-modals.tsx calls t(`No
namespace found for "${filter}"`) but that exact key is missing from
plugin__monitoring-plugin.json; either change the call to reuse the existing
interpolated key t('No project found for "{{filter}}"') (replace string
interpolation with the i18n {{filter}} syntax) or add a new JSON entry "No
namespace found for \"{{filter}}\"": "No namespace found for \"{{filter}}\"" to
the locales file so t('No namespace found for \"{{filter}}\"') will resolve;
update the call site to use the same {{filter}} placeholder rather than JS
string interpolation.
- Line 200: Update the translation value for the key "You don't have permissions
to dashboard actions" to a grammatically correct phrase; locate the JSON entry
with that exact key and replace the value string with a clearer alternative such
as "You don't have permission to perform dashboard actions" (or "You don't have
permission for dashboard actions") so the key/value reads naturally in the
locales file.

In `@web/src/components/dashboards/perses/dashboard-action-modals.tsx`:
- Line 307: The modal title is hardcoded in the Duplicate Dashboard
modal—replace the literal "Duplicate Dashboard" passed to ModalHeader with a
translated string (e.g. title={t('Duplicate Dashboard')}) and ensure the
translation function is available in this component (import/use the existing
useTranslation hook or the component's t prop as used in RenameActionModal and
other modals); update the ModalHeader call in this duplicate-dashboard modal to
use t('Duplicate Dashboard') instead of the hardcoded string.
- Around line 375-384: When handling onClearSelection for the TypeaheadSelect,
after calling setSelectedProject(null) also clear the form's projectName so the
form state and UI stay consistent; update the onClearSelection handler in
dashboard-action-modals.tsx to call the form API (e.g.
setValue('projectName','') or resetField('projectName')) and clear validation
(clearErrors('projectName')) in addition to setSelectedProject(null) so the form
value, errors, and selectedProject are all cleared together.
- Around line 371-384: The translation call inside the TypeaheadSelect's
noOptionsFoundMessage currently uses a JS template literal (t(`No namespace
found for "${filter}"`)) which prevents i18next interpolation; change it to use
i18next interpolation syntax by passing a translation key string and an
interpolation object (e.g., t('No namespace found for "{{filter}}"', { filter
})) in the noOptionsFoundMessage prop so i18next can match the locale entry and
substitute the runtime filter value; update the TypeaheadSelect usage where
noOptionsFoundMessage is defined to use t('No namespace found for "{{filter}}"',
{ filter }) instead of the template literal.

In `@web/src/components/dashboards/perses/dashboard-create-dialog.tsx`:
- Around line 143-149: The modal toggle handler handleModalToggle currently
flips isModalOpen and resets dashboardName and formErrors only when closing;
extend it to also reset selectedProject when the modal is being closed so the
selection isn't stale on next open. Update handleModalToggle to call
setSelectedProject(undefined or '' depending on the selectedProject type)
alongside setDashboardName('') and setFormErrors({}) in the branch where
isModalOpen is true.
- Around line 159-170: The inline CreateBtn component is recreated each render;
instead, stop defining function CreateBtn inside the component—either extract it
to a top-level component that accepts props (e.g., handleModalToggle, disabled,
permissionsLoading, t, persesDashboardDataTestIDs.createDashboardButtonToolbar)
or convert it to a stable JSX variable (e.g., const createBtn = (<Button
...>{permissionsLoading ? t('Loading...') : t('Create')}</Button>)) and replace
<CreateBtn /> with {createBtn} to avoid unmount/remount and preserve DOM state.

In `@web/src/components/dashboards/perses/hooks/useEditableProjects.ts`:
- Around line 32-60: The getEditableProjects function (useEditableProjects) can
produce duplicate project names when a wildcard project '*' and specific project
entries coexist; update the logic to deduplicate results (e.g., collect names
into a Set or filter uniques before returning) and simplify the conditional by
replacing the redundant "else if (projectName !== '*')" with just "else" so
wildcard expansion and specific project additions don't double-add the same
project names.

In `@web/src/components/dashboards/perses/perses-client.ts`:
- Around line 30-33: The PersesPermission interface declares scopes as a string
but the code treats it like an array (using .includes) and actions is correctly
string[]; update the PersesPermission declaration so scopes: string[] to match
usage and allow array membership checks, then audit usages of PersesPermission
(e.g., any logic reading PersesPermission.scopes and calling .includes or
iterating) to ensure they handle an array; alternatively, if scopes should be a
single value instead, change call sites that use .includes('Dashboard') to an
explicit equality check (===) — prefer changing PersesPermission.scopes to
string[] to align with actions: string[] and existing .includes usage.
🧹 Nitpick comments (3)
web/src/components/dashboards/perses/hooks/useOcpProjects.ts (1)

5-10: useK8sWatchResource error return value is discarded.

useK8sWatchResource returns [data, loaded, error], but only the first two values are destructured. If the watch fails (e.g., RBAC denial for non-optional resources, network issues), the error is silently swallowed, and consumers will see an empty project list with no indication of failure.

Consider capturing and exposing the error so useEditableProjects can surface it:

Suggested change
-  const [ocpProjects, ocpProjectsLoaded] = useK8sWatchResource<K8sResourceKind[]>({
+  const [ocpProjects, ocpProjectsLoaded, ocpProjectsError] = useK8sWatchResource<K8sResourceKind[]>({
     isList: true,
     kind: ProjectModel.kind,
     optional: true,
   });
web/src/components/dashboards/perses/dashboard-create-dialog.tsx (1)

86-98: Dashboard name duplicate check may miss concurrently created dashboards.

The duplicate check on lines 86-98 relies on dashboards fetched from usePerses(selectedProject), which is populated only when the modal is open and a project is selected. If the data hasn't loaded yet or is stale, a duplicate name could slip through. Consider also handling a 409 Conflict response from the server as a fallback.

web/src/components/dashboards/perses/hooks/useEditableProjects.ts (1)

12-16: useUsername directly accesses internal Redux state (state.sdkCore?.user) — fragile coupling to SDK internals.

This pattern relies on the internal state structure of @openshift-console/dynamic-plugin-sdk. While no public hooks currently exist in SDK v4.19.0 to replace this, the SDK team has acknowledged this limitation with a TODO noting that public user access utilities will be available in future SDK versions. Revisit this when the SDK provides public alternatives (e.g., useActiveUser).

Comment on lines +169 to +229
"Rename Dashboard": "Rename Dashboard",
"Dashboard name": "Dashboard name",
"Renaming...": "Renaming...",
"Rename": "Rename",
"Loading...": "Loading...",
"Failed to load project permissions. Please refresh the page and try again.": "Failed to load project permissions. Please refresh the page and try again.",
"Select namespace": "Select namespace",
"Duplicate": "Duplicate",
"this dashboard": "this dashboard",
"Permanently delete dashboard?": "Permanently delete dashboard?",
"Are you sure you want to delete ": "Are you sure you want to delete ",
"? This action can not be undone.": "? This action can not be undone.",
"Deleting...": "Deleting...",
"Delete": "Delete",
"Must be 75 or fewer characters long": "Must be 75 or fewer characters long",
"Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!": "Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!",
"Project is required": "Project is required",
"Dashboard name is required": "Dashboard name is required",
"Failed to create dashboard. Please try again.": "Failed to create dashboard. Please try again.",
"Create": "Create",
"You don't have permissions to create dashboards": "You don't have permissions to create dashboards",
"Create Dashboard": "Create Dashboard",
"Select project": "Select project",
"Select a project": "Select a project",
"No project found for \"{{filter}}\"": "No project found for \"{{filter}}\"",
"my-new-dashboard": "my-new-dashboard",
"Creating...": "Creating...",
"View and manage dashboards.": "View and manage dashboards.",
"Rename dashboard": "Rename dashboard",
"Duplicate dashboard": "Duplicate dashboard",
"Delete dashboard": "Delete dashboard",
"You don't have permissions to dashboard actions": "You don't have permissions to dashboard actions",
"Dashboard": "Dashboard",
"Project": "Project",
"Created on": "Created on",
"Last Modified": "Last Modified",
"Filter by name": "Filter by name",
"Filter by project": "Filter by project",
"No dashboards found": "No dashboards found",
"No results match the filter criteria. Clear filters to show results.": "No results match the filter criteria. Clear filters to show results.",
"No Perses dashboards are currently available in this project.": "No Perses dashboards are currently available in this project.",
"Clear all filters": "Clear all filters",
"Dashboard not found": "Dashboard not found",
"The dashboard \"{{name}}\" was not found in project \"{{project}}\".": "The dashboard \"{{name}}\" was not found in project \"{{project}}\".",
"Empty Dashboard": "Empty Dashboard",
"To get started add something to your dashboard": "To get started add something to your dashboard",
"Edit": "Edit",
"You don't have permission to edit this dashboard": "You don't have permission to edit this dashboard",
"No matching datasource found": "No matching datasource found",
"No Dashboard Available in Selected Project": "No Dashboard Available in Selected Project",
"To explore data, create a dashboard for this project": "To explore data, create a dashboard for this project",
"No Perses Project Available": "No Perses Project Available",
"To explore data, create a Perses Project": "To explore data, create a Perses Project",
"Empty Dashboard": "Empty Dashboard",
"To get started add something to your dashboard": "To get started add something to your dashboard",
"Project is required for fetching project dashboards": "Project is required for fetching project dashboards",
"No projects found": "No projects found",
"No results match the filter criteria.": "No results match the filter criteria.",
"Clear filters": "Clear filters",
"Select project...": "Select project...",
"Projects": "Projects",
"Project": "Project",
"Dashboard": "Dashboard",
"All Projects": "All Projects",
"useToast must be used within ToastProvider": "useToast must be used within ToastProvider",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing translation key used in dashboard-action-modals.tsx.

In dashboard-action-modals.tsx line 376, the code uses t(`No namespace found for "${filter}"`) with string interpolation, but this key does not appear in the localization file. Only "No project found for \"{{filter}}\"" (line 193) exists. The modals file should either reuse the existing key with {{filter}} interpolation syntax or a new namespace-specific key should be added here.

🤖 Prompt for AI Agents
In `@web/locales/en/plugin__monitoring-plugin.json` around lines 169 - 229, The
code in dashboard-action-modals.tsx calls t(`No namespace found for
"${filter}"`) but that exact key is missing from plugin__monitoring-plugin.json;
either change the call to reuse the existing interpolated key t('No project
found for "{{filter}}"') (replace string interpolation with the i18n {{filter}}
syntax) or add a new JSON entry "No namespace found for \"{{filter}}\"": "No
namespace found for \"{{filter}}\"" to the locales file so t('No namespace found
for \"{{filter}}\"') will resolve; update the call site to use the same
{{filter}} placeholder rather than JS string interpolation.

"Rename dashboard": "Rename dashboard",
"Duplicate dashboard": "Duplicate dashboard",
"Delete dashboard": "Delete dashboard",
"You don't have permissions to dashboard actions": "You don't have permissions to dashboard actions",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Grammatically awkward translation string.

"You don't have permissions to dashboard actions" reads unnaturally. Consider revising to something like "You don't have permissions to perform dashboard actions" or "You don't have permissions for dashboard actions".

🤖 Prompt for AI Agents
In `@web/locales/en/plugin__monitoring-plugin.json` at line 200, Update the
translation value for the key "You don't have permissions to dashboard actions"
to a grammatically correct phrase; locate the JSON entry with that exact key and
replace the value string with a clearer alternative such as "You don't have
permission to perform dashboard actions" (or "You don't have permission for
dashboard actions") so the key/value reads naturally in the locales file.

@@ -318,10 +305,15 @@ export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModal
aria-labelledby="duplicate-modal"
>
<ModalHeader title="Duplicate Dashboard" labelId="duplicate-modal-title" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing t() wrapper for modal title.

"Duplicate Dashboard" is a hardcoded string and won't be translated, unlike the RenameActionModal which uses t('Rename Dashboard') on line 120.

Suggested fix
-      <ModalHeader title="Duplicate Dashboard" labelId="duplicate-modal-title" />
+      <ModalHeader title={t('Duplicate Dashboard')} labelId="duplicate-modal-title" />
📝 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
<ModalHeader title="Duplicate Dashboard" labelId="duplicate-modal-title" />
<ModalHeader title={t('Duplicate Dashboard')} labelId="duplicate-modal-title" />
🤖 Prompt for AI Agents
In `@web/src/components/dashboards/perses/dashboard-action-modals.tsx` at line
307, The modal title is hardcoded in the Duplicate Dashboard modal—replace the
literal "Duplicate Dashboard" passed to ModalHeader with a translated string
(e.g. title={t('Duplicate Dashboard')}) and ensure the translation function is
available in this component (import/use the existing useTranslation hook or the
component's t prop as used in RenameActionModal and other modals); update the
ModalHeader call in this duplicate-dashboard modal to use t('Duplicate
Dashboard') instead of the hardcoded string.

Comment on lines +371 to +384
<TypeaheadSelect
key={selectedProject || 'no-selection'}
initialOptions={projectOptions}
placeholder={t('Select namespace')}
noOptionsFoundMessage={(filter) =>
t(`No namespace found for "${filter}"`)
}
onClearSelection={() => {
setSelectedProject(null);
}}
onSelect={onProjectSelect}
onOpenChange={setIsProjectSelectOpen}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onProjectToggle}
isExpanded={isProjectSelectOpen}
isFullWidth
>
{selectedProjectDisplay}
</MenuToggle>
)}
>
<SelectList>
{filteredProjects.map((project) => (
<SelectOption
key={project.metadata.name}
value={project.metadata.name}
>
{getResourceDisplayName(project)}
</SelectOption>
))}
</SelectList>
</Select>
isCreatable={false}
maxMenuHeight="200px"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Translation string uses JS template literal instead of i18next interpolation — translation will never match.

Line 376 uses t(`No namespace found for "${filter}"`) which embeds the runtime filter value into the translation key. This means i18next will look up a key like No namespace found for "myInput" which won't exist. Use i18next interpolation syntax instead:

Suggested fix
-                          noOptionsFoundMessage={(filter) =>
-                            t(`No namespace found for "${filter}"`)
-                          }
+                          noOptionsFoundMessage={(filter) =>
+                            t('No project found for "{{filter}}"', { filter })
+                          }

This also aligns the key with the existing localization entry "No project found for \"{{filter}}\"" in the JSON file. If a namespace-specific key is desired, add it to the locale file with {{filter}} interpolation syntax.

📝 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
<TypeaheadSelect
key={selectedProject || 'no-selection'}
initialOptions={projectOptions}
placeholder={t('Select namespace')}
noOptionsFoundMessage={(filter) =>
t(`No namespace found for "${filter}"`)
}
onClearSelection={() => {
setSelectedProject(null);
}}
onSelect={onProjectSelect}
onOpenChange={setIsProjectSelectOpen}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onProjectToggle}
isExpanded={isProjectSelectOpen}
isFullWidth
>
{selectedProjectDisplay}
</MenuToggle>
)}
>
<SelectList>
{filteredProjects.map((project) => (
<SelectOption
key={project.metadata.name}
value={project.metadata.name}
>
{getResourceDisplayName(project)}
</SelectOption>
))}
</SelectList>
</Select>
isCreatable={false}
maxMenuHeight="200px"
/>
<TypeaheadSelect
key={selectedProject || 'no-selection'}
initialOptions={projectOptions}
placeholder={t('Select namespace')}
noOptionsFoundMessage={(filter) =>
t('No project found for "{{filter}}"', { filter })
}
onClearSelection={() => {
setSelectedProject(null);
}}
onSelect={onProjectSelect}
isCreatable={false}
maxMenuHeight="200px"
/>
🤖 Prompt for AI Agents
In `@web/src/components/dashboards/perses/dashboard-action-modals.tsx` around
lines 371 - 384, The translation call inside the TypeaheadSelect's
noOptionsFoundMessage currently uses a JS template literal (t(`No namespace
found for "${filter}"`)) which prevents i18next interpolation; change it to use
i18next interpolation syntax by passing a translation key string and an
interpolation object (e.g., t('No namespace found for "{{filter}}"', { filter
})) in the noOptionsFoundMessage prop so i18next can match the locale entry and
substitute the runtime filter value; update the TypeaheadSelect usage where
noOptionsFoundMessage is defined to use t('No namespace found for "{{filter}}"',
{ filter }) instead of the template literal.

Comment on lines +375 to +384
noOptionsFoundMessage={(filter) =>
t(`No namespace found for "${filter}"`)
}
onClearSelection={() => {
setSelectedProject(null);
}}
onSelect={onProjectSelect}
onOpenChange={setIsProjectSelectOpen}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onProjectToggle}
isExpanded={isProjectSelectOpen}
isFullWidth
>
{selectedProjectDisplay}
</MenuToggle>
)}
>
<SelectList>
{filteredProjects.map((project) => (
<SelectOption
key={project.metadata.name}
value={project.metadata.name}
>
{getResourceDisplayName(project)}
</SelectOption>
))}
</SelectList>
</Select>
isCreatable={false}
maxMenuHeight="200px"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

onClearSelection does not reset the form value for projectName.

When the user clears the TypeaheadSelect, selectedProject is set to null but the form's projectName field retains its old value. This leaves the form in an inconsistent state — validation may pass with a stale project, or the submit button may remain enabled when no project is visually selected.

Suggested fix
                          onClearSelection={() => {
                            setSelectedProject(null);
+                           form.setValue('projectName', '');
                          }}
🤖 Prompt for AI Agents
In `@web/src/components/dashboards/perses/dashboard-action-modals.tsx` around
lines 375 - 384, When handling onClearSelection for the TypeaheadSelect, after
calling setSelectedProject(null) also clear the form's projectName so the form
state and UI stay consistent; update the onClearSelection handler in
dashboard-action-modals.tsx to call the form API (e.g.
setValue('projectName','') or resetField('projectName')) and clear validation
(clearErrors('projectName')) in addition to setSelectedProject(null) so the form
value, errors, and selectedProject are all cleared together.

Comment on lines 143 to 149
const handleModalToggle = () => {
setIsModalOpen(!isModalOpen);
setIsDropdownOpen(false);
if (isModalOpen) {
setDashboardName('');
setFormErrors({});
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

selectedProject is not reset when the modal closes.

When the modal is closed, dashboardName and formErrors are reset, but selectedProject retains its previous value. The next time the modal opens, the user will see the stale project pre-selected.

Suggested fix
 const handleModalToggle = () => {
   setIsModalOpen(!isModalOpen);
   if (isModalOpen) {
     setDashboardName('');
     setFormErrors({});
+    setSelectedProject(null);
   }
 };
🤖 Prompt for AI Agents
In `@web/src/components/dashboards/perses/dashboard-create-dialog.tsx` around
lines 143 - 149, The modal toggle handler handleModalToggle currently flips
isModalOpen and resets dashboardName and formErrors only when closing; extend it
to also reset selectedProject when the modal is being closed so the selection
isn't stale on next open. Update handleModalToggle to call
setSelectedProject(undefined or '' depending on the selectedProject type)
alongside setDashboardName('') and setFormErrors({}) in the branch where
isModalOpen is true.

Comment on lines +159 to +170
const CreateBtn = () => {
return (
<Button
variant="primary"
onClick={handleModalToggle}
isDisabled={disabled || loading || globalPermissionsLoading}
isDisabled={disabled}
data-test={persesDashboardDataTestIDs.createDashboardButtonToolbar}
>
{loading || globalPermissionsLoading ? t('Loading...') : t('Create')}
{permissionsLoading ? t('Loading...') : t('Create')}
</Button>
);
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Inline component CreateBtn is re-created on every render.

Defining a component inside the render body means React sees a new component type each render, causing it to unmount/remount the button (losing focus, DOM state, etc.). Extract it outside the component or use a plain JSX variable instead.

Suggested approach — use a JSX variable
-  const CreateBtn = () => {
-    return (
-      <Button
-        variant="primary"
-        onClick={handleModalToggle}
-        isDisabled={disabled}
-        data-test={persesDashboardDataTestIDs.createDashboardButtonToolbar}
-      >
-        {permissionsLoading ? t('Loading...') : t('Create')}
-      </Button>
-    );
-  };
+  const createBtn = (
+    <Button
+      variant="primary"
+      onClick={handleModalToggle}
+      isDisabled={disabled}
+      data-test={persesDashboardDataTestIDs.createDashboardButtonToolbar}
+    >
+      {permissionsLoading ? t('Loading...') : t('Create')}
+    </Button>
+  );

Then replace <CreateBtn /> with {createBtn} in the JSX below.

🤖 Prompt for AI Agents
In `@web/src/components/dashboards/perses/dashboard-create-dialog.tsx` around
lines 159 - 170, The inline CreateBtn component is recreated each render;
instead, stop defining function CreateBtn inside the component—either extract it
to a top-level component that accepts props (e.g., handleModalToggle, disabled,
permissionsLoading, t, persesDashboardDataTestIDs.createDashboardButtonToolbar)
or convert it to a stable JSX variable (e.g., const createBtn = (<Button
...>{permissionsLoading ? t('Loading...') : t('Create')}</Button>)) and replace
<CreateBtn /> with {createBtn} to avoid unmount/remount and preserve DOM state.

Comment on lines +32 to +60
const getEditableProjects = (
persesUserPermissions: PersesUserPermissions,
allAvailableProjects: string[],
): string[] => {
const editableProjectNames: string[] = [];
Object.entries(persesUserPermissions).forEach(([projectName, permissions]) => {
const hasDashboardPermissions = permissions.some((permission) => {
const allActions = permission.actions.includes('*');
const individualActions =
permission.actions.includes('create') &&
permission.actions.includes('update') &&
permission.actions.includes('delete');
const hasPermission =
permission.scopes.includes('Dashboard') && (individualActions || allActions);
return hasPermission;
});

if (hasDashboardPermissions) {
// Handle wildcard permissions to all projects
if (projectName === '*') {
editableProjectNames.push(...allAvailableProjects);
} else if (projectName !== '*') {
// Handle specific project permissions
editableProjectNames.push(projectName);
}
}
});
return editableProjectNames;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

editableProjects can contain duplicates when wildcard and project-specific permissions coexist.

When a user has both wildcard (*) and project-specific permissions, editableProjectNames will include all projects from the wildcard expansion plus the specific project again. This will produce duplicate entries in the TypeaheadSelect dropdown.

Also, the else if (projectName !== '*') on line 53 is redundant — the else branch already guarantees projectName !== '*'.

Suggested fix — deduplicate and simplify
 const getEditableProjects = (
   persesUserPermissions: PersesUserPermissions,
   allAvailableProjects: string[],
 ): string[] => {
-  const editableProjectNames: string[] = [];
+  const editableProjectNames = new Set<string>();
   Object.entries(persesUserPermissions).forEach(([projectName, permissions]) => {
     const hasDashboardPermissions = permissions.some((permission) => {
       const allActions = permission.actions.includes('*');
       const individualActions =
         permission.actions.includes('create') &&
         permission.actions.includes('update') &&
         permission.actions.includes('delete');
       const hasPermission =
         permission.scopes.includes('Dashboard') && (individualActions || allActions);
       return hasPermission;
     });
 
     if (hasDashboardPermissions) {
-      // Handle wildcard permissions to all projects
       if (projectName === '*') {
-        editableProjectNames.push(...allAvailableProjects);
-      } else if (projectName !== '*') {
-        // Handle specific project permissions
-        editableProjectNames.push(projectName);
+        allAvailableProjects.forEach((p) => editableProjectNames.add(p));
+      } else {
+        editableProjectNames.add(projectName);
       }
     }
   });
-  return editableProjectNames;
+  return Array.from(editableProjectNames);
 };
🤖 Prompt for AI Agents
In `@web/src/components/dashboards/perses/hooks/useEditableProjects.ts` around
lines 32 - 60, The getEditableProjects function (useEditableProjects) can
produce duplicate project names when a wildcard project '*' and specific project
entries coexist; update the logic to deduplicate results (e.g., collect names
into a Set or filter uniques before returning) and simplify the conditional by
replacing the redundant "else if (projectName !== '*')" with just "else" so
wildcard expansion and specific project additions don't double-add the same
project names.

Comment on lines +30 to +33
export interface PersesPermission {
scopes: string;
actions: string[];
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "\.scopes" --type=ts -C3

Repository: openshift/monitoring-plugin

Length of output: 800


🏁 Script executed:

cat -n web/src/components/dashboards/perses/hooks/useEditableProjects.ts | head -60

Repository: openshift/monitoring-plugin

Length of output: 2635


🏁 Script executed:

cat -n web/src/components/dashboards/perses/perses-client.ts

Repository: openshift/monitoring-plugin

Length of output: 4468


🏁 Script executed:

fd -t f "test\|spec" --type=ts web/src/components/dashboards/perses/ | head -20

Repository: openshift/monitoring-plugin

Length of output: 240


🏁 Script executed:

rg -n "scopes" --type=ts web/src/components/dashboards/perses/ -B2 -A2

Repository: openshift/monitoring-plugin

Length of output: 978


🏁 Script executed:

find web/src/components/dashboards/perses -name "*.test.*" -o -name "*.spec.*"

Repository: openshift/monitoring-plugin

Length of output: 53


🏁 Script executed:

rg "Dashboard" web/src/components/dashboards/perses --type-list | head -5

Repository: openshift/monitoring-plugin

Length of output: 460


🏁 Script executed:

rg -i "scope" web/src/components/dashboards/perses/ -A1 -B1

Repository: openshift/monitoring-plugin

Length of output: 650


🏁 Script executed:

rg "@perses-dev/core" web/ -l

Repository: openshift/monitoring-plugin

Length of output: 1098


🏁 Script executed:

cat web/package.json | grep -A2 -B2 "@perses-dev/core"

Repository: openshift/monitoring-plugin

Length of output: 286


🏁 Script executed:

rg "scope" web/src/components/dashboards/perses/ -i --type-list

Repository: openshift/monitoring-plugin

Length of output: 460


🏁 Script executed:

rg "permission" web/src/components/dashboards/perses/ -i -C2 | grep -i "scope\|comment" | head -20

Repository: openshift/monitoring-plugin

Length of output: 294


🏁 Script executed:

git log --oneline -p web/src/components/dashboards/perses/perses-client.ts | grep -A5 -B5 "scopes" | head -30

Repository: openshift/monitoring-plugin

Length of output: 306


Type mismatch: scopes should be string[] to match usage pattern and actions field.

scopes is typed as string but used with .includes('Dashboard') for substring matching. This is inconsistent with the actions field which is correctly typed as string[]. If scopes can contain multiple values (e.g., ["Dashboard", "Variable"]), the type should be string[] and the code would naturally use .includes() for element matching. If scopes is always a single scope, consider explicitly comparing with === 'Dashboard' instead of substring matching.

🤖 Prompt for AI Agents
In `@web/src/components/dashboards/perses/perses-client.ts` around lines 30 - 33,
The PersesPermission interface declares scopes as a string but the code treats
it like an array (using .includes) and actions is correctly string[]; update the
PersesPermission declaration so scopes: string[] to match usage and allow array
membership checks, then audit usages of PersesPermission (e.g., any logic
reading PersesPermission.scopes and calling .includes or iterating) to ensure
they handle an array; alternatively, if scopes should be a single value instead,
change call sites that use .includes('Dashboard') to an explicit equality check
(===) — prefer changing PersesPermission.scopes to string[] to align with
actions: string[] and existing .includes usage.

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 16, 2026

@zhuje: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants