OU-1195 [DashboardListPage] List OCP namespaces in Create Dashboard ProjectSelector #765
OU-1195 [DashboardListPage] List OCP namespaces in Create Dashboard ProjectSelector #765zhuje wants to merge 2 commits intoopenshift:mainfrom
Conversation
|
[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 DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
c781f9b to
c191b22
Compare
…oard ProjectSelector
6fa563f to
d6f3b6d
Compare
| ): string[] => { | ||
| const persesProjectNames = Object.keys(persesUserPermissions).filter((name) => name !== '*'); | ||
| const allAvailableProjects = new Set<string>([...persesProjectNames]); | ||
| ocpProjects?.forEach((project) => { |
There was a problem hiding this comment.
This seems to be required.
| ocpProjects?.forEach((project) => { | |
| ocpProjects.forEach((project) => { |
There was a problem hiding this comment.
Included in the latest commit
| return ( | ||
| <> | ||
| {disabled ? ( | ||
| <Tooltip content={t('Create button is disabled because you do not have permission')}> |
There was a problem hiding this comment.
| <Tooltip content={t('Create button is disabled because you do not have permission')}> | |
| <Tooltip content={t('You don't have permissions to create dashboards')}> |
There was a problem hiding this comment.
Included in the latest commit
| key={selectedProject || 'no-selection'} | ||
| initialOptions={projectOptions} | ||
| placeholder={t('Select a project')} | ||
| noOptionsFoundMessage={(filter) => t(`No project found for "${filter}"`)} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Updated in the latest commit and run make i18n-frontend to update translations list in plugins__monitoring-plugin.json
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🔴 CriticalRuntime crash:
editableProjectsandallProjectscan beundefined.When permissions are loading or errored,
useEditableProjectsreturnsundefinedfor botheditableProjectsandallProjects. Line 200 callseditableProjects.includes(...)and line 204 accessesallProjects[0], both of which will throwTypeErroronundefined.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:useK8sWatchResourceerror return value is discarded.
useK8sWatchResourcereturns[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
useEditableProjectscan 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
dashboardsfetched fromusePerses(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:useUsernamedirectly 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).
| "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", |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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" /> | |||
There was a problem hiding this comment.
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.
| <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.
| <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" | ||
| /> |
There was a problem hiding this comment.
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.
| <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.
| 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" | ||
| /> |
There was a problem hiding this comment.
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.
| const handleModalToggle = () => { | ||
| setIsModalOpen(!isModalOpen); | ||
| setIsDropdownOpen(false); | ||
| if (isModalOpen) { | ||
| setDashboardName(''); | ||
| setFormErrors({}); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
| export interface PersesPermission { | ||
| scopes: string; | ||
| actions: string[]; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "\.scopes" --type=ts -C3Repository: openshift/monitoring-plugin
Length of output: 800
🏁 Script executed:
cat -n web/src/components/dashboards/perses/hooks/useEditableProjects.ts | head -60Repository: openshift/monitoring-plugin
Length of output: 2635
🏁 Script executed:
cat -n web/src/components/dashboards/perses/perses-client.tsRepository: openshift/monitoring-plugin
Length of output: 4468
🏁 Script executed:
fd -t f "test\|spec" --type=ts web/src/components/dashboards/perses/ | head -20Repository: openshift/monitoring-plugin
Length of output: 240
🏁 Script executed:
rg -n "scopes" --type=ts web/src/components/dashboards/perses/ -B2 -A2Repository: 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 -5Repository: openshift/monitoring-plugin
Length of output: 460
🏁 Script executed:
rg -i "scope" web/src/components/dashboards/perses/ -A1 -B1Repository: openshift/monitoring-plugin
Length of output: 650
🏁 Script executed:
rg "@perses-dev/core" web/ -lRepository: 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-listRepository: openshift/monitoring-plugin
Length of output: 460
🏁 Script executed:
rg "permission" web/src/components/dashboards/perses/ -i -C2 | grep -i "scope\|comment" | head -20Repository: 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 -30Repository: 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.
|
@zhuje: all tests passed! Full PR test history. Your PR dashboard. DetailsInstructions 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. |
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