From abf60d42c340aa3f98c6ebc6061d491e67d2c72a Mon Sep 17 00:00:00 2001 From: PeterYurkovich Date: Tue, 25 Aug 2026 16:16:25 -0400 Subject: [PATCH 1/2] feat: remove perses /permissions api call for direct RBAC checks --- web/locales/en/plugin__monitoring-plugin.json | 416 +++++++++--------- .../components/DashboardActionModals.tsx | 95 ++-- .../components/DashboardActionsMenu.tsx | 19 +- .../components/DashboardCreateDialog.tsx | 131 +++--- .../components/DashboardDialogHelpers.tsx | 123 +++--- .../components/DashboardImportDialog.tsx | 190 ++++---- .../components/project/ProjectBar.tsx | 33 -- .../components/project/ProjectDropdown.tsx | 265 ----------- .../components/project/ProjectMenuToggle.tsx | 97 ---- .../components/project/utils.ts | 14 - .../hooks/useDashboardsData.ts | 2 +- .../hooks/useEditableProjects.ts | 116 ----- .../hooks/usePersesDashboardAccess.ts | 15 + .../hooks/usePersesEditPermissions.ts | 29 -- .../dashboard-list-page/DashboardList.tsx | 68 ++- .../DashboardListFrame.tsx | 17 +- .../pages/dashboard-page/DashboardFrame.tsx | 13 +- .../pages/dashboard-page/DashboardToolbar.tsx | 37 +- .../perses-dashboards/utils/perses-client.ts | 41 -- web/src/shared/constants/data-test.ts | 3 + 20 files changed, 552 insertions(+), 1172 deletions(-) delete mode 100644 web/src/features/perses-dashboards/components/project/ProjectBar.tsx delete mode 100644 web/src/features/perses-dashboards/components/project/ProjectDropdown.tsx delete mode 100644 web/src/features/perses-dashboards/components/project/ProjectMenuToggle.tsx delete mode 100644 web/src/features/perses-dashboards/components/project/utils.ts delete mode 100644 web/src/features/perses-dashboards/hooks/useEditableProjects.ts create mode 100644 web/src/features/perses-dashboards/hooks/usePersesDashboardAccess.ts delete mode 100644 web/src/features/perses-dashboards/hooks/usePersesEditPermissions.ts diff --git a/web/locales/en/plugin__monitoring-plugin.json b/web/locales/en/plugin__monitoring-plugin.json index 49b7a3101..369835427 100644 --- a/web/locales/en/plugin__monitoring-plugin.json +++ b/web/locales/en/plugin__monitoring-plugin.json @@ -1,72 +1,10 @@ { - "Recreate silence": "Recreate silence", - "Edit silence": "Edit silence", - "Expire silence": "Expire silence", - "Starts": "Starts", - "Ends": "Ends", - "Expired": "Expired", - "Name": "Name", - "Firing alerts": "Firing alerts", - "State": "State", - "Creator": "Creator", - "Alerts": "Alerts", - "Silences": "Silences", - "Alerting rules": "Alerting rules", - "Alerting": "Alerting", - "Severity": "Severity", - "Namespace": "Namespace", - "Source": "Source", - "Cluster": "Cluster", - "Silence alert": "Silence alert", - "View AI Investigation": "View AI Investigation", - "Loading investigations...": "Loading investigations...", - "User": "User", - "Platform": "Platform", - "Export as CSV": "Export as CSV", - "Description": "Description", - "Active since": "Active since", - "Value": "Value", - "{{name}} details": "{{name}} details", - "Alerting rule details": "Alerting rule details", - "Summary": "Summary", - "Message": "Message", - "Runbook": "Runbook", - "For": "For", - "Expression": "Expression", - "Labels": "Labels", - "Active alerts": "Active alerts", - "None found": "None found", - "Filter by Name": "Filter by Name", - "Alert State": "Alert State", - "Filter by State": "Filter by State", + "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.", "Firing": "Firing", "Pending": "Pending", "Silenced": "Silenced", "Not Firing": "Not Firing", - "Filter by Severity": "Filter by Severity", - "Critical": "Critical", - "Warning": "Warning", - "Info": "Info", - "None": "None", - "Filter by Source": "Filter by Source", - "Label": "Label", - "Filter by Label": "Filter by Label", - "Alert Rules Table": "Alert Rules Table", - "No alerting rules found": "No alerting rules found", - "Alert details": "Alert details", - "Alerting rule": "Alerting rule", - "Silenced by": "Silenced by", - "Pending: ": "Pending: ", - "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.", - "Firing: ": "Firing: ", - "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.": "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.", - "Silenced: ": "Silenced: ", - "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.", - "Alert Name": "Alert Name", - "Total": "Total", - "Filter by Cluster": "Filter by Cluster", - "No alerts found": "No alerts found", - "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.", + "Ends": "Ends", "Since": "Since", "Inspect": "Inspect", "The condition that triggered the alert could have a critical impact. The alert requires immediate attention when fired and is typically paged to an individual or to a critical response team.": "The condition that triggered the alert could have a critical impact. The alert requires immediate attention when fired and is typically paged to an individual or to a critical response team.", @@ -78,9 +16,22 @@ "Platform-level alerts relate only to OpenShift namespaces. OpenShift namespaces provide core OpenShift functionality.": "Platform-level alerts relate only to OpenShift namespaces. OpenShift namespaces provide core OpenShift functionality.", "User: ": "User: ", "User workload alerts relate to user-defined namespaces. These alerts are user-created and are customizable. User workload monitoring can be enabled post-installation to provide observability into your own services.": "User workload alerts relate to user-defined namespaces. These alerts are user-created and are customizable. User workload monitoring can be enabled post-installation to provide observability into your own services.", - "Create silence": "Create silence", - "Overwriting current silence": "Overwriting current silence", - "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.", + "Platform": "Platform", + "User": "User", + "Severity": "Severity", + "Critical": "Critical", + "Warning": "Warning", + "Info": "Info", + "None": "None", + "Recreate silence": "Recreate silence", + "Edit silence": "Edit silence", + "Expire silence": "Expire silence", + "Starts": "Starts", + "Expired": "Expired", + "Name": "Name", + "Firing alerts": "Firing alerts", + "State": "State", + "Creator": "Creator", "Invalid date / time": "Invalid date / time", "Datetime": "Datetime", "Select the negative matcher option to update the label value to a not equals matcher.": "Select the negative matcher option to update the label value to a not equals matcher.", @@ -110,6 +61,7 @@ "regular expression": "regular expression", "Label name": "Label name", "Label value": "Label value", + "Value": "Value", "Select all that apply:": "Select all that apply:", "RegEx": "RegEx", "Negative matcher": "Negative matcher", @@ -119,6 +71,65 @@ "Comment": "Comment", "Silence": "Silence", "Cancel": "Cancel", + "Active": "Active", + "Expire Silence": "Expire Silence", + "Are you sure you want to expire this silence?": "Are you sure you want to expire this silence?", + "An error occurred": "An error occurred", + "Source": "Source", + "Filter by Name": "Filter by Name", + "Alert State": "Alert State", + "Filter by State": "Filter by State", + "Filter by Severity": "Filter by Severity", + "Filter by Source": "Filter by Source", + "Label": "Label", + "Filter by Label": "Filter by Label", + "Alerting": "Alerting", + "Alert Rules Table": "Alert Rules Table", + "No alerting rules found": "No alerting rules found", + "Alerts": "Alerts", + "Silences": "Silences", + "Alerting rules": "Alerting rules", + "Description": "Description", + "Active since": "Active since", + "Silence alert": "Silence alert", + "{{name}} details": "{{name}} details", + "Alerting rule details": "Alerting rule details", + "Summary": "Summary", + "Message": "Message", + "Runbook": "Runbook", + "For": "For", + "Expression": "Expression", + "Labels": "Labels", + "Active alerts": "Active alerts", + "None found": "None found", + "Namespace": "Namespace", + "Cluster": "Cluster", + "Alert Name": "Alert Name", + "Total": "Total", + "Filter by Cluster": "Filter by Cluster", + "No alerts found": "No alerts found", + "View AI Investigation": "View AI Investigation", + "Loading investigations...": "Loading investigations...", + "Export as CSV": "Export as CSV", + "Alert details": "Alert details", + "Alerting rule": "Alerting rule", + "Silenced by": "Silenced by", + "Pending: ": "Pending: ", + "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.", + "Firing: ": "Firing: ", + "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.": "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.", + "Silenced: ": "Silenced: ", + "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.", + "Create silence": "Create silence", + "Overwriting current silence": "Overwriting current silence", + "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.", + "Silence State": "Silence State", + "Silences Table": "Silences Table", + "Error loading silences from Alertmanager. Alertmanager may be unavailable.": "Error loading silences from Alertmanager. Alertmanager may be unavailable.", + "No silences found": "No silences found", + "Error": "Error", + "Expire {{count}} silence_one": "Expire {{count}} silence", + "Expire {{count}} silence_other": "Expire {{count}} silences", "Silence details": "Silence details", "Actions": "Actions", "Matchers": "Matchers", @@ -129,41 +140,49 @@ "Created by": "Created by", "No Alerts found": "No Alerts found", "View alerting rule": "View alerting rule", - "Silence State": "Silence State", - "Active": "Active", - "Silences Table": "Silences Table", - "Error loading silences from Alertmanager. Alertmanager may be unavailable.": "Error loading silences from Alertmanager. Alertmanager may be unavailable.", - "No silences found": "No silences found", - "Error": "Error", - "Expire {{count}} silence_one": "Expire {{count}} silence", - "Expire {{count}} silence_other": "Expire {{count}} silences", - "Expire Silence": "Expire Silence", - "Are you sure you want to expire this silence?": "Are you sure you want to expire this silence?", - "An error occurred": "An error occurred", - "Restricted access": "Restricted access", - "You don't have access to this section due to cluster policy": "You don't have access to this section due to cluster policy", - "Error details": "Error details", - "No {{label}} found": "No {{label}} found", - "Not found": "Not found", - "Try again": "Try again", - "Error loading {{label}}": "Error loading {{label}}", - "404: Not Found": "404: Not Found", - "{{labels}} content is not available in the catalog at this time due to loading failures.": "{{labels}} content is not available in the catalog at this time due to loading failures.", - "No datapoints found.": "No datapoints found.", - "Filter {{label}}...": "Filter {{label}}...", - "Create new option \"{{option}}\"": "Create new option \"{{option}}\"", - "Filter options": "Filter options", - "Clear input value": "Clear input value", - "No results found": "No results found", + "Alerts Timeline": "Alerts Timeline", + "To view alerts, select an incident from the chart above or from the filters.": "To view alerts, select an incident from the chart above or from the filters.", + "Component": "Component", + "Start": "Start", + "End": "End", + "Resolved": "Resolved", + "Unknown": "Unknown", + "Incidents Timeline": "Incidents Timeline", + "ID": "ID", + "Component(s)": "Component(s)", + "Alert": "Alert", + "No incident selected.": "No incident selected.", + "The incident is critical.": "The incident is critical.", + "The incident might lead to critical.": "The incident might lead to critical.", + "Informative": "Informative", + "The incident is not critical.": "The incident is not critical.", + "The incident is currently firing.": "The incident is currently firing.", + "The incident is not currently firing.": "The incident is not currently firing.", + "Incidents": "Incidents", + "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.": "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.", + "Clear all filters": "Clear all filters", + "Filter type selection": "Filter type selection", + "Incident ID": "Incident ID", + "Severity filters": "Severity filters", + "State filters": "State filters", + "Incident ID filters": "Incident ID filters", + "Last 1 day": "Last 1 day", + "Last 3 days": "Last 3 days", + "Last 7 days": "Last 7 days", + "Last 15 days": "Last 15 days", + "Show graph": "Show graph", + "Hide graph": "Hide graph", + "component": "component", + "components": "components", "Custom time range": "Custom time range", "From": "From", "To": "To", "Save": "Save", "Dashboards": "Dashboards", "Metrics dashboards": "Metrics dashboards", - "Error Loading Dashboards": "Error Loading Dashboards", "Loading": "Loading", "Error loading card": "Error loading card", + "Dashboard": "Dashboard", "Error loading options": "Error loading options", "Select a dashboard from the dropdown": "Select a dashboard from the dropdown", "panel.styles attribute not found": "panel.styles attribute not found", @@ -179,6 +198,47 @@ "Time range": "Time range", "Refresh interval": "Refresh interval", "Could not parse JSON data for dashboard \"{{dashboard}}\"": "Could not parse JSON data for dashboard \"{{dashboard}}\"", + "Error Loading Dashboards": "Error Loading Dashboards", + "Expression (press Shift+Enter for newlines)": "Expression (press Shift+Enter for newlines)", + "Access restricted.": "Access restricted.", + "Failed to load metrics list.": "Failed to load metrics list.", + "Clear query": "Clear query", + "Queries": "Queries", + "Select query": "Select query", + "Add query": "Add query", + "Collapse all query tables": "Collapse all query tables", + "Expand all query tables": "Expand all query tables", + "Delete all queries": "Delete all queries", + "Show series": "Show series", + "Hide series": "Hide series", + "Disable query": "Disable query", + "Enable query": "Enable query", + "Hide all series": "Hide all series", + "Show all series": "Show all series", + "Query must be enabled": "Query must be enabled", + "Delete query": "Delete query", + "Duplicate query": "Duplicate query", + "Error loading values": "Error loading values", + "No datapoints found.": "No datapoints found.", + "Unselect all": "Unselect all", + "Select all": "Select all", + "Error loading custom data source": "Error loading custom data source", + "An error occurred while loading the custom data source.": "An error occurred while loading the custom data source.", + "No query entered": "No query entered", + "Enter a query in the box below to explore metrics for this cluster.": "Enter a query in the box below to explore metrics for this cluster.", + "Insert example query": "Insert example query", + "Run queries": "Run queries", + "Bytes Binary (KiB, MiB)": "Bytes Binary (KiB, MiB)", + "Bytes Decimal (kb, MB)": "Bytes Decimal (kb, MB)", + "Bytes Binary Per Second (KiB/s, MiB/s)": "Bytes Binary Per Second (KiB/s, MiB/s)", + "Bytes Decimal Per Second (kB/s, MB/s)": "Bytes Decimal Per Second (kB/s, MB/s)", + "Packets Per Second": "Packets Per Second", + "Miliseconds": "Miliseconds", + "Seconds": "Seconds", + "Percentage": "Percentage", + "No Units": "No Units", + "Metrics": "Metrics", + "This dropdown only formats results.": "This dropdown only formats results.", "Rename Dashboard": "Rename Dashboard", "Dashboard name": "Dashboard name", "Renaming...": "Renaming...", @@ -187,8 +247,6 @@ "Failed to create project \"{{project}}\". Please try again.": "Failed to create project \"{{project}}\". Please try again.", "Error creating project: {{error}}": "Error creating project: {{error}}", "Duplicate Dashboard": "Duplicate Dashboard", - "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", "No namespace found for \"{{filter}}\"": "No namespace found for \"{{filter}}\"", "Duplicate": "Duplicate", @@ -198,16 +256,15 @@ "? 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!", - "Checking permissions...": "Checking permissions...", "Create": "Create", "Dashboard actions": "Dashboard actions", "Import": "Import", - "To create dashboards, contact your cluster administrator for permission.": "To create dashboards, contact your cluster administrator for permission.", "Create Dashboard": "Create Dashboard", "my-new-dashboard": "my-new-dashboard", "Select project": "Select project", + "You do not have permission to create dashboards in this project.": "You do not have permission to create dashboards in this project.", + "You do not have permission to edit dashboards in this project.": "You do not have permission to edit dashboards in this project.", + "You do not have permission to delete dashboards in this project.": "You do not have permission to delete dashboards in this project.", "View and manage dashboards.": "View and manage dashboards.", "Unable to detect dashboard format. Please provide a valid Perses or Grafana dashboard.": "Unable to detect dashboard format. Please provide a valid Perses or Grafana dashboard.", "Invalid {{format}}: {{error}}": "Invalid {{format}}: {{error}}", @@ -229,38 +286,55 @@ "Perses dashboard detected.": "Perses dashboard detected.", "2. Select project": "2. Select project", "Importing...": "Importing...", + "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", + "useToast must be used within ToastProvider": "useToast must be used within ToastProvider", + "Project is required for fetching project dashboards": "Project is required for fetching project dashboards", + "Add To Dashboard": "Add To Dashboard", + "Add to dashboard": "Add to dashboard", + "Query: {{query}}": "Query: {{query}}", "Rename dashboard": "Rename dashboard", "Duplicate dashboard": "Duplicate dashboard", "Delete dashboard": "Delete dashboard", - "You don't have permissions for dashboard actions": "You don't have permissions for 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 results found": "No results found", "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", + "Loading...": "Loading...", "Edit": "Edit", - "You don't have permission to edit this dashboard": "You don't have permission to edit this dashboard", - "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", - "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", - "All Projects": "All Projects", - "useToast must be used within ToastProvider": "useToast must be used within ToastProvider", + "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!", + "Up": "Up", + "Down": "Down", + "Target details": "Target details", + "Targets": "Targets", + "Error loading service monitor data": "Error loading service monitor data", + "Error loading pod monitor data": "Error loading pod monitor data", + "Endpoint": "Endpoint", + "Last scrape": "Last scrape", + "Scrape failed": "Scrape failed", + "Status": "Status", + "Monitor": "Monitor", + "Text": "Text", + "Last Scrape": "Last Scrape", + "Scrape Duration": "Scrape Duration", + "Search by endpoint or namespace...": "Search by endpoint or namespace...", + "Filter by Status": "Filter by Status", + "Metrics targets": "Metrics targets", + "Error loading latest targets data": "Error loading latest targets data", + "Targets Table": "Targets Table", + "No metrics targets found": "No metrics targets found", "Refresh off": "Refresh off", "{{count}} second_one": "{{count}} second", "{{count}} second_other": "{{count}} seconds", @@ -270,82 +344,7 @@ "{{count}} hour_other": "{{count}} hours", "{{count}} day_one": "{{count}} day", "{{count}} day_other": "{{count}} days", - "Alerts Timeline": "Alerts Timeline", - "To view alerts, select an incident from the chart above or from the filters.": "To view alerts, select an incident from the chart above or from the filters.", - "Component": "Component", - "Start": "Start", - "End": "End", - "Resolved": "Resolved", - "Unknown": "Unknown", - "Incidents Timeline": "Incidents Timeline", - "ID": "ID", - "Component(s)": "Component(s)", - "Alert": "Alert", - "Incidents": "Incidents", - "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.": "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.", - "Filter type selection": "Filter type selection", - "Incident ID": "Incident ID", - "Severity filters": "Severity filters", - "State filters": "State filters", - "Incident ID filters": "Incident ID filters", - "Last 1 day": "Last 1 day", - "Last 3 days": "Last 3 days", - "Last 7 days": "Last 7 days", - "Last 15 days": "Last 15 days", - "Show graph": "Show graph", - "Hide graph": "Hide graph", - "No incident selected.": "No incident selected.", - "The incident is critical.": "The incident is critical.", - "The incident might lead to critical.": "The incident might lead to critical.", - "Informative": "Informative", - "The incident is not critical.": "The incident is not critical.", - "The incident is currently firing.": "The incident is currently firing.", - "The incident is not currently firing.": "The incident is not currently firing.", - "component": "component", - "components": "components", "No labels": "No labels", - "Expression (press Shift+Enter for newlines)": "Expression (press Shift+Enter for newlines)", - "Access restricted.": "Access restricted.", - "Failed to load metrics list.": "Failed to load metrics list.", - "Clear query": "Clear query", - "Queries": "Queries", - "Select query": "Select query", - "Add query": "Add query", - "Collapse all query tables": "Collapse all query tables", - "Expand all query tables": "Expand all query tables", - "Delete all queries": "Delete all queries", - "Show series": "Show series", - "Hide series": "Hide series", - "Disable query": "Disable query", - "Enable query": "Enable query", - "Hide all series": "Hide all series", - "Show all series": "Show all series", - "Query must be enabled": "Query must be enabled", - "Delete query": "Delete query", - "Duplicate query": "Duplicate query", - "Error loading values": "Error loading values", - "Unselect all": "Unselect all", - "Select all": "Select all", - "Error loading custom data source": "Error loading custom data source", - "An error occurred while loading the custom data source.": "An error occurred while loading the custom data source.", - "No query entered": "No query entered", - "Enter a query in the box below to explore metrics for this cluster.": "Enter a query in the box below to explore metrics for this cluster.", - "Insert example query": "Insert example query", - "Run queries": "Run queries", - "Bytes Binary (KiB, MiB)": "Bytes Binary (KiB, MiB)", - "Bytes Decimal (kb, MB)": "Bytes Decimal (kb, MB)", - "Bytes Binary Per Second (KiB/s, MiB/s)": "Bytes Binary Per Second (KiB/s, MiB/s)", - "Bytes Decimal Per Second (kB/s, MB/s)": "Bytes Decimal Per Second (kB/s, MB/s)", - "Packets Per Second": "Packets Per Second", - "Miliseconds": "Miliseconds", - "Seconds": "Seconds", - "Percentage": "Percentage", - "No Units": "No Units", - "Metrics": "Metrics", - "This dropdown only formats results.": "This dropdown only formats results.", - "Add To Dashboard": "Add To Dashboard", - "Add to dashboard": "Add to dashboard", - "Query: {{query}}": "Query: {{query}}", "graph timespan": "graph timespan", "Reset zoom": "Reset zoom", "Displaying with reduced resolution due to large dataset.": "Displaying with reduced resolution due to large dataset.", @@ -367,24 +366,17 @@ "Current page": "Current page", "Pagination": "Pagination", "of": "of", - "Up": "Up", - "Down": "Down", - "Target details": "Target details", - "Targets": "Targets", - "Error loading service monitor data": "Error loading service monitor data", - "Error loading pod monitor data": "Error loading pod monitor data", - "Endpoint": "Endpoint", - "Last scrape": "Last scrape", - "Scrape failed": "Scrape failed", - "Status": "Status", - "Monitor": "Monitor", - "Text": "Text", - "Last Scrape": "Last Scrape", - "Scrape Duration": "Scrape Duration", - "Search by endpoint or namespace...": "Search by endpoint or namespace...", - "Filter by Status": "Filter by Status", - "Metrics targets": "Metrics targets", - "Error loading latest targets data": "Error loading latest targets data", - "Targets Table": "Targets Table", - "No metrics targets found": "No metrics targets found" -} + "Restricted access": "Restricted access", + "You don't have access to this section due to cluster policy": "You don't have access to this section due to cluster policy", + "Error details": "Error details", + "No {{label}} found": "No {{label}} found", + "Not found": "Not found", + "Try again": "Try again", + "Error loading {{label}}": "Error loading {{label}}", + "404: Not Found": "404: Not Found", + "{{labels}} content is not available in the catalog at this time due to loading failures.": "{{labels}} content is not available in the catalog at this time due to loading failures.", + "Filter {{label}}...": "Filter {{label}}...", + "Create new option \"{{option}}\"": "Create new option \"{{option}}\"", + "Filter options": "Filter options", + "Clear input value": "Clear input value" +} \ No newline at end of file diff --git a/web/src/features/perses-dashboards/components/DashboardActionModals.tsx b/web/src/features/perses-dashboards/components/DashboardActionModals.tsx index 8a17590e5..009635c55 100644 --- a/web/src/features/perses-dashboards/components/DashboardActionModals.tsx +++ b/web/src/features/perses-dashboards/components/DashboardActionModals.tsx @@ -12,7 +12,6 @@ import { ModalFooter, ModalHeader, ModalVariant, - Spinner, Stack, StackItem, TextInput, @@ -20,17 +19,21 @@ import { } from '@patternfly/react-core'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; import { TypeaheadSelect, TypeaheadSelectOption } from '@patternfly/react-templates'; -import { t_global_font_weight_200, t_global_spacer_200 } from '@patternfly/react-tokens'; import { DashboardResource } from '@perses-dev/client'; import { getResourceExtendedDisplayName } from '@perses-dev/components'; -import { CSSProperties, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { Controller, FormProvider, SubmitHandler, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; +import { + DashboardDeniedHelperText, + formGroupStyle, + LabelSpacer, + useDashboardProjects, +} from '@/features/perses-dashboards/components/DashboardDialogHelpers'; import { useToast } from '@/features/perses-dashboards/components/ToastProvider'; -import { useEditableProjects } from '@/features/perses-dashboards/hooks/useEditableProjects'; -import { usePerses } from '@/features/perses-dashboards/hooks/usePerses'; +import { usePersesDashboardAccess } from '@/features/perses-dashboards/hooks/usePersesDashboardAccess'; import { createDashboardDialogValidationSchema, CreateDashboardValidationType, @@ -47,14 +50,6 @@ import { import { generateMetadataName } from '@/features/perses-dashboards/utils/dashboard-utils'; import { getDashboardUrl, usePerspective } from '@/shared/hooks/usePerspective'; -export const formGroupStyle = { - fontWeight: t_global_font_weight_200.value, -} as CSSProperties; - -export const LabelSpacer = () => { - return
; -}; - interface ActionModalProps { dashboard: DashboardResource; isOpen: boolean; @@ -73,6 +68,11 @@ export const RenameActionModal = ({ dashboard, isOpen, onClose }: ActionModalPro }); const updateDashboardMutation = useUpdateDashboardMutation(); + const [canUpdate, updateChecking] = usePersesDashboardAccess( + 'update', + dashboard?.metadata?.project ?? null, + ); + const updateDenied = !updateChecking && !canUpdate; if (!dashboard) { return null; @@ -154,6 +154,7 @@ export const RenameActionModal = ({ dashboard, isOpen, onClose }: ActionModalPro )} /> + - - - - - ); -}; - -/* ****************************************** */ - -export const Filter: FC<{ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - filterRef: Ref; - onFilterChange: (filterText: string) => void; - filterText: string; -}> = ({ filterText, filterRef, onFilterChange }) => { - const { t } = useTranslation(process.env.I18N_NAMESPACE); - return ( - - - onFilterChange(value)} - ref={filterRef} - /> - - - ); -}; - -/* ****************************************** */ - -export const ProjectGroup: FC<{ - options: { key: string; title: string }[]; - selectedKey: string; -}> = ({ options, selectedKey }) => { - const { t } = useTranslation(process.env.I18N_NAMESPACE); - - return options.length === 0 ? null : ( - <> - - - - {options.map((option) => { - return ( - - {option.title} - - ); - })} - - - - ); -}; - -/* ****************************************** */ - -const ProjectMenu: FC<{ - setOpen: (isOpen: boolean) => void; - onSelect: (event: ReactMouseEvent, itemId: string) => void; - selected?: string; - menuRef: MutableRefObject; -}> = ({ setOpen, onSelect, selected, menuRef }) => { - const filterRef = useRef(null); - const { t } = useTranslation(process.env.I18N_NAMESPACE); - - const [filterText, setFilterText] = useState(''); - - const { allProjects } = useEditableProjects(); - - const optionItems = useMemo(() => { - const items = - allProjects?.map((projectName) => { - return { title: projectName, key: projectName }; - }) || []; - - items.sort((a, b) => alphanumericCompare(a.title, b.title)); - items.unshift({ title: t('All Projects'), key: ALL_NAMESPACES_KEY }); - if (selected && !items.some((option) => option.key === selected)) { - items.unshift({ title: selected, key: selected }); // Add current project if it isn't included - } - - return items; - }, [allProjects, selected, t]); - - const isOptionShown = useCallback( - (option) => { - return fuzzysearch(filterText.toLowerCase(), option.title.toLowerCase()); - }, - [filterText], - ); - - const { filteredOptions } = useMemo( - () => - optionItems.reduce( - (filtered, option) => { - if (isOptionShown(option)) { - filtered.filteredOptions.push(option); - } - return filtered; - }, - { filteredOptions: [] }, - ), - [isOptionShown, optionItems], - ); - - return ( - { - setOpen(false); - onSelect(event, itemId); - }} - activeItemId={selected} - data-test="project-dropdown-menu" - isScrollable - > - - - {filteredOptions.length === 0 ? ( - { - event.preventDefault(); - event.stopPropagation(); - setFilterText(''); - filterRef.current?.focus(); - }} - /> - ) : null} - - - - ); -}; - -/* ****************************************** */ - -const ProjectDropdown: FC = ({ disabled, onSelect, selected, shortCut }) => { - const { t } = useTranslation(process.env.I18N_NAMESPACE); - const menuRef = useRef(null); - const [isOpen, setOpen] = useState(false); - const { allProjects, permissionsLoading, permissionsError } = useEditableProjects(); - - const menuProps = { - setOpen, - onSelect, - selected, - menuRef, - }; - - let title = t('All Projects'); - // While loading permissions, or if there is a permission error fallback to the "selected" value - // 'All Projects' is the user friendly ALL_NAMESPACES_KEY - if ( - selected && - (allProjects?.includes(selected) || permissionsLoading || !!permissionsError) && - selected !== ALL_NAMESPACES_KEY - ) { - title = selected; - } - - const toggle = ( - } - menuRef={menuRef} - isOpen={isOpen} - title={`${t('Project')}: ${title}`} - onToggle={(menuState) => { - setOpen(menuState); - }} - shortCut={shortCut} - /> - ); - - return ( -
- {permissionsLoading ? ( - - {toggle} - - ) : permissionsError ? ( - - {toggle} - - ) : ( - toggle - )} -
- ); -}; - -type ProjectDropdownProps = { - disabled?: boolean; - onSelect?: (event: ReactMouseEvent | ChangeEvent, value: string) => void; - shortCut?: string; - selected?: string; -}; - -export default ProjectDropdown; diff --git a/web/src/features/perses-dashboards/components/project/ProjectMenuToggle.tsx b/web/src/features/perses-dashboards/components/project/ProjectMenuToggle.tsx deleted file mode 100644 index 6c073bf84..000000000 --- a/web/src/features/perses-dashboards/components/project/ProjectMenuToggle.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { MenuToggle, Popper } from '@patternfly/react-core'; -import classNames from 'classnames'; -import type { ReactElement, RefObject } from 'react'; -import { useEffect, useRef } from 'react'; - -const ProjectMenuToggle = (props: { - disabled: boolean; - menu: ReactElement; - menuRef: RefObject; - isOpen: boolean; - shortCut?: string; - title: string; - onToggle: (state: boolean) => void; - className?: string; -}) => { - const { menu, isOpen, shortCut, title, onToggle, disabled, menuRef, className } = props; - - const toggleRef = useRef(null); - const containerRef = useRef(null); - - const handleMenuKeys = (event) => { - if ( - shortCut && - event.key === shortCut && - event.target.nodeName !== 'INPUT' && - event.target.nodeName !== 'TEXTAREA' && - event.target.role !== 'textbox' && - event.target.role !== 'code' - ) { - onToggle(true); - event.stopPropagation(); - event.preventDefault(); - } - - if (menuRef.current) { - if (event.key === 'Escape') { - onToggle(false); - toggleRef.current.focus(); - } - if (!menuRef.current?.contains(event.target) && event.key === 'Tab') { - onToggle(false); - } - } - }; - - const handleMenuClick = (event) => { - if ( - menuRef.current && - !menuRef.current?.contains(event.target) && - // Checking to see if user clicked on a favorite icon. This is needed because - // if unfavoriting a item, PF removes the item from the DOM before - // the click event is registered - !event.target.closest?.('.pf-m-favorite') && - !toggleRef.current.contains(event.target) - ) { - onToggle(false); - } - }; - - useEffect(() => { - window.addEventListener('keyup', handleMenuKeys); - window.addEventListener('click', handleMenuClick); - return () => { - window.removeEventListener('keyup', handleMenuKeys); - window.removeEventListener('click', handleMenuClick); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // This needs to be run only on component mount/unmount - - const toggle = ( - onToggle(!isOpen)} - isExpanded={isOpen} - disabled={disabled} - className={classNames('co-namespace-dropdown__menu-toggle', className)} - > - {title} - - ); - - return ( -
- -
- ); -}; - -export default ProjectMenuToggle; diff --git a/web/src/features/perses-dashboards/components/project/utils.ts b/web/src/features/perses-dashboards/components/project/utils.ts deleted file mode 100644 index 4a0352ea7..000000000 --- a/web/src/features/perses-dashboards/components/project/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const alphanumericCompare = (a: string, b: string): number => { - const safeA = a || ''; - const safeB = b || ''; - - return safeA.localeCompare(safeB, undefined, { - numeric: true, - sensitivity: 'base', - }); -}; - -// Common shortcuts than span pages. -export const KEYBOARD_SHORTCUTS = Object.freeze({ - focusNamespaceDropdown: 'n', -}); diff --git a/web/src/features/perses-dashboards/hooks/useDashboardsData.ts b/web/src/features/perses-dashboards/hooks/useDashboardsData.ts index ca7532b9f..36372146d 100644 --- a/web/src/features/perses-dashboards/hooks/useDashboardsData.ts +++ b/web/src/features/perses-dashboards/hooks/useDashboardsData.ts @@ -12,7 +12,7 @@ import { useBoolean } from '@/shared/hooks/useBoolean'; import { getDashboardUrl, usePerspective } from '@/shared/hooks/usePerspective'; import { ALL_NAMESPACES_KEY } from '@/shared/utils/utils'; -// This hook syncs with mutliple external API's, redux, and URL state. Its a lot, but needs to all +// This hook syncs with multiple external API's, redux, and URL state. Its a lot, but needs to all // be in a single location export const useDashboardsData = () => { const navigate = useNavigate(); diff --git a/web/src/features/perses-dashboards/hooks/useEditableProjects.ts b/web/src/features/perses-dashboards/hooks/useEditableProjects.ts deleted file mode 100644 index c610c7ade..000000000 --- a/web/src/features/perses-dashboards/hooks/useEditableProjects.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { K8sResourceKind } from '@openshift-console/dynamic-plugin-sdk'; -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; - -import { useOcpProjects } from '@/features/perses-dashboards/hooks/useOcpProjects'; -import { - PersesUserPermissions, - useFetchPersesPermissions, -} from '@/features/perses-dashboards/utils/perses-client'; - -interface Projects { - editableProjects: string[] | undefined; - allProjects: string[] | undefined; -} - -const useUsername = (): string => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getUser = (state: any) => state.sdkCore?.user; - const user = useSelector(getUser); - return user?.metadata?.name || user?.username; -}; - -const combinePersesAndOcpProjects = ( - persesUserPermissions: PersesUserPermissions, - ocpProjects: K8sResourceKind[], -): string[] => { - const persesProjectNames = Object.keys(persesUserPermissions).filter((name) => name !== '*'); - const allAvailableProjects = new Set([...persesProjectNames]); - ocpProjects.forEach((project) => { - if (project.metadata?.name) { - allAvailableProjects.add(project.metadata.name); - } - }); - return Array.from(allAvailableProjects); -}; - -const getEditableProjects = ( - persesUserPermissions: PersesUserPermissions, - allAvailableProjects: string[], -): string[] => { - const editableProjectNames = new Set(); - 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) { - if (projectName === '*') { - allAvailableProjects.forEach((p) => editableProjectNames.add(p)); - } else { - editableProjectNames.add(projectName); - } - } - }); - return Array.from(editableProjectNames); -}; - -export const useEditableProjects = () => { - const username = useUsername(); - const { ocpProjects } = useOcpProjects(); - - const { persesUserPermissions, persesPermissionsLoading, persesPermissionsError } = - useFetchPersesPermissions(username); - - const { editableProjects, allProjects }: Projects = useMemo(() => { - if (persesPermissionsLoading) { - return { - editableProjects: undefined, - allProjects: undefined, - }; - } - if (!persesUserPermissions) { - return { - editableProjects: undefined, - allProjects: undefined, - }; - } - if (persesPermissionsError) { - return { - editableProjects: undefined, - allProjects: undefined, - }; - } - - const allAvailableProjects = combinePersesAndOcpProjects(persesUserPermissions, ocpProjects); - const editableProjectNames = getEditableProjects(persesUserPermissions, allAvailableProjects); - - // Sort projects alphabetically - const sortedEditableProjects = editableProjectNames.sort((a, b) => a.localeCompare(b)); - const sortedProjects = allAvailableProjects.sort((a, b) => a.localeCompare(b)); - - return { - editableProjects: sortedEditableProjects, - allProjects: sortedProjects, - }; - }, [persesPermissionsLoading, persesUserPermissions, persesPermissionsError, ocpProjects]); - - const hasEditableProject = useMemo(() => { - return editableProjects ? editableProjects.length > 0 : false; - }, [editableProjects]); - - return { - editableProjects, - allProjects, - hasEditableProject, - permissionsLoading: persesPermissionsLoading, - permissionsError: persesPermissionsError, - }; -}; diff --git a/web/src/features/perses-dashboards/hooks/usePersesDashboardAccess.ts b/web/src/features/perses-dashboards/hooks/usePersesDashboardAccess.ts new file mode 100644 index 000000000..7143e54b4 --- /dev/null +++ b/web/src/features/perses-dashboards/hooks/usePersesDashboardAccess.ts @@ -0,0 +1,15 @@ +import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; + +export type DashboardVerb = 'create' | 'update' | 'delete'; + +export const usePersesDashboardAccess = ( + verb: DashboardVerb, + namespace: string | null = null, +): [boolean, boolean] => { + return useAccessReview({ + group: 'perses.dev', + resource: 'persesdashboards', + verb, + namespace, + }); +}; diff --git a/web/src/features/perses-dashboards/hooks/usePersesEditPermissions.ts b/web/src/features/perses-dashboards/hooks/usePersesEditPermissions.ts deleted file mode 100644 index acfd8a875..000000000 --- a/web/src/features/perses-dashboards/hooks/usePersesEditPermissions.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; - -export const usePersesEditPermissions = (namespace: string | null = null) => { - const [canCreate, createLoading] = useAccessReview({ - group: 'perses.dev', - resource: 'persesdashboards', - verb: 'create', - namespace, - }); - - const [canUpdate, updateLoading] = useAccessReview({ - group: 'perses.dev', - resource: 'persesdashboards', - verb: 'update', - namespace, - }); - - const [canDelete, deleteLoading] = useAccessReview({ - group: 'perses.dev', - resource: 'persesdashboards', - verb: 'delete', - namespace, - }); - - const loading = createLoading || updateLoading || deleteLoading; - const canEdit = canUpdate && canCreate && canDelete; - - return { canEdit, loading }; -}; diff --git a/web/src/features/perses-dashboards/pages/dashboard-list-page/DashboardList.tsx b/web/src/features/perses-dashboards/pages/dashboard-list-page/DashboardList.tsx index d52321ccc..9b0b055e5 100644 --- a/web/src/features/perses-dashboards/pages/dashboard-list-page/DashboardList.tsx +++ b/web/src/features/perses-dashboards/pages/dashboard-list-page/DashboardList.tsx @@ -7,7 +7,6 @@ import { FlexItem, Label, Title, - Tooltip, } from '@patternfly/react-core'; import DataView from '@patternfly/react-data-view/dist/dynamic/DataView'; import { @@ -28,8 +27,7 @@ import { RenameActionModal, } from '@/features/perses-dashboards/components/DashboardActionModals'; import { useDashboardsData } from '@/features/perses-dashboards/hooks/useDashboardsData'; -import { useEditableProjects } from '@/features/perses-dashboards/hooks/useEditableProjects'; -import { usePersesEditPermissions } from '@/features/perses-dashboards/hooks/usePersesEditPermissions'; +import { usePersesDashboardAccess } from '@/features/perses-dashboards/hooks/usePersesDashboardAccess'; import { DashboardListFrame } from '@/features/perses-dashboards/pages/dashboard-list-page/DashboardListFrame'; import { useTableColumns } from '@/shared/components/table/hooks/useTableColumns'; import { rowFilter, useTableFilters } from '@/shared/components/table/hooks/useTableFilters'; @@ -54,26 +52,28 @@ const DashboardActionsCell = memo( onRename, onDuplicate, onDelete, - emptyActions, }: { project: string; dashboard: DashboardResource; onRename: (dashboard: DashboardResource) => void; onDuplicate: (dashboard: DashboardResource) => void; onDelete: (dashboard: DashboardResource) => void; - emptyActions: { title: string; onClick: () => void }[]; }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); - const { permissionsLoading } = useEditableProjects(); - const { canEdit } = usePersesEditPermissions(project); - const disabled = !canEdit; + const [canUpdate, updateChecking] = usePersesDashboardAccess('update', project); + const [canDelete, deleteChecking] = usePersesDashboardAccess('delete', project); const rowSpecificActions = useMemo( () => [ { title: t('Rename dashboard'), onClick: () => onRename(dashboard), + isAriaDisabled: updateChecking || !canUpdate, + tooltipProps: + !updateChecking && !canUpdate + ? { content: t('You do not have permission to edit dashboards in this project.') } + : undefined, }, { title: t('Duplicate dashboard'), @@ -82,31 +82,27 @@ const DashboardActionsCell = memo( { title: t('Delete dashboard'), onClick: () => onDelete(dashboard), + isAriaDisabled: deleteChecking || !canDelete, + tooltipProps: + !deleteChecking && !canDelete + ? { content: t('You do not have permission to delete dashboards in this project.') } + : undefined, }, ], - [dashboard, onRename, onDuplicate, onDelete, t], + [ + dashboard, + onRename, + onDuplicate, + onDelete, + t, + canUpdate, + updateChecking, + canDelete, + deleteChecking, + ], ); - if (disabled) { - return ( - -
- -
-
- ); - } - if (permissionsLoading) { - return ( - -
- -
-
- ); - } - - return ; + return ; }, ); @@ -309,16 +305,6 @@ const DashboardsTable: FC = ({ setTargetedDashboard(undefined); }, []); - const emptyRowActions = useMemo( - () => [ - { - title: t("You don't have permissions for dashboard actions"), - onClick: () => {}, - }, - ], - [t], - ); - const pageRows: DataViewTr[] = useMemo(() => { return sortedAndFilteredData .slice((page - 1) * perPage, (page - 1) * perPage + perPage) @@ -336,7 +322,6 @@ const DashboardsTable: FC = ({ onRename={handleRenameModalOpen} onDuplicate={handleDuplicateModalOpen} onDelete={handleDeleteModalOpen} - emptyActions={emptyRowActions} /> ), props: { isActionCell: true }, @@ -346,7 +331,6 @@ const DashboardsTable: FC = ({ sortedAndFilteredData, page, perPage, - emptyRowActions, handleRenameModalOpen, handleDuplicateModalOpen, handleDeleteModalOpen, @@ -466,7 +450,7 @@ export const DashboardList: FC = () => { const { activeProject, persesDashboards, combinedInitialLoad } = useDashboardsData(); return ( - + = ({ activeProject, children }) => { +export const DashboardListFrame: FC = ({ children }) => { + const { perspective } = usePerspective(); + const navigate = useNavigate(); + return ( <> - + { + const url = `${getDashboardsListUrl(perspective)}`; + navigate(url); + }} + /> {children} ); diff --git a/web/src/features/perses-dashboards/pages/dashboard-page/DashboardFrame.tsx b/web/src/features/perses-dashboards/pages/dashboard-page/DashboardFrame.tsx index 0c3513bc6..ec7eb139e 100644 --- a/web/src/features/perses-dashboards/pages/dashboard-page/DashboardFrame.tsx +++ b/web/src/features/perses-dashboards/pages/dashboard-page/DashboardFrame.tsx @@ -1,12 +1,14 @@ +import { NamespaceBar } from '@openshift-console/dynamic-plugin-sdk'; import { FC, ReactNode } from 'react'; +import { useNavigate } from 'react-router'; import { DashboardHeader } from '@/features/perses-dashboards/components/DashboardHeader'; import { DashboardEmptyState } from '@/features/perses-dashboards/components/emptystates/DashboardEmptyState'; import { PersesWrapper } from '@/features/perses-dashboards/components/PersesWrapper'; -import { ProjectBar } from '@/features/perses-dashboards/components/project/ProjectBar'; import { ToastProvider } from '@/features/perses-dashboards/components/ToastProvider'; import { PagePadding } from '@/features/perses-dashboards/pages/dashboard-page/DashboardPagePadding'; import type { DashboardMetadata } from '@/features/perses-dashboards/types/types'; +import { getDashboardsListUrl, usePerspective } from '@/shared/hooks/usePerspective'; interface DashboardFrameProps { activeProject: string | null; @@ -21,9 +23,16 @@ export const DashboardFrame: FC = ({ dashboardDisplayName, children, }) => { + const { perspective } = usePerspective(); + const navigate = useNavigate(); return ( <> - + { + const url = `${getDashboardsListUrl(perspective)}`; + navigate(url); + }} + /> {activeProjectDashboardsMetadata?.length === 0 ? ( diff --git a/web/src/features/perses-dashboards/pages/dashboard-page/DashboardToolbar.tsx b/web/src/features/perses-dashboards/pages/dashboard-page/DashboardToolbar.tsx index fa8a0599f..4a671f431 100644 --- a/web/src/features/perses-dashboards/pages/dashboard-page/DashboardToolbar.tsx +++ b/web/src/features/perses-dashboards/pages/dashboard-page/DashboardToolbar.tsx @@ -22,7 +22,7 @@ import { useTranslation } from 'react-i18next'; import { DashboardDropdown } from '@/features/perses-dashboards/components/DashboardDropdown'; import { useDashboardsData } from '@/features/perses-dashboards/hooks/useDashboardsData'; -import { usePersesEditPermissions } from '@/features/perses-dashboards/hooks/usePersesEditPermissions'; +import { usePersesDashboardAccess } from '@/features/perses-dashboards/hooks/usePersesDashboardAccess'; import { persesDashboardDataTestIDs } from '@/shared/constants/data-test'; export interface DashboardToolbarProps { @@ -38,40 +38,17 @@ export interface DashboardToolbarProps { } export interface EditButtonProps { - /** - * The label used inside the button. - */ - label?: string; - - /** - * Handler that puts the dashboard into editing mode. - */ onClick: () => void; - - /** - * Whether the button is disabled. - */ - disabled?: boolean; - - /** - * Tooltip text to show when button is disabled. - */ - disabledTooltip?: string; - - /** - * Whether permissions are still loading. - */ - loading?: boolean; - - /** - * The active project/namespace for permissions check. - */ activeProject?: string | null; } export const EditButton = ({ onClick, activeProject }: EditButtonProps): ReactElement => { const { t } = useTranslation(process.env.I18N_NAMESPACE); - const { canEdit, loading } = usePersesEditPermissions(activeProject); + const [canCreate, createLoading] = usePersesDashboardAccess('create', activeProject); + const [canUpdate, updateLoading] = usePersesDashboardAccess('update', activeProject); + const [canDelete, deleteLoading] = usePersesDashboardAccess('delete', activeProject); + const loading = createLoading || updateLoading || deleteLoading; + const canEdit = canUpdate && canCreate && canDelete; const disabled = !canEdit; const button = ( @@ -90,7 +67,7 @@ export const EditButton = ({ onClick, activeProject }: EditButtonProps): ReactEl if (disabled && !loading) { return ( - + {button} ); diff --git a/web/src/features/perses-dashboards/utils/perses-client.ts b/web/src/features/perses-dashboards/utils/perses-client.ts index c221f0a87..5c180a7e9 100644 --- a/web/src/features/perses-dashboards/utils/perses-client.ts +++ b/web/src/features/perses-dashboards/utils/perses-client.ts @@ -28,22 +28,6 @@ export const fetchPersesProjects = (): Promise => { return consoleFetchJSON(persesURL); }; -export interface PersesPermission { - scopes: string[]; - actions: string[]; -} - -export type PersesUserPermissions = { - [projectName: string]: PersesPermission[]; -}; - -export const fetchPersesUserPermissions = (username: string): Promise => { - const userPermissionsURL = `/api/v1/users/${encodeURIComponent(username)}/permissions`; - const persesURL = `${PERSES_PROXY_BASE_PATH}${userPermissionsURL}`; - - return consoleFetchJSON(persesURL); -}; - export const fetchPersesDashboard = async ( project: string, dashboardName: string, @@ -74,28 +58,3 @@ export const useFetchPersesDashboard = (project: string, dashboardName: string) persesDashboardLoading, }; }; - -export const useFetchPersesPermissions = (username: string) => { - const { - isLoading: persesPermissionsLoading, - error: persesPermissionsError, - data: persesUserPermissions, - } = useQuery({ - queryKey: ['perses-user-permissions', username], - queryFn: () => fetchPersesUserPermissions(username), - enabled: !!username, - staleTime: 5 * 60 * 1000, // Cache for 5 minutes - refetchOnWindowFocus: true, - retry: 2, - onError: (error) => { - // eslint-disable-next-line no-console - console.warn('Failed to fetch Perses user permissions:', error); - }, - }); - - return { - persesUserPermissions, - persesPermissionsError, - persesPermissionsLoading, - }; -}; diff --git a/web/src/shared/constants/data-test.ts b/web/src/shared/constants/data-test.ts index 5c8704c34..59bea5b7b 100644 --- a/web/src/shared/constants/data-test.ts +++ b/web/src/shared/constants/data-test.ts @@ -324,6 +324,9 @@ export const persesDashboardDataTestIDs = { importDashboardButtonToolbar: 'import-dashboard-button-list-page', editDashboardButtonToolbar: 'edit-dashboard-button-toolbar', cancelButtonToolbar: 'cancel-button-toolbar', + createAccessDeniedHelperText: 'perses-create-access-denied-helper-text', + updateAccessDeniedHelperText: 'perses-update-access-denied-helper-text', + deleteAccessDeniedHelperText: 'perses-delete-access-denied-helper-text', }; export const listPersesDashboardsDataTestIDs = { From 54e3f1eb42f6caa2cb7ffe31f565b0c8d3a0faf1 Mon Sep 17 00:00:00 2001 From: PeterYurkovich Date: Tue, 25 Aug 2026 16:17:01 -0400 Subject: [PATCH 2/2] test: update tests for removal of /permissions api cal --- .../perses/99.coo_rbac_perses_user1.cy.ts | 28 +++++---- .../perses/99.coo_rbac_perses_user2.cy.ts | 59 ++++++++++--------- .../perses/99.coo_rbac_perses_user3.cy.ts | 12 ++-- .../perses/99.coo_rbac_perses_user4.cy.ts | 43 +++++++++----- .../perses/99.coo_rbac_perses_user6.cy.ts | 24 ++++++-- .../perses-dashboards-create-dashboard.ts | 30 ++++++++-- .../perses-dashboards-import-dashboard.ts | 18 +++++- .../perses-dashboards-list-dashboards.ts | 51 +++++++++------- 8 files changed, 175 insertions(+), 90 deletions(-) diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user1.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user1.cy.ts index cdd6f6568..1f6f3166c 100644 --- a/web/cypress/support/perses/99.coo_rbac_perses_user1.cy.ts +++ b/web/cypress/support/perses/99.coo_rbac_perses_user1.cy.ts @@ -266,11 +266,14 @@ export function testCOORBACPersesTestsDevUser1(perspective: PerspectiveConfig) { listPersesDashboardsPage.assertCreateButtonIsEnabled(); listPersesDashboardsPage.clickCreateButton(); persesCreateDashboardsPage.createDashboardShouldBeLoaded(); - persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + // Non-viewable projects are absent from the dropdown. persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); - persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace3'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + // Viewable but non-creatable projects are listed but creation is denied. + persesCreateDashboardsPage.assertCreateAccessDenied('observ-test'); + persesCreateDashboardsPage.assertCreateAccessDenied('openshift-monitoring'); + // Editable project is selectable and creatable. persesCreateDashboardsPage.assertProjectDropdown('openshift-cluster-observability-operator'); persesCreateDashboardsPage.createDashboardDialogCancelButton(); @@ -434,11 +437,11 @@ export function testCOORBACPersesTestsDevUser1(perspective: PerspectiveConfig) { cy.log(`6.2. Change namespace to observ-test`); cy.changeNamespace('observ-test'); - cy.log(`6.3. Assert Kebab icon is disabled`); + cy.log(`6.3. Assert Rename/Delete row actions are disabled`); listPersesDashboardsPage.filter.byName( persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0], ); - listPersesDashboardsPage.assertKebabIconDisabled(); + listPersesDashboardsPage.assertKebabRowActionsDisabled(); cy.log(`6.4. Change namespace to openshift-cluster-observability-operator`); cy.changeNamespace('openshift-cluster-observability-operator'); @@ -464,8 +467,7 @@ export function testCOORBACPersesTestsDevUser1(perspective: PerspectiveConfig) { persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0], ); listPersesDashboardsPage.countDashboards('1'); - listPersesDashboardsPage.clickKebabIcon(); - listPersesDashboardsPage.assertKebabIconDisabled(); + listPersesDashboardsPage.assertKebabRowActionsDisabled(); listPersesDashboardsPage.clearAllFilters(); cy.log(`6.8. Filter by Project and Name`); @@ -568,11 +570,14 @@ export function testCOORBACPersesTestsDevUser1(perspective: PerspectiveConfig) { listPersesDashboardsPage.clickDuplicateOption(); cy.log(`8.5. Assert project dropdown options`); - listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('observ-test'); + // Non-viewable projects are absent from the dropdown. listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('perses-dev'); listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('empty-namespace3'); listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('empty-namespace4'); - listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('openshift-monitoring'); + // Viewable but non-creatable projects are listed but duplication is denied. + listPersesDashboardsPage.assertDuplicateProjectDenied('observ-test'); + listPersesDashboardsPage.assertDuplicateProjectDenied('openshift-monitoring'); + // Editable project is selectable and creatable. listPersesDashboardsPage.assertDuplicateProjectDropdownExists( 'openshift-cluster-observability-operator', ); @@ -648,11 +653,14 @@ export function testCOORBACPersesTestsDevUser1(perspective: PerspectiveConfig) { persesImportDashboardsPage.assertPersesDashboardDetected(); cy.log(`10.4. Verify project dropdown options`); - persesImportDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + // Non-viewable projects are absent from the dropdown. persesImportDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); - persesImportDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); persesImportDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace3'); persesImportDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + // Viewable but non-creatable projects are listed but import is denied. + persesImportDashboardsPage.assertImportAccessDenied('observ-test'); + persesImportDashboardsPage.assertImportAccessDenied('openshift-monitoring'); + // Editable project is selectable and creatable. persesImportDashboardsPage.assertProjectDropdown('openshift-cluster-observability-operator'); persesImportDashboardsPage.clickCancelButton(); diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user2.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user2.cy.ts index 27d5d74e8..4a7bd4a7b 100644 --- a/web/cypress/support/perses/99.coo_rbac_perses_user2.cy.ts +++ b/web/cypress/support/perses/99.coo_rbac_perses_user2.cy.ts @@ -1,5 +1,7 @@ import { persesDashboardsPage } from '../../views/perses-dashboards'; import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { persesCreateDashboardsPage } from '../../views/perses-dashboards-create-dashboard'; +import { persesImportDashboardsPage } from '../../views/perses-dashboards-import-dashboard'; import { persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev, @@ -120,67 +122,68 @@ export function testCOORBACPersesTestsDevUser2(perspective: PerspectiveConfig) { persesDashboardsPage.assertEditButtonIsDisabled(); }); - it(`3.${perspective.name} perspective - Create button validation - Disabled`, () => { + it(`3.${perspective.name} perspective - Create button validation - Access denied`, () => { cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses)`); listPersesDashboardsPage.shouldBeLoaded(perspective.dashboardsPageName); - cy.log(`3.2. Verify Create button is disabled`); - listPersesDashboardsPage.assertCreateButtonIsDisabled(); - - cy.log(`3.3 change namespace to perses-dev`); + cy.log(`3.2 change namespace to perses-dev`); cy.changeNamespace('perses-dev'); - cy.log(`3.4. Verify Create button is disabled`); - listPersesDashboardsPage.assertCreateButtonIsDisabled(); - - cy.log(`3.5. Change namespace to openshift-monitoring`); - cy.changeNamespace('openshift-monitoring'); - listPersesDashboardsPage.assertCreateButtonIsDisabled(); + cy.log(`3.3. Verify Create button is enabled but creation is denied for perses-dev`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertCreateAccessDenied('perses-dev'); + persesCreateDashboardsPage.assertCreateAccessDenied('openshift-monitoring'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); }); - it(`4.${perspective.name} perspective - Kebab icon - Disabled`, () => { + it(`4.${perspective.name} perspective - Kebab icon - Row actions denied`, () => { cy.log(`4.1. use sidebar nav to go to Observe > Dashboards (Perses)`); listPersesDashboardsPage.shouldBeLoaded(perspective.dashboardsPageName); cy.log(`4.2. Change namespace to perses-dev`); cy.changeNamespace('perses-dev'); - cy.log(`4.3. Assert Kebab icon is disabled`); + cy.log(`4.3. Assert Rename/Delete row actions are disabled`); listPersesDashboardsPage.filter.byName( persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0], ); - listPersesDashboardsPage.assertKebabIconDisabled(); + listPersesDashboardsPage.assertKebabRowActionsDisabled(); + + cy.log(`4.4. Assert Duplicate is blocked by access-denied in the modal`); + listPersesDashboardsPage.assertDuplicateAccessDenied('perses-dev'); listPersesDashboardsPage.clearAllFilters(); - cy.log(`4.4. Change namespace to All Projects`); + cy.log(`4.5. Change namespace to All Projects`); cy.changeNamespace('All Projects'); - cy.log(`4.5. Assert Kebab icon is disabled`); + cy.log(`4.6. Assert Rename/Delete row actions are disabled`); listPersesDashboardsPage.filter.byProject('perses-dev'); listPersesDashboardsPage.filter.byName( persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0], ); listPersesDashboardsPage.countDashboards('1'); - listPersesDashboardsPage.assertKebabIconDisabled(); + listPersesDashboardsPage.assertKebabRowActionsDisabled(); listPersesDashboardsPage.clearAllFilters(); }); - it(`5.${perspective.name} perspective - Import button validation - Disabled`, () => { + it(`5.${perspective.name} perspective - Import button validation - Access denied`, () => { cy.log(`5.1. use sidebar nav to go to Observe > Dashboards (Perses)`); listPersesDashboardsPage.shouldBeLoaded(perspective.dashboardsPageName); cy.log(`5.2. Change namespace to perses-dev`); cy.changeNamespace('perses-dev'); - cy.log(`5.3. Verify Import button is disabled`); - listPersesDashboardsPage.assertImportButtonIsDisabled(); - - cy.log(`5.5. Change namespace to openshift-monitoring`); - cy.changeNamespace('openshift-monitoring'); - listPersesDashboardsPage.assertImportButtonIsDisabled(); - - cy.log(`5.6. Change namespace to All Projects`); - cy.changeNamespace('All Projects'); - listPersesDashboardsPage.assertImportButtonIsDisabled(); + cy.log(`5.3. Verify Import button is enabled but import is denied for perses-dev`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile( + './cypress/fixtures/coo/coo140_perses/import/testing-perses-dashboard.json', + ); + persesImportDashboardsPage.assertPersesDashboardDetected(); + persesImportDashboardsPage.assertImportAccessDenied('perses-dev'); + persesImportDashboardsPage.clickCancelButton(); }); } diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user3.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user3.cy.ts index d4d2d87d1..2d00dd2b9 100644 --- a/web/cypress/support/perses/99.coo_rbac_perses_user3.cy.ts +++ b/web/cypress/support/perses/99.coo_rbac_perses_user3.cy.ts @@ -62,7 +62,7 @@ export function testCOORBACPersesTestsDevUser3(perspective: PerspectiveConfig) { ); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); - persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertCreateAccessDenied('openshift-monitoring'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); persesCreateDashboardsPage.assertProjectDropdown('empty-namespace3'); persesCreateDashboardsPage.createDashboardDialogCancelButton(); @@ -78,7 +78,7 @@ export function testCOORBACPersesTestsDevUser3(perspective: PerspectiveConfig) { ); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); - persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertCreateAccessDenied('openshift-monitoring'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); persesCreateDashboardsPage.assertProjectDropdown('empty-namespace3'); @@ -95,7 +95,7 @@ export function testCOORBACPersesTestsDevUser3(perspective: PerspectiveConfig) { ); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); - persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertCreateAccessDenied('openshift-monitoring'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); persesCreateDashboardsPage.assertProjectDropdown('empty-namespace3'); persesCreateDashboardsPage.createDashboardDialogCancelButton(); @@ -119,7 +119,7 @@ export function testCOORBACPersesTestsDevUser3(perspective: PerspectiveConfig) { persesCreateDashboardsPage.createDashboardShouldBeLoaded(); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); - persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertCreateAccessDenied('openshift-monitoring'); persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); persesCreateDashboardsPage.assertProjectDropdown('empty-namespace3'); persesCreateDashboardsPage.createDashboardDialogCancelButton(); @@ -378,10 +378,10 @@ export function testCOORBACPersesTestsDevUser3(perspective: PerspectiveConfig) { listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists( 'openshift-cluster-observability-operator', ); - listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('openshift-monitoring'); listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('observ-test'); listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('perses-dev'); listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('empty-namespace4'); + listPersesDashboardsPage.assertDuplicateProjectDenied('openshift-monitoring'); listPersesDashboardsPage.assertDuplicateProjectDropdownExists('empty-namespace3'); cy.log(`6.6. Enter new dashboard name`); @@ -458,8 +458,8 @@ export function testCOORBACPersesTestsDevUser3(perspective: PerspectiveConfig) { ); persesImportDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); persesImportDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); - persesImportDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); persesImportDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesImportDashboardsPage.assertImportAccessDenied('openshift-monitoring'); persesImportDashboardsPage.assertProjectDropdown('empty-namespace3'); persesImportDashboardsPage.clickCancelButton(); diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user4.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user4.cy.ts index a023cee80..62e2503fc 100644 --- a/web/cypress/support/perses/99.coo_rbac_perses_user4.cy.ts +++ b/web/cypress/support/perses/99.coo_rbac_perses_user4.cy.ts @@ -1,4 +1,6 @@ import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { persesCreateDashboardsPage } from '../../views/perses-dashboards-create-dashboard'; +import { persesImportDashboardsPage } from '../../views/perses-dashboards-import-dashboard'; export interface PerspectiveConfig { name: string; @@ -35,38 +37,51 @@ export function testCOORBACPersesTestsDevUser4(perspective: PerspectiveConfig) { cy.log(`1.2. All Projects validation - Dashboard search - empty state`); cy.changeNamespace('All Projects'); listPersesDashboardsPage.noDashboardsFoundState(); - listPersesDashboardsPage.assertCreateButtonIsDisabled(); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); cy.log(`1.3. empty-namespace4 validation - Dashboard search - empty state`); cy.changeNamespace('empty-namespace4'); listPersesDashboardsPage.noDashboardsFoundState(); - listPersesDashboardsPage.assertCreateButtonIsDisabled(); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); cy.log(`1.4. openshift-monitoring validation - Dashboard search - empty state`); cy.changeNamespace('openshift-monitoring'); listPersesDashboardsPage.noDashboardsFoundState(); - listPersesDashboardsPage.assertCreateButtonIsDisabled(); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); }, ); - it(`2.${perspective.name} perspective - Import button validation - Disabled`, () => { + it(`2.${perspective.name} perspective - Create button validation - Access denied`, () => { cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); listPersesDashboardsPage.noDashboardsFoundState(); cy.log(`2.2 change namespace to empty-namespace4`); cy.changeNamespace('empty-namespace4'); - cy.log(`2.3. Verify Import button is disabled`); - listPersesDashboardsPage.assertImportButtonIsDisabled(); + cy.log(`2.3. Verify Create button is enabled but creation is denied for empty-namespace4`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertCreateAccessDenied('empty-namespace4'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); + }); + + it(`3.${perspective.name} perspective - Import button validation - Access denied`, () => { + cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); - cy.log(`2.4. Change namespace to openshift-monitoring`); - cy.changeNamespace('openshift-monitoring'); - cy.log(`2.5. Verify Import button is disabled`); - listPersesDashboardsPage.assertImportButtonIsDisabled(); + cy.log(`3.2 change namespace to empty-namespace4`); + cy.changeNamespace('empty-namespace4'); - cy.log(`2.6. Change namespace to All Projects`); - cy.changeNamespace('All Projects'); - cy.log(`2.7. Verify Import button is disabled`); - listPersesDashboardsPage.assertImportButtonIsDisabled(); + cy.log(`3.3. Verify Import button is enabled but import is denied for empty-namespace4`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile( + './cypress/fixtures/coo/coo140_perses/import/testing-perses-dashboard.json', + ); + persesImportDashboardsPage.assertPersesDashboardDetected(); + persesImportDashboardsPage.assertImportAccessDenied('empty-namespace4'); + persesImportDashboardsPage.clickCancelButton(); }); } diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user6.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user6.cy.ts index 77a08d1d7..a5bf35d62 100644 --- a/web/cypress/support/perses/99.coo_rbac_perses_user6.cy.ts +++ b/web/cypress/support/perses/99.coo_rbac_perses_user6.cy.ts @@ -1,4 +1,6 @@ import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { persesCreateDashboardsPage } from '../../views/perses-dashboards-create-dashboard'; +import { persesImportDashboardsPage } from '../../views/perses-dashboards-import-dashboard'; export interface PerspectiveConfig { name: string; @@ -29,16 +31,28 @@ export function testCOORBACPersesTestsDevUser6(perspective: PerspectiveConfig) { cy.assertNamespace('empty-namespace3', false); cy.assertNamespace('empty-namespace4', false); - cy.log(`1.2. Create button validation`); - listPersesDashboardsPage.assertCreateButtonIsDisabled(); + cy.log(`1.2. Create button is enabled but no project is selectable, so Create is disabled`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + cy.byPFRole('dialog').find('button').contains('Create').should('be.disabled'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); }, ); - it(`2.${perspective.name} perspective - Import button validation - Disabled`, () => { + it(`2.${perspective.name} perspective - Import button validation - Access denied`, () => { cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); listPersesDashboardsPage.noDashboardsFoundState(); - cy.log(`2.2. Verify Import button is disabled`); - listPersesDashboardsPage.assertImportButtonIsDisabled(); + cy.log(`2.2. Verify Import button is enabled but import is disabled without a project`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile( + './cypress/fixtures/coo/coo140_perses/import/testing-perses-dashboard.json', + ); + persesImportDashboardsPage.assertPersesDashboardDetected(); + cy.byPFRole('dialog').find('button').contains('Import').should('be.disabled'); + persesImportDashboardsPage.clickCancelButton(); }); } diff --git a/web/cypress/views/perses-dashboards-create-dashboard.ts b/web/cypress/views/perses-dashboards-create-dashboard.ts index b3c549672..123b312f2 100644 --- a/web/cypress/views/perses-dashboards-create-dashboard.ts +++ b/web/cypress/views/perses-dashboards-create-dashboard.ts @@ -1,4 +1,9 @@ -import { Classes, IDs, persesAriaLabels } from '@/shared/constants/data-test'; +import { + Classes, + IDs, + persesAriaLabels, + persesDashboardDataTestIDs, +} from '@/shared/constants/data-test'; import { persesCreateDashboard, persesDashboardsModalTitles } from '../fixtures/perses/constants'; export const persesCreateDashboardsPage = { @@ -44,6 +49,23 @@ export const persesCreateDashboardsPage = { cy.byPFRole('dialog').find(Classes.PersesCreateDashboardProjectDropdown).click({ force: true }); }, + /** + * Selects a project the user can view but cannot create dashboards in and asserts the create + * access-denied helper text is shown and the Create button is disabled. + */ + assertCreateAccessDenied: (project: string) => { + cy.log('persesCreateDashboardsPage.assertCreateAccessDenied'); + cy.get('#' + IDs.persesDashboardCreateDashboardName) + .should('be.visible') + .clear() + .type('access-denied-check'); + cy.byPFRole('dialog').find(Classes.PersesCreateDashboardProjectDropdown).click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible').click({ force: true }); + cy.byTestID(persesDashboardDataTestIDs.createAccessDeniedHelperText).should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Create').should('be.disabled'); + }, + enterDashboardName: (name: string) => { cy.log('persesCreateDashboardsPage.enterDashboardName'); cy.get('#' + IDs.persesDashboardCreateDashboardName) @@ -72,9 +94,9 @@ export const persesCreateDashboardsPage = { assertDuplicatedNameValidation: () => { cy.log('persesCreateDashboardsPage.assertDuplicatedNameValidation'); - cy.byPFRole('list') - .find('h4') - .should('contain.text', persesCreateDashboard.DIALOG_CREATE_NAME_BKD_VALIDATION) + cy.byPFRole('dialog') + .find('.pf-m-error') + .should('contain.text', persesCreateDashboard.DIALOG_DUPLICATED_NAME_PF_VALIDATION_SUFFIX) .should('be.visible'); }, diff --git a/web/cypress/views/perses-dashboards-import-dashboard.ts b/web/cypress/views/perses-dashboards-import-dashboard.ts index 9c6a89eb6..8958f707b 100644 --- a/web/cypress/views/perses-dashboards-import-dashboard.ts +++ b/web/cypress/views/perses-dashboards-import-dashboard.ts @@ -1,4 +1,9 @@ -import { Classes, IDs, persesAriaLabels } from '@/shared/constants/data-test'; +import { + Classes, + IDs, + persesAriaLabels, + persesDashboardDataTestIDs, +} from '@/shared/constants/data-test'; import { persesDashboardsImportDashboard, persesDashboardsModalTitles, @@ -140,6 +145,17 @@ export const persesImportDashboardsPage = { .click({ force: true }); }, + assertImportAccessDenied: (project: string) => { + cy.log('persesImportDashboardsPage.assertImportAccessDenied'); + cy.byAriaLabel(persesAriaLabels.importDashboardProjectInputButton) + .should('be.visible') + .click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible').click({ force: true }); + cy.byTestID(persesDashboardDataTestIDs.createAccessDeniedHelperText).should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Import').should('be.disabled'); + }, + assertFailedToMigrateGrafanaDashboard: () => { cy.log('persesImportDashboardsPage.assertFailedToMigrateGrafanaDashboard'); cy.get('h4') diff --git a/web/cypress/views/perses-dashboards-list-dashboards.ts b/web/cypress/views/perses-dashboards-list-dashboards.ts index 02c40af2b..ace08c8db 100644 --- a/web/cypress/views/perses-dashboards-list-dashboards.ts +++ b/web/cypress/views/perses-dashboards-list-dashboards.ts @@ -7,6 +7,7 @@ import { listPersesDashboardsDataTestIDs, listPersesDashboardsOUIAIDs, persesAriaLabels, + persesDashboardDataTestIDs, } from '@/shared/constants/data-test'; import { listPersesDashboardsEmptyState, @@ -181,14 +182,6 @@ export const listPersesDashboardsPage = { .should('not.have.attr', 'disabled'); }, - assertCreateButtonIsDisabled: () => { - cy.log('persesDashboardsPage.assertCreateButtonIsDisabled'); - cy.byTestID(DataTestIDs.PersesCreateDashboardButton) - .scrollIntoView() - .should('be.visible') - .should('have.attr', 'disabled'); - }, - clickKebabIcon: (index?: number) => { const idx = index !== undefined ? index : 0; cy.log('persesDashboardsPage.clickKebabIcon'); @@ -207,12 +200,34 @@ export const listPersesDashboardsPage = { cy.byPFRole('menuitem').contains('Delete dashboard').should('be.visible'); }, - assertKebabIconDisabled: () => { - cy.log('persesDashboardsPage.assertKebabIconDisabled'); - cy.byAriaLabel(persesAriaLabels.persesDashboardKebabIcon) - .scrollIntoView() - .should('be.visible') - .should('have.attr', 'disabled'); + assertKebabRowActionsDisabled: (index?: number) => { + cy.log('persesDashboardsPage.assertKebabRowActionsDisabled'); + listPersesDashboardsPage.clickKebabIcon(index); + cy.byPFRole('menuitem') + .contains('Rename dashboard') + .should('have.attr', 'aria-disabled', 'true'); + cy.byPFRole('menuitem') + .contains('Delete dashboard') + .should('have.attr', 'aria-disabled', 'true'); + cy.byPFRole('menuitem') + .contains('Duplicate dashboard') + .should('not.have.attr', 'aria-disabled', 'true'); + listPersesDashboardsPage.clickKebabIcon(index); + }, + + assertDuplicateProjectDenied: (project: string) => { + cy.log('persesDashboardsPage.assertDuplicateProjectDenied'); + listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown(project); + cy.byTestID(persesDashboardDataTestIDs.createAccessDeniedHelperText).should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Duplicate').should('be.disabled'); + }, + + assertDuplicateAccessDenied: (project: string) => { + cy.log('persesDashboardsPage.assertDuplicateAccessDenied'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDuplicateOption(); + listPersesDashboardsPage.assertDuplicateProjectDenied(project); + listPersesDashboardsPage.duplicateDashboardCancelButton(); }, clickRenameDashboardOption: () => { @@ -411,12 +426,4 @@ export const listPersesDashboardsPage = { .should('be.visible') .should('not.have.attr', 'disabled'); }, - - assertImportButtonIsDisabled: () => { - cy.log('listPersesDashboardsPage.assertImportButtonIsDisabled'); - cy.byAriaLabel(persesAriaLabels.dashboardActionsMenu) - .scrollIntoView() - .should('be.visible') - .should('have.attr', 'disabled'); - }, };