From 66c0025cab960f5f8b2cc7bca157b97ad4a8f250 Mon Sep 17 00:00:00 2001 From: David Rajnoha Date: Tue, 6 Jan 2026 17:55:59 +0100 Subject: [PATCH 01/15] docs(tests): add test cases for incident loading and related tooltip time display issues - Add test case for boundary end times not rounded to 5 minutes (OU-1205) Interval calculation uses 1-second offsets causing unrounded times like "23:29:59" instead of "23:30" in multi-severity incident tooltips - Add test case for start times mismatch between UI elements (OU-1221) Multi-severity incident tooltip start times are 5 minutes off from alert tooltip and alerts table values - Document absolute start date fix implementation using new API call (OU-1040) Uses min_over_time(timestamp()) instant queries to retrieve actual start timestamps regardless of "Last N Days" filter selection --- .../tests/2.ui_display_flows.md | 68 +++++++++++++++++++ .../tests/3.api_calls_data_loading_flows.md | 37 +++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/docs/incident_detection/tests/2.ui_display_flows.md b/docs/incident_detection/tests/2.ui_display_flows.md index c4f45cb37..eb444b9ce 100644 --- a/docs/incident_detection/tests/2.ui_display_flows.md +++ b/docs/incident_detection/tests/2.ui_display_flows.md @@ -109,6 +109,74 @@ start,end,alertname,namespace,severity,silenced,labels - Check the "last updated date" field - Verify that the format changes without the need to reload the page +### 2.3.2 Incident Bar Trimming on Time Filter Change +**BUG**: When "Last N Days" is shorter than an incident's duration, the bar should be visually trimmed to show only the portion within the selected time window. +**Automation Status**: NOT AUTOMATED + +**Background**: +- Incorrect behavior: Changing time filter "squashes" the X-axis to the right while still showing the full incident bar +- Correct behavior: Incident bar is trimmed/clipped to only display the portion that falls within the selected time range + +- [ ] **Bar Trimming - Long Incident with Short Filter**: Create an incident spanning 15 days + - With "Last 15 days": Full incident bar visible with correct proportional width + - Set time filter to "Last 7 days" + - Verify the incident bar is trimmed to show only the 7-day portion (not the full 15-day bar) + - Verify the X-axis scale matches the 7-day window (not squashed to the right) + - Verify the bar starts at the left edge of the chart (since incident started before the 7-day window) + +- [ ] **Tooltip Accuracy After Trimming**: Hover over a trimmed incident bar + - Verify tooltip shows the actual absolute Start date (which may be before the visible window) + - Verify tooltip shows correct End date + - Verify the displayed duration reflects the full incident, not just the visible portion + +### 2.3.3 Mixed Severity Interval Boundary Times +**Automation Status**: NOT AUTOMATED + +This section covers two related time display bugs in multi-severity incident tooltips. + +#### Issue 1: Boundary End Times Not Rounded (OU-1205) +**BUG**: Consecutive interval end times within the same incident bar are not 5-minute rounded in tooltips. + +**Background**: +- The interval calculation logic in `utils.ts` uses 1-second offsets to prevent overlapping intervals +- Example: If timestamps are at 100, 200, 300 seconds, intervals are calculated as: + - Interval 1: [100, 199, 'critical'] + - Interval 2: [200, 299, 'warning'] +- This results in end times like "23:29:59" instead of the expected rounded "23:30" +- The fix should round the displayed tooltip text, not change the interval calculation logic + +- [ ] **End Times Should Be 5-Minute Rounded**: Use AlertC from test data (multi-severity incident) + - Hover over each severity segment within the incident bar + - Verify End time in tooltip shows rounded value (e.g., "23:30") not unrounded (e.g., "23:29:59") + - Verify all interval boundaries display at 5-minute precision in tooltips + +#### Issue 2: Start Times 5 Minutes Off (OU-1221) +**BUG**: Multi-severity incident bar tooltip start times are 5 minutes off from the values shown in alert tooltips and alerts table. + +**Background**: +- When hovering over a severity segment in an incident bar, the Start time shown differs from: + - The Start time shown in the corresponding alert tooltip + - The Start time shown in the alerts details table +- This creates user confusion as the same data point shows different times in different UI elements + +- [ ] **Start Times Match Alert Tooltip**: Use AlertC from test data + - Click incident to open alerts chart + - Hover over an alert bar: Note the Start time in alert tooltip + - Hover over the corresponding severity segment in the incident bar + - Verify incident tooltip Start time matches alert tooltip Start time exactly + +- [ ] **Start Times Match Alerts Table**: + - With incident selected, check alerts table Start column + - Hover over the corresponding severity segment in incident bar + - Verify incident tooltip Start time matches alerts table Start time exactly + +- [ ] **Consecutive Interval Boundaries Match**: Check multi-severity incident + - Hover over first severity segment: Note the End time + - Hover over next severity segment: Note the Start time + - Verify End time of previous segment matches Start time of next segment (no 5-minute gap) + - Example failure: Info ends at "10:15" but Warning starts at "10:20" + - Expected: Info ends at "10:15" and Warning starts at "10:15" + ### 2.4 Silences labels (Not Automated) - Verify that information about silences is contained in the alert name as `NetworkLatencyHigh (silenced)` instead of the additional `silenced=true` diff --git a/docs/incident_detection/tests/3.api_calls_data_loading_flows.md b/docs/incident_detection/tests/3.api_calls_data_loading_flows.md index 1ae58d35f..9e2f7a6e0 100644 --- a/docs/incident_detection/tests/3.api_calls_data_loading_flows.md +++ b/docs/incident_detection/tests/3.api_calls_data_loading_flows.md @@ -84,8 +84,41 @@ start,end,alertname,namespace,severity,silenced,labels - Verify the latest query end time param is within the last 5 minutes - ### 3.4 Data Integrity - **NEW, NOT AUTOMATED, TODO COO 1.4** +### 3.4 15-Day Data Loading with "Last N Days" Filtering +**FEATURE**: UI always loads 15 days of data (one query_range call per day), then filters client-side based on "Last N Days" selection. +**Automation Status**: NOT AUTOMATED + +**Background**: +- Before: Data was downloaded only for "Last N Days", causing Start dates to be relative to N days +- After: Start displays an absolute date, even when "Last N Days" is shorter than the incident's actual start +- Limit: Start is capped at max 15 days (the maximum supported range) + +**Fix Implementation**: +The absolute start date of an incident/alert is always displayed, regardless of the selected "Last N Days" filter. + +Solution uses a new API call: +- Absolute timestamps are retrieved by performing an **instant query** call to Prometheus +- For incidents: `min_over_time(timestamp(cluster_health_components_map{}))` +- For alerts: `min_over_time(timestamp(ALERTS{}))` +- This returns the timestamp of the first datapoint for that metric +- The result is saved into Redux store and matched to related incident/alert to update the Start date displayed in the tooltip + +**Manual Testing Data**: +Use `docs/incident_detection/simulate_scenarios/long-incident-15-days.csv` which creates a 15-day spanning incident for testing absolute start date display. + +- [ ] **Absolute Start Date Display**: Use `long-incident-15-days.csv` (creates 15-day incident) + - Set time filter to "Last 7 days" + - Verify incident Start date shows the absolute date (15 days ago), NOT a date relative to 7 days + - Verify the incident bar in the chart is trimmed to show only the portion within the 7-day window + - Verify tooltip shows the actual absolute start time + +- [ ] **API Call Pattern Verification**: Monitor network requests on initial page load + - Verify 15 query_range calls are made on initial page load (one per day) + - Verify instant query calls for `min_over_time(timestamp(cluster_health_components_map{}))` and `min_over_time(timestamp(ALERTS{}))` + - Verify the time ranges cover the full 15-day window regardless of "Last N Days" selection + +### 3.5 Data Integrity +**NEW, NOT AUTOMATED, TODO COO 1.4** - [ ] Incident grouping by `group_id` works correctly - [ ] Values deduplicated across multiple time range queries - [ ] Component lists combined for same group_id From 2168bff29718af60a06ad1ac3fdaf193f5121257 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Mon, 5 Jan 2026 10:40:44 +0100 Subject: [PATCH 02/15] fix: update qs vulnerable dependency Signed-off-by: Gabriel Bernal --- web/package-lock.json | 6 +++--- web/package.json | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index b3350fd02..943894964 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -21927,9 +21927,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/web/package.json b/web/package.json index 60f432b6d..71bfa0652 100644 --- a/web/package.json +++ b/web/package.json @@ -172,7 +172,8 @@ "react-router-dom": "<7" }, "overrides": { - "echarts": "^5.6.0" + "echarts": "^5.6.0", + "qs": "^6.14.1" }, "consolePlugin": { "name": "monitoring-plugin", From cb33e32959a9029b225c6747c05a8c2a7df857ee Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Thu, 8 Jan 2026 18:47:11 +0100 Subject: [PATCH 03/15] fix esbuild optional dependencies for cross arch builds Signed-off-by: Gabriel Bernal --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6eee3e26a..380b3a309 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ install-frontend: .PHONY: install-frontend-ci install-frontend-ci: - cd web && npm ci --omit=optional --ignore-scripts + cd web && npm ci --ignore-scripts .PHONY: install-frontend-ci-clean install-frontend-ci-clean: install-frontend-ci From 04ef2184705de2f3eab87dfcbd2cb9b9dcb91f8f Mon Sep 17 00:00:00 2001 From: Evelyn Tanigawa Murasaki Date: Fri, 9 Jan 2026 08:29:19 -0300 Subject: [PATCH 04/15] fix click and type timespan --- web/cypress/views/metrics.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web/cypress/views/metrics.ts b/web/cypress/views/metrics.ts index 27c5a80b8..d2cf4bac6 100644 --- a/web/cypress/views/metrics.ts +++ b/web/cypress/views/metrics.ts @@ -293,19 +293,15 @@ export const metricsPage = { cy.get(Classes.MenuItem).contains(timespan).should('be.visible').click(); cy.byPFRole('progressbar').should('be.visible'); cy.byPFRole('progressbar').should('not.exist'); - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('be.visible'); - cy.byTestID(DataTestIDs.MetricGraph).find('[data-ouia-component-id^="' + DataTestIDs.MetricsGraphAlertDanger + '"]').should('not.exist'); }, enterGraphTimespan: (timespan: GraphTimespan) => { cy.log('metricsPage.enterGraphTimespan'); - cy.byTestID(DataTestIDs.MetricGraphTimespanInput).type('{selectall}{backspace}', {delay: 1000}); + cy.byTestID(DataTestIDs.MetricGraphTimespanInput).scrollIntoView().should('be.visible').type('{selectall}{backspace}', {delay: 1000}); cy.byTestID(DataTestIDs.MetricGraphTimespanInput).type(timespan); cy.byTestID(DataTestIDs.MetricGraphTimespanInput).should('have.attr', 'value', timespan); cy.byPFRole('progressbar').should('be.visible'); cy.byPFRole('progressbar').should('not.exist'); - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('be.visible'); - cy.byTestID(DataTestIDs.MetricGraph).find('[data-ouia-component-id^="' + DataTestIDs.MetricsGraphAlertDanger + '"]').should('not.exist'); }, graphTimespanDropdownAssertion: () => { From 4878015475abd43af1563efdd682a6715c423546 Mon Sep 17 00:00:00 2001 From: David Rajnoha Date: Thu, 8 Jan 2026 13:12:34 +0100 Subject: [PATCH 05/15] feat(cypress): custom cluster-health-analyzer builds Add support for patching custom cluster-health-analyzer images in COO CSV, enabling CI jobs on cluster-health-analyzer PRs to run the monitoring-plugin test suite against custom builds. - Add update-cha-image.sh script to patch CHA image in COO CSV - Refactor setupMonitoringConsolePlugin into generic patchCOOCSVImage function - Add CHA_IMAGE to session key for proper cache invalidation - Update configure-env.sh with interactive CHA_IMAGE configuration - Document CYPRESS_CHA_IMAGE usage in README.md --- web/cypress/README.md | 25 +++++++-- web/cypress/configure-env.sh | 9 ++++ web/cypress/fixtures/coo/update-cha-image.sh | 46 ++++++++++++++++ web/cypress/fixtures/export.sh | 3 ++ .../support/commands/operator-commands.ts | 53 +++++++++++++++---- 5 files changed, 124 insertions(+), 12 deletions(-) create mode 100755 web/cypress/fixtures/coo/update-cha-image.sh diff --git a/web/cypress/README.md b/web/cypress/README.md index a3757a850..17c589e6b 100644 --- a/web/cypress/README.md +++ b/web/cypress/README.md @@ -75,6 +75,7 @@ Creates `export-env.sh` that you can source later: `source export-env.sh` |----------|-------------|----------| | `CYPRESS_MP_IMAGE` | Custom Monitoring Plugin image | Testing custom MP builds | | `CYPRESS_MCP_CONSOLE_IMAGE` | Custom Monitoring Console Plugin image | Testing custom MCP builds | +| `CYPRESS_CHA_IMAGE` | Custom cluster-health-analyzer image | Testing custom CHA builds | ### Operator Installation Control @@ -160,7 +161,25 @@ export CYPRESS_MP_IMAGE=quay.io/myorg/monitoring-plugin:my-branch export CYPRESS_MCP_CONSOLE_IMAGE=quay.io/myorg/monitoring-console-plugin:my-branch ``` -### Example 4: Pre-Provisioned Cluster (Skip Installations) +### Example 4: Testing Custom cluster-health-analyzer Build + +For CI jobs testing PRs to cluster-health-analyzer: + +```bash +# Required variables +export CYPRESS_BASE_URL=https://... +export CYPRESS_LOGIN_IDP=flexy-htpasswd-provider +export CYPRESS_LOGIN_USERS=username:password +export CYPRESS_KUBECONFIG_PATH=~/Downloads/kubeconfig + +# Custom cluster-health-analyzer image built from PR +export CYPRESS_CHA_IMAGE=quay.io/myorg/cluster-health-analyzer:pr-123 + +# Use COO bundle (required for incidents feature testing) +export CYPRESS_KONFLUX_COO_BUNDLE_IMAGE=quay.io/rhobs/observability-operator-bundle:latest +``` + +### Example 5: Pre-Provisioned Cluster (Skip Installations) ```bash # Required variables @@ -173,14 +192,14 @@ export CYPRESS_KUBECONFIG_PATH=~/Downloads/kubeconfig export CYPRESS_SKIP_ALL_INSTALL=true ``` -### Example 5: Configurable COO Namespace +### Example 6: Configurable COO Namespace Set the following var to specify the Cluster Observability Operator namespace. Defaults to `openshift-cluster-observability-operator` if not set. This is useful when testing with different namespace configurations (e.g., using `coo` instead of the default). ```bash export CYPRESS_COO_NAMESPACE=openshift-cluster-observability-operator ``` -### Example 6: Debug Mode +### Example 7: Debug Mode ```bash # Required variables + debug diff --git a/web/cypress/configure-env.sh b/web/cypress/configure-env.sh index 782d517b8..e8df45372 100755 --- a/web/cypress/configure-env.sh +++ b/web/cypress/configure-env.sh @@ -175,6 +175,7 @@ print_current_config() { print_var "CYPRESS_KONFLUX_COO_BUNDLE_IMAGE" "${CYPRESS_KONFLUX_COO_BUNDLE_IMAGE-}" print_var "CYPRESS_CUSTOM_COO_BUNDLE_IMAGE" "${CYPRESS_CUSTOM_COO_BUNDLE_IMAGE-}" print_var "CYPRESS_MCP_CONSOLE_IMAGE" "${CYPRESS_MCP_CONSOLE_IMAGE-}" + print_var "CYPRESS_CHA_IMAGE" "${CYPRESS_CHA_IMAGE-}" print_var "CYPRESS_TIMEZONE" "${CYPRESS_TIMEZONE-}" print_var "CYPRESS_MOCK_NEW_METRICS" "${CYPRESS_MOCK_NEW_METRICS-}" print_var "CYPRESS_SESSION" "${CYPRESS_SESSION-}" @@ -226,6 +227,7 @@ main() { local def_konflux_bundle=${CYPRESS_KONFLUX_COO_BUNDLE_IMAGE-} local def_custom_coo_bundle=${CYPRESS_CUSTOM_COO_BUNDLE_IMAGE-} local def_mcp_console_image=${CYPRESS_MCP_CONSOLE_IMAGE-} + local def_cha_image=${CYPRESS_CHA_IMAGE-} local def_timezone=${CYPRESS_TIMEZONE-} local def_mock_new_metrics=${CYPRESS_MOCK_NEW_METRICS-} local def_session=${CYPRESS_SESSION-} @@ -434,6 +436,9 @@ main() { local mcp_console_image mcp_console_image=$(ask "Monitoring Console Plugin UI image (CYPRESS_MCP_CONSOLE_IMAGE)" "$def_mcp_console_image") + local cha_image + cha_image=$(ask "Cluster Health Analyzer image (CYPRESS_CHA_IMAGE)" "$def_cha_image") + local timezone timezone=$(ask "Cluster timezone (CYPRESS_TIMEZONE)" "${def_timezone:-UTC}") @@ -500,6 +505,9 @@ main() { if [[ -n "$mcp_console_image" ]]; then export_lines+=("export CYPRESS_MCP_CONSOLE_IMAGE='$(printf %s "$mcp_console_image" | escape_for_single_quotes)'" ) fi + if [[ -n "$cha_image" ]]; then + export_lines+=("export CYPRESS_CHA_IMAGE='$(printf %s "$cha_image" | escape_for_single_quotes)'" ) + fi if [[ -n "$timezone" ]]; then export_lines+=("export CYPRESS_TIMEZONE='$(printf %s "$timezone" | escape_for_single_quotes)'" ) fi @@ -553,6 +561,7 @@ main() { [[ -n "$konflux_bundle" ]] && echo " CYPRESS_KONFLUX_COO_BUNDLE_IMAGE=$konflux_bundle" [[ -n "$custom_coo_bundle" ]] && echo " CYPRESS_CUSTOM_COO_BUNDLE_IMAGE=$custom_coo_bundle" [[ -n "$mcp_console_image" ]] && echo " CYPRESS_MCP_CONSOLE_IMAGE=$mcp_console_image" + [[ -n "$cha_image" ]] && echo " CYPRESS_CHA_IMAGE=$cha_image" [[ -n "$timezone" ]] && echo " CYPRESS_TIMEZONE=$timezone" echo " CYPRESS_MOCK_NEW_METRICS=$mock_new_metrics" echo " CYPRESS_SESSION=$session" diff --git a/web/cypress/fixtures/coo/update-cha-image.sh b/web/cypress/fixtures/coo/update-cha-image.sh new file mode 100755 index 000000000..4b57f8cb4 --- /dev/null +++ b/web/cypress/fixtures/coo/update-cha-image.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Script to patch the cluster-health-analyzer image in COO CSV +# Used by Cypress tests to test custom CHA builds + +echo "--------------------------------" +echo "CHA_IMAGE: ${CHA_IMAGE}" +echo "--------------------------------" + +# Generate a random filename +RANDOM_FILE="/tmp/coo_cha_csv_$(date +%s%N).yaml" + +COO_CSV_NAME=$(oc get csv --kubeconfig "${KUBECONFIG}" --namespace="${MCP_NAMESPACE}" | grep "cluster-observability-operator" | awk '{print $1}') + +if [ -z "${COO_CSV_NAME}" ]; then + echo "Error: Could not find cluster-observability-operator CSV in namespace ${MCP_NAMESPACE}" + exit 1 +fi + +echo "Found COO CSV: ${COO_CSV_NAME}" + +oc get csv "${COO_CSV_NAME}" -n "${MCP_NAMESPACE}" -o yaml > "${RANDOM_FILE}" --kubeconfig "${KUBECONFIG}" + +# Patch the CSV file env vars for cluster-health-analyzer +# Handle both US and UK spellings (analyser/analyzer) for compatibility +sed -i "s#value: .*cluster-health-analy[sz]er.*#value: ${CHA_IMAGE}#g" "${RANDOM_FILE}" + +# Patch the CSV file related images +sed -i "s#^\([[:space:]]*- image:\).*cluster-health-analy[sz]er.*#\1 ${CHA_IMAGE}#g" "${RANDOM_FILE}" + +# Apply the patched CSV resource file +oc replace -f "${RANDOM_FILE}" --kubeconfig "${KUBECONFIG}" + +# Wait for the operator to reconcile the change +sleep 25 + +# Wait for health-analyzer pod to be ready with the new image +OUTPUT=$(oc wait --for=condition=ready pods -l app.kubernetes.io/instance=health-analyzer -n "${MCP_NAMESPACE}" --timeout=120s --kubeconfig "${KUBECONFIG}") +echo "${OUTPUT}" + +echo "--------------------------------" +echo "Health-analyzer pod status:" +echo "--------------------------------" +oc get pods -l app.kubernetes.io/instance=health-analyzer -n "${MCP_NAMESPACE}" -o wide --kubeconfig "${KUBECONFIG}" +echo "--------------------------------" + diff --git a/web/cypress/fixtures/export.sh b/web/cypress/fixtures/export.sh index f1e455923..3e094c197 100755 --- a/web/cypress/fixtures/export.sh +++ b/web/cypress/fixtures/export.sh @@ -36,6 +36,9 @@ export CYPRESS_FBC_STAGE_COO_IMAGE= # Set the following var to use custom Monitoring Console Plugin UI plugin image. The image will be patched in Cluster Observability Operator CSV. export CYPRESS_MCP_CONSOLE_IMAGE= +# Set the following var to use custom cluster-health-analyzer image. The image will be patched in Cluster Observability Operator CSV. +export CYPRESS_CHA_IMAGE= + # Set the following var to specify the cluster timezone for incident timeline calculations. Defaults to UTC if not specified. export CYPRESS_TIMEZONE= diff --git a/web/cypress/support/commands/operator-commands.ts b/web/cypress/support/commands/operator-commands.ts index 9e8514535..ef2ee97e4 100644 --- a/web/cypress/support/commands/operator-commands.ts +++ b/web/cypress/support/commands/operator-commands.ts @@ -135,7 +135,8 @@ export const operatorAuthUtils = { Cypress.env('CUSTOM_COO_BUNDLE_IMAGE'), Cypress.env('FBC_STAGE_COO_IMAGE'), Cypress.env('MP_IMAGE'), - Cypress.env('MCP_CONSOLE_IMAGE') + Cypress.env('MCP_CONSOLE_IMAGE'), + Cypress.env('CHA_IMAGE') ]; return [...baseKey, ...envVars.filter(Boolean)]; @@ -309,15 +310,32 @@ const operatorUtils = { cy.get('[data-test="status-text"]', { timeout: installTimeoutMilliseconds }).eq(0).should('contain.text', 'Succeeded', { timeout: installTimeoutMilliseconds }); }, - setupMonitoringConsolePlugin(MCP: { namespace: string }): void { - cy.log('Set Monitoring Console Plugin image in operator CSV'); - if (Cypress.env('MCP_CONSOLE_IMAGE')) { - cy.log('MCP_CONSOLE_IMAGE is set. the image will be patched in COO operator CSV'); + /** + * Generic function to patch a component image in the COO CSV + * @param MCP - The MCP namespace configuration + * @param config - Configuration for the image patch + * @param config.envVar - The Cypress environment variable name (also used as the shell script env var) + * @param config.scriptPath - Path to the shell script that performs the patch + * @param config.componentName - Human-readable name for logging + */ + patchCOOCSVImage( + MCP: { namespace: string }, + config: { + envVar: string; + scriptPath: string; + componentName: string; + } + ): void { + const imageValue = Cypress.env(config.envVar); + cy.log(`Set ${config.componentName} image in operator CSV`); + + if (imageValue) { + cy.log(`${config.envVar} is set. The image will be patched in COO operator CSV`); cy.exec( - './cypress/fixtures/coo/update-mcp-image.sh', + config.scriptPath, { env: { - MCP_CONSOLE_IMAGE: Cypress.env('MCP_CONSOLE_IMAGE'), + [config.envVar]: imageValue, KUBECONFIG: Cypress.env('KUBECONFIG_PATH'), MCP_NAMESPACE: `${MCP.namespace}` }, @@ -326,14 +344,30 @@ const operatorUtils = { } ).then((result) => { expect(result.code).to.eq(0); - cy.log(`COO CSV updated successfully with Monitoring Console Plugin image: ${result.stdout}`); + cy.log(`COO CSV updated successfully with ${config.componentName} image: ${result.stdout}`); cy.reload(true); }); } else { - cy.log('MCP_CONSOLE_IMAGE is NOT set. Skipping patching the image in COO operator CSV.'); + cy.log(`${config.envVar} is NOT set. Skipping patching the image in COO operator CSV.`); } }, + setupMonitoringConsolePlugin(MCP: { namespace: string }): void { + operatorUtils.patchCOOCSVImage(MCP, { + envVar: 'MCP_CONSOLE_IMAGE', + scriptPath: './cypress/fixtures/coo/update-mcp-image.sh', + componentName: 'Monitoring Console Plugin' + }); + }, + + setupClusterHealthAnalyzer(MCP: { namespace: string }): void { + operatorUtils.patchCOOCSVImage(MCP, { + envVar: 'CHA_IMAGE', + scriptPath: './cypress/fixtures/coo/update-cha-image.sh', + componentName: 'cluster-health-analyzer' + }); + }, + setupDashboardsAndPlugins(MCP: { namespace: string }): void { cy.log('Create perses-dev namespace.'); @@ -797,6 +831,7 @@ Cypress.Commands.add('beforeBlock', (MP: { namespace: string, operatorName: stri operatorUtils.installCOO(MCP); operatorUtils.waitForCOOReady(MCP); operatorUtils.setupMonitoringConsolePlugin(MCP); + operatorUtils.setupClusterHealthAnalyzer(MCP); operatorUtils.setupDashboardsAndPlugins(MCP); operatorUtils.setupTroubleshootingPanel(MCP); operatorUtils.setupMonitoringPluginImage(MP); From bfd06d5549e1e6bfca939eca5529a554a7348e49 Mon Sep 17 00:00:00 2001 From: AOS Automation Release Team Date: Sat, 20 Dec 2025 01:01:20 +0000 Subject: [PATCH 06/15] Updating monitoring-plugin-container image to be consistent with ART for 4.22 Reconciling with https://github.com/openshift/ocp-build-data/tree/087d1930e36b609f77d73bd8a313d85c940cff4d/images/monitoring-plugin.yml --- .ci-operator.yaml | 2 +- Dockerfile.art | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci-operator.yaml b/.ci-operator.yaml index e307e5af6..284a91009 100644 --- a/.ci-operator.yaml +++ b/.ci-operator.yaml @@ -1,4 +1,4 @@ build_root_image: name: release namespace: openshift - tag: rhel-9-release-golang-1.24-openshift-4.21 + tag: rhel-9-release-golang-1.24-openshift-4.22 diff --git a/Dockerfile.art b/Dockerfile.art index c399a90c4..951cccac9 100644 --- a/Dockerfile.art +++ b/Dockerfile.art @@ -1,4 +1,4 @@ -FROM registry.ci.openshift.org/ocp/builder:rhel-9-base-nodejs-openshift-4.21 AS web-builder +FROM registry.ci.openshift.org/ocp/builder:rhel-9-base-nodejs-openshift-4.22 AS web-builder # Copy app sources COPY $REMOTE_SOURCES $REMOTE_SOURCES_DIR @@ -17,7 +17,7 @@ RUN test -d ${REMOTE_SOURCES_DIR}/cachito-gomod-with-deps || exit 1; \ && make build-frontend -FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.24-openshift-4.21 AS go-builder +FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.24-openshift-4.22 AS go-builder COPY $REMOTE_SOURCES $REMOTE_SOURCES_DIR WORKDIR $REMOTE_SOURCES_DIR/cachito-gomod-with-deps/app @@ -28,7 +28,7 @@ ENV CGO_ENABLED=1 RUN source $REMOTE_SOURCES_DIR/cachito-gomod-with-deps/cachito.env \ && make build-backend BUILD_OPTS="-tags strictfipsruntime" -FROM registry.ci.openshift.org/ocp/4.21:base-rhel9 +FROM registry.ci.openshift.org/ocp/4.22:base-rhel9 USER 1001 From 15d26cdb9f9eb91f584bc0c7b1bc5644e8de1282 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Mon, 12 Jan 2026 15:36:29 +0100 Subject: [PATCH 07/15] fix: remove unnecessary package.lock Signed-off-by: Gabriel Bernal --- package-lock.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6325b8a5e..000000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "monitoring-plugin", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} From 6e3b3d843b2afab42938d73dbaae8aa22af4a80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Reme=C5=A1?= Date: Tue, 16 Dec 2025 11:59:52 +0100 Subject: [PATCH 08/15] fix: reuse existing handler to set alerts table data --- .../components/Incidents/IncidentsPage.tsx | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/web/src/components/Incidents/IncidentsPage.tsx b/web/src/components/Incidents/IncidentsPage.tsx index c8557b24e..75f0fb231 100644 --- a/web/src/components/Incidents/IncidentsPage.tsx +++ b/web/src/components/Incidents/IncidentsPage.tsx @@ -131,9 +131,6 @@ const IncidentsPage = () => { (state: MonitoringState) => state.plugins.mcp.incidentsData.incidentsActiveFilters, ); - const alertsData = useSelector( - (state: MonitoringState) => state.plugins.mcp.incidentsData?.alertsData, - ); const alertsAreLoading = useSelector( (state: MonitoringState) => state.plugins.mcp.incidentsData?.alertsAreLoading, ); @@ -239,15 +236,23 @@ const IncidentsPage = () => { ) .then((results) => { const prometheusResults = results.flat(); + const alerts = convertToAlerts( + prometheusResults, + incidentForAlertProcessing, + currentTime, + ); dispatch( setAlertsData({ - alertsData: convertToAlerts( - prometheusResults, - incidentForAlertProcessing, - currentTime, - ), + alertsData: alerts, }), ); + if (rules && alerts) { + dispatch( + setAlertsTableData({ + alertsTableData: groupAlertsForTable(alerts, rules), + }), + ); + } if (!isEmpty(filteredData)) { dispatch(setAlertsAreLoading({ alertsAreLoading: false })); } else { @@ -261,16 +266,6 @@ const IncidentsPage = () => { })(); }, [incidentForAlertProcessing]); - useEffect(() => { - if (rules && alertsData) { - dispatch( - setAlertsTableData({ - alertsTableData: groupAlertsForTable(alertsData, rules), - }), - ); - } - }, [alertsData, rules]); - useEffect(() => { if (!isInitialized) return; From 5c4215bd8db53f4194a723b07e2f5d2ce2e816b3 Mon Sep 17 00:00:00 2001 From: Evelyn Tanigawa Murasaki Date: Mon, 29 Dec 2025 18:54:40 -0300 Subject: [PATCH 09/15] edit perses dashboards --- web/cypress/e2e/coo/01.coo_bvt.cy.ts | 2 + .../e2e/perses/02.coo_edit_perses_admin.cy.ts | 45 + .../openshift-cluster-sample-dashboard.yaml | 0 .../perses-dashboard-sample.yaml | 0 .../perses-datasource-sample.yaml | 14 + .../prometheus-overview-variables.yaml | 0 .../thanos-compact-overview-1var.yaml | 0 .../thanos-querier-datasource.yaml | 0 .../openshift-cluster-sample-dashboard.yaml | 1041 ++++++++++++ .../perses-dashboard-sample.yaml | 564 +++++++ .../prometheus-overview-variables.yaml | 461 ++++++ .../thanos-compact-overview-1var.yaml | 1421 +++++++++++++++++ .../thanos-querier-datasource.yaml | 24 + web/cypress/fixtures/perses/constants.ts | 63 +- .../support/commands/operator-commands.ts | 98 +- .../perses/00.coo_bvt_perses_admin.cy.ts | 12 +- .../perses/00.coo_bvt_perses_admin_1.cy.ts | 27 +- .../perses/02.coo_edit_perses_admin.cy.ts | 517 ++++++ .../perses/02.coo_edit_perses_admin_1.cy.ts | 318 ++++ web/cypress/views/common.ts | 6 + web/cypress/views/list-perses-dashboards.ts | 1 + .../perses-dashboards-edit-datasources.ts | 95 ++ .../views/perses-dashboards-edit-variables.ts | 139 ++ web/cypress/views/perses-dashboards-panel.ts | 116 ++ .../views/perses-dashboards-panelgroup.ts | 96 ++ web/cypress/views/perses-dashboards.ts | 342 +++- web/cypress/views/troubleshooting-panel.ts | 2 + web/src/components/data-test.ts | 66 + 28 files changed, 5381 insertions(+), 89 deletions(-) create mode 100644 web/cypress/e2e/perses/02.coo_edit_perses_admin.cy.ts rename web/cypress/fixtures/coo/{ => coo121_perses_dashboards}/openshift-cluster-sample-dashboard.yaml (100%) rename web/cypress/fixtures/coo/{ => coo121_perses_dashboards}/perses-dashboard-sample.yaml (100%) create mode 100644 web/cypress/fixtures/coo/coo121_perses_dashboards/perses-datasource-sample.yaml rename web/cypress/fixtures/coo/{ => coo121_perses_dashboards}/prometheus-overview-variables.yaml (100%) rename web/cypress/fixtures/coo/{ => coo121_perses_dashboards}/thanos-compact-overview-1var.yaml (100%) rename web/cypress/fixtures/coo/{ => coo121_perses_dashboards}/thanos-querier-datasource.yaml (100%) create mode 100644 web/cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml create mode 100644 web/cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml create mode 100644 web/cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml create mode 100644 web/cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml create mode 100644 web/cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml create mode 100644 web/cypress/support/perses/02.coo_edit_perses_admin.cy.ts create mode 100644 web/cypress/support/perses/02.coo_edit_perses_admin_1.cy.ts create mode 100644 web/cypress/views/perses-dashboards-edit-datasources.ts create mode 100644 web/cypress/views/perses-dashboards-edit-variables.ts create mode 100644 web/cypress/views/perses-dashboards-panel.ts create mode 100644 web/cypress/views/perses-dashboards-panelgroup.ts diff --git a/web/cypress/e2e/coo/01.coo_bvt.cy.ts b/web/cypress/e2e/coo/01.coo_bvt.cy.ts index 3a23242b6..fd997194f 100644 --- a/web/cypress/e2e/coo/01.coo_bvt.cy.ts +++ b/web/cypress/e2e/coo/01.coo_bvt.cy.ts @@ -28,6 +28,8 @@ describe('BVT: COO', { tags: ['@smoke', '@coo'] }, () => { it('1. Admin perspective - Observe Menu', () => { cy.log('Admin perspective - Observe Menu and verify all submenus'); + cy.reload(true); + cy.wait(10000); nav.sidenav.clickNavLink(['Observe', 'Alerting']); commonPages.titleShouldHaveText('Alerting'); nav.tabs.switchTab('Silences'); diff --git a/web/cypress/e2e/perses/02.coo_edit_perses_admin.cy.ts b/web/cypress/e2e/perses/02.coo_edit_perses_admin.cy.ts new file mode 100644 index 000000000..014174163 --- /dev/null +++ b/web/cypress/e2e/perses/02.coo_edit_perses_admin.cy.ts @@ -0,0 +1,45 @@ +import { nav } from '../../views/nav'; +import { runCOOEditPersesTests1 } from '../../support/perses/02.coo_edit_perses_admin_1.cy'; +import { runCOOEditPersesTests } from '../../support/perses/02.coo_edit_perses_admin.cy'; + +// Set constants for the operators that need to be installed for tests. +const MCP = { + namespace: 'openshift-cluster-observability-operator', + packageName: 'cluster-observability-operator', + operatorName: 'Cluster Observability Operator', + config: { + kind: 'UIPlugin', + name: 'monitoring', + }, +}; + +const MP = { + namespace: 'openshift-monitoring', + operatorName: 'Cluster Monitoring Operator', +}; + +//TODO: change tag to @dashboards when customizable-dashboards gets merged +describe('COO - Dashboards (Perses) - Edit perses dashboard', { tags: ['@perses', '@dashboards-'] }, () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(5000); + cy.changeNamespace('All Projects'); + }); + + runCOOEditPersesTests({ + name: 'Administrator', + }); + + runCOOEditPersesTests1({ + name: 'Administrator', + }); + +}); + + + diff --git a/web/cypress/fixtures/coo/openshift-cluster-sample-dashboard.yaml b/web/cypress/fixtures/coo/coo121_perses_dashboards/openshift-cluster-sample-dashboard.yaml similarity index 100% rename from web/cypress/fixtures/coo/openshift-cluster-sample-dashboard.yaml rename to web/cypress/fixtures/coo/coo121_perses_dashboards/openshift-cluster-sample-dashboard.yaml diff --git a/web/cypress/fixtures/coo/perses-dashboard-sample.yaml b/web/cypress/fixtures/coo/coo121_perses_dashboards/perses-dashboard-sample.yaml similarity index 100% rename from web/cypress/fixtures/coo/perses-dashboard-sample.yaml rename to web/cypress/fixtures/coo/coo121_perses_dashboards/perses-dashboard-sample.yaml diff --git a/web/cypress/fixtures/coo/coo121_perses_dashboards/perses-datasource-sample.yaml b/web/cypress/fixtures/coo/coo121_perses_dashboards/perses-datasource-sample.yaml new file mode 100644 index 000000000..cf7df0241 --- /dev/null +++ b/web/cypress/fixtures/coo/coo121_perses_dashboards/perses-datasource-sample.yaml @@ -0,0 +1,14 @@ +apiVersion: perses.dev/v1alpha1 +kind: PersesDatasource +metadata: + name: perses-datasource-sample + namespace: perses-dev +spec: + config: + display: + name: 'Default Datasource' + default: true + plugin: + kind: 'PrometheusDatasource' + spec: + directUrl: 'https://prometheus.demo.prometheus.io' \ No newline at end of file diff --git a/web/cypress/fixtures/coo/prometheus-overview-variables.yaml b/web/cypress/fixtures/coo/coo121_perses_dashboards/prometheus-overview-variables.yaml similarity index 100% rename from web/cypress/fixtures/coo/prometheus-overview-variables.yaml rename to web/cypress/fixtures/coo/coo121_perses_dashboards/prometheus-overview-variables.yaml diff --git a/web/cypress/fixtures/coo/thanos-compact-overview-1var.yaml b/web/cypress/fixtures/coo/coo121_perses_dashboards/thanos-compact-overview-1var.yaml similarity index 100% rename from web/cypress/fixtures/coo/thanos-compact-overview-1var.yaml rename to web/cypress/fixtures/coo/coo121_perses_dashboards/thanos-compact-overview-1var.yaml diff --git a/web/cypress/fixtures/coo/thanos-querier-datasource.yaml b/web/cypress/fixtures/coo/coo121_perses_dashboards/thanos-querier-datasource.yaml similarity index 100% rename from web/cypress/fixtures/coo/thanos-querier-datasource.yaml rename to web/cypress/fixtures/coo/coo121_perses_dashboards/thanos-querier-datasource.yaml diff --git a/web/cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml b/web/cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml new file mode 100644 index 000000000..e8c14b903 --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml @@ -0,0 +1,1041 @@ +apiVersion: perses.dev/v1alpha2 +kind: PersesDashboard +metadata: + name: openshift-cluster-sample-dashboard + namespace: openshift-cluster-observability-operator +spec: + config: + display: + name: Kubernetes / Compute Resources / Cluster + variables: + - kind: ListVariable + spec: + display: + hidden: false + allowAllValue: false + allowMultiple: false + sort: alphabetical-asc + plugin: + kind: PrometheusLabelValuesVariable + spec: + labelName: cluster + matchers: + - up{job="kubelet", metrics_path="/metrics/cadvisor"} + name: cluster + panels: + "0_0": + kind: Panel + spec: + display: + name: CPU Utilisation + plugin: + kind: StatChart + spec: + calculation: mean + format: + unit: percent-decimal + thresholds: + steps: + - color: green + value: 0 + - color: red + value: 80 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: cluster:node_cpu:ratio_rate5m{cluster="$cluster"} + "0_1": + kind: Panel + spec: + display: + name: CPU Requests Commitment + plugin: + kind: StatChart + spec: + calculation: mean + format: + unit: percent-decimal + thresholds: + steps: + - color: green + value: 0 + - color: red + value: 80 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(namespace_cpu:kube_pod_container_resource_requests:sum{cluster="$cluster"}) / sum(kube_node_status_allocatable{job="kube-state-metrics",resource="cpu",cluster="$cluster"}) + "0_2": + kind: Panel + spec: + display: + name: CPU Limits Commitment + plugin: + kind: StatChart + spec: + calculation: mean + format: + unit: percent-decimal + thresholds: + steps: + - color: green + value: 0 + - color: red + value: 80 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(namespace_cpu:kube_pod_container_resource_limits:sum{cluster="$cluster"}) / sum(kube_node_status_allocatable{job="kube-state-metrics",resource="cpu",cluster="$cluster"}) + "0_3": + kind: Panel + spec: + display: + name: Memory Utilisation + plugin: + kind: StatChart + spec: + calculation: mean + format: + unit: percent-decimal + thresholds: + steps: + - color: green + value: 0 + - color: red + value: 80 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: 1 - sum(:node_memory_MemAvailable_bytes:sum{cluster="$cluster"}) / sum(node_memory_MemTotal_bytes{job="node-exporter",cluster="$cluster"}) + "0_4": + kind: Panel + spec: + display: + name: Memory Requests Commitment + plugin: + kind: StatChart + spec: + calculation: mean + format: + unit: percent-decimal + thresholds: + steps: + - color: green + value: 0 + - color: red + value: 80 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(namespace_memory:kube_pod_container_resource_requests:sum{cluster="$cluster"}) / sum(kube_node_status_allocatable{job="kube-state-metrics",resource="memory",cluster="$cluster"}) + "0_5": + kind: Panel + spec: + display: + name: Memory Limits Commitment + plugin: + kind: StatChart + spec: + calculation: mean + format: + unit: percent-decimal + thresholds: + steps: + - color: green + value: 0 + - color: red + value: 80 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(namespace_memory:kube_pod_container_resource_limits:sum{cluster="$cluster"}) / sum(kube_node_status_allocatable{job="kube-state-metrics",resource="memory",cluster="$cluster"}) + "1_0": + kind: Panel + spec: + display: + name: CPU Usage + plugin: + kind: TimeSeriesChart + spec: + legend: + mode: list + position: bottom + values: [] + visual: + areaOpacity: 1 + connectNulls: false + display: line + lineWidth: 0.25 + stack: all + yAxis: + format: + unit: decimal + min: 0 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate{cluster="$cluster"}) by (namespace) + seriesNameFormat: "{{namespace}}" + "2_0": + kind: Panel + spec: + display: + name: CPU Quota + plugin: + kind: Table + spec: + columnSettings: + - header: Time + hide: true + name: Time + - header: Pods + name: "Value #A" + - header: Workloads + name: "Value #B" + - header: CPU Usage + name: "Value #C" + - header: CPU Requests + name: "Value #D" + - header: CPU Requests % + name: "Value #E" + - header: CPU Limits + name: "Value #F" + - header: CPU Limits % + name: "Value #G" + - header: Namespace + name: namespace + - header: "" + name: /.*/ + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(kube_pod_owner{job="kube-state-metrics", cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: count(avg(namespace_workload_pod:kube_pod_owner:relabel{cluster="$cluster"}) by (workload, namespace)) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(namespace_cpu:kube_pod_container_resource_requests:sum{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate{cluster="$cluster"}) by (namespace) / sum(namespace_cpu:kube_pod_container_resource_requests:sum{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(namespace_cpu:kube_pod_container_resource_limits:sum{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate{cluster="$cluster"}) by (namespace) / sum(namespace_cpu:kube_pod_container_resource_limits:sum{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + "3_0": + kind: Panel + spec: + display: + name: Memory Usage (w/o cache) + plugin: + kind: TimeSeriesChart + spec: + legend: + mode: list + position: bottom + values: [] + visual: + areaOpacity: 1 + connectNulls: false + display: line + lineWidth: 0.25 + stack: all + yAxis: + format: + unit: bytes + min: 0 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(container_memory_rss{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", container!=""}) by (namespace) + seriesNameFormat: "{{namespace}}" + "4_0": + kind: Panel + spec: + display: + name: Requests by Namespace + plugin: + kind: Table + spec: + columnSettings: + - header: Time + hide: true + name: Time + - header: Pods + name: "Value #A" + - header: Workloads + name: "Value #B" + - header: Memory Usage + name: "Value #C" + - header: Memory Requests + name: "Value #D" + - header: Memory Requests % + name: "Value #E" + - header: Memory Limits + name: "Value #F" + - header: Memory Limits % + name: "Value #G" + - header: Namespace + name: namespace + - header: "" + name: /.*/ + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(kube_pod_owner{job="kube-state-metrics", cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: count(avg(namespace_workload_pod:kube_pod_owner:relabel{cluster="$cluster"}) by (workload, namespace)) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(container_memory_rss{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", container!=""}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(namespace_memory:kube_pod_container_resource_requests:sum{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(container_memory_rss{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", container!=""}) by (namespace) / sum(namespace_memory:kube_pod_container_resource_requests:sum{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(namespace_memory:kube_pod_container_resource_limits:sum{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(container_memory_rss{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", container!=""}) by (namespace) / sum(namespace_memory:kube_pod_container_resource_limits:sum{cluster="$cluster"}) by (namespace) + seriesNameFormat: "" + "5_0": + kind: Panel + spec: + display: + name: Current Network Usage + plugin: + kind: Table + spec: + columnSettings: + - header: Time + hide: true + name: Time + - header: Current Receive Bandwidth + name: "Value #A" + - header: Current Transmit Bandwidth + name: "Value #B" + - header: Rate of Received Packets + name: "Value #C" + - header: Rate of Transmitted Packets + name: "Value #D" + - header: Rate of Received Packets Dropped + name: "Value #E" + - header: Rate of Transmitted Packets Dropped + name: "Value #F" + - header: Namespace + name: namespace + - header: "" + name: /.*/ + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_receive_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_transmit_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_receive_packets_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_transmit_packets_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_receive_packets_dropped_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_transmit_packets_dropped_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "" + "6_0": + kind: Panel + spec: + display: + name: Receive Bandwidth + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_receive_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "{{namespace}}" + "6_1": + kind: Panel + spec: + display: + name: Transmit Bandwidth + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_transmit_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "{{namespace}}" + "7_0": + kind: Panel + spec: + display: + name: "Average Container Bandwidth by Namespace: Received" + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: avg(irate(container_network_receive_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "{{namespace}}" + "7_1": + kind: Panel + spec: + display: + name: "Average Container Bandwidth by Namespace: Transmitted" + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: avg(irate(container_network_transmit_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "{{namespace}}" + "8_0": + kind: Panel + spec: + display: + name: Rate of Received Packets + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_receive_packets_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "{{namespace}}" + "8_1": + kind: Panel + spec: + display: + name: Rate of Transmitted Packets + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_transmit_packets_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "{{namespace}}" + "9_0": + kind: Panel + spec: + display: + name: Rate of Received Packets Dropped + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_receive_packets_dropped_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "{{namespace}}" + "9_1": + kind: Panel + spec: + display: + name: Rate of Transmitted Packets Dropped + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum(irate(container_network_transmit_packets_dropped_total{job="kubelet", metrics_path="/metrics/cadvisor", cluster="$cluster", namespace=~".+"}[$__rate_interval])) by (namespace) + seriesNameFormat: "{{namespace}}" + "10_0": + kind: Panel + spec: + display: + name: IOPS(Reads+Writes) + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: ceil(sum by(namespace) (rate(container_fs_reads_total{job="kubelet", metrics_path="/metrics/cadvisor", id!="", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", cluster="$cluster", namespace!=""}[$__rate_interval]) + rate(container_fs_writes_total{job="kubelet", metrics_path="/metrics/cadvisor", id!="", cluster="$cluster", namespace!=""}[$__rate_interval]))) + seriesNameFormat: "{{namespace}}" + "10_1": + kind: Panel + spec: + display: + name: ThroughPut(Read+Write) + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by(namespace) (rate(container_fs_reads_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", id!="", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", cluster="$cluster", namespace!=""}[$__rate_interval]) + rate(container_fs_writes_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", id!="", cluster="$cluster", namespace!=""}[$__rate_interval])) + seriesNameFormat: "{{namespace}}" + "11_0": + kind: Panel + spec: + display: + name: Current Storage IO + plugin: + kind: Table + spec: + columnSettings: + - header: Time + hide: true + name: Time + - header: IOPS(Reads) + name: "Value #A" + - header: IOPS(Writes) + name: "Value #B" + - header: IOPS(Reads + Writes) + name: "Value #C" + - header: Throughput(Read) + name: "Value #D" + - header: Throughput(Write) + name: "Value #E" + - header: Throughput(Read + Write) + name: "Value #F" + - header: Namespace + name: namespace + - header: "" + name: /.*/ + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by(namespace) (rate(container_fs_reads_total{job="kubelet", metrics_path="/metrics/cadvisor", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", id!="", cluster="$cluster", namespace!=""}[$__rate_interval])) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by(namespace) (rate(container_fs_writes_total{job="kubelet", metrics_path="/metrics/cadvisor", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", id!="", cluster="$cluster", namespace!=""}[$__rate_interval])) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by(namespace) (rate(container_fs_reads_total{job="kubelet", metrics_path="/metrics/cadvisor", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", id!="", cluster="$cluster", namespace!=""}[$__rate_interval]) + rate(container_fs_writes_total{job="kubelet", metrics_path="/metrics/cadvisor", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", id!="", cluster="$cluster", namespace!=""}[$__rate_interval])) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by(namespace) (rate(container_fs_reads_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", id!="", cluster="$cluster", namespace!=""}[$__rate_interval])) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by(namespace) (rate(container_fs_writes_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", id!="", cluster="$cluster", namespace!=""}[$__rate_interval])) + seriesNameFormat: "" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by(namespace) (rate(container_fs_reads_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", id!="", cluster="$cluster", namespace!=""}[$__rate_interval]) + rate(container_fs_writes_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", device=~"(/dev.+)|mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+", id!="", cluster="$cluster", namespace!=""}[$__rate_interval])) + seriesNameFormat: "" + layouts: + - kind: Grid + spec: + display: + title: Headlines + collapse: + open: true + items: + - x: 0 + "y": 1 + width: 4 + height: 3 + content: + $ref: "#/spec/panels/0_0" + - x: 4 + "y": 1 + width: 4 + height: 3 + content: + $ref: "#/spec/panels/0_1" + - x: 8 + "y": 1 + width: 4 + height: 3 + content: + $ref: "#/spec/panels/0_2" + - x: 12 + "y": 1 + width: 4 + height: 3 + content: + $ref: "#/spec/panels/0_3" + - x: 16 + "y": 1 + width: 4 + height: 3 + content: + $ref: "#/spec/panels/0_4" + - x: 20 + "y": 1 + width: 4 + height: 3 + content: + $ref: "#/spec/panels/0_5" + - kind: Grid + spec: + display: + title: CPU + collapse: + open: true + items: + - x: 0 + "y": 5 + width: 24 + height: 7 + content: + $ref: "#/spec/panels/1_0" + - kind: Grid + spec: + display: + title: CPU Quota + collapse: + open: true + items: + - x: 0 + "y": 13 + width: 24 + height: 7 + content: + $ref: "#/spec/panels/2_0" + - kind: Grid + spec: + display: + title: Memory + collapse: + open: true + items: + - x: 0 + "y": 21 + width: 24 + height: 7 + content: + $ref: "#/spec/panels/3_0" + - kind: Grid + spec: + display: + title: Memory Requests + collapse: + open: true + items: + - x: 0 + "y": 29 + width: 24 + height: 7 + content: + $ref: "#/spec/panels/4_0" + - kind: Grid + spec: + display: + title: Current Network Usage + collapse: + open: true + items: + - x: 0 + "y": 37 + width: 24 + height: 7 + content: + $ref: "#/spec/panels/5_0" + - kind: Grid + spec: + display: + title: Bandwidth + collapse: + open: true + items: + - x: 0 + "y": 45 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/6_0" + - x: 12 + "y": 45 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/6_1" + - kind: Grid + spec: + display: + title: Average Container Bandwidth by Namespace + collapse: + open: true + items: + - x: 0 + "y": 53 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/7_0" + - x: 12 + "y": 53 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/7_1" + - kind: Grid + spec: + display: + title: Rate of Packets + collapse: + open: true + items: + - x: 0 + "y": 61 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/8_0" + - x: 12 + "y": 61 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/8_1" + - kind: Grid + spec: + display: + title: Rate of Packets Dropped + collapse: + open: true + items: + - x: 0 + "y": 69 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/9_0" + - x: 12 + "y": 69 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/9_1" + - kind: Grid + spec: + display: + title: Storage IO + collapse: + open: true + items: + - x: 0 + "y": 77 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/10_0" + - x: 12 + "y": 77 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/10_1" + - kind: Grid + spec: + display: + title: Storage IO - Distribution + collapse: + open: true + items: + - x: 0 + "y": 85 + width: 24 + height: 7 + content: + $ref: "#/spec/panels/11_0" + duration: 1h \ No newline at end of file diff --git a/web/cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml b/web/cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml new file mode 100644 index 000000000..43e3dfe4e --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml @@ -0,0 +1,564 @@ +apiVersion: perses.dev/v1alpha2 +kind: PersesDashboard +metadata: + name: perses-dashboard-sample + namespace: perses-dev +spec: + config: + display: + name: Perses Dashboard Sample + description: This is a sample dashboard + duration: 5m + datasources: + PrometheusLocal: + default: false + plugin: + kind: PrometheusDatasource + spec: + proxy: + kind: HTTPProxy + spec: + url: http://localhost:9090 + variables: + - kind: ListVariable + spec: + name: job + allowMultiple: false + allowAllValue: false + plugin: + kind: PrometheusLabelValuesVariable + spec: + labelName: job + - kind: ListVariable + spec: + name: instance + allowMultiple: false + allowAllValue: false + plugin: + kind: PrometheusLabelValuesVariable + spec: + labelName: instance + matchers: + - up{job=~"$job"} + - kind: ListVariable + spec: + name: interval + plugin: + kind: StaticListVariable + spec: + values: + - 1m + - 5m + - kind: TextVariable + spec: + name: text + value: test + constant: true + panels: + defaultTimeSeriesChart: + kind: Panel + spec: + display: + name: Default Time Series Panel + plugin: + kind: TimeSeriesChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: up + seriesTest: + kind: Panel + spec: + display: + name: "~130 Series" + description: This is a line chart + plugin: + kind: TimeSeriesChart + spec: + yAxis: + format: + unit: bytes + shortValues: true + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: rate(caddy_http_response_duration_seconds_sum[$interval]) + basicEx: + kind: Panel + spec: + display: + name: Single Query + plugin: + kind: TimeSeriesChart + spec: + yAxis: + format: + unit: decimal + legend: + position: right + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + seriesNameFormat: Node memory - {{device}} {{instance}} + query: + 1 - node_filesystem_free_bytes{job='$job',instance=~'$instance',fstype!="rootfs",mountpoint!~"/(run|var).*",mountpoint!=""} + / node_filesystem_size_bytes{job='$job',instance=~'$instance'} + legendEx: + kind: Panel + spec: + display: + name: Legend Example + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + yAxis: + show: true + format: + unit: bytes + shortValues: true + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + seriesNameFormat: Node memory total + query: + node_memory_MemTotal_bytes{job='$job',instance=~'$instance'} + - node_memory_MemFree_bytes{job='$job',instance=~'$instance'} - + node_memory_Buffers_bytes{job='$job',instance=~'$instance'} - node_memory_Cached_bytes{job='$job',instance=~'$instance'} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + seriesNameFormat: Memory (buffers) - {{instance}} + query: node_memory_Buffers_bytes{job='$job',instance=~'$instance'} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + seriesNameFormat: Cached Bytes + query: node_memory_Cached_bytes{job='$job',instance=~'$instance'} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + seriesNameFormat: MemFree Bytes + query: node_memory_MemFree_bytes{job='$job',instance=~'$instance'} + testNodeQuery: + kind: Panel + spec: + display: + name: Test Query + description: Description text + plugin: + kind: TimeSeriesChart + spec: + yAxis: + format: + unit: decimal + decimalPlaces: 2 + legend: + position: right + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: node_load15{instance=~"(demo.do.prometheus.io:9100)",job='$job'} + seriesNameFormat: Test {{job}} {{instance}} + testQueryAlt: + kind: Panel + spec: + display: + name: Test Query Alt + description: Description text + plugin: + kind: TimeSeriesChart + spec: + legend: + position: right + yAxis: + format: + unit: percent-decimal + decimalPlaces: 1 + thresholds: + steps: + - value: 0.4 + name: "Alert: Warning condition example" + - value: 0.75 + name: "Alert: Critical condition example" + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: node_load1{instance=~"(demo.do.prometheus.io:9100)",job='$job'} + cpuLine: + kind: Panel + spec: + display: + name: CPU - Line (Multi Series) + description: This is a line chart test + plugin: + kind: TimeSeriesChart + spec: + yAxis: + show: false + label: CPU Label + format: + unit: percent-decimal + decimalPlaces: 0 + legend: + position: bottom + thresholds: + steps: + - value: 0.2 + - value: 0.35 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + seriesNameFormat: "{{mode}} mode - {{job}} {{instance}}" + query: avg without (cpu)(rate(node_cpu_seconds_total{job='$job',instance=~'$instance',mode!="nice",mode!="steal",mode!="irq"}[$interval])) + cpuGauge: + kind: Panel + spec: + display: + name: CPU - Gauge (Multi Series) + description: This is a gauge chart test + plugin: + kind: GaugeChart + spec: + calculation: last-number + format: + unit: percent-decimal + thresholds: + steps: + - value: 0.2 + - value: 0.35 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + seriesNameFormat: "{{mode}} mode - {{job}} {{instance}}" + query: avg without (cpu)(rate(node_cpu_seconds_total{job='$job',instance=~'$instance',mode!="nice",mode!="steal",mode!="irq"}[$interval])) + statSm: + kind: Panel + spec: + display: + name: Stat Sm + plugin: + kind: StatChart + spec: + calculation: mean + format: + unit: decimal + decimalPlaces: 1 + shortValues: true + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: node_time_seconds{job='$job',instance=~'$instance'} - node_boot_time_seconds{job='$job',instance=~'$instance'} + gaugeRAM: + kind: Panel + spec: + display: + name: RAM Used + description: This is a stat chart + plugin: + kind: GaugeChart + spec: + calculation: last-number + format: + unit: percent + thresholds: + steps: + - value: 85 + - value: 95 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: + 100 - ((node_memory_MemAvailable_bytes{job='$job',instance=~'$instance'} + * 100) / node_memory_MemTotal_bytes{job='$job',instance=~'$instance'}) + statRAM: + kind: Panel + spec: + display: + name: RAM Used + description: This is a stat chart + plugin: + kind: StatChart + spec: + calculation: last-number + format: + unit: percent + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: + 100 - ((node_memory_MemAvailable_bytes{job='$job',instance=~'$instance'} + * 100) / node_memory_MemTotal_bytes{job='$job',instance=~'$instance'}) + statTotalRAM: + kind: Panel + spec: + display: + name: RAM Total + description: This is a stat chart + plugin: + kind: StatChart + spec: + calculation: last-number + format: + unit: bytes + decimalPlaces: 1 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: node_memory_MemTotal_bytes{job='$job',instance=~'$instance'} + statMd: + kind: Panel + spec: + display: + name: Stat Md + plugin: + kind: StatChart + spec: + calculation: sum + format: + unit: decimal + decimalPlaces: 2 + shortValues: true + sparkline: + color: "#e65013" + width: 1.5 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: + avg(node_load15{job='node',instance=~'$instance'}) / count(count(node_cpu_seconds_total{job='node',instance=~'$instance'}) + by (cpu)) * 100 + statLg: + kind: Panel + spec: + display: + name: Stat Lg + description: This is a stat chart + plugin: + kind: StatChart + spec: + calculation: mean + format: + unit: percent + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: + (((count(count(node_cpu_seconds_total{job='$job',instance=~'$instance'}) + by (cpu))) - avg(sum by (mode)(rate(node_cpu_seconds_total{mode="idle",job='$job',instance=~'$instance'}[$interval])))) + * 100) / count(count(node_cpu_seconds_total{job='$job',instance=~'$instance'}) + by (cpu)) + gaugeEx: + kind: Panel + spec: + display: + name: Gauge Ex + description: This is a gauge chart + plugin: + kind: GaugeChart + spec: + calculation: last-number + format: + unit: percent + thresholds: + steps: + - value: 85 + - value: 95 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: + (((count(count(node_cpu_seconds_total{job='$job',instance=~'$instance'}) + by (cpu))) - avg(sum by (mode)(rate(node_cpu_seconds_total{mode="idle",job='$job',instance=~'$instance'}[$interval])))) + * 100) / count(count(node_cpu_seconds_total{job='$job',instance=~'$instance'}) + by (cpu)) + gaugeAltEx: + kind: Panel + spec: + display: + name: Gauge Alt Ex + description: GaugeChart description text + plugin: + kind: GaugeChart + spec: + calculation: last-number + format: + unit: percent-decimal + decimalPlaces: 1 + thresholds: + steps: + - value: 0.5 + name: "Alert: Warning condition example" + - value: 0.75 + name: "Alert: Critical condition example" + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: node_load15{instance=~'$instance',job='$job'} + gaugeFormatTest: + kind: Panel + spec: + display: + name: Gauge Format Test + plugin: + kind: GaugeChart + spec: + calculation: last-number + format: + unit: bytes + max: 95000000 + thresholds: + steps: + - value: 71000000 + - value: 82000000 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: node_time_seconds{job='$job',instance=~'$instance'} - node_boot_time_seconds{job='$job',instance=~'$instance'} + layouts: + - kind: Grid + spec: + display: + title: Row 1 + collapse: + open: true + items: + - x: 0 + "y": 0 + width: 2 + height: 3 + content: + "$ref": "#/spec/panels/statRAM" + - x: 0 + "y": 4 + width: 2 + height: 3 + content: + "$ref": "#/spec/panels/statTotalRAM" + - x: 2 + "y": 0 + width: 4 + height: 6 + content: + "$ref": "#/spec/panels/statMd" + - x: 6 + "y": 0 + width: 10 + height: 6 + content: + "$ref": "#/spec/panels/statLg" + - x: 16 + "y": 0 + width: 4 + height: 6 + content: + "$ref": "#/spec/panels/gaugeFormatTest" + - x: 20 + "y": 0 + width: 4 + height: 6 + content: + "$ref": "#/spec/panels/gaugeRAM" + - kind: Grid + spec: + display: + title: Row 2 + collapse: + open: true + items: + - x: 0 + "y": 0 + width: 12 + height: 6 + content: + "$ref": "#/spec/panels/legendEx" + - x: 12 + "y": 0 + width: 12 + height: 6 + content: + "$ref": "#/spec/panels/basicEx" + - kind: Grid + spec: + display: + title: Row 3 + collapse: + open: false + items: + - x: 0 + "y": 0 + width: 24 + height: 6 + content: + "$ref": "#/spec/panels/cpuGauge" + - x: 0 + "y": 6 + width: 12 + height: 8 + content: + "$ref": "#/spec/panels/cpuLine" + - x: 12 + "y": 0 + width: 12 + height: 8 + content: + "$ref": "#/spec/panels/defaultTimeSeriesChart" \ No newline at end of file diff --git a/web/cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml b/web/cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml new file mode 100644 index 000000000..6c8c4439a --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml @@ -0,0 +1,461 @@ +apiVersion: perses.dev/v1alpha2 +kind: PersesDashboard +metadata: + name: prometheus-overview + namespace: perses-dev +spec: + config: + display: + name: Prometheus / Overview + variables: + - kind: ListVariable + spec: + display: + name: job + hidden: false + allowAllValue: false + allowMultiple: false + plugin: + kind: PrometheusLabelValuesVariable + spec: + datasource: + kind: PrometheusDatasource + + labelName: job + matchers: + - prometheus_build_info{} + name: job + - kind: ListVariable + spec: + display: + name: instance + hidden: false + allowAllValue: false + allowMultiple: false + plugin: + kind: PrometheusLabelValuesVariable + spec: + datasource: + kind: PrometheusDatasource + + labelName: instance + matchers: + - prometheus_build_info{job="$job"} + name: instance + panels: + "0_0": + kind: Panel + spec: + display: + name: Prometheus Stats + plugin: + kind: Table + spec: + columnSettings: + - name: job + header: Job + - name: instance + header: Instance + - name: version + header: Version + - name: value + hide: true + - name: timestamp + hide: true + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: count by (job, instance, version) (prometheus_build_info{instance=~"$instance",job=~"$job"}) + "1_0": + kind: Panel + spec: + display: + name: Target Sync + description: Monitors target synchronization time for Prometheus instances + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: seconds + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (job, scrape_job, instance) ( + rate(prometheus_target_sync_length_seconds_sum{instance=~"$instance",job=~"$job"}[$__rate_interval]) + ) + seriesNameFormat: "{{job}} - {{instance}} - Metrics" + "1_1": + kind: Panel + spec: + display: + name: Targets + description: Shows discovered targets across Prometheus instances + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by (job, instance) (prometheus_sd_discovered_targets{instance=~"$instance",job=~"$job"}) + seriesNameFormat: "{{job}} - {{instance}} - Metrics" + "2_0": + kind: Panel + spec: + display: + name: Average Scrape Interval Duration + description: Shows average interval between scrapes for Prometheus targets + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: seconds + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |4- + rate( + prometheus_target_interval_length_seconds_sum{instance=~"$instance",job=~"$job"}[$__rate_interval] + ) + / + rate( + prometheus_target_interval_length_seconds_count{instance=~"$instance",job=~"$job"}[$__rate_interval] + ) + seriesNameFormat: "{{job}} - {{instance}} - {{interval}} Configured" + "2_1": + kind: Panel + spec: + display: + name: Scrape failures + description: Shows scrape failure metrics for Prometheus targets + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (job, instance) ( + rate( + prometheus_target_scrapes_exceeded_body_size_limit_total{instance=~"$instance",job=~"$job"}[$__rate_interval] + ) + ) + seriesNameFormat: "exceeded body size limit: {{job}} - {{instance}} - Metrics" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (job, instance) ( + rate( + prometheus_target_scrapes_exceeded_sample_limit_total{instance=~"$instance",job=~"$job"}[$__rate_interval] + ) + ) + seriesNameFormat: "exceeded sample limit: {{job}} - {{instance}} - Metrics" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (job, instance) ( + rate( + prometheus_target_scrapes_sample_duplicate_timestamp_total{instance=~"$instance",job=~"$job"}[$__rate_interval] + ) + ) + seriesNameFormat: "duplicate timestamp: {{job}} - {{instance}} - Metrics" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (job, instance) ( + rate( + prometheus_target_scrapes_sample_out_of_bounds_total{instance=~"$instance",job=~"$job"}[$__rate_interval] + ) + ) + seriesNameFormat: "out of bounds: {{job}} - {{instance}} - Metrics" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (job, instance) ( + rate( + prometheus_target_scrapes_sample_out_of_order_total{instance=~"$instance",job=~"$job"}[$__rate_interval] + ) + ) + seriesNameFormat: "out of order: {{job}} - {{instance}} - Metrics" + "2_2": + kind: Panel + spec: + display: + name: Appended Samples + description: Shows rate of samples appended to Prometheus TSDB + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + rate( + prometheus_tsdb_head_samples_appended_total{instance=~"$instance",job=~"$job"}[$__rate_interval] + ) + seriesNameFormat: "{{job}} - {{instance}} - {{remote_name}} - {{url}}" + "3_0": + kind: Panel + spec: + display: + name: Head Series + description: Shows number of series in Prometheus TSDB head + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: prometheus_tsdb_head_series{instance=~"$instance",job=~"$job"} + seriesNameFormat: "{{job}} - {{instance}} - Head Series" + "3_1": + kind: Panel + spec: + display: + name: Head Chunks + description: Shows number of chunks in Prometheus TSDB head + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: prometheus_tsdb_head_chunks{instance=~"$instance",job=~"$job"} + seriesNameFormat: "{{job}} - {{instance}} - Head Chunks" + "4_0": + kind: Panel + spec: + display: + name: Query Rate + description: Shows Prometheus query rate metrics + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + rate( + prometheus_engine_query_duration_seconds_count{instance=~"$instance",job=~"$job",slice="inner_eval"}[$__rate_interval] + ) + seriesNameFormat: "{{job}} - {{instance}} - Query Rate" + "4_1": + kind: Panel + spec: + display: + name: Stage Duration + description: Shows duration of different Prometheus query stages + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: seconds + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + max by (slice) ( + prometheus_engine_query_duration_seconds{instance=~"$instance",job=~"$job",quantile="0.9"} + ) + seriesNameFormat: "{{slice}} - Duration" + layouts: + - kind: Grid + spec: + display: + title: Prometheus Stats + items: + - x: 0 + "y": 0 + width: 24 + height: 8 + content: + $ref: "#/spec/panels/0_0" + - kind: Grid + spec: + display: + title: Discovery + items: + - x: 0 + "y": 0 + width: 12 + height: 8 + content: + $ref: "#/spec/panels/1_0" + - x: 12 + "y": 0 + width: 12 + height: 8 + content: + $ref: "#/spec/panels/1_1" + - kind: Grid + spec: + display: + title: Retrieval + items: + - x: 0 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/2_0" + - x: 8 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/2_1" + - x: 16 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/2_2" + - kind: Grid + spec: + display: + title: Storage + items: + - x: 0 + "y": 0 + width: 12 + height: 8 + content: + $ref: "#/spec/panels/3_0" + - x: 12 + "y": 0 + width: 12 + height: 8 + content: + $ref: "#/spec/panels/3_1" + - kind: Grid + spec: + display: + title: Query + items: + - x: 0 + "y": 0 + width: 12 + height: 8 + content: + $ref: "#/spec/panels/4_0" + - x: 12 + "y": 0 + width: 12 + height: 8 + content: + $ref: "#/spec/panels/4_1" + duration: 1h \ No newline at end of file diff --git a/web/cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml b/web/cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml new file mode 100644 index 000000000..caa580603 --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml @@ -0,0 +1,1421 @@ +apiVersion: perses.dev/v1alpha2 +kind: PersesDashboard +metadata: + name: thanos-compact-overview + namespace: perses-dev +spec: + config: + display: + name: Thanos / Compact / Overview + variables: + - kind: ListVariable + spec: + display: + name: job + hidden: false + allowAllValue: false + allowMultiple: true + plugin: + kind: PrometheusLabelValuesVariable + spec: + datasource: + kind: PrometheusDatasource + + labelName: job + matchers: + - thanos_build_info{container="thanos-compact"} + name: job + - kind: ListVariable + spec: + display: + name: namespace + hidden: false + allowAllValue: false + allowMultiple: false + plugin: + kind: PrometheusLabelValuesVariable + spec: + datasource: + kind: PrometheusDatasource + + labelName: namespace + matchers: + - thanos_status{} + name: namespace + panels: + "0_0": + kind: Panel + spec: + display: + name: TODO Compaction Blocks + description: Shows number of blocks planned to be compacted. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: decimal + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by (namespace, job) (thanos_compact_todo_compaction_blocks{job=~"$job",namespace="$namespace"}) + seriesNameFormat: "{{job}} {{namespace}}" + "0_1": + kind: Panel + spec: + display: + name: TODO Compactions + description: Shows number of compaction operations to be done. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: decimal + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by (namespace, job) (thanos_compact_todo_compactions{job=~"$job",namespace="$namespace"}) + seriesNameFormat: "{{job}} {{namespace}}" + "0_2": + kind: Panel + spec: + display: + name: TODO Deletions + description: Shows number of blocks that have crossed their retention periods. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: decimal + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by (namespace, job) (thanos_compact_todo_deletion_blocks{job=~"$job",namespace="$namespace"}) + seriesNameFormat: "{{job}} {{namespace}}" + "0_3": + kind: Panel + spec: + display: + name: TODO Downsamples + description: Shows number of blocks to be downsampled. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: decimal + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by (namespace, job) (thanos_compact_todo_downsample_blocks{job=~"$job",namespace="$namespace"}) + seriesNameFormat: "{{job}} {{namespace}}" + "1_0": + kind: Panel + spec: + display: + name: Group Compactions + description: Shows rate of execution of compaction operations against blocks in a bucket, split by compaction resolution. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: counts/sec + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (namespace, job, resolution) ( + rate(thanos_compact_group_compactions_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + seriesNameFormat: "Resolution: {{resolution}} - {{job}} {{namespace}}" + "1_1": + kind: Panel + spec: + display: + name: Group Compaction Errors + description: Shows the percentage of errors compared to the total number of executed compaction operations against blocks stored in bucket. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: percent + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |4- + sum by (namespace, job) ( + rate( + thanos_compact_group_compactions_failures_total{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + / + sum by (namespace, job) ( + rate(thanos_compact_group_compactions_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + * + 100 + seriesNameFormat: "{{job}} {{namespace}}" + "2_0": + kind: Panel + spec: + display: + name: Downsample Rate + description: Shows rate of execution of downsample operations against blocks stored in a bucket, split by resolution. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: counts/sec + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (namespace, job, resolution) ( + rate(thanos_compact_downsample_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + seriesNameFormat: "Resolution: {{resolution}} - {{job}} {{namespace}}" + "2_1": + kind: Panel + spec: + display: + name: Downsample Errors + description: Shows the percentage of downsample errors compared to the total number of downsample operations done on block in buckets. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: percent + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |4- + sum by (namespace, job) ( + rate(thanos_compact_downsample_failed_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + / + sum by (namespace, job) ( + rate(thanos_compact_downsample_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + * + 100 + seriesNameFormat: "{{job}} {{namespace}}" + "2_2": + kind: Panel + spec: + display: + name: Downsample Durations + description: Shows the p50, p90, and p99 of the time it takes to complete downsample operation against blocks in a bucket, split by resolution. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: seconds + visual: + display: line + lineWidth: 0.25 + areaOpacity: 0.5 + palette: + mode: auto + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.5, + sum by (namespace, job, resolution, le) ( + rate( + thanos_compact_downsample_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: "p50 Resolution: {{resolution}} - {{job}} {{namespace}}" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.9, + sum by (namespace, job, resolution, le) ( + rate( + thanos_compact_downsample_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: "p90 Resolution: {{resolution}} - {{job}} {{namespace}}" + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.99, + sum by (namespace, job, resolution, le) ( + rate( + thanos_compact_downsample_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: "p99 Resolution: {{resolution}} - {{job}} {{namespace}}" + "3_0": + kind: Panel + spec: + display: + name: Sync Meta Rate + description: Shows rate of syncing block meta files from bucket into memory. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: counts/sec + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (namespace, job) ( + rate(thanos_blocks_meta_syncs_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + seriesNameFormat: "{{job}} {{namespace}}" + "3_1": + kind: Panel + spec: + display: + name: Sync Meta Errors + description: Shows percentage of errors of meta file sync operation compared to total number of meta file syncs from bucket. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: percent + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |4- + sum by (namespace, job) ( + rate(thanos_blocks_meta_sync_failures_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + / + sum by (namespace, job) ( + rate(thanos_blocks_meta_syncs_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + * + 100 + seriesNameFormat: "{{job}} {{namespace}}" + "3_2": + kind: Panel + spec: + display: + name: Sync Meta Durations + description: Shows p50, p90 and p99 durations of the time it takes to sync meta files from blocks in bucket. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: seconds + visual: + display: line + lineWidth: 0.25 + areaOpacity: 0.5 + palette: + mode: auto + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.5, + sum by (namespace, job, le) ( + rate( + thanos_blocks_meta_sync_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p50 {{job}} {{namespace}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.9, + sum by (namespace, job, le) ( + rate( + thanos_blocks_meta_sync_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p90 {{job}} {{namespace}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.99, + sum by (namespace, job, le) ( + rate( + thanos_blocks_meta_sync_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p99 {{job}} {{namespace}} + "4_0": + kind: Panel + spec: + display: + name: Deletion Rate + description: Shows the deletion rate of blocks already marked for deletion. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: counts/sec + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (namespace, job) ( + rate(thanos_compact_blocks_cleaned_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + seriesNameFormat: "{{job}} {{namespace}}" + "4_1": + kind: Panel + spec: + display: + name: Deletion Errors + description: Shows rate of deletion failures for blocks already marked for deletion. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: counts/sec + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (namespace, job) ( + rate( + thanos_compact_block_cleanup_failures_total{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + seriesNameFormat: "{{job}} {{namespace}}" + "4_2": + kind: Panel + spec: + display: + name: Marking Rate + description: Shows the rate at which blocks are marked for deletion (from GC and retention policy). + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: counts/sec + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (namespace, job) ( + rate( + thanos_compact_blocks_marked_total{job=~"$job",marker="deletion-mark.json",namespace="$namespace"}[$__rate_interval] + ) + ) + seriesNameFormat: "{{job}} {{namespace}}" + "5_0": + kind: Panel + spec: + display: + name: Bucket Operations + description: Shows rate of executions of operations against object storage bucket. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: requests/sec + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (namespace, job, operation) ( + rate(thanos_objstore_bucket_operations_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + seriesNameFormat: "{{job}} {{operation}} {{namespace}}" + "5_1": + kind: Panel + spec: + display: + name: Bucket Operation Errors + description: Shows percentage of errors of operations against object storage bucket. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: percent + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |4- + sum by (namespace, job, operation) ( + rate( + thanos_objstore_bucket_operation_failures_total{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + / + sum by (namespace, job, operation) ( + rate(thanos_objstore_bucket_operations_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + * + 100 + seriesNameFormat: "{{job}} {{operation}} {{namespace}}" + "5_2": + kind: Panel + spec: + display: + name: Bucket Operation Latency + description: Shows latency of operations against object storage bucket. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: seconds + visual: + display: line + lineWidth: 0.25 + areaOpacity: 0.5 + palette: + mode: auto + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.99, + sum by (namespace, job, operation, le) ( + rate( + thanos_objstore_bucket_operation_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p99 {{job}} {{operation}} {{namespace}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.9, + sum by (namespace, job, operation, le) ( + rate( + thanos_objstore_bucket_operation_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p90 {{job}} {{operation}} {{namespace}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.5, + sum by (namespace, job, operation, le) ( + rate( + thanos_objstore_bucket_operation_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p50 {{job}} {{operation}} {{namespace}} + "6_0": + kind: Panel + spec: + display: + name: Halted Compactors + description: Shows compactors that have been halted due to unexpected errors. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: decimal + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: sum by (namespace, job) (thanos_compact_halted{job=~"$job",namespace="$namespace"}) + seriesNameFormat: "{{job}} {{namespace}}" + "7_0": + kind: Panel + spec: + display: + name: Garbage Collection + description: Shows rate of execution of removal of blocks, if their data is available as part of a block with a higher compaction level. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: counts/sec + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + sum by (namespace, job) ( + rate(thanos_compact_garbage_collection_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + seriesNameFormat: "{{job}} {{namespace}}" + "7_1": + kind: Panel + spec: + display: + name: Garbage Collection Errors + description: Shows the percentage of garbage collection operations that resulted in errors. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: percent + visual: + display: line + lineWidth: 0.25 + areaOpacity: 1 + palette: + mode: auto + stack: all + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |4- + sum by (namespace, job) ( + rate( + thanos_compact_garbage_collection_failures_total{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + / + sum by (namespace, job) ( + rate(thanos_compact_garbage_collection_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + ) + * + 100 + seriesNameFormat: "{{job}} {{namespace}}" + "7_2": + kind: Panel + spec: + display: + name: Garbage Collection Durations + description: Shows p50, p90 and p99 of how long it takes to execute garbage collection operations. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + yAxis: + format: + unit: seconds + visual: + display: line + lineWidth: 0.25 + areaOpacity: 0.5 + palette: + mode: auto + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.5, + sum by (namespace, job, le) ( + rate( + thanos_compact_garbage_collection_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p50 {{job}} {{namespace}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.9, + sum by (namespace, job, le) ( + rate( + thanos_compact_garbage_collection_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p90 {{job}} {{namespace}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: |- + histogram_quantile( + 0.99, + sum by (namespace, job, le) ( + rate( + thanos_compact_garbage_collection_duration_seconds_bucket{job=~"$job",namespace="$namespace"}[$__rate_interval] + ) + ) + ) + seriesNameFormat: p99 {{job}} {{namespace}} + "8_0": + kind: Panel + spec: + display: + name: CPU Usage + description: Shows the CPU usage of the component. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + values: + - last + yAxis: + format: + unit: decimal + visual: + display: line + lineWidth: 0.25 + areaOpacity: 0.5 + palette: + mode: auto + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: rate(process_cpu_seconds_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + seriesNameFormat: "{{pod}}" + "8_1": + kind: Panel + spec: + display: + name: Memory Usage + description: Shows various memory usage metrics of the component. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + values: + - last + yAxis: + format: + unit: bytes + visual: + display: line + lineWidth: 0.25 + areaOpacity: 0.5 + palette: + mode: auto + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: go_memstats_alloc_bytes{job=~"$job",namespace="$namespace"} + seriesNameFormat: Alloc All {{pod}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: go_memstats_heap_alloc_bytes{job=~"$job",namespace="$namespace"} + seriesNameFormat: Alloc Heap {{pod}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: rate(go_memstats_alloc_bytes_total{job=~"$job",namespace="$namespace"}[$__rate_interval]) + seriesNameFormat: Alloc Rate All {{pod}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: rate(go_memstats_heap_alloc_bytes{job=~"$job",namespace="$namespace"}[$__rate_interval]) + seriesNameFormat: Alloc Rate Heap {{pod}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: go_memstats_stack_inuse_bytes{job=~"$job",namespace="$namespace"} + seriesNameFormat: Inuse Stack {{pod}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: go_memstats_heap_inuse_bytes{job=~"$job",namespace="$namespace"} + seriesNameFormat: Inuse Heap {{pod}} + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: process_resident_memory_bytes{job=~"$job",namespace="$namespace"} + seriesNameFormat: Resident Memory {{pod}} + "8_2": + kind: Panel + spec: + display: + name: Goroutines + description: Shows the number of goroutines being used by the component. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + values: + - last + yAxis: + format: + unit: decimal + visual: + display: line + lineWidth: 0.25 + areaOpacity: 0.5 + palette: + mode: auto + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: go_goroutines{job=~"$job",namespace="$namespace"} + seriesNameFormat: "{{pod}}" + "8_3": + kind: Panel + spec: + display: + name: GC Duration + description: Shows the Go garbage collection pause durations for the component. + plugin: + kind: TimeSeriesChart + spec: + legend: + position: bottom + mode: table + values: + - last + yAxis: + format: + unit: seconds + visual: + display: line + lineWidth: 0.25 + areaOpacity: 0.5 + palette: + mode: auto + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + datasource: + kind: PrometheusDatasource + + query: go_gc_duration_seconds{job=~"$job",namespace="$namespace"} + seriesNameFormat: "{{quantile}} - {{pod}}" + layouts: + - kind: Grid + spec: + display: + title: TODO Operations + items: + - x: 0 + "y": 0 + width: 6 + height: 6 + content: + $ref: "#/spec/panels/0_0" + - x: 6 + "y": 0 + width: 6 + height: 6 + content: + $ref: "#/spec/panels/0_1" + - x: 12 + "y": 0 + width: 6 + height: 6 + content: + $ref: "#/spec/panels/0_2" + - x: 18 + "y": 0 + width: 6 + height: 6 + content: + $ref: "#/spec/panels/0_3" + - kind: Grid + spec: + display: + title: Group Compactions + items: + - x: 0 + "y": 0 + width: 12 + height: 8 + content: + $ref: "#/spec/panels/1_0" + - x: 12 + "y": 0 + width: 12 + height: 8 + content: + $ref: "#/spec/panels/1_1" + - kind: Grid + spec: + display: + title: Downsample Operations + items: + - x: 0 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/2_0" + - x: 8 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/2_1" + - x: 16 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/2_2" + - kind: Grid + spec: + display: + title: Sync Meta + items: + - x: 0 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/3_0" + - x: 8 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/3_1" + - x: 16 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/3_2" + - kind: Grid + spec: + display: + title: Block Deletion + items: + - x: 0 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/4_0" + - x: 8 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/4_1" + - x: 16 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/4_2" + - kind: Grid + spec: + display: + title: Bucket Operations + items: + - x: 0 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/5_0" + - x: 8 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/5_1" + - x: 16 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/5_2" + - kind: Grid + spec: + display: + title: Halted Compactors + items: + - x: 0 + "y": 0 + width: 24 + height: 8 + content: + $ref: "#/spec/panels/6_0" + - kind: Grid + spec: + display: + title: Garbage Collection + items: + - x: 0 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/7_0" + - x: 8 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/7_1" + - x: 16 + "y": 0 + width: 8 + height: 8 + content: + $ref: "#/spec/panels/7_2" + - kind: Grid + spec: + display: + title: Resources + items: + - x: 0 + "y": 0 + width: 6 + height: 8 + content: + $ref: "#/spec/panels/8_0" + - x: 6 + "y": 0 + width: 6 + height: 8 + content: + $ref: "#/spec/panels/8_1" + - x: 12 + "y": 0 + width: 6 + height: 8 + content: + $ref: "#/spec/panels/8_2" + - x: 18 + "y": 0 + width: 6 + height: 8 + content: + $ref: "#/spec/panels/8_3" + duration: 1h \ No newline at end of file diff --git a/web/cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml b/web/cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml new file mode 100644 index 000000000..3303b8e4f --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml @@ -0,0 +1,24 @@ +apiVersion: perses.dev/v1alpha2 +kind: PersesDatasource +metadata: + name: thanos-querier-datasource + namespace: perses-dev +spec: + config: + display: + name: "Thanos Querier Datasource" + default: true + plugin: + kind: "PrometheusDatasource" + spec: + proxy: + kind: HTTPProxy + spec: + url: https://thanos-querier.openshift-monitoring.svc.cluster.local:9091 + secret: thanos-querier-datasource-secret + client: + tls: + enable: true + caCert: + type: file + certPath: /ca/service-ca.crt \ No newline at end of file diff --git a/web/cypress/fixtures/perses/constants.ts b/web/cypress/fixtures/perses/constants.ts index 9c34395cd..ca231b4bc 100644 --- a/web/cypress/fixtures/perses/constants.ts +++ b/web/cypress/fixtures/perses/constants.ts @@ -48,4 +48,65 @@ export const listPersesDashboardsEmptyState = { TITLE: 'No results found', BODY: 'No results match the filter criteria. Clear filters to show results.', -} \ No newline at end of file +} + +export const persesDashboardsModalTitles ={ + EDIT_DASHBOARD_VARIABLES: 'Edit Dashboard Variables', + DASHBOARD_BUILT_IN_VARIABLES: 'Dashboard Built-in Variables', + ADD_VARIABLE: 'Add Variable', + EDIT_VARIABLE: 'Edit Variable', + EDIT_DASHBOARD_DATASOURCES: 'Edit Dashboard Datasources', + ADD_DATASOURCE: 'Add Datasource', + EDIT_DATASOURCE: 'Edit Datasource', + ADD_PANEL: 'Add Panel', + EDIT_PANEL: 'Edit Panel', + DELETE_PANEL: 'Delete Panel', + ADD_PANEL_GROUP: 'Add Panel Group', + EDIT_PANEL_GROUP: 'Edit Panel Group', + DELETE_PANEL_GROUP: 'Delete Panel Group', + EDIT_DASHBOARD_JSON: 'Edit Dashboard JSON', + SAVE_DASHBOARD: 'Save Dashboard', + DISCARD_CHANGES: 'Discard Changes', + VIEW_JSON_DIALOG: 'Dashboard JSON', +} + +export enum persesDashboardsAddListVariableSource { + STATIC_LIST_VARIABLE= 'Static List Variable', + DATASOURCE_VARIABLE= 'Datasource Variable', + PROMETHEUS_LABEL_VARIABLE= 'Prometheus Label Variable', + PROMETHEUS_NAMES_VARIABLE= 'Prometheus Names Variable', + PROMETHEUS_PROMQL_VARIABLE= 'Prometheus PromQL Variable', +} + +export enum persesDashboardsAddListVariableSort { + NONE = 'None', + ALPHABETICAL_ASC = 'Alphabetical, asc', + ALPHABETICAL_DESC = 'Alphabetical, desc', + NUMERICAL_ASC = 'Numerical, asc', + NUMERICAL_DESC = 'Numerical, desc', + ALPHABETICAL_CI_ASC = 'Alphabetical, case-insensitive, asc', + ALPHABETICAL_CI_DESC = 'Alphabetical, case-insensitive, desc', +} + +export const persesDashboardsRequiredFields = { + AddVariableNameField: 'String must contain at least 1 character(s)' +} + +export const persesDashboardsAddListPanelType = { + BAR_CHART: 'Bar Chart', + FLAME_CHART: 'Flame Chart', + GAUGE_CHART: 'Gauge Chart', + HEATMAP_CHART: 'HeatMap Chart', + HISTOGRAM_CHART: 'Histogram Chart', + MARKDOWN: 'Markdown', + LOGS_TABLE: 'Logs Table', + PIE_CHART: 'Pie Chart', + SCATTER_CHART: 'Scatter Chart', + STAT_CHART: 'Stat Chart', + STATUS_HISTORY_CHART: 'Status History Chart', + TABLE: 'Table', + TIME_SERIES_CHART: 'Time Series Chart', + TIME_SERIES_TABLE: 'Time Series Table', + TRACE_TABLE: 'Trace Table', + TRACING_GANTT_CHART: 'Tracing Gantt Chart', +} diff --git a/web/cypress/support/commands/operator-commands.ts b/web/cypress/support/commands/operator-commands.ts index ef2ee97e4..927ace76d 100644 --- a/web/cypress/support/commands/operator-commands.ts +++ b/web/cypress/support/commands/operator-commands.ts @@ -240,7 +240,7 @@ const operatorUtils = { `oc label namespace ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); cy.exec( - `operator-sdk run bundle --timeout=10m --namespace ${MCP.namespace} ${Cypress.env('KONFLUX_COO_BUNDLE_IMAGE')} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')} --verbose `, + `operator-sdk run bundle --timeout=10m --namespace ${MCP.namespace} --security-context-config restricted ${Cypress.env('KONFLUX_COO_BUNDLE_IMAGE')} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')} --verbose `, { timeout: installTimeoutMilliseconds }, ); } else if (Cypress.env('CUSTOM_COO_BUNDLE_IMAGE')) { @@ -256,7 +256,7 @@ const operatorUtils = { `oc label namespace ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); cy.exec( - `operator-sdk run bundle --timeout=10m --namespace ${MCP.namespace} ${Cypress.env('CUSTOM_COO_BUNDLE_IMAGE')} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')} --verbose `, + `operator-sdk run bundle --timeout=10m --namespace ${MCP.namespace} --security-context-config restricted ${Cypress.env('CUSTOM_COO_BUNDLE_IMAGE')} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')} --verbose `, { timeout: installTimeoutMilliseconds }, ); } else if (Cypress.env('FBC_STAGE_COO_IMAGE')) { @@ -283,7 +283,9 @@ const operatorUtils = { waitForCOOReady(MCP: { namespace: string }): void { cy.log('Check Cluster Observability Operator status'); - cy.exec(`sleep 60 && oc get pods -n ${MCP.namespace} | grep observability-operator | awk '{print $1}'`, { timeout: readyTimeoutMilliseconds, failOnNonZeroExit: true }) + cy.exec(`oc project ${MCP.namespace} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.exec(`sleep 60 && oc get pods -n ${MCP.namespace} | grep observability-operator | grep -v bundle | awk '{print $1}'`, { timeout: readyTimeoutMilliseconds, failOnNonZeroExit: true }) .its('stdout') // Get the captured output string .then((podName) => { // Trim any extra whitespace (newline, etc.) @@ -306,7 +308,7 @@ const operatorUtils = { nav.sidenav.clickNavLink([section, 'Installed Operators']); }); - cy.byTestID('name-filter-input').should('be.visible').type('Cluster Observability{enter}'); + cy.byTestID('name-filter-input').should('be.visible').type('Observability{enter}'); cy.get('[data-test="status-text"]', { timeout: installTimeoutMilliseconds }).eq(0).should('contain.text', 'Succeeded', { timeout: installTimeoutMilliseconds }); }, @@ -373,20 +375,45 @@ const operatorUtils = { cy.log('Create perses-dev namespace.'); cy.exec(`oc new-project perses-dev --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create openshift-cluster-sample-dashboard instance.'); - cy.exec(`sed 's/namespace: openshift-cluster-observability-operator/namespace: ${MCP.namespace}/g' ./cypress/fixtures/coo/openshift-cluster-sample-dashboard.yaml | oc apply -f - --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + /** + * TODO: When COO1.4.0 is released, points COO_UI_INSTALL to install dashboards on COO1.4.0 folder + */ + if (Cypress.env('COO_UI_INSTALL')) { + cy.log('COO_UI_INSTALL is set. Installing dashboards on COO1.2.0 folder'); + + cy.log('Create openshift-cluster-sample-dashboard instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/openshift-cluster-sample-dashboard.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create perses-dashboard-sample instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/perses-dashboard-sample.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create prometheus-overview-variables instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/prometheus-overview-variables.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create thanos-compact-overview-1var instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-compact-overview-1var.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create Thanos Querier instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-querier-datasource.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + } else { + cy.log('COO_UI_INSTALL is not set. Installing dashboards on COO1.4.0 folder'); + + cy.log('Create openshift-cluster-sample-dashboard instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create perses-dashboard-sample instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create perses-dashboard-sample instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/perses-dashboard-sample.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.log('Create prometheus-overview-variables instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create prometheus-overview-variables instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/prometheus-overview-variables.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.log('Create thanos-compact-overview-1var instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create thanos-compact-overview-1var instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/thanos-compact-overview-1var.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.log('Create Thanos Querier instance.'); + cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create Thanos Querier instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/thanos-querier-datasource.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + } cy.exec( `oc label namespace ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, @@ -459,7 +486,8 @@ const operatorUtils = { cy.log(`Korrel8r pod is now running in namespace: ${MCP.namespace}`); }); - cy.reload(true); + cy.wait(30000); + cy.log(`Clicking the application launcher`); cy.byLegacyTestID(LegacyTestIDs.ApplicationLauncher).should('be.visible').click(); cy.byTestID(DataTestIDs.MastHeadApplicationItem).contains('Signal Correlation').should('be.visible'); }, @@ -521,22 +549,40 @@ const operatorUtils = { cy.executeAndDelete( `oc delete ${config.kind} ${config.name} --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); - - cy.log('Remove openshift-cluster-sample-dashboard instance.'); - cy.executeAndDelete(`sed 's/namespace: openshift-cluster-observability-operator/namespace: ${MCP.namespace}/g' ./cypress/fixtures/coo/openshift-cluster-sample-dashboard.yaml | oc delete -f - --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + if (Cypress.env('COO_UI_INSTALL')) { + cy.log('Remove openshift-cluster-sample-dashboard instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/openshift-cluster-sample-dashboard.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove perses-dashboard-sample instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/perses-dashboard-sample.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.log('Remove perses-dashboard-sample instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/perses-dashboard-sample.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove prometheus-overview-variables instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/prometheus-overview-variables.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.log('Remove prometheus-overview-variables instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/prometheus-overview-variables.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove thanos-compact-overview-1var instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/thanos-compact-overview-1var.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.log('Remove thanos-compact-overview-1var instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-compact-overview-1var.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove Thanos Querier instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-querier-datasource.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + } else { + cy.log('COO_UI_INSTALL is not set. Removing dashboards on COO1.4.0 folder'); - cy.log('Remove Thanos Querier instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/thanos-querier-datasource.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.log('Remove openshift-cluster-sample-dashboard instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.log('Remove perses-dashboard-sample instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove prometheus-overview-variables instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove thanos-compact-overview-1var instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove Thanos Querier instance.'); + cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + } cy.log('Remove perses-dev namespace'); cy.executeAndDelete(`oc delete namespace perses-dev --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); diff --git a/web/cypress/support/perses/00.coo_bvt_perses_admin.cy.ts b/web/cypress/support/perses/00.coo_bvt_perses_admin.cy.ts index 981ed40a9..cee213e00 100644 --- a/web/cypress/support/perses/00.coo_bvt_perses_admin.cy.ts +++ b/web/cypress/support/perses/00.coo_bvt_perses_admin.cy.ts @@ -32,7 +32,7 @@ export function testBVTCOOPerses(perspective: PerspectiveConfig) { cy.changeNamespace('openshift-cluster-observability-operator'); persesDashboardsPage.clickDashboardDropdown(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0] as keyof typeof persesDashboardsDashboardDropdownCOO); cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-cluster').should('be.visible'); - persesDashboardsPage.panelGroupHeaderAssertion('Accelerators'); + persesDashboardsPage.panelGroupHeaderAssertion('Accelerators', 'Open'); persesDashboardsPage.panelHeadersAcceleratorsCommonMetricsAssertion(); persesDashboardsPage.expandPanel(persesDashboardsAcceleratorsCommonMetricsPanels.GPU_UTILIZATION); persesDashboardsPage.collapsePanel(persesDashboardsAcceleratorsCommonMetricsPanels.GPU_UTILIZATION); @@ -46,12 +46,12 @@ export function testBVTCOOPerses(perspective: PerspectiveConfig) { cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-instance').should('be.visible'); cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-interval').should('be.visible'); cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-text').should('be.visible'); - persesDashboardsPage.panelGroupHeaderAssertion('Row 1'); - persesDashboardsPage.expandPanel('RAM Used'); - persesDashboardsPage.collapsePanel('RAM Used'); - persesDashboardsPage.statChartValueAssertion('RAM Used', true); + persesDashboardsPage.panelGroupHeaderAssertion('Row 1', 'Open'); + persesDashboardsPage.expandPanel('RAM Total'); + persesDashboardsPage.collapsePanel('RAM Total'); + persesDashboardsPage.statChartValueAssertion('RAM Total', true); persesDashboardsPage.searchAndSelectVariable('job', 'node-exporter'); - persesDashboardsPage.statChartValueAssertion('RAM Used', false); + persesDashboardsPage.statChartValueAssertion('RAM Total', false); }); diff --git a/web/cypress/support/perses/00.coo_bvt_perses_admin_1.cy.ts b/web/cypress/support/perses/00.coo_bvt_perses_admin_1.cy.ts index 1251a94eb..496cfbd96 100644 --- a/web/cypress/support/perses/00.coo_bvt_perses_admin_1.cy.ts +++ b/web/cypress/support/perses/00.coo_bvt_perses_admin_1.cy.ts @@ -18,7 +18,7 @@ export function testBVTCOOPerses1(perspective: PerspectiveConfig) { cy.log(`1.1. use sidebar nav to go to Observe > Dashboards (Perses)`); listPersesDashboardsPage.shouldBeLoaded(); listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]); - persesDashboardsPage.shouldBeLoaded(); + persesDashboardsPage.shouldBeLoaded1(); }); it(`2.${perspective.name} perspective - Accelerators common metrics dashboard `, () => { @@ -26,9 +26,11 @@ export function testBVTCOOPerses1(perspective: PerspectiveConfig) { cy.changeNamespace('openshift-cluster-observability-operator'); listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]); cy.wait(2000); + + cy.log(`2.2. Select dashboard`); persesDashboardsPage.clickDashboardDropdown(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0] as keyof typeof persesDashboardsDashboardDropdownCOO); cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-cluster').should('be.visible'); - persesDashboardsPage.panelGroupHeaderAssertion('Accelerators'); + persesDashboardsPage.panelGroupHeaderAssertion('Accelerators', 'Open'); persesDashboardsPage.panelHeadersAcceleratorsCommonMetricsAssertion(); persesDashboardsPage.expandPanel(persesDashboardsAcceleratorsCommonMetricsPanels.GPU_UTILIZATION); persesDashboardsPage.collapsePanel(persesDashboardsAcceleratorsCommonMetricsPanels.GPU_UTILIZATION); @@ -44,13 +46,22 @@ export function testBVTCOOPerses1(perspective: PerspectiveConfig) { cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-instance').should('be.visible'); cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-interval').should('be.visible'); cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-text').should('be.visible'); - persesDashboardsPage.panelGroupHeaderAssertion('Row 1'); - persesDashboardsPage.expandPanel('RAM Used'); - persesDashboardsPage.collapsePanel('RAM Used'); - persesDashboardsPage.statChartValueAssertion('RAM Used', true); + persesDashboardsPage.panelGroupHeaderAssertion('Row 1', 'Open'); + persesDashboardsPage.expandPanel('RAM Total'); + persesDashboardsPage.collapsePanel('RAM Total'); + persesDashboardsPage.statChartValueAssertion('RAM Total', true); persesDashboardsPage.searchAndSelectVariable('job', 'node-exporter'); - persesDashboardsPage.statChartValueAssertion('RAM Used', false); - + persesDashboardsPage.statChartValueAssertion('RAM Total', false); + }); + + it(`4.${perspective.name} perspective - Download and View JSON`, () => { + cy.log(`4.1. use sidebar nav to go to Observe > Dashboards (Perses) > Download and View JSON`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]); + persesDashboardsPage.downloadDashboard(true, persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2], 'JSON'); + persesDashboardsPage.downloadDashboard(true, persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2], 'YAML'); + persesDashboardsPage.downloadDashboard(true, persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2], 'YAML (CR)'); + persesDashboardsPage.viewJSON(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2], 'openshift-cluster-observability-operator'); + }); } diff --git a/web/cypress/support/perses/02.coo_edit_perses_admin.cy.ts b/web/cypress/support/perses/02.coo_edit_perses_admin.cy.ts new file mode 100644 index 000000000..86c3b0414 --- /dev/null +++ b/web/cypress/support/perses/02.coo_edit_perses_admin.cy.ts @@ -0,0 +1,517 @@ +import { editPersesDashboardsAddVariable, persesMUIDataTestIDs, IDs, editPersesDashboardsAddDatasource } from '../../../src/components/data-test'; +import { persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev } from '../../fixtures/perses/constants'; +import { commonPages } from '../../views/common'; +import { listPersesDashboardsPage } from "../../views/list-perses-dashboards"; +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { persesDashboardsPanelGroup } from '../../views/perses-dashboards-panelgroup'; +import { persesDashboardsEditDatasources } from '../../views/perses-dashboards-edit-datasources'; +import { persesDashboardsEditVariables } from '../../views/perses-dashboards-edit-variables'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOOEditPersesTests(perspective: PerspectiveConfig) { + testCOOEditPerses(perspective); +} + +export function testCOOEditPerses(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - Edit perses dashboard page`, () => { + cy.log(`1.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`1.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`1.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`1.4. Click on Edit button`); + cy.wait(15000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.assertEditModeButtons(); + persesDashboardsPage.assertEditModePanelGroupButtons('Headlines'); + //already expanded + persesDashboardsPage.assertPanelActionButtons('CPU Usage'); + // tiny panel and modal is opened. So, expand first and then assert the buttons and finally collapse + // due to modal is opened and page is refreshed, it is not easy to assert buttons in the modal + persesDashboardsPage.assertPanelActionButtons('CPU Utilisation'); + + cy.log(`1.5. Click on Cancel button`); + persesDashboardsPage.clickEditActionButton('Cancel'); + + cy.log(`1.6. Change namespace to All Projects`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.shouldBeLoaded(); + + }); + + it(`2.${perspective.name} perspective - Edit Toolbar - Edit Variables - Add List Variable`, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`2.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`2.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`2.4. Click on Edit button`); + cy.wait(10000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickButton('Add Variable'); + //https://issues.redhat.com/browse/OU-1159 - Custom All Value is not working + persesDashboardsEditVariables.addListVariable('ListVariable', true, true, 'AAA', 'Test', 'Test', undefined, undefined); + + cy.log(`2.5. Run query`); + persesDashboardsEditVariables.clickButton('Run Query'); + cy.get('h4').should('contain', 'Preview Values').should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardAddVariablePreviewValuesCopy).should('be.visible'); + + cy.log(`2.6. Add variable`); + persesDashboardsEditVariables.clickButton('Add'); + + cy.log(`2.7. Apply changes`); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`2.8. Save dashboard`); + persesDashboardsPage.clickEditActionButton('Save'); + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + //https://issues.redhat.com/browse/OU-1159 - Custom All Value is not working, so selecting "All" for now + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + //TODO: END testing more to check if it is time constraint or cache issue + + }); + + it(`3.${perspective.name} perspective - Edit Toolbar - Edit Variables - Add Text Variable`, () => { + cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`3.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`3.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`3.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditVariables'); + + cy.log(`3.5. Click on Dashboard Built-in Variables button`); + cy.get('#'+IDs.persesDashboardEditVariablesModalBuiltinButton).should('have.attr', 'aria-expanded', 'false').click(); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('#'+IDs.persesDashboardEditVariablesModalBuiltinButton).should('have.attr', 'aria-expanded', 'true') + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('#'+IDs.persesDashboardEditVariablesModalBuiltinButton).click(); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('#'+IDs.persesDashboardEditVariablesModalBuiltinButton).should('have.attr', 'aria-expanded', 'false'); + + cy.log(`3.6. Add variable`); + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addTextVariable('TextVariable', true, 'Test', 'Test', 'Test'); + persesDashboardsEditVariables.clickButton('Add'); + + cy.log(`3.7. Apply changes`); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`3.8. Save dashboard`); + persesDashboardsPage.clickEditActionButton('Save'); + + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + + cy.log(`3.9. Search and type variable`); + persesDashboardsPage.searchAndTypeVariable('TextVariable', ''); + + }); + + it(`4.${perspective.name} perspective - Edit Toolbar - Edit Variables - Visibility, Move up/down, Edit and Delete Variable`, () => { + cy.log(`4.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`4.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`4.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`4.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + + cy.log(`4.5. Click on Edit Variables button`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + + cy.log(`4.6. Toggle variable visibility`); + persesDashboardsEditVariables.toggleVariableVisibility(0, false); + + cy.log(`4.7. Move variable up`); + persesDashboardsEditVariables.moveVariableUp(1); + + cy.log(`4.8. Click on Edit variable button`); + persesDashboardsEditVariables.clickEditVariableButton(0); + + cy.log(`4.9. Edit list variable`); + //https://issues.redhat.com/browse/OU-1159 - Custom All Value is not working + persesDashboardsEditVariables.addListVariable('ListVariable123', false, false, '123', 'Test123', 'Test123', undefined, undefined); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`4.10. Delete variable`); + persesDashboardsEditVariables.clickDeleteVariableButton(2); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`4.11. Save dashboard`); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`4.12. Search and select variable`); + //https://issues.redhat.com/browse/OU-1159 - Custom All Value is not working, so selecting "All" for now + persesDashboardsPage.searchAndSelectVariable('ListVariable123', 'All'); + + cy.log(`4.13. Assert variable not be visible`); + persesDashboardsPage.assertVariableNotBeVisible('cluster'); + + cy.log(`4.14. Assert variable not exist`); + persesDashboardsPage.assertVariableNotExist('TextVariable'); + + cy.log(`4.15. Recover dashboard`); + persesDashboardsPage.clickEditButton(); + + cy.log(`4.16. Click on Edit Variables button`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + + cy.log(`4.16. Toggle variable visibility`); + persesDashboardsEditVariables.toggleVariableVisibility(1, true); + + cy.log(`4.17. Delete variable`); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + + cy.log(`4.17. Apply changes`); + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`4.18. Assert variable be visible`); + persesDashboardsPage.assertVariableBeVisible('cluster'); + + cy.log(`4.19. Assert variable not exist`); + persesDashboardsPage.assertVariableNotExist('TextVariable'); + + }); + + it(`5.${perspective.name} perspective - Edit Toolbar - Edit Variables - Add Variable - Required field validation`, () => { + cy.log(`5.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`5.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`5.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`5.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditVariables'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('button').contains('Add Variable').should('be.visible').click(); + cy.get('input[name="'+editPersesDashboardsAddVariable.inputName+'"]').clear(); + persesDashboardsEditVariables.clickButton('Add'); + persesDashboardsEditVariables.assertRequiredFieldValidation('Name'); + persesDashboardsEditVariables.clickButton('Cancel'); + persesDashboardsEditVariables.clickButton('Cancel'); + }); + + /**TODO: https://issues.redhat.com/browse/OU-1054 is targeted for COO1.5.0, so, commenting all Datasources related scenarios + it(`6.${perspective.name} perspective - Edit Toolbar - Edit Datasources - Add and Delete Prometheus Datasource`, () => { + cy.log(`6.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`6.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`6.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`6.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditDatasources'); + + cy.log(`6.5. Verify existing datasources`); + persesDashboardsEditDatasources.assertDatasource(0, 'PrometheusLocal', 'PrometheusDatasource', ''); + + cy.log(`6.6. Add datasource`); + persesDashboardsEditDatasources.clickButton('Add Datasource'); + persesDashboardsEditDatasources.addDatasource('Datasource1', true, 'Prometheus Datasource', 'Datasource1', 'Datasource1'); + persesDashboardsEditDatasources.clickButton('Add'); + persesDashboardsEditDatasources.assertDatasource(1, 'Datasource1', 'PrometheusDatasource', 'Datasource1'); + + cy.log(`6.7. Add second datasource`); + persesDashboardsEditDatasources.clickButton('Add Datasource'); + persesDashboardsEditDatasources.addDatasource('Datasource2', true, 'Prometheus Datasource', 'Datasource2', 'Datasource2'); + persesDashboardsEditDatasources.clickButton('Add'); + persesDashboardsEditDatasources.assertDatasource(2, 'Datasource2', 'PrometheusDatasource', 'Datasource2'); + + cy.log(`6.8. Delete first datasource`); + persesDashboardsEditDatasources.clickDeleteDatasourceButton(1); + persesDashboardsEditDatasources.assertDatasourceNotExist('Datasource1'); + + persesDashboardsEditDatasources.clickButton('Apply'); + //https://issues.redhat.com/browse/OU-1160 - Datasource is not saved + // persesDashboardsPage.clickEditActionButton('Save'); + }); + + it(`7.${perspective.name} perspective - Edit Toolbar - Edit Datasources - Edit Prometheus Datasource`, () => { + cy.log(`7.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`7.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`7.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`7.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditDatasources'); + + cy.log(`7.5. Verify existing datasources`); + persesDashboardsEditDatasources.assertDatasource(0,'PrometheusLocal', 'PrometheusDatasource', ''); + + cy.log(`7.6. Edit datasource`); + persesDashboardsEditDatasources.clickEditDatasourceButton(0); + persesDashboardsEditDatasources.addDatasource('PrometheusLocal', false, 'Prometheus Datasource', 'Datasource1', 'Datasource1'); + persesDashboardsEditDatasources.clickButton('Apply'); + persesDashboardsEditDatasources.assertDatasource(0,'PrometheusLocal', 'PrometheusDatasource', 'Datasource1'); + persesDashboardsEditDatasources.clickButton('Cancel'); + persesDashboardsPage.clickEditActionButton('Cancel'); + + }); + + // it(`8.${perspective.name} perspective - Edit Toolbar - Edit Datasources - Add Tempo Datasource`, () => { + // }); + + it(`8.${perspective.name} perspective - Edit Toolbar - Edit Datasources - Required field validation`, () => { + cy.log(`8.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`8.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`8.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`8.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditDatasources'); + + cy.log(`8.5. Add datasource`); + persesDashboardsEditDatasources.clickButton('Add Datasource'); + + cy.log(`8.6. Clear out Name field`); + cy.get('input[name="'+editPersesDashboardsAddDatasource.inputName+'"]').clear(); + persesDashboardsEditDatasources.clickButton('Add'); + + cy.log(`8.7. Assert required field validation`); + persesDashboardsEditDatasources.assertRequiredFieldValidation('Name'); + persesDashboardsEditDatasources.clickButton('Cancel'); + + cy.log(`8.8. Cancel changes`); + persesDashboardsEditDatasources.clickButton('Cancel'); + persesDashboardsPage.clickEditActionButton('Cancel'); + + }); +*/ + + it(`6.${perspective.name} perspective - Edit Toolbar - Add Panel Group`, () => { + cy.log(`6.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`6.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`6.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`6.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('AddGroup'); + + cy.log(`6.5. Add panel group`); + persesDashboardsPanelGroup.addPanelGroup('PanelGroup1', 'Open', ''); + + cy.log(`6.6. Save panel group`); + persesDashboardsPage.clickEditActionButton('Save'); + persesDashboardsPage.panelGroupHeaderAssertion('PanelGroup1', 'Open'); + + cy.log(`6.7. Back and check panel group`); + //TODO: START testing more to check if it is time constraint or cache issue + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + persesDashboardsPage.panelGroupHeaderAssertion('PanelGroup1', 'Open'); + + }); + + it(`7.${perspective.name} perspective - Edit Toolbar - Edit Panel Group`, () => { + cy.log(`7.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`7.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`7.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`7.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup1', 'edit'); + persesDashboardsPanelGroup.editPanelGroup('PanelGroup2', 'Closed', ''); + persesDashboardsPage.clickEditActionButton('Save'); + persesDashboardsPage.panelGroupHeaderAssertion('PanelGroup2', 'Closed'); + + cy.log(`7.5. Back and check panel group`); + //TODO: START testing more to check if it is time constraint or cache issue + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + persesDashboardsPage.panelGroupHeaderAssertion('PanelGroup2', 'Closed'); + + }); + + it(`8.${perspective.name} perspective - Edit Toolbar - Move Panel Group Down and Up`, () => { + cy.log(`8.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`8.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`8.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`8.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup2', 'moveDown'); + + cy.log(`8.5. Save panel group`); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`8.6. Assert panel group order`); + persesDashboardsPage.assertPanelGroupOrder('Row 1', 0); + persesDashboardsPage.assertPanelGroupOrder('PanelGroup2', 1); + persesDashboardsPage.assertPanelGroupOrder('Row 2', 2); + + cy.log(`8.7. Back and check panel group order`); + //TODO: START testing more to check if it is time constraint or cache issue + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + persesDashboardsPage.assertPanelGroupOrder('Row 1', 0); + persesDashboardsPage.assertPanelGroupOrder('PanelGroup2', 1); + persesDashboardsPage.assertPanelGroupOrder('Row 2', 2); + + cy.log(`8.8. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + + cy.log(`8.9. Move panel group up`); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup2', 'moveUp'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`8.10. Assert panel group order`); + persesDashboardsPage.assertPanelGroupOrder('PanelGroup2', 0); + persesDashboardsPage.assertPanelGroupOrder('Row 1', 1); + persesDashboardsPage.assertPanelGroupOrder('Row 2', 2); + + cy.log(`8.11. Back and check panel group order`); + //TODO: START testing more to check if it is time constraint or cache issue + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + persesDashboardsPage.assertPanelGroupOrder('PanelGroup2', 0); + persesDashboardsPage.assertPanelGroupOrder('Row 1', 1); + persesDashboardsPage.assertPanelGroupOrder('Row 2', 2); + }); + + it(`9.${perspective.name} perspective - Edit Toolbar - Delete Panel Group`, () => { + cy.log(`9.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`9.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`9.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`9.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup2', 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + persesDashboardsPage.assertPanelGroupNotExist('PanelGroup2'); + + cy.log(`9.5. Back and check panel group`); + //TODO: START testing more to check if it is time constraint or cache issue + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + persesDashboardsPage.assertPanelGroupNotExist('PanelGroup2'); + }); + +} diff --git a/web/cypress/support/perses/02.coo_edit_perses_admin_1.cy.ts b/web/cypress/support/perses/02.coo_edit_perses_admin_1.cy.ts new file mode 100644 index 000000000..8a3282e39 --- /dev/null +++ b/web/cypress/support/perses/02.coo_edit_perses_admin_1.cy.ts @@ -0,0 +1,318 @@ +import { IDs, editPersesDashboardsAddPanel } from '../../../src/components/data-test'; +import { persesDashboardsAddListPanelType, persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev } from '../../fixtures/perses/constants'; +import { commonPages } from '../../views/common'; +import { listPersesDashboardsPage } from "../../views/list-perses-dashboards"; +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { persesDashboardsEditDatasources } from '../../views/perses-dashboards-edit-datasources'; +import { persesDashboardsEditVariables } from '../../views/perses-dashboards-edit-variables'; +import { persesDashboardsPanel } from '../../views/perses-dashboards-panel'; +import { persesDashboardsPanelGroup } from '../../views/perses-dashboards-panelgroup'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOOEditPersesTests1(perspective: PerspectiveConfig) { + testCOOEditPerses1(perspective); +} + +export function testCOOEditPerses1(perspective: PerspectiveConfig) { + + it(`10.${perspective.name} perspective - Edit Toolbar - Add Panel`, () => { + cy.log(`10.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`10.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`10.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + + const panelTypeKeys = Object.keys(persesDashboardsAddListPanelType) as (keyof typeof persesDashboardsAddListPanelType)[]; + panelTypeKeys.forEach((typeKey) => { + const panelName = persesDashboardsAddListPanelType[typeKey]; // e.g., 'Bar Chart' + + cy.log(`10.4. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`10.5. Click on Add Group - PanelGroup ` + panelName); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('PanelGroup ' + panelName, 'Open', ''); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`10.6. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`10.7. Click on Add Panel button` + panelName); + persesDashboardsPage.clickEditActionButton('AddPanel'); + persesDashboardsPanel.addPanelShouldBeLoaded(); + persesDashboardsPanel.addPanel(panelName, 'PanelGroup ' + panelName, panelName); + persesDashboardsPage.assertPanel(panelName, 'PanelGroup ' + panelName, 'Open'); + persesDashboardsPage.clickEditActionButton('Save'); + }); + }); + + it(`11.${perspective.name} perspective - Edit Toolbar - Edit Panel`, () => { + cy.log(`11.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`11.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`11.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + + cy.log(`11.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + + const panelTypeKeys = Object.keys(persesDashboardsAddListPanelType) as (keyof typeof persesDashboardsAddListPanelType)[]; + const lastKey = panelTypeKeys[panelTypeKeys.length - 1]; // Get the last KEY from the array + const lastPanelName = persesDashboardsAddListPanelType[lastKey]; // Use the KEY to get the VALUE + + cy.log(`11.5. Click on Edit Panel button` + lastPanelName + ' to Panel1'); + persesDashboardsPage.clickPanelAction(lastPanelName, 'edit'); + persesDashboardsPanel.editPanel('Panel1', 'PanelGroup ' + lastPanelName, persesDashboardsAddListPanelType.BAR_CHART, 'Description1'); + persesDashboardsPage.assertPanel('Panel1', 'PanelGroup ' + lastPanelName, 'Open'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`11.6. Click on Edit Panel button from Panel 1 to` + lastPanelName); + persesDashboardsPage.clickEditButton(); + + persesDashboardsPage.clickPanelAction('Panel1', 'edit'); + persesDashboardsPanel.editPanel(lastPanelName, 'PanelGroup ' + lastPanelName, lastPanelName, 'Description1'); + persesDashboardsPage.assertPanel(lastPanelName, 'PanelGroup ' + lastPanelName, 'Open'); + persesDashboardsPage.clickEditActionButton('Save'); + + }); + + it(`12.${perspective.name} perspective - Edit Toolbar - Delete Panel`, () => { + cy.log(`12.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`12.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`12.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + + const panelTypeKeys = Object.keys(persesDashboardsAddListPanelType) as (keyof typeof persesDashboardsAddListPanelType)[]; + + panelTypeKeys.reverse().forEach((typeKey) => { + const panelName = persesDashboardsAddListPanelType[typeKey]; // e.g., 'Bar Chart' + cy.log(`12.4. Delete Panel` + panelName); + persesDashboardsPage.clickEditButton(); + persesDashboardsPanel.deletePanel(panelName); + persesDashboardsPanel.clickDeletePanelButton(); + + cy.log(`12.5. Delete Panel Group` + panelName); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup ' + panelName, 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + }); + }); + + it(`13.${perspective.name} perspective - Edit Toolbar - Duplicate Panel`, () => { + cy.log(`13.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`13.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`13.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + + cy.log(`13.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + + cy.log(`13.5. Collapse Row 1 Panel Group`); + persesDashboardsPage.collapsePanelGroup('Row 1'); + + cy.log(`13.6. Click on Duplicate Panel button`); + persesDashboardsPage.clickPanelAction('Legend Example', 'duplicate'); + + cy.log(`13.7. Assert duplicated panel`); + persesDashboardsPage.assertDuplicatedPanel('Legend Example', 2); + + }); + + it(`14.${perspective.name} perspective - Edit Toolbar - Add Panel - Required field validation`, () => { + cy.log(`14.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`14.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`14.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + + cy.log(`14.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + + cy.log(`14.5. Click on Add Panel Group button`); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('PanelGroup Required Field Validation', 'Open', ''); + + cy.log(`14.6. Click on Add Panel button`); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup Required Field Validation', 'addPanel'); + + cy.get('input[name="'+editPersesDashboardsAddPanel.inputName+'"]').clear().type('Required Field Validation'); + + persesDashboardsPanel.clickDropdownAndSelectOption('Type', persesDashboardsAddListPanelType.BAR_CHART); + cy.get('input[name="'+editPersesDashboardsAddPanel.inputName+'"]').clear().type('Required Field Validation'); + persesDashboardsPanel.clickDropdownAndSelectOption('Type', persesDashboardsAddListPanelType.BAR_CHART); + cy.get('input[name="'+editPersesDashboardsAddPanel.inputName+'"]').clear(); + cy.get('#'+IDs.persesDashboardAddPanelForm).parent('div').find('h2').siblings('div').find('button').contains('Add').should('be.visible').click(); + + cy.log(`14.7. Assert required field validation`); + persesDashboardsPanel.assertRequiredFieldValidation('Name'); + persesDashboardsPanel.clickButton('Cancel'); + persesDashboardsPage.clickEditActionButton('Cancel'); + }); + + it(`15.${perspective.name} perspective - Edit Toolbar - Perform changes and Cancel`, () => { + cy.log(`15.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`15.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`15.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`15.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickButton('Add Variable'); + //https://issues.redhat.com/browse/OU-1159 - Custom All Value is not working + persesDashboardsEditVariables.addListVariable('ListVariable', true, true, 'AAA', 'Test', 'Test', undefined, undefined); + + cy.log(`15.5. Add variable`); + persesDashboardsEditVariables.clickButton('Add'); + + cy.log(`15.6. Apply changes`); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`15.7. Assert Variable before cancelling`); + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + + cy.log(`15.8. Click on Add Panel Group button`); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('PanelGroup Perform Changes and Cancel', 'Open', ''); + + cy.log(`15.9. Click on Add Panel button`); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup Perform Changes and Cancel', 'addPanel'); + persesDashboardsPanel.addPanel('Panel Perform Changes and Cancel', 'PanelGroup Perform Changes and Cancel', 'Bar Chart'); + + cy.log(`15.10. Click on Cancel button`); + persesDashboardsPage.clickEditActionButton('Cancel'); + + cy.log(`15.11. Assert variable not exist`); + persesDashboardsPage.assertVariableNotExist('ListVariable'); + + cy.log(`15.12. Assert panel group not exist`); + persesDashboardsPage.assertPanelGroupNotExist('PanelGroup Perform Changes and Cancel'); + + cy.log(`15.13. Assert panel not exist`); + persesDashboardsPage.assertPanelNotExist('Panel Perform Changes and Cancel'); + + }); + + /** + * OU-886 Mark dashboards and datasources created using CRD as readonly + * + * Admin user and dev users with persesdashboard-editor-role will be able to edit dashboards using CRD. + * + */ + it(`16.${perspective.name} perspective - Try to editAccelerators and APM dashboards`, () => { + cy.log(`16.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`16.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`16.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`16.4. Click on Edit button`); + cy.wait(2000); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickButton('Add Variable'); + //https://issues.redhat.com/browse/OU-1159 - Custom All Value is not working + persesDashboardsEditVariables.addListVariable('ListVariable', true, true, 'AAA', 'Test', 'Test', undefined, undefined); + + cy.log(`16.5. Add variable`); + persesDashboardsEditVariables.clickButton('Add'); + + cy.log(`16.6. Apply changes`); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`16.7. Assert Variable before saving`); + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + + cy.log(`16.8. Click on Add Panel Group button`); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('PanelGroup Perform Changes and Save', 'Open', ''); + + cy.log(`16.9. Click on Add Panel button`); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup Perform Changes and Save', 'addPanel'); + persesDashboardsPanel.addPanel('Panel Perform Changes and Save', 'PanelGroup Perform Changes and Save', 'Bar Chart'); + + cy.log(`16.10. Click on Save button`); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`16.11. Back and check panel group`); + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]); + + cy.log(`16.12. Assert Variable before deleting`); + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + + cy.log(`16.13. Assert panel group exists`); + persesDashboardsPage.panelGroupHeaderAssertion('PanelGroup Perform Changes and Save', 'Open'); + + cy.log(`16.14. Assert panel exists`); + persesDashboardsPage.assertPanel('Panel Perform Changes and Save', 'PanelGroup Perform Changes and Save', 'Open'); + + cy.log (`16.15. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`16.16. Delete variable`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickDeleteVariableButton(1); + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.assertVariableNotExist('ListVariable'); + + cy.log(`16.17. Delete panel group`); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup Perform Changes and Save', 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + persesDashboardsPage.assertPanelGroupNotExist('PanelGroup Perform Changes and Save'); + + }); + +} diff --git a/web/cypress/views/common.ts b/web/cypress/views/common.ts index e69fd524a..80e21a4aa 100644 --- a/web/cypress/views/common.ts +++ b/web/cypress/views/common.ts @@ -6,10 +6,16 @@ export const commonPages = { projectDropdownShouldNotExist: () => cy.byLegacyTestID('namespace-bar-dropdown').should('not.exist'), projectDropdownShouldExist: () => cy.byLegacyTestID('namespace-bar-dropdown').should('exist'), titleShouldHaveText: (title: string) => { + cy.wait(15000); cy.log('commonPages.titleShouldHaveText - ' + `${title}`); cy.bySemanticElement('h1', title).scrollIntoView().should('be.visible'); }, + titleModalShouldHaveText: (title: string) => { + cy.log('commonPages.titleModalShouldHaveText - ' + `${title}`); + cy.bySemanticElement('h2', title).scrollIntoView().should('be.visible'); + }, + linkShouldExist: (linkName: string) => { cy.log('commonPages.linkShouldExist - ' + `${linkName}`); cy.bySemanticElement('button', linkName).should('be.visible'); diff --git a/web/cypress/views/list-perses-dashboards.ts b/web/cypress/views/list-perses-dashboards.ts index 5c7311189..bc9c22977 100644 --- a/web/cypress/views/list-perses-dashboards.ts +++ b/web/cypress/views/list-perses-dashboards.ts @@ -78,5 +78,6 @@ export const listPersesDashboardsPage = { const idx = index !== undefined ? index : 0; cy.log('listPersesDashboardsPage.clickDashboard'); cy.byTestID(listPersesDashboardsDataTestIDs.DashboardLinkPrefix+name).eq(idx).should('be.visible').click(); + cy.wait(15000); }, } diff --git a/web/cypress/views/perses-dashboards-edit-datasources.ts b/web/cypress/views/perses-dashboards-edit-datasources.ts new file mode 100644 index 000000000..c7c1bcfdc --- /dev/null +++ b/web/cypress/views/perses-dashboards-edit-datasources.ts @@ -0,0 +1,95 @@ +import { commonPages } from "./common"; +import { persesAriaLabels, persesMUIDataTestIDs, editPersesDashboardsAddDatasource, IDs } from "../../src/components/data-test"; +import { persesDashboardsModalTitles, persesDashboardsRequiredFields } from "../fixtures/perses/constants"; + +export const persesDashboardsEditDatasources = { + + shouldBeLoaded: () => { + cy.log('persesDashboardsEditVariables.shouldBeLoaded'); + commonPages.titleModalShouldHaveText(persesDashboardsModalTitles.EDIT_DASHBOARD_DATASOURCES); + cy.byAriaLabel(persesAriaLabels.EditDashboardDatasourcesTable).should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('button').contains('Apply').should('be.visible').and('have.attr', 'disabled'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('button').contains('Cancel').should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('button').contains('Add Datasource').should('be.visible'); + }, + + assertDatasource: (index: number, name: string, type: 'PrometheusDatasource' | 'TempoDatasource', description: string) => { + cy.log('persesDashboardsEditDatasources.assertDatasource'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('tbody').find('tr').eq(index).find('th').contains(name).should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('tbody').find('tr').eq(index).find('td').eq(0).contains(type).should('be.visible'); + if (description !== '') { + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('tbody').find('tr').eq(index).find('td').eq(1).contains(description).should('be.visible'); + } + }, + + assertDatasourceNotExist: (name: string) => { + cy.log('persesDashboardsEditDatasources.assertDatasource'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('th').contains(name).should('not.exist'); + }, + + clickButton: (button: 'Apply' | 'Cancel' | 'Add Datasource' | 'Add') => { + cy.log('persesDashboardsEditDatasources.clickButton'); + if (button === 'Cancel') { + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('button').contains(button).should('be.visible').click(); + cy.wait(1000); + cy.get('body').then((body) => { + if (body.find('#'+IDs.persesDashboardDiscardChangesDialog).length > 0 && body.find('#'+IDs.persesDashboardDiscardChangesDialog).is(':visible')) { + cy.bySemanticElement('button', 'Discard Changes').scrollIntoView().should('be.visible').click({ force: true }); + } + }); + } else { + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('button').contains(button).should('be.visible').click(); + } + }, + + addDatasource: (name: string, defaultDatasource: boolean, pluginOptions: 'Prometheus Datasource' | 'Tempo Datasource', displayLabel?: string, description?: string) => { + cy.log('persesDashboardsEditDatasources.addDatasource'); + cy.get('input[name="'+editPersesDashboardsAddDatasource.inputName+'"]').clear().type(name); + if (displayLabel !== undefined) { + cy.get('input[name="'+editPersesDashboardsAddDatasource.inputDisplayLabel+'"]').clear().type(displayLabel); + } + if (description !== undefined) { + cy.get('input[name="'+editPersesDashboardsAddDatasource.inputDescription+'"]').clear().type(description); + } + + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('input[name="'+editPersesDashboardsAddDatasource.inputDefaultDatasource+'"]').then((checkbox) => { + if ((checkbox.not(':checked') && defaultDatasource) || (checkbox.is(':checked') && !defaultDatasource)) { + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('input[name="'+editPersesDashboardsAddDatasource.inputDefaultDatasource+'"]').click(); + } + }); + + persesDashboardsEditDatasources.clickDropdownAndSelectOption('Source', pluginOptions); + + }, + + clickDropdownAndSelectOption: (label: string, option: string) => { + cy.log('persesDashboardsEditVariables.selectVariableType'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('label').contains(label).siblings('div').click(); + cy.get('li').contains(option).should('be.visible').click(); + }, + + assertRequiredFieldValidation: (field: string) => { + cy.log('persesDashboardsEditVariables.assertRequiredFieldValidation'); + + switch (field) { + case 'Name': + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('label').contains(field).siblings('p').should('have.text', persesDashboardsRequiredFields.AddVariableNameField); + break; + } + }, + + clickDiscardChangesButton: () => { + cy.log('persesDashboardsEditVariables.clickDiscardChangesButton'); + cy.bySemanticElement('button', 'Discard Changes').scrollIntoView().should('be.visible').click({ force: true }); + }, + + clickEditDatasourceButton: (index: number) => { + cy.log('persesDashboardsEditDatasources.clickEditDatasourceButton'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('[data-testid="'+persesMUIDataTestIDs.editDashboardEditVariableDatasourceEditButton+'"]').eq(index).should('be.visible').click(); + }, + + clickDeleteDatasourceButton: (index: number) => { + cy.log('persesDashboardsEditDatasources.clickDeleteDatasourceButton'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardDatasourcesModal).find('[data-testid="'+persesMUIDataTestIDs.editDashboardEditVariableDatasourceDeleteButton+'"]').eq(index).should('be.visible').click(); + }, +} diff --git a/web/cypress/views/perses-dashboards-edit-variables.ts b/web/cypress/views/perses-dashboards-edit-variables.ts new file mode 100644 index 000000000..9a1b393e9 --- /dev/null +++ b/web/cypress/views/perses-dashboards-edit-variables.ts @@ -0,0 +1,139 @@ +import { commonPages } from "./common"; +import { persesAriaLabels, persesMUIDataTestIDs, IDs, editPersesDashboardsAddVariable } from "../../src/components/data-test"; +import { persesDashboardsModalTitles, persesDashboardsAddListVariableSource, persesDashboardsAddListVariableSort, persesDashboardsRequiredFields } from "../fixtures/perses/constants"; + +export const persesDashboardsEditVariables = { + + shouldBeLoaded: () => { + cy.log('persesDashboardsEditVariables.shouldBeLoaded'); + commonPages.titleModalShouldHaveText(persesDashboardsModalTitles.EDIT_DASHBOARD_VARIABLES); + cy.byAriaLabel(persesAriaLabels.EditDashboardVariablesTable).should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('button').contains('Apply').should('be.visible').and('have.attr', 'disabled'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('button').contains('Cancel').should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('button').contains('Add Variable').should('be.visible'); + commonPages.titleModalShouldHaveText(persesDashboardsModalTitles.DASHBOARD_BUILT_IN_VARIABLES); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('#'+IDs.persesDashboardEditVariablesModalBuiltinButton).should('have.attr', 'aria-expanded', 'false'); + }, + + clickButton: (button: 'Apply' | 'Cancel' | 'Add Variable' | 'Add' | 'Run Query') => { + cy.log('persesDashboardsEditVariables.clickButton'); + cy.wait(3000); + if (button === 'Cancel') { + cy.get('body').then((body) => { + if (body.find('#'+IDs.persesDashboardDiscardChangesDialog).length > 0 && body.find('#'+IDs.persesDashboardDiscardChangesDialog).is(':visible')) { + cy.bySemanticElement('button', 'Discard Changes').scrollIntoView().should('be.visible').click({ force: true }); + } + }); + } else { + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('button').contains(button).should('be.visible').click(); + } + }, + + addTextVariable: (name: string, constant: boolean, displayLabel?: string, description?: string, value?: string) => { + cy.log('persesDashboardsEditVariables.addTextVariable'); + cy.get('input[name="'+editPersesDashboardsAddVariable.inputName+'"]').clear().type(name); + + const displayLabelInput = displayLabel !== undefined ? displayLabel : name; + const descriptionInput = description !== undefined ? description : name; + const valueInput = value !== undefined ? value : ''; + + cy.get('input[name="'+editPersesDashboardsAddVariable.inputDisplayLabel+'"]').clear().type(displayLabelInput); + cy.get('input[name="'+editPersesDashboardsAddVariable.inputDescription+'"]').clear().type(descriptionInput); + cy.get('input[name="'+editPersesDashboardsAddVariable.inputValue+'"]').clear().type(valueInput); + if (constant) { + cy.get('input[name="'+editPersesDashboardsAddVariable.inputConstant+'"]').click(); + } + }, + + addListVariable: ( + name: string, + allowMultiple: boolean, + allowAllValue: boolean, + customAllValue?: string, + displayLabel?: string, + description?: string, + source?: persesDashboardsAddListVariableSource, + sort?: persesDashboardsAddListVariableSort) => { + cy.log('persesDashboardsEditVariables.addListVariable'); + cy.get('input[name="'+editPersesDashboardsAddVariable.inputName+'"]').clear().type(name); + + if (displayLabel !== undefined && displayLabel !== '') { + cy.get('input[name="'+editPersesDashboardsAddVariable.inputDisplayLabel+'"]').clear().type(displayLabel); + } + if (description !== undefined && description !== '' ) { + cy.get('input[name="'+editPersesDashboardsAddVariable.inputDescription+'"]').clear().type(description); + } + persesDashboardsEditVariables.clickDropdownAndSelectOption('Type', 'List'); + + if (source !== undefined) { + persesDashboardsEditVariables.clickDropdownAndSelectOption('Source', source); + } + if (sort !== undefined) { + persesDashboardsEditVariables.clickDropdownAndSelectOption('Sort', sort); + } + if (allowMultiple) { + cy.get('input[name="'+editPersesDashboardsAddVariable.inputAllowMultiple+'"]').click(); + } + if (allowAllValue) { + cy.get('input[name="'+editPersesDashboardsAddVariable.inputAllowAllValue+'"]').click(); + if (customAllValue !== undefined && customAllValue !== '') { + cy.get('input[name="'+editPersesDashboardsAddVariable.inputCustomAllValue+'"]').clear().type(customAllValue); + } + } + }, + + /** + * + * @param label - label of the dropdown + * @param option - option to select + */ + clickDropdownAndSelectOption: (label: string, option: string) => { + cy.log('persesDashboardsEditVariables.selectVariableType'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('label').contains(label).siblings('div').click(); + cy.get('li').contains(option).should('be.visible').click(); + }, + + assertRequiredFieldValidation: (field: string) => { + cy.log('persesDashboardsEditVariables.assertRequiredFieldValidation'); + + switch (field) { + case 'Name': + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('label').contains(field).siblings('p').should('have.text', persesDashboardsRequiredFields.AddVariableNameField); + break; + } + }, + + clickDiscardChangesButton: () => { + cy.log('persesDashboardsEditVariables.clickDiscardChangesButton'); + cy.bySemanticElement('button', 'Discard Changes').scrollIntoView().should('be.visible').click({ force: true }); + }, + + toggleVariableVisibility: (index: number, visible: boolean) => { + cy.log('persesDashboardsEditVariables.toggleVariableVisibility'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('input[type="checkbox"]').eq(index).then((checkbox) => { + if ((checkbox.not(':checked') && visible) || (checkbox.is(':checked') && !visible)) { + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('input[type="checkbox"]').eq(index).click(); + } + }); + }, + + moveVariableUp: (index: number) => { + cy.log('persesDashboardsEditVariables.moveVariableUp'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('[data-testid="'+persesMUIDataTestIDs.editDashboardEditVariableMoveUpButton+'"]').eq(index).should('be.visible').click(); + }, + + moveVariableDown: (index: number) => { + cy.log('persesDashboardsEditVariables.moveVariableDown'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('[data-testid="'+persesMUIDataTestIDs.editDashboardEditVariableMoveDownButton+'"]').eq(index).should('be.visible').click(); + }, + + clickEditVariableButton: (index: number) => { + cy.log('persesDashboardsEditVariables.clickEditVariableButton'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('[data-testid="'+persesMUIDataTestIDs.editDashboardEditVariableDatasourceEditButton+'"]').eq(index).should('be.visible').click(); + }, + + clickDeleteVariableButton: (index: number) => { + cy.log('persesDashboardsEditVariables.clickDeleteVariableButton'); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('[data-testid="'+persesMUIDataTestIDs.editDashboardEditVariableDatasourceDeleteButton+'"]').eq(index).should('be.visible').click(); + }, +} diff --git a/web/cypress/views/perses-dashboards-panel.ts b/web/cypress/views/perses-dashboards-panel.ts new file mode 100644 index 000000000..a016281f2 --- /dev/null +++ b/web/cypress/views/perses-dashboards-panel.ts @@ -0,0 +1,116 @@ +import { commonPages } from "./common"; +import { persesAriaLabels, persesMUIDataTestIDs, IDs, editPersesDashboardsAddPanel } from "../../src/components/data-test"; +import { persesDashboardsModalTitles, persesDashboardsRequiredFields, persesDashboardsAddListPanelType } from "../fixtures/perses/constants"; +import { persesDashboardsPage } from "./perses-dashboards"; + +export const persesDashboardsPanel = { + + addPanelShouldBeLoaded: () => { + cy.log('persesDashboardsPanel.addPanelShouldBeLoaded'); + commonPages.titleModalShouldHaveText(persesDashboardsModalTitles.ADD_PANEL); + cy.get('input[name="'+editPersesDashboardsAddPanel.inputName+'"]').should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelForm).find('label').contains('Group').should('be.visible'); + cy.get('input[name="'+editPersesDashboardsAddPanel.inputDescription+'"]').should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelForm).find('label').contains('Type').should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelForm).parent('div').find('h2').siblings('div').find('button').contains('Add').should('be.visible').and('have.attr', 'disabled'); + cy.get('#'+IDs.persesDashboardAddPanelForm).parent('div').find('h2').siblings('div').find('button').contains('Cancel').should('be.visible'); + }, + + clickButton: (button: 'Add' | 'Cancel') => { + cy.log('persesDashboardsPanel.clickButton'); + if (button === 'Cancel') { + cy.get('#'+IDs.persesDashboardAddPanelForm).siblings('div').find('button').contains(button).should('be.visible').click(); + cy.wait(1000); + cy.get('body').then((body) => { + if (body.find('#'+IDs.persesDashboardDiscardChangesDialog).length > 0 && body.find('#'+IDs.persesDashboardDiscardChangesDialog).is(':visible')) { + cy.bySemanticElement('button', 'Discard Changes').scrollIntoView().should('be.visible').click({ force: true }); + } + }); + } else { + cy.get('#'+IDs.persesDashboardAddPanelForm).siblings('div').find('button').contains(button).should('be.visible').click(); + } + }, + + clickDropdownAndSelectOption: (label: string, option: string) => { + cy.log('persesDashboardsPanel.clickDropdownAndSelectOption'); + cy.get('#'+IDs.persesDashboardAddPanelForm).find('label').contains(label).siblings('div').click(); + cy.wait(1000); + cy.get('li').contains(option).should('be.visible').click(); + }, + + assertRequiredFieldValidation: (field: string) => { + cy.log('persesDashboardsPanel.assertRequiredFieldValidation'); + + switch (field) { + case 'Name': + cy.get('#'+IDs.persesDashboardAddPanelForm).find('label').contains(field).siblings('p').should('have.text', 'Required'); + break; + } + }, + + addPanel: (name: string, group: string, type: string, description?: string) => { + cy.log('persesDashboardsPanel.addPanel'); + cy.wait(2000); + cy.get('input[name="'+editPersesDashboardsAddPanel.inputName+'"]').clear().type(name); + if (description !== undefined && description !== '') { + cy.get('input[name="'+editPersesDashboardsAddPanel.inputDescription+'"]').clear().type(description); + } + persesDashboardsPanel.clickDropdownAndSelectOption('Group', group); + persesDashboardsPanel.clickDropdownAndSelectOption('Type', type); + cy.get('#'+IDs.persesDashboardAddPanelForm).parent('div').find('h2').siblings('div').find('button').contains('Add').should('be.visible').click(); + }, + + editPanelShouldBeLoaded: () => { + cy.log('persesDashboardsPanel.editPanelShouldBeLoaded'); + commonPages.titleModalShouldHaveText(persesDashboardsModalTitles.ADD_PANEL); + cy.get('input[name="'+editPersesDashboardsAddPanel.inputName+'"]').should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelForm).find('label').contains('Group').should('be.visible'); + cy.get('input[name="'+editPersesDashboardsAddPanel.inputDescription+'"]').should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelForm).find('label').contains('Type').should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelForm).parent('div').find('h2').siblings('div').find('button').contains('Apply').should('be.visible').and('have.attr', 'disabled'); + cy.get('#'+IDs.persesDashboardAddPanelForm).parent('div').find('h2').siblings('div').find('button').contains('Cancel').should('be.visible'); + }, + + editPanel: (name: string, group: string, type: string, description?: string) => { + cy.log('persesDashboardsPanel.editPanel'); + cy.get('input[name="'+editPersesDashboardsAddPanel.inputName+'"]').clear().type(name); + if (description !== undefined && description !== '') { + cy.get('input[name="'+editPersesDashboardsAddPanel.inputDescription+'"]').clear().type(description); + } + persesDashboardsPanel.clickDropdownAndSelectOption('Group', group); + persesDashboardsPanel.clickDropdownAndSelectOption('Type', type); + cy.get('#'+IDs.persesDashboardAddPanelForm).parent('div').find('h2').siblings('div').find('button').contains('Apply').should('be.visible').click(); + }, + + clickPanelGroupAction: (panelGroup: string, button: 'addPanel' | 'edit' | 'delete' | 'moveDown' | 'moveUp') => { + cy.log('persesDashboardsPage.clickPanelActions'); + + switch (button) { + case 'addPanel': + cy.byAriaLabel(persesAriaLabels.AddPanelToGroupPrefix + panelGroup).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'edit': + cy.byAriaLabel(persesAriaLabels.EditPanelGroupPrefix + panelGroup).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'delete': + cy.byAriaLabel(persesAriaLabels.DeletePanelGroupPrefix + panelGroup).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'moveDown': + cy.byAriaLabel(persesAriaLabels.MovePanelGroupPrefix + panelGroup + persesAriaLabels.MovePanelGroupDownSuffix).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'moveUp': + cy.byAriaLabel(persesAriaLabels.MovePanelGroupPrefix + panelGroup + persesAriaLabels.MovePanelGroupUpSuffix).scrollIntoView().should('be.visible').click({ force: true }); + break; + } + }, + + deletePanel: (panel: string) => { + cy.log('persesDashboardsPage.deletePanel'); + cy.byAriaLabel(persesAriaLabels.EditPanelDeleteButtonPrefix + panel).scrollIntoView().should('be.visible').click({ force: true }); + }, + + clickDeletePanelButton: () => { + cy.log('persesDashboardsPage.clickDeletePanelButton'); + cy.bySemanticElement('button', 'Delete').scrollIntoView().should('be.visible').click({ force: true }); + }, +} \ No newline at end of file diff --git a/web/cypress/views/perses-dashboards-panelgroup.ts b/web/cypress/views/perses-dashboards-panelgroup.ts new file mode 100644 index 000000000..37e9e77cc --- /dev/null +++ b/web/cypress/views/perses-dashboards-panelgroup.ts @@ -0,0 +1,96 @@ +import { commonPages } from "./common"; +import { persesAriaLabels, persesMUIDataTestIDs, IDs } from "../../src/components/data-test"; +import { persesDashboardsModalTitles, persesDashboardsRequiredFields } from "../fixtures/perses/constants"; + +export const persesDashboardsPanelGroup = { + + addPanelGroupShouldBeLoaded: () => { + cy.log('persesDashboardsPanelGroup.addPanelGroupShouldBeLoaded'); + commonPages.titleModalShouldHaveText(persesDashboardsModalTitles.ADD_PANEL_GROUP); + cy.byDataTestID(persesMUIDataTestIDs.addPanelGroupFormName).should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).find('input').eq(1).should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).find('input').eq(2).should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).parent('div').siblings('div').find('button').contains('Apply').should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).parent('div').siblings('div').find('button').contains('Cancel').should('be.visible'); + }, + + clickButton: (button: 'Add' | 'Cancel') => { + cy.log('persesDashboardsPanelGroup.clickButton'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).parent('div').siblings('div').find('button').contains(button).should('be.visible').click(); + }, + + assertRequiredFieldValidation: (field: string) => { + cy.log('persesDashboardsPanelGroup.assertRequiredFieldValidation'); + + switch (field) { + case 'Name': + cy.byDataTestID(persesMUIDataTestIDs.editDashboardVariablesModal).find('label').contains(field).siblings('p').should('have.text', persesDashboardsRequiredFields.AddVariableNameField); + break; + } + }, + + addPanelGroup: (name: string, collapse_state: 'Open' | 'Closed', repeat_variable: string) => { + cy.log('persesDashboardsPanelGroup.addPanelGroup'); + cy.wait(2000); + cy.byDataTestID(persesMUIDataTestIDs.addPanelGroupFormName).find('input').clear().type(name); + cy.byPFRole('dialog').find('div[role="combobox"]').eq(0).click(); + cy.byPFRole('option').contains(collapse_state).click(); + if (repeat_variable !== undefined && repeat_variable !== '') { + cy.byPFRole('dialog').find('div[role="combobox"]').eq(1).click(); + cy.byPFRole('option').contains(repeat_variable).click(); + } + cy.bySemanticElement('button', 'Add').should('be.visible').click(); + }, + + editPanelGroupShouldBeLoaded: () => { + cy.log('persesDashboardsPanelGroup.editPanelGroupShouldBeLoaded'); + commonPages.titleModalShouldHaveText(persesDashboardsModalTitles.EDIT_PANEL_GROUP); + cy.byDataTestID(persesMUIDataTestIDs.addPanelGroupFormName).should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).find('input').eq(1).should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).find('input').eq(2).should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).parent('div').siblings('div').find('button').contains('Apply').should('be.visible'); + cy.get('#'+IDs.persesDashboardAddPanelGroupForm).parent('div').siblings('div').find('button').contains('Cancel').should('be.visible'); + }, + + editPanelGroup: (name: string, collapse_state: 'Open' | 'Closed', repeat_variable: string) => { + cy.log('persesDashboardsPanelGroup.editPanelGroup'); + cy.byDataTestID(persesMUIDataTestIDs.addPanelGroupFormName).find('input').clear().type(name); + cy.byPFRole('dialog').find('div[role="combobox"]').eq(0).click(); + cy.byPFRole('option').contains(collapse_state).click(); + if (repeat_variable !== undefined && repeat_variable !== '') { + cy.byPFRole('dialog').find('div[role="combobox"]').eq(1).click(); + cy.byPFRole('option').contains(repeat_variable).click(); + } + cy.bySemanticElement('button', 'Apply').should('be.visible').click(); + }, + + clickPanelGroupAction: (panelGroup: string, button: 'addPanel' | 'edit' | 'delete' | 'moveDown' | 'moveUp') => { + cy.log('persesDashboardsPage.clickPanelActions'); + + switch (button) { + case 'addPanel': + cy.byAriaLabel(persesAriaLabels.AddPanelToGroupPrefix + panelGroup).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'edit': + cy.byAriaLabel(persesAriaLabels.EditPanelGroupPrefix + panelGroup).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'delete': + cy.byAriaLabel(persesAriaLabels.DeletePanelGroupPrefix + panelGroup).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'moveDown': + cy.byAriaLabel(persesAriaLabels.MovePanelGroupPrefix + panelGroup + persesAriaLabels.MovePanelGroupDownSuffix).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'moveUp': + cy.byAriaLabel(persesAriaLabels.MovePanelGroupPrefix + panelGroup + persesAriaLabels.MovePanelGroupUpSuffix).scrollIntoView().should('be.visible').click({ force: true }); + break; + } + + }, + + clickDeletePanelGroupButton: () => { + cy.log('persesDashboardsPage.clickDeletePanelGroupButton'); + cy.bySemanticElement('button', 'Delete').scrollIntoView().should('be.visible').click({ force: true }); + }, + + +} \ No newline at end of file diff --git a/web/cypress/views/perses-dashboards.ts b/web/cypress/views/perses-dashboards.ts index 4eef67563..73e850fd4 100644 --- a/web/cypress/views/perses-dashboards.ts +++ b/web/cypress/views/perses-dashboards.ts @@ -1,7 +1,7 @@ import { commonPages } from "./common"; -import { DataTestIDs, Classes, LegacyTestIDs, persesAriaLabels, persesMUIDataTestIDs, listPersesDashboardsOUIAIDs, IDs, persesDashboardDataTestIDs } from "../../src/components/data-test"; +import { DataTestIDs, Classes, LegacyTestIDs, persesAriaLabels, persesMUIDataTestIDs, listPersesDashboardsOUIAIDs, IDs, persesDashboardDataTestIDs, listPersesDashboardsDataTestIDs } from "../../src/components/data-test"; import { MonitoringPageTitles } from "../fixtures/monitoring/constants"; -import { listPersesDashboardsPageSubtitle } from "../fixtures/perses/constants"; +import { listPersesDashboardsPageSubtitle, persesDashboardsModalTitles } from "../fixtures/perses/constants"; import { persesDashboardsTimeRange, persesDashboardsRefreshInterval, persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev, persesDashboardsAcceleratorsCommonMetricsPanels } from "../fixtures/perses/constants"; export const persesDashboardsPage = { @@ -9,14 +9,14 @@ export const persesDashboardsPage = { shouldBeLoaded: () => { cy.log('persesDashboardsPage.shouldBeLoaded'); commonPages.titleShouldHaveText(MonitoringPageTitles.DASHBOARDS); - cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).contains(persesDashboardsTimeRange.LAST_30_MINUTES).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.ZoomInButton).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.ZoomOutButton).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.RefreshButton).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.RefreshIntervalDropdown).contains(persesDashboardsRefreshInterval.OFF).should('be.visible'); - cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('input').should('be.visible'); - cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').should('be.visible'); - cy.byLegacyTestID(LegacyTestIDs.PersesDashboardSection).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).contains(persesDashboardsTimeRange.LAST_30_MINUTES).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.ZoomInButton).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.ZoomOutButton).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.RefreshButton).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.RefreshIntervalDropdown).contains(persesDashboardsRefreshInterval.OFF).scrollIntoView().should('be.visible'); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('input').scrollIntoView().should('be.visible'); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').scrollIntoView().should('be.visible'); + cy.byLegacyTestID(LegacyTestIDs.PersesDashboardSection).scrollIntoView().should('be.visible'); }, @@ -24,91 +24,107 @@ export const persesDashboardsPage = { shouldBeLoaded1: () => { cy.log('persesDashboardsPage.shouldBeLoaded'); commonPages.titleShouldHaveText(MonitoringPageTitles.DASHBOARDS); - cy.byOUIAID(listPersesDashboardsOUIAIDs.PageHeaderSubtitle).should('contain', listPersesDashboardsPageSubtitle).should('be.visible'); + cy.byOUIAID(listPersesDashboardsOUIAIDs.PageHeaderSubtitle).scrollIntoView().should('contain', listPersesDashboardsPageSubtitle).should('be.visible'); - cy.byTestID(persesDashboardDataTestIDs.editDashboardButtonToolbar).should('be.visible'); + cy.byTestID(persesDashboardDataTestIDs.editDashboardButtonToolbar).scrollIntoView().should('be.visible'); - cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).contains(persesDashboardsTimeRange.LAST_30_MINUTES).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.ZoomInButton).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.ZoomOutButton).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.RefreshButton).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.RefreshIntervalDropdown).contains(persesDashboardsRefreshInterval.OFF).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).contains(persesDashboardsTimeRange.LAST_30_MINUTES).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.ZoomInButton).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.ZoomOutButton).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.RefreshButton).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.RefreshIntervalDropdown).contains(persesDashboardsRefreshInterval.OFF).scrollIntoView().should('be.visible'); - cy.get('#'+IDs.persesDashboardDownloadButton).should('be.visible'); - cy.byAriaLabel(persesAriaLabels.ViewJSONButton).should('be.visible'); + cy.get('#' + IDs.persesDashboardDownloadButton).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.ViewJSONButton).scrollIntoView().should('be.visible'); - cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('input').should('be.visible'); - cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').should('be.visible'); - cy.byLegacyTestID(LegacyTestIDs.PersesDashboardSection).should('be.visible'); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('input').scrollIntoView().should('be.visible'); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').scrollIntoView().should('be.visible'); + cy.byLegacyTestID(LegacyTestIDs.PersesDashboardSection).scrollIntoView().should('be.visible'); }, clickTimeRangeDropdown: (timeRange: persesDashboardsTimeRange) => { cy.log('persesDashboardsPage.clickTimeRangeDropdown'); - cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).should('be.visible').click({force: true}); - cy.byPFRole('option').contains(timeRange).should('be.visible').click({force: true}); + cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).scrollIntoView().should('be.visible').click({ force: true }); + cy.byPFRole('option').contains(timeRange).scrollIntoView().should('be.visible').click({ force: true }); }, timeRangeDropdownAssertion: () => { cy.log('persesDashboardsPage.timeRangeDropdownAssertion'); - cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).should('be.visible').click({force: true}); + cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).scrollIntoView().should('be.visible').click({ force: true }); const timeRanges = Object.values(persesDashboardsTimeRange); timeRanges.forEach((timeRange) => { cy.log('Time range: ' + timeRange); - cy.byPFRole('option').contains(timeRange).should('be.visible'); + cy.byPFRole('option').contains(timeRange).scrollIntoView().should('be.visible'); }); - cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).should('be.visible').click({force: true}); + cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).scrollIntoView().should('be.visible').click({ force: true }); }, clickRefreshButton: () => { cy.log('persesDashboardsPage.clickRefreshButton'); - cy.byAriaLabel(persesAriaLabels.RefreshButton).should('be.visible').click(); + cy.byAriaLabel(persesAriaLabels.RefreshButton).scrollIntoView().should('be.visible').click(); }, clickRefreshIntervalDropdown: (interval: persesDashboardsRefreshInterval) => { cy.log('persesDashboardsPage.clickRefreshIntervalDropdown'); - cy.byAriaLabel(persesAriaLabels.RefreshIntervalDropdown).should('be.visible').click({force: true}); - cy.byPFRole('option').contains(interval).should('be.visible').click({force: true}); + cy.byAriaLabel(persesAriaLabels.RefreshIntervalDropdown).scrollIntoView().should('be.visible').click({ force: true }); + cy.byPFRole('option').contains(interval).scrollIntoView().should('be.visible').click({ force: true }); }, refreshIntervalDropdownAssertion: () => { cy.log('persesDashboardsPage.refreshIntervalDropdownAssertion'); - cy.byAriaLabel(persesAriaLabels.RefreshIntervalDropdown).should('be.visible').click({force: true}); + cy.byAriaLabel(persesAriaLabels.RefreshIntervalDropdown).scrollIntoView().should('be.visible').click({ force: true }); const intervals = Object.values(persesDashboardsRefreshInterval); intervals.forEach((interval) => { cy.log('Refresh interval: ' + interval); - cy.byPFRole('option').contains(interval).should('be.visible'); + cy.byPFRole('option').contains(interval).scrollIntoView().should('be.visible'); }); //Closing the dropdown by clicking on the OFF option, because the dropdown is not accessible while the menu is open, even forcing it - cy.byPFRole('option').contains(persesDashboardsRefreshInterval.OFF).should('be.visible').click({force: true}); - + cy.byPFRole('option').contains(persesDashboardsRefreshInterval.OFF).scrollIntoView().should('be.visible').click({ force: true }); + }, clickDashboardDropdown: (dashboard: keyof typeof persesDashboardsDashboardDropdownCOO | keyof typeof persesDashboardsDashboardDropdownPersesDev) => { cy.log('persesDashboardsPage.clickDashboardDropdown'); - cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').should('be.visible').click({force: true}); - cy.byPFRole('option').contains(dashboard).should('be.visible').click({force: true}); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').scrollIntoView().should('be.visible').click({ force: true }); + cy.byPFRole('option').contains(dashboard).scrollIntoView().should('be.visible').click({ force: true }); }, dashboardDropdownAssertion: (constants: typeof persesDashboardsDashboardDropdownCOO | typeof persesDashboardsDashboardDropdownPersesDev) => { cy.log('persesDashboardsPage.dashboardDropdownAssertion'); - cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').should('be.visible').click({force: true}); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').scrollIntoView().should('be.visible').click({ force: true }); const dashboards = Object.values(constants); dashboards.forEach((dashboard) => { cy.log('Dashboard: ' + dashboard[0]); - cy.get(Classes.MenuItem).contains(dashboard[0]).should('be.visible'); + cy.get(Classes.MenuItem).contains(dashboard[0]).scrollIntoView().should('be.visible'); if (dashboard[1] !== '') { - cy.get(Classes.MenuItem).should('contain', dashboard[0]).and('contain', dashboard[1]); + cy.get(Classes.MenuItem).scrollIntoView().should('contain', dashboard[0]).and('contain', dashboard[1]); } }); cy.wait(1000); - cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').should('be.visible').click({force: true}); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').scrollIntoView().should('be.visible').click({ force: true }); }, - panelGroupHeaderAssertion: (panelGroupHeader: string) => { + panelGroupHeaderAssertion: (panelGroupHeader: string, collapse_state: 'Open' | 'Closed') => { cy.log('persesDashboardsPage.panelGroupHeaderAssertion'); - cy.byDataTestID(persesMUIDataTestIDs.panelGroupHeader).contains(panelGroupHeader).should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.panelGroupHeader).contains(panelGroupHeader).scrollIntoView().should('be.visible'); + if (collapse_state === 'Open') { + cy.byAriaLabel(persesAriaLabels.CollapseGroupButtonPrefix + panelGroupHeader).scrollIntoView().should('be.visible'); + } else { + cy.byAriaLabel(persesAriaLabels.OpenGroupButtonPrefix + panelGroupHeader).scrollIntoView().should('be.visible'); + } + }, + + assertPanelGroupNotExist: (panelGroup: string) => { + cy.log('persesDashboardsPage.assertPanelGroupNotExist'); + cy.byAriaLabel(persesAriaLabels.OpenGroupButtonPrefix + panelGroup).should('not.exist'); + cy.byAriaLabel(persesAriaLabels.CollapseGroupButtonPrefix + panelGroup).should('not.exist'); + }, + + assertPanelGroupOrder: (panelGroup: string, order: number) => { + cy.log('persesDashboardsPage.assertPanelGroupOrder'); + cy.byDataTestID(persesMUIDataTestIDs.panelGroupHeader).eq(order).find('h2').contains(panelGroup).scrollIntoView().should('be.visible'); }, panelHeadersAcceleratorsCommonMetricsAssertion: () => { @@ -121,14 +137,24 @@ export const persesDashboardsPage = { }); }, - expandPanel: (panel: keyof typeof persesDashboardsAcceleratorsCommonMetricsPanels | string) => { + expandPanel: (panel: string) => { cy.log('persesDashboardsPage.expandPanel'); - cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(panel).scrollIntoView().siblings('div').eq(2).find('[data-testid="ArrowExpandIcon"]').click({force: true}); + persesDashboardsPage.clickPanelAction(panel, 'expand'); }, - collapsePanel: (panel: keyof typeof persesDashboardsAcceleratorsCommonMetricsPanels | string) => { + collapsePanel: (panel: string) => { cy.log('persesDashboardsPage.collapsePanel'); - cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(panel).scrollIntoView().siblings('div').eq(2).find('[data-testid="ArrowCollapseIcon"]').click({force: true}); + persesDashboardsPage.clickPanelAction(panel, 'collapse'); + }, + + expandPanelGroup: (panelGroup: string) => { + cy.log('persesDashboardsPage.expandPanelGroup'); + cy.byAriaLabel(persesAriaLabels.OpenGroupButtonPrefix + panelGroup).scrollIntoView().should('be.visible').click({ force: true }); + }, + + collapsePanelGroup: (panelGroup: string) => { + cy.log('persesDashboardsPage.collapsePanelGroup'); + cy.byAriaLabel(persesAriaLabels.CollapseGroupButtonPrefix + panelGroup).scrollIntoView().should('be.visible').click({ force: true }); }, statChartValueAssertion: (panel: keyof typeof persesDashboardsAcceleratorsCommonMetricsPanels | string, noData: boolean) => { @@ -143,8 +169,228 @@ export const persesDashboardsPage = { searchAndSelectVariable: (variable: string, value: string) => { cy.log('persesDashboardsPage.searchAndSelectVariable'); - cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-'+variable).find('input').type(value); - cy.byPFRole('option').contains(value).click({force: true}); + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown + '-' + variable).find('input').type(value); + cy.byPFRole('option').contains(value).click({ force: true }); + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown + '-' + variable).find('button').click({ force: true }); + cy.wait(1000); + }, + + searchAndTypeVariable: (variable: string, value: string) => { + cy.log('persesDashboardsPage.searchAndTypeVariable'); + if (value !== undefined && value !== '') { + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown + '-' + variable).find('input').type(value); + } + cy.wait(1000); + }, + + assertVariableBeVisible: (variable: string) => { + cy.log('persesDashboardsPage.assertVariableBeVisible'); + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown + '-' + variable).should('be.visible'); + }, + + assertVariableNotExist: (variable: string) => { + cy.log('persesDashboardsPage.assertVariableNotExist'); + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown + '-' + variable).should('not.exist'); + }, + + assertVariableNotBeVisible: (variable: string) => { + cy.log('persesDashboardsPage.assertVariableNotBeVisible'); + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown + '-' + variable).should('not.be.visible'); + }, + + clickEditButton: () => { + cy.log('persesDashboardsPage.clickEditButton'); + cy.byTestID(persesDashboardDataTestIDs.editDashboardButtonToolbar).scrollIntoView().should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertEditModeButtons: () => { + cy.log('persesDashboardsPage.assertEditModeButtons'); + cy.byTestID(persesDashboardDataTestIDs.editDashboardButtonToolbar).should('not.exist'); + cy.byAriaLabel(persesAriaLabels.EditVariablesButton).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.EditDatasourcesButton).should('not.exist'); + cy.byAriaLabel(persesAriaLabels.AddPanelButton).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.AddGroupButton).should('be.visible'); + cy.bySemanticElement('button', 'Save').should('be.visible'); + cy.byTestID(persesDashboardDataTestIDs.cancelButtonToolbar).should('be.visible'); + }, + + clickEditActionButton: (button: 'EditVariables' | 'AddPanel' | 'AddGroup' | 'Save' | 'Cancel') => { + cy.log('persesDashboardsPage.clickEditActionButton'); + cy.wait(2000); + switch (button) { + case 'EditVariables': + cy.byAriaLabel(persesAriaLabels.EditVariablesButton).scrollIntoView().should('be.visible').click({ force: true }); + break; + //TODO: OU-1054 target for COO1.5.0, so, commenting out for now + // case 'EditDatasources': + // cy.byAriaLabel(persesAriaLabels.EditDatasourcesButton).scrollIntoView().should('be.visible').click({ force: true }); + // break; + case 'AddPanel': + cy.byAriaLabel(persesAriaLabels.AddPanelButton).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'AddGroup': + cy.byAriaLabel(persesAriaLabels.AddGroupButton).scrollIntoView().should('be.visible').click({ force: true }); + break; + case 'Save': + cy.bySemanticElement('button', 'Save').scrollIntoView().should('be.visible').click({ force: true }); + persesDashboardsPage.clickSaveDashboardButton(); + break; + case 'Cancel': + cy.byTestID(persesDashboardDataTestIDs.cancelButtonToolbar).scrollIntoView().should('be.visible').click({ force: true }); + cy.wait(1000); + persesDashboardsPage.clickDiscardChangesButton(); + break; + } + }, + + assertEditModePanelGroupButtons: (panelGroup: string) => { + cy.log('persesDashboardsPage.assertEditModePanelGroupButtons'); + cy.byAriaLabel(persesAriaLabels.AddPanelToGroupPrefix + panelGroup).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.EditPanelGroupPrefix + panelGroup).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.DeletePanelGroupPrefix + panelGroup).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.MovePanelGroupPrefix + panelGroup + persesAriaLabels.MovePanelGroupDownSuffix).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.MovePanelGroupPrefix + panelGroup + persesAriaLabels.MovePanelGroupUpSuffix).scrollIntoView().should('be.visible'); + }, + + clickPanelAction: (panel: string, button: 'expand' | 'collapse' | 'edit' | 'duplicate' | 'delete') => { + cy.log('persesDashboardsPage.clickPanelActions'); + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(panel).siblings('div').eq(0).then((element1) => { + if (element1.find('[data-testid="MenuIcon"]').length > 0 && element1.find('[data-testid="MenuIcon"]').is(':visible')) { + cy.byAriaLabel(persesAriaLabels.EditPanelActionMenuButtonPrefix + panel).should('be.visible').click({ force: true }); + } + }); + + switch (button) { + case 'expand': + cy.byAriaLabel(persesAriaLabels.EditPanelExpandCollapseButtonPrefix + panel + persesAriaLabels.EditPanelExpandCollapseButtonSuffix).find('[data-testid="ArrowExpandIcon"]').eq(0).invoke('show').click({ force: true }); + break; + case 'collapse': + cy.byAriaLabel(persesAriaLabels.EditPanelExpandCollapseButtonPrefix + panel + persesAriaLabels.EditPanelExpandCollapseButtonSuffix).find('[data-testid="ArrowCollapseIcon"]').eq(1).should('be.visible').click({ force: true }); + break; + case 'edit': + cy.byAriaLabel(persesAriaLabels.EditPanelPrefix + panel).should('be.visible').click({ force: true }); + break; + case 'duplicate': + cy.byAriaLabel(persesAriaLabels.EditPanelDuplicateButtonPrefix + panel).should('be.visible').click({ force: true }); + break; + case 'delete': + cy.byAriaLabel(persesAriaLabels.EditPanelDeleteButtonPrefix + panel).should('be.visible').click({ force: true }); + break; + } + }, + + assertPanelActionButtons: (panel: string) => { + cy.log('persesDashboardsPage.assertPanelActionButtons'); + + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(panel).siblings('div').eq(1).then((element1) => { + if (element1.find('[data-testid="MenuIcon"]').length > 0 && element1.find('[data-testid="MenuIcon"]').is(':visible')) { + cy.byAriaLabel(persesAriaLabels.EditPanelExpandCollapseButtonPrefix + panel + persesAriaLabels.EditPanelExpandCollapseButtonSuffix).find('[data-testid="ArrowExpandIcon"]').eq(0).should('be.visible').click(); + } + cy.byAriaLabel(persesAriaLabels.EditPanelExpandCollapseButtonPrefix + panel + persesAriaLabels.EditPanelExpandCollapseButtonSuffix).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.EditPanelPrefix + panel).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.EditPanelDuplicateButtonPrefix + panel).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.EditPanelDeleteButtonPrefix + panel).should('be.visible'); + + }); + }, + + clickSaveDashboardButton: () => { + cy.log('persesDashboardsPage.clickSaveDashboardButton'); + + cy.get('body').then((body) => { + if (body.find('[data-testid="CloseIcon"]').length > 0 && body.find('[data-testid="CloseIcon"]').is(':visible')) { + cy.bySemanticElement('button', 'Save Changes').scrollIntoView().should('be.visible').click({ force: true }); + } + }); + }, + + backToListPersesDashboardsPage: () => { + cy.log('persesDashboardsPage.backToListPersesDashboardsPage'); + cy.byTestID(listPersesDashboardsDataTestIDs.PersesBreadcrumbDashboardItem).scrollIntoView().should('be.visible').click({ force: true }); + }, + + clickDiscardChangesButton: () => { + cy.log('persesDashboardsPage.clickDiscardChangesButton'); + cy.get('body').then((body) => { + if (body.find('#'+IDs.persesDashboardDiscardChangesDialog).length > 0 && body.find('#'+IDs.persesDashboardDiscardChangesDialog).is(':visible')) { + cy.bySemanticElement('button', 'Discard Changes').scrollIntoView().should('be.visible').click({ force: true }); + } + }); + }, + + assertPanel: (name: string, group: string, collapse_state: 'Open' | 'Closed') => { + cy.log('persesDashboardsPage.assertPanel'); + persesDashboardsPage.panelGroupHeaderAssertion(group, collapse_state); + if (collapse_state === 'Open') { + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(name).scrollIntoView().should('be.visible'); + } else { + cy.byAriaLabel(persesAriaLabels.OpenGroupButtonPrefix + group).scrollIntoView().should('be.visible').click({ force: true }); + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(name).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.CollapseGroupButtonPrefix + group).scrollIntoView().should('be.visible').click({ force: true }); + } + }, + + assertPanelNotExist: (name: string) => { + cy.log('persesDashboardsPage.assertPanelNotExist'); + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(name).should('not.exist'); + }, + + downloadDashboard: (clearFolder: boolean, dashboardName: string, format: 'JSON' | 'YAML' | 'YAML (CR)') => { + cy.log('persesDashboardsPage.downloadDashboard'); + + if (clearFolder) { + cy.task('clearDownloads'); + } + + cy.get('#'+ IDs.persesDashboardDownloadButton).scrollIntoView().should('be.visible').click({ force: true }); + cy.byPFRole('menuitem').contains(format).should('be.visible').click({ force: true }); cy.wait(1000); + let filename: string; + if (format === 'YAML (CR)') { + filename = dashboardName + '-cr' + '.yaml'; + } else { + filename = dashboardName + '.' + format.toLowerCase(); + } + persesDashboardsPage.assertFilename(true, filename); + }, + + assertFilename: (clearFolder: boolean, fileNameExp: string) => { + cy.log('persesDashboardsPage.assertFilename'); + let downloadedFileName: string | null = null; + const downloadsFolder = Cypress.config('downloadsFolder'); + const expectedFileNamePattern = fileNameExp; + + cy.waitUntil(() => { + return cy.task('getFilesInFolder', downloadsFolder).then((currentFiles: string[]) => { + const matchingFile = currentFiles.find(file => file.includes(expectedFileNamePattern)); + if (matchingFile) { + downloadedFileName = matchingFile; + return true; + } + return false; + }); + }, { + timeout: 20000, + interval: 1000, + errorMsg: `File matching "${expectedFileNamePattern}" was not downloaded within timeout.` + }); + + cy.then(() => { + expect(downloadedFileName).to.not.be.null; + cy.task('doesFileExist', { fileName: downloadedFileName }).should('be.true'); + }); + }, + + viewJSON: (dashboardName: string, namespace: string) => { + cy.log('persesDashboardsPage.viewJSON'); + cy.byAriaLabel(persesAriaLabels.ViewJSONButton).scrollIntoView().should('be.visible').click({ force: true }); + cy.byPFRole('dialog').find('h2').contains(persesDashboardsModalTitles.VIEW_JSON_DIALOG).scrollIntoView().should('be.visible'); + cy.byAriaLabel('Close').should('be.visible').click({ force: true }); + }, + + assertDuplicatedPanel: (panel: string, amount: number) => { + cy.log('persesDashboardsPage.assertDuplicatedPanel'); + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').filter(`:contains("${panel}")`).should('have.length', amount); }, } diff --git a/web/cypress/views/troubleshooting-panel.ts b/web/cypress/views/troubleshooting-panel.ts index e11b91a5f..0238a7656 100644 --- a/web/cypress/views/troubleshooting-panel.ts +++ b/web/cypress/views/troubleshooting-panel.ts @@ -5,7 +5,9 @@ export const troubleshootingPanelPage = { openSignalCorrelation: () => { cy.log('troubleshootingPanelPage.openSignalCorrelation'); cy.byLegacyTestID(LegacyTestIDs.ApplicationLauncher).should('be.visible').click(); + cy.wait(3000); cy.byTestID(DataTestIDs.MastHeadApplicationItem).contains('Signal Correlation').should('be.visible').click(); + cy.wait(3000); }, signalCorrelationShouldNotBeVisible: () => { diff --git a/web/src/components/data-test.ts b/web/src/components/data-test.ts index ec4f74f6d..d4aa5b5c3 100644 --- a/web/src/components/data-test.ts +++ b/web/src/components/data-test.ts @@ -178,6 +178,11 @@ export const IDs = { ChartAxis1ChartLabel: 'chart-axis-1-ChartLabel', //id^=IDs.ChartAxis1ChartLabel AxisY persesDashboardCount: 'options-menu-top-pagination', persesDashboardDownloadButton: 'download-dashboard-button', + persesDashboardActionMenuModal: 'action-menu', + persesDashboardEditVariablesModalBuiltinButton: 'builtin', + persesDashboardAddPanelGroupForm: 'panel-group-editor-form', + persesDashboardAddPanelForm: 'panel-editor-form', + persesDashboardDiscardChangesDialog: 'discard-dialog', }; export const Classes = { @@ -226,13 +231,46 @@ export const persesAriaLabels = { ZoomInButton: 'Zoom in', ZoomOutButton: 'Zoom out', ViewJSONButton: 'View JSON', + EditVariablesButton: 'Edit variables', + EditDatasourcesButton: 'Edit datasources', + AddPanelButton: 'Add panel', + AddGroupButton: 'Add panel group', + OpenGroupButtonPrefix: 'expand group ', + CollapseGroupButtonPrefix: 'collapse group ', + //PanelGroup toolbar buttons + AddPanelToGroupPrefix: 'add panel to group ', + EditPanelGroupPrefix: 'edit group ', + DeletePanelGroupPrefix: 'delete group ', + MovePanelGroupPrefix: 'move group ', + MovePanelGroupDownSuffix: ' down', + MovePanelGroupUpSuffix: ' up', + EditDashboardVariablesTable: 'table of variables', + EditDashboardDatasourcesTable: 'table of datasources', + //Panel toolbar buttons + EditPanelActionMenuButtonPrefix: 'show panel actions for ', + EditPanelExpandCollapseButtonPrefix: 'toggle panel ', + EditPanelExpandCollapseButtonSuffix: ' view mode', + EditPanelPrefix: 'edit panel ', + EditPanelDuplicateButtonPrefix: 'duplicate panel ', + EditPanelDeleteButtonPrefix: 'delete panel ', + EditPanelMovePanelButtonPrefix: 'move panel ', }; //data-testid from MUI components export const persesMUIDataTestIDs = { variableDropdown: 'variable', + panelGroup: 'panel-group', panelGroupHeader: 'panel-group-header', panelHeader: 'panel', + editDashboardVariablesModal: 'variable-editor', + editDashboardDatasourcesModal: 'datasource-editor', + editDashboardAddVariableRunQueryButton: 'run_query_button', + editDashboardAddVariablePreviewValuesCopy: 'ClipboardOutlineIcon', + editDashboardEditVariableMoveDownButton: 'ArrowDownIcon', + editDashboardEditVariableMoveUpButton: 'ArrowUpIcon', + editDashboardEditVariableDatasourceEditButton: 'PencilIcon', + editDashboardEditVariableDatasourceDeleteButton: 'TrashCanIcon', + addPanelGroupFormName: 'panel-group-editor-name', }; export const persesDashboardDataTestIDs = { @@ -260,3 +298,31 @@ export const listPersesDashboardsOUIAIDs = { persesListDataViewHeaderSortButton: 'PersesDashList-DataViewTable-th', persesListDataViewTableDashboardNameTD: 'PersesDashList-DataViewTable-td-', }; + +//name attribute from MUI components +export const editPersesDashboardsAddVariable = { + inputName: 'spec.name', + inputDisplayLabel: 'spec.display.name', + inputDescription: 'spec.display.description', + //type='Text' + inputValue: 'spec.value', + inputConstant: 'spec.constant', + //type='List' + inputCapturingRegexp: 'spec.capturingRegexp', + inputAllowMultiple: 'spec.allowMultiple', + inputAllowAllValue: 'spec.allowAllValue', + inputCustomAllValue: 'spec.customAllValue', +}; + +//name attribute from MUI components +export const editPersesDashboardsAddDatasource = { + inputName: 'name', + inputDefaultDatasource: 'spec.default', + inputDisplayLabel: 'title', + inputDescription: 'description', +}; + +export const editPersesDashboardsAddPanel = { + inputName: 'panelDefinition.spec.display.name', + inputDescription: 'panelDefinition.spec.display.description', +}; From 61d158f0cc2099c72bf41b9a2a26effa866ab94a Mon Sep 17 00:00:00 2001 From: Evelyn Tanigawa Murasaki Date: Tue, 13 Jan 2026 23:58:26 -0300 Subject: [PATCH 10/15] removing ungraphable due to dependency on data --- .../support/monitoring/02.reg_metrics_2.cy.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/web/cypress/support/monitoring/02.reg_metrics_2.cy.ts b/web/cypress/support/monitoring/02.reg_metrics_2.cy.ts index 2718c62bb..33cf58461 100644 --- a/web/cypress/support/monitoring/02.reg_metrics_2.cy.ts +++ b/web/cypress/support/monitoring/02.reg_metrics_2.cy.ts @@ -265,23 +265,8 @@ export function testMetricsRegression2(perspective: PerspectiveConfig) { }); - it(`${perspective.name} perspective - Metrics > Ungraphable results`, () => { - cy.log('8.1 Ungraphable results'); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.CPU_USAGE); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.MEMORY_USAGE); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.FILESYSTEM_USAGE); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.RECEIVE_BANDWIDTH); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.TRANSMIT_BANDWIDTH); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.RATE_OF_RECEIVED_PACKETS); - cy.byLegacyTestID(LegacyTestIDs.NamespaceBarDropdown).scrollIntoView(); - - cy.get(Classes.MetricsPageUngraphableResults).contains(MetricGraphEmptyState.UNGRAPHABLE_RESULTS).should('be.visible'); - cy.get(Classes.MetricsPageUngraphableResultsDescription).contains(MetricGraphEmptyState.UNGRAPHABLE_RESULTS_DESCRIPTION).should('be.visible'); - - }); - it(`${perspective.name} perspective - Metrics > No Datapoints`, () => { - cy.log('9.1 No Datapoints'); + cy.log('8.1 No Datapoints'); metricsPage.enterQueryInput(0, 'aaaaaaaaaa'); metricsPage.clickRunQueriesButton(); cy.byTestID(DataTestIDs.MetricGraphNoDatapointsFound).scrollIntoView().contains(MetricGraphEmptyState.NO_DATAPOINTS_FOUND).should('be.visible'); @@ -295,7 +280,7 @@ export function testMetricsRegression2(perspective: PerspectiveConfig) { }); it(`${perspective.name} perspective - Metrics > No Datapoints with alert`, () => { - cy.log('10.1 No Datapoints with alert'); + cy.log('9.1 No Datapoints with alert'); metricsPage.enterQueryInput(0, MetricsPageQueryInput.QUERY_WITH_ALERT); metricsPage.clickRunQueriesButton(); cy.byOUIAID(DataTestIDs.MetricsGraphAlertDanger).should('be.visible'); From 6016e7b858862c605c680d8f1d25ebd61a001d2b Mon Sep 17 00:00:00 2001 From: PeterYurkovich Date: Tue, 13 Jan 2026 14:47:51 -0500 Subject: [PATCH 11/15] fix: remove top level dispatch; refactor to avoid late useEffect a --- web/src/components/MetricsPage.tsx | 281 ++++++++++++++++------------- 1 file changed, 158 insertions(+), 123 deletions(-) diff --git a/web/src/components/MetricsPage.tsx b/web/src/components/MetricsPage.tsx index b412c4f42..397dea5ea 100644 --- a/web/src/components/MetricsPage.tsx +++ b/web/src/components/MetricsPage.tsx @@ -710,135 +710,170 @@ export const QueryTable: FC = ({ index, namespace, customDataso setSortBy({}); }, [namespace, query]); - if (!isEnabled || !isExpanded || !query) { - return null; - } - - if (error) { - return ; - } - - if (!data) { - return ; - } - - // Add any data series from `series` (those displayed in the graph) that are not in `data.result`. - // This happens for queries that exclude a series currently, but included that same series at some - // point during the graph's range. - const expiredSeries = _.differenceWith(series, data.result, (s, r) => _.isEqual(s, r.metric)); - const result = expiredSeries.length - ? [...data.result, ...expiredSeries.map((metric) => ({ metric }))] - : data.result; - - if (!result || result.length === 0) { - return ( -
- {t('No datapoints found.')} -
- ); - } + const isUnused = !isEnabled || !isExpanded || !query; + const isError = !!error; + const isLoading = !data; + const result = useMemo(() => { + if (isUnused || isError || isLoading) { + return []; + } + // Add any data series from `series` (those displayed in the graph) that are not + // in `data.result`.This happens for queries that exclude a series currently, but + // included that same series at some point during the graph's range. + const expiredSeries = _.differenceWith(series, data.result, (s, r) => _.isEqual(s, r.metric)); + return expiredSeries.length + ? [...data.result, ...expiredSeries.map((metric) => ({ metric }))] + : data.result; + }, [data?.result, series, isUnused, isError, isLoading]); + const isEmptyGraph = !result || result.length === 0; + + const tableData = useMemo(() => { + if (isUnused || isError || isLoading || isEmptyGraph) { + return {}; + } + const transforms: ITransform[] = [sortable, wrappable]; - const transforms: ITransform[] = [sortable, wrappable]; - - const buttonCell = (labels) => ({ title: }); - - let columns, rows; - if (data.resultType === 'scalar') { - columns = [ - '', - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; - rows = [[buttonCell({}), _.get(result, '[1]')]]; - } else if (data.resultType === 'string') { - columns = [ - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; - rows = [[result?.[1]]]; - } else { - const allLabelKeys = _.uniq(_.flatMap(result, ({ metric }) => Object.keys(metric))).sort(); - - columns = [ - '', - ...allLabelKeys.map((k) => ({ - title: {k === '__name__' ? t('Name') : k}, - transforms, - })), - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; + const buttonCell = (labels) => ({ title: }); - let rowMapper; - if (data.resultType === 'matrix') { - rowMapper = ({ metric, values }) => [ + let columns, rows; + if (data.resultType === 'scalar') { + columns = [ '', - ..._.map(allLabelKeys, (k) => metric[k]), { - title: ( - <> - {_.map(values, ([time, v]) => ( -
- {v} @{time} -
- ))} - - ), + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], + }, + ]; + rows = [[buttonCell({}), _.get(result, '[1]')]]; + } else if (data.resultType === 'string') { + columns = [ + { + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], }, ]; + rows = [[result?.[1]]]; } else { - rowMapper = ({ metric, value }) => [ - buttonCell(metric), - ..._.map(allLabelKeys, (k) => metric[k]), - _.get(value, '[1]', { title: {t('None')} }), + const allLabelKeys = _.uniq(_.flatMap(result, ({ metric }) => Object.keys(metric))).sort(); + + columns = [ + '', + ...allLabelKeys.map((k) => ({ + title: {k === '__name__' ? t('Name') : k}, + transforms, + })), + { + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], + }, ]; - } - rows = _.map(result, rowMapper); - if (sortBy) { - // Sort Values column numerically and sort all the other columns alphabetically - const valuesColIndex = allLabelKeys.length + 1; - const sort = - sortBy.index === valuesColIndex - ? (cells) => { - const v = Number(cells[valuesColIndex]); - return Number.isNaN(v) ? 0 : v; - } - : `${sortBy.index}`; - rows = _.orderBy(rows, [sort], [sortBy.direction]); + let rowMapper; + if (data.resultType === 'matrix') { + rowMapper = ({ metric, values }) => [ + '', + ..._.map(allLabelKeys, (k) => metric[k]), + { + title: ( + <> + {_.map(values, ([time, v]) => ( +
+ {v} @{time} +
+ ))} + + ), + }, + ]; + } else { + rowMapper = ({ metric, value }) => [ + buttonCell(metric), + ..._.map(allLabelKeys, (k) => metric[k]), + _.get(value, '[1]', { title: {t('None')} }), + ]; + } + + rows = _.map(result, rowMapper); + if (sortBy) { + // Sort Values column numerically and sort all the other columns alphabetically + const valuesColIndex = allLabelKeys.length + 1; + const sort = + sortBy.index === valuesColIndex + ? (cells) => { + const v = Number(cells[valuesColIndex]); + return Number.isNaN(v) ? 0 : v; + } + : `${sortBy.index}`; + rows = _.orderBy(rows, [sort], [sortBy.direction]); + } } - } - // Dispatch query table result so QueryKebab can access it for data export - dispatch(queryBrowserPatchQuery(index, { queryTableData: { columns, rows } })); + const onSort = (e, i, direction) => setSortBy({ index: i, direction }); - const onSort = (e, i, direction) => setSortBy({ index: i, direction }); + const tableRows = rows.slice((page - 1) * perPage, page * perPage).map((cells) => ({ cells })); - const tableRows = rows.slice((page - 1) * perPage, page * perPage).map((cells) => ({ cells })); + return { + onSort, + tableRows, + columns, + rows, + }; + }, [ + data?.resultType, + isEmptyGraph, + index, + isUnused, + isError, + isLoading, + page, + perPage, + result, + sortBy, + t, + valueFormat, + ]); + + useEffect(() => { + if (tableData.columns && tableData.rows) { + dispatch( + queryBrowserPatchQuery(index, { + queryTableData: { columns: tableData.columns, rows: tableData.rows }, + }), + ); + } + }, [dispatch, index, tableData?.columns, tableData?.rows]); + + if (isUnused) { + return null; + } else if (isError) { + return ; + } else if (isLoading) { + return ; + } else if (isEmptyGraph) { + return ( +
+ {t('No datapoints found.')} +
+ ); + } return ( <> @@ -854,19 +889,19 @@ export const QueryTable: FC = ({ index, namespace, customDataso - {columns.map((col, columnIndex) => { + {tableData?.columns.map((col, columnIndex) => { const sortParams = columnIndex !== 0 ? { sort: { sortBy, - onSort, + onSort: tableData?.onSort, columnIndex, }, } @@ -880,15 +915,15 @@ export const QueryTable: FC = ({ index, namespace, customDataso - {tableRows.map((row, rowIndex) => ( + {tableData?.tableRows.map((row, rowIndex) => ( {row.cells?.map((cell, cellIndex) => (
- {columns[cellIndex].cellTransforms - ? columns[cellIndex].cellTransforms[0]( + {tableData?.columns[cellIndex].cellTransforms + ? tableData?.columns[cellIndex].cellTransforms[0]( typeof cell === 'string' ? cell : cell?.title, ) : typeof cell === 'string' @@ -902,7 +937,7 @@ export const QueryTable: FC = ({ index, namespace, customDataso
Date: Tue, 20 Jan 2026 07:35:18 +0100 Subject: [PATCH 12/15] COO-1515: add the cluster-health-analyzer feature --- ...nts.patch.json => cluster-health-analyzer.patch.json} | 0 pkg/plugin_handler.go | 7 +++++++ pkg/server.go | 9 +++++---- 3 files changed, 12 insertions(+), 4 deletions(-) rename config/{incidents.patch.json => cluster-health-analyzer.patch.json} (100%) diff --git a/config/incidents.patch.json b/config/cluster-health-analyzer.patch.json similarity index 100% rename from config/incidents.patch.json rename to config/cluster-health-analyzer.patch.json diff --git a/pkg/plugin_handler.go b/pkg/plugin_handler.go index f56c9304a..842bfa12b 100644 --- a/pkg/plugin_handler.go +++ b/pkg/plugin_handler.go @@ -44,7 +44,14 @@ func patchManifest(baseManifestData []byte, cfg *Config) []byte { patchedManifest = performPatch(baseManifestData, filepath.Join(cfg.ConfigPath, "clear-extensions.patch.json")) } + if cfg.Features[Incidents] || cfg.Features[ClusterHealthAnalyzer] { + patchedManifest = performPatch(patchedManifest, filepath.Join(cfg.ConfigPath, "cluster-health-analyzer.patch.json")) + } + for feature := range cfg.Features { + if feature == ClusterHealthAnalyzer || feature == Incidents { + continue + } patchedManifest = performPatch(patchedManifest, filepath.Join(cfg.ConfigPath, fmt.Sprintf("%s.patch.json", feature))) } diff --git a/pkg/server.go b/pkg/server.go index d20af1700..c87eeb2eb 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -56,10 +56,11 @@ type PluginConfig struct { type Feature string const ( - AcmAlerting Feature = "acm-alerting" - Incidents Feature = "incidents" - DevConfig Feature = "dev-config" - PersesDashboards Feature = "perses-dashboards" + AcmAlerting Feature = "acm-alerting" + Incidents Feature = "incidents" + DevConfig Feature = "dev-config" + PersesDashboards Feature = "perses-dashboards" + ClusterHealthAnalyzer Feature = "cluster-health-analyzer" ) func (pluginConfig *PluginConfig) MarshalJSON() ([]byte, error) { From 0c74bc66c838ef3fbbd5bc4c8c42b8a0202ee643 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 27 Jan 2026 16:15:18 +0100 Subject: [PATCH 13/15] fix: update vulnerable dependencies Signed-off-by: Gabriel Bernal --- web/package-lock.json | 54 +++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 943894964..d439cb4cc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -6715,9 +6715,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -19361,15 +19361,15 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/lodash.clonedeepwith": { @@ -20641,9 +20641,9 @@ "license": "MIT" }, "node_modules/mochawesome/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -22395,14 +22395,14 @@ } }, "node_modules/react-router-dom-v5-compat": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.30.0.tgz", - "integrity": "sha512-MAVRASbdQ3+ZOTPPjAa7jKcF0F9LkHWKB/iib3hf+jzzIazL4GEpMDDdTswCsqRQNU+zNnT3qD0WiNbzJ6ncPw==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.30.3.tgz", + "integrity": "sha512-WWZtwGYyoaeUDNrhzzDkh4JvN5nU0MIz80Dxim6pznQrfS+dv0mvtVoHTA6HlUl/OiJl7WWjbsQwjTnYXejEHg==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", + "@remix-run/router": "1.23.2", "history": "^5.3.0", - "react-router": "6.30.0" + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -22423,12 +22423,12 @@ } }, "node_modules/react-router-dom-v5-compat/node_modules/react-router": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", - "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -25091,9 +25091,9 @@ } }, "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -25368,9 +25368,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.1.tgz", + "integrity": "sha512-Gpq0iNm5M6cQWlyHQv9MV+uOj1jWk7LpkoE5vSp/7zjb4zMdAcUD+VL5y0nH4p9EbUklq00eVIIX/XcDHzu5xg==", "dev": true, "license": "MIT", "engines": { From 2a00271f22b176bdfeca3720117bcbe3cb37326c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Reme=C5=A1?= Date: Fri, 23 Jan 2026 08:52:47 +0100 Subject: [PATCH 14/15] OU-1039: add info alert to the Incident page --- web/locales/en/plugin__monitoring-plugin.json | 3 +- .../components/Incidents/IncidentsPage.tsx | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/web/locales/en/plugin__monitoring-plugin.json b/web/locales/en/plugin__monitoring-plugin.json index 2f7a72a11..de00f6f98 100644 --- a/web/locales/en/plugin__monitoring-plugin.json +++ b/web/locales/en/plugin__monitoring-plugin.json @@ -303,5 +303,6 @@ "No metrics targets found": "No metrics targets found", "Error loading latest targets data": "Error loading latest targets data", "Search by endpoint or namespace...": "Search by endpoint or namespace...", - "Text": "Text" + "Text": "Text", + "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." } \ No newline at end of file diff --git a/web/src/components/Incidents/IncidentsPage.tsx b/web/src/components/Incidents/IncidentsPage.tsx index 75f0fb231..c23456b1e 100644 --- a/web/src/components/Incidents/IncidentsPage.tsx +++ b/web/src/components/Incidents/IncidentsPage.tsx @@ -20,6 +20,8 @@ import { ToolbarGroup, Flex, FlexItem, + Alert, + AlertActionCloseButton, } from '@patternfly/react-core'; import { IncidentsTable } from './IncidentsTable'; import { @@ -89,6 +91,11 @@ const IncidentsPage = () => { >([]); const [hideCharts, setHideCharts] = useState(false); const [isInitialized, setIsInitialized] = useState(false); + const INCIDENTS_DATA_ALERT_DISPLAYED = 'monitoring/incidents/data-alert-displayed'; + const [showDataDelayAlert, setShowDataDelayAlert] = useState(() => { + const alertDisplayed = localStorage.getItem(INCIDENTS_DATA_ALERT_DISPLAYED); + return !alertDisplayed; + }); const [filtersExpanded, setFiltersExpanded] = useState({ severity: false, @@ -350,6 +357,18 @@ const IncidentsPage = () => { } }, [incidentsActiveFilters, filteredData, dispatch]); + useEffect(() => { + // Set up 5-minute timer to hide banner automatically on first visit + if (showDataDelayAlert) { + const timer = setTimeout(() => { + setShowDataDelayAlert(false); + localStorage.setItem(INCIDENTS_DATA_ALERT_DISPLAYED, 'true'); + }, 5 * 60 * 1000); + + return () => clearTimeout(timer); + } + }, [showDataDelayAlert]); + const handleIncidentChartClick = useCallback( (groupId) => { closeDropDownFilters(); @@ -389,6 +408,26 @@ const IncidentsPage = () => { ) : ( + {showDataDelayAlert && ( + { + setShowDataDelayAlert(false); + localStorage.setItem(INCIDENTS_DATA_ALERT_DISPLAYED, 'true'); + }} + /> + } + > + {t( + 'Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.', + )} + + )} Date: Thu, 12 Feb 2026 17:00:33 +0100 Subject: [PATCH 15/15] WIP: incidents 15-day data loading regression test with mock timestamp fixes Add regression test (05.reg_15day_data_loading) verifying absolute start dates remain consistent across "Last N Days" filter changes for ongoing and escalating-severity incidents (Section 3.4). Fix mock timestamp generation to align to 5-minute grid matching real Prometheus query_range step=300 behavior, and use unrounded timestamps in instant query mocks to surface tooltip date edge cases. Phase 2 (resolved incident date verification) temporarily disabled due to alert bar refresh timing issue in the test harness. --- .../tests/3.api_calls_data_loading_flows.md | 1 - .../05.reg_15day_data_loading.cy.ts | 367 ++++++++++++++++++ .../fixtures/coo/monitoring-ui-plugin.yaml | 2 +- .../19-15-day-data-loading.yaml | 83 ++++ .../support/commands/operator-commands.ts | 196 +++++----- web/cypress/support/e2e.js | 11 +- .../mock-generators.ts | 100 ++++- .../prometheus-mocks.ts | 57 ++- .../incidents_prometheus_query_mocks/types.ts | 13 + web/cypress/views/incidents-page.ts | 103 ++++- 10 files changed, 812 insertions(+), 121 deletions(-) create mode 100644 web/cypress/e2e/incidents/regression/05.reg_15day_data_loading.cy.ts create mode 100644 web/cypress/fixtures/incident-scenarios/19-15-day-data-loading.yaml diff --git a/docs/incident_detection/tests/3.api_calls_data_loading_flows.md b/docs/incident_detection/tests/3.api_calls_data_loading_flows.md index 9e2f7a6e0..157e15dcd 100644 --- a/docs/incident_detection/tests/3.api_calls_data_loading_flows.md +++ b/docs/incident_detection/tests/3.api_calls_data_loading_flows.md @@ -124,4 +124,3 @@ Use `docs/incident_detection/simulate_scenarios/long-incident-15-days.csv` which - [ ] Component lists combined for same group_id - [ ] Watchdog alerts filtered out - diff --git a/web/cypress/e2e/incidents/regression/05.reg_15day_data_loading.cy.ts b/web/cypress/e2e/incidents/regression/05.reg_15day_data_loading.cy.ts new file mode 100644 index 000000000..5000fe5c9 --- /dev/null +++ b/web/cypress/e2e/incidents/regression/05.reg_15day_data_loading.cy.ts @@ -0,0 +1,367 @@ +/* +Regression test for 15-Day Data Loading with "Last N Days" Filtering (Section 3.4) + +FEATURE: UI always loads 15 days of data (one query_range call per day), then filters +client-side based on "Last N Days" selection. Absolute start dates are retrieved via +instant queries (min_over_time(timestamp(...))) and always displayed regardless of the +selected time filter. + +Before fix: Start dates were relative to the "Last N Days" selection. +After fix: Start dates show absolute timestamps from instant query results. + +Note: API call pattern verification (exact count of 15 query_range calls and instant +queries) is partially covered by the mocking infrastructure. Full API call counting +requires integration tests with a real Prometheus endpoint. + +All start dates — table dates (incidents table "Start", alerts details "Start") AND +tooltip dates (incident bar tooltip, alert bar tooltip) — should remain identical +regardless of which "Last N Days" filter is selected. If tooltip dates drift when +switching filters, that indicates a bug in the chart rendering pipeline. + +Edge cases: +- Test 2: Escalating severity incident — tooltip shows segment-specific boundaries, + not the overall incident start. When the severity change point is beyond the visible + time window, showing the overall incident start is acceptable. + +Verifies: Section 3.4 +*/ + +import { incidentsPage } from '../../../views/incidents-page'; + +const MCP = { + namespace: Cypress.env('COO_NAMESPACE'), + packageName: 'cluster-observability-operator', + operatorName: 'Cluster Observability Operator', + config: { + kind: 'UIPlugin', + name: 'monitoring', + }, +}; + +const MP = { + namespace: 'openshift-monitoring', + operatorName: 'Cluster Monitoring Operator', +}; + +describe('Regression: 15-Day Data Loading', { tags: ['@incidents'] }, () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + }); + + beforeEach(() => { + cy.log('Navigate to Observe -> Incidents'); + incidentsPage.goTo(); + cy.log('Loading 15-day data loading test scenarios'); + cy.mockIncidentFixture('incident-scenarios/19-15-day-data-loading.yaml'); + }); + + it('1. Absolute start date remains consistent across time filter changes for ongoing and resolved incidents', () => { + // All start dates — tables AND tooltips — should be identical regardless of the + // selected time filter. If tooltip dates drift, that is a bug to investigate. + + interface AllDates { + incidentTable: string; + alertTable: string; + incidentTooltip: string; + alertTooltip: string; + } + + let ongoingBaseline: AllDates; + let resolvedBaseline: AllDates; + + const collectAllStartDates = (incidentId: string): Cypress.Chainable => { + let iTable: string; + let iTooltip: string; + let aTooltip: string; + + incidentsPage.hoverOverIncidentBarById(incidentId); + incidentsPage.getTooltipStartDate().then(d => { iTooltip = d; }); + + incidentsPage.selectIncidentById(incidentId); + incidentsPage.elements.alertsChartContainer().should('be.visible'); + incidentsPage.hoverOverAlertBar(0); + incidentsPage.getAlertsTooltipStartDate().then(d => { aTooltip = d; }); + + incidentsPage.expandRow(0); + incidentsPage.elements.incidentsDetailsTable().should('be.visible'); + + incidentsPage.elements.incidentsDetailsStartCell(0) + .invoke('text').then(t => { iTable = t.trim(); }); + + return incidentsPage.elements.incidentsDetailsFiringStartCell(0) + .invoke('text').then(t => { + return cy.wrap({ + incidentTable: iTable, + alertTable: t.trim(), + incidentTooltip: iTooltip, + alertTooltip: aTooltip, + }); + }); + }; + + const expectAllDatesMatch = (actual: AllDates, expected: AllDates) => { + // date does not change on different timeline lengths + expect(actual.incidentTable).to.equal(expected.incidentTable, 'Incident table start'); + expect(actual.alertTable).to.equal(expected.alertTable, 'Alert table start'); + // TODO: Currently not working as expected, the tooltip dates are 5 minute behind the table dates + // on the incident start (no date filter trimming applied) + expect(actual.incidentTooltip).to.equal(expected.incidentTooltip, 'Incident tooltip start'); + expect(actual.alertTooltip).to.equal(expected.alertTooltip, 'Alert tooltip start'); + + // consistency within same filter view + expect(actual.incidentTooltip).to.equal(actual.incidentTable, 'Incident tooltip and table match'); + expect(actual.alertTooltip).to.equal(actual.alertTable, 'Alert tooltip and table match'); + expect(actual.incidentTable).to.equal(actual.alertTable, 'Incident table and alert table match'); + }; + + // Phase 1: Ongoing 14-day incident — baseline at 15d (widest), then 7d, then 3d + + cy.log('1.1 Switch to 15-day filter so all incident data is fully visible'); + incidentsPage.clearAllFilters(); + incidentsPage.setDays('15 days'); + incidentsPage.elements.incidentsChartContainer().should('be.visible'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 4); + + cy.log('1.2 Collect all start dates for 14-day ongoing incident (15d baseline)'); + collectAllStartDates('LONG-14d-monitoring-ongoing').then((dates) => { + ongoingBaseline = dates; + cy.log(`Incident table: ${dates.incidentTable}`); + cy.log(`Alert table: ${dates.alertTable}`); + cy.log(`Incident tooltip: ${dates.incidentTooltip}`); + cy.log(`Alert tooltip: ${dates.alertTooltip}`); + expect(dates.incidentTable).to.not.be.empty; + expect(dates.alertTable).to.not.be.empty; + expect(dates.incidentTooltip).to.not.be.empty; + expect(dates.alertTooltip).to.not.be.empty; + + expect(dates.incidentTooltip).to.equal(dates.incidentTable, 'Incident tooltip and table match'); + expect(dates.alertTooltip).to.equal(dates.alertTable, 'Alert tooltip and table match'); + expect(dates.incidentTable).to.equal(dates.alertTable, 'Incident table and alert table match'); + + }); + + cy.log('1.3 Switch to 7-day filter and verify all start dates identical'); + incidentsPage.deselectIncidentById('LONG-14d-monitoring-ongoing'); + incidentsPage.setDays('7 days'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 4); + + collectAllStartDates('LONG-14d-monitoring-ongoing').then((dates) => { + expectAllDatesMatch(dates, ongoingBaseline); + cy.log('Verified: All start dates match baseline (7d vs 15d)'); + }); + + cy.log('1.4 Switch to 3-day filter, verify all start dates still identical'); + incidentsPage.deselectIncidentById('LONG-14d-monitoring-ongoing'); + incidentsPage.setDays('3 days'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 4); + + collectAllStartDates('LONG-14d-monitoring-ongoing').then((dates) => { + expectAllDatesMatch(dates, ongoingBaseline); + cy.log('Verified: Ongoing incident dates absolute across 15d, 7d, and 3d filters'); + }); + + // Phase 2: Resolved 10-day incident — baseline at 15d, then 7d + // Failing due to late refresh of the alert bar (cypress testsuite logic related) + + // cy.log('1.5 Switch to 15 days, collect all start dates for resolved 10-day incident'); + // incidentsPage.deselectIncidentById('LONG-14d-monitoring-ongoing'); + // incidentsPage.setDays('15 days'); + + // collectAllStartDates('MEDIUM-10d-storage-resolved').then((dates) => { + // resolvedBaseline = dates; + // cy.log(`Resolved incident table: ${dates.incidentTable}`); + // cy.log(`Resolved alert table: ${dates.alertTable}`); + // cy.log(`Resolved incident tooltip: ${dates.incidentTooltip}`); + // cy.log(`Resolved alert tooltip: ${dates.alertTooltip}`); + // expect(dates.incidentTable).to.not.be.empty; + // expect(dates.alertTable).to.not.be.empty; + // expect(dates.incidentTooltip).to.not.be.empty; + // expect(dates.alertTooltip).to.not.be.empty; + // }); + + // cy.log('1.6 Switch to 7-day filter and verify resolved incident dates unchanged'); + // incidentsPage.deselectIncidentById('MEDIUM-10d-storage-resolved'); + // incidentsPage.setDays('7 days'); + + // // TODO: Remove this wait with more elegant waiting + // cy.wait(5000); + + // collectAllStartDates('MEDIUM-10d-storage-resolved').then((dates) => { + // expectAllDatesMatch(dates, resolvedBaseline); + // cy.log('Verified: Resolved incident dates also absolute across filter changes'); + // }); + + // cy.log('Verified: All start dates (tables and tooltips) are absolute for both ongoing and resolved incidents'); + }); + + it('2. Escalating severity - incident chart segment tooltips show segment-specific boundaries', () => { + // Incident "ESCALATING-14d-monitoring-severity" has 3 severity segments: + // Info (14d-10d ago) → Warning (10d-5d ago) → Critical (5d ago-now) + // + // With 15d filter: all 3 segments visible, tooltip start/end should match each segment's boundaries + // With 7d filter: info segment hidden, warning partially visible, critical fully visible + // - Critical segment start should be unchanged from 15d view + // - Warning segment start may differ because the info→warning boundary (10d ago) is beyond the + // 7-day window. Showing the overall incident start is acceptable in this case. + // 3 alerts (info, warning, critical) have timelines matching the severity change points. + + interface SegmentDates { + severity: string; + start: string; + end: string; + } + + const collectSegmentTooltip = (incidentId: string, segmentIndex: number): Cypress.Chainable => { + let severity: string; + let start: string; + + incidentsPage.hoverOverIncidentBarById(incidentId, segmentIndex); + incidentsPage.getTooltipSeverity().then(s => { severity = s; }); + incidentsPage.getTooltipStartDate().then(d => { start = d; }); + return incidentsPage.getTooltipEndDate().then(end => { + return cy.wrap({ severity, start, end }); + }); + }; + + let infoSegment: SegmentDates; + let warningSegment: SegmentDates; + let criticalSegment: SegmentDates; + + cy.log('2.1 Switch to 15-day filter to see all severity segments'); + incidentsPage.clearAllFilters(); + incidentsPage.setDays('15 days'); + incidentsPage.elements.incidentsChartContainer().should('be.visible'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 4); + + cy.log('2.2 Verify escalating incident has 3 visible severity segments'); + incidentsPage.elements.incidentsChartBar('ESCALATING-14d-monitoring-severity') + .find('path[role="presentation"]') + .then(($paths) => { + const visibleCount = $paths.filter((i, el) => { + const fillOpacity = Cypress.$(el).css('fill-opacity') || Cypress.$(el).attr('fill-opacity'); + return parseFloat(fillOpacity || '0') > 0; + }).length; + expect(visibleCount).to.equal(3); + }); + + cy.log('2.3 Hover info segment (oldest, index 0) and record tooltip'); + collectSegmentTooltip('ESCALATING-14d-monitoring-severity', 0).then(d => { + infoSegment = d; + expect(d.severity).to.equal('Info'); + expect(d.start).to.not.be.empty; + expect(d.end).to.not.be.empty; + cy.log(`Info segment: Start=${d.start}, End=${d.end}`); + }); + + cy.log('2.4 Hover warning segment (middle, index 1) and record tooltip'); + collectSegmentTooltip('ESCALATING-14d-monitoring-severity', 1).then(d => { + warningSegment = d; + expect(d.severity).to.equal('Warning'); + expect(d.start).to.not.be.empty; + expect(d.end).to.not.be.empty; + cy.log(`Warning segment: Start=${d.start}, End=${d.end}`); + }); + + cy.log('2.5 Hover critical segment (latest, index 2) and record tooltip'); + collectSegmentTooltip('ESCALATING-14d-monitoring-severity', 2).then(d => { + criticalSegment = d; + expect(d.severity).to.equal('Critical'); + expect(d.start).to.not.be.empty; + cy.log(`Critical segment: Start=${d.start}, End=${d.end}`); + }); + + cy.log('2.6 Verify each segment has a distinct start date (not the overall incident start)'); + cy.then(() => { + expect(infoSegment.start).to.not.equal(warningSegment.start); + expect(warningSegment.start).to.not.equal(criticalSegment.start); + expect(infoSegment.start).to.not.equal(criticalSegment.start); + cy.log('Verified: Each severity segment has a unique start date'); + }); + + cy.log('2.7 Verify 3 alerts match the severity change points (still in 15d view)'); + incidentsPage.selectIncidentById('ESCALATING-14d-monitoring-severity'); + incidentsPage.elements.alertsChartContainer().should('be.visible'); + + let alertInfoStart: string; + let alertWarningStart: string; + let alertCriticalStart: string; + + incidentsPage.hoverOverAlertBar(0); + incidentsPage.getAlertsTooltipStartDate().then(d => { + alertInfoStart = d; + expect(d).to.not.be.empty; + cy.log(`Alert 0 (info) start: ${d}`); + }); + incidentsPage.hoverOverAlertBar(1); + incidentsPage.getAlertsTooltipStartDate().then(d => { + alertWarningStart = d; + expect(d).to.not.be.empty; + cy.log(`Alert 1 (warning) start: ${d}`); + }); + incidentsPage.hoverOverAlertBar(2); + incidentsPage.getAlertsTooltipStartDate().then(d => { + alertCriticalStart = d; + expect(d).to.not.be.empty; + cy.log(`Alert 2 (critical) start: ${d}`); + }); + + cy.log('2.8 Verify alert start dates correspond to incident severity change points'); + cy.then(() => { + expect(alertInfoStart).to.not.equal(alertWarningStart); + expect(alertWarningStart).to.not.equal(alertCriticalStart); + expect(alertInfoStart).to.not.equal(alertCriticalStart); + cy.log(`Alert starts: info=${alertInfoStart}, warning=${alertWarningStart}, critical=${alertCriticalStart}`); + cy.log(`Incident segment starts: info=${infoSegment.start}, warning=${warningSegment.start}, critical=${criticalSegment.start}`); + }); + + cy.log('2.9 Switch to 7-day filter - info segment now hidden in the past'); + incidentsPage.deselectIncidentById('ESCALATING-14d-monitoring-severity'); + incidentsPage.setDays('7 days'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 4); + + cy.log('2.10 Hover critical segment and verify start date unchanged from 15d view'); + incidentsPage.elements.incidentsChartBar('ESCALATING-14d-monitoring-severity') + .find('path[role="presentation"]') + .then(($paths) => { + const visiblePaths = $paths.filter((i, el) => { + const fillOpacity = Cypress.$(el).css('fill-opacity') || Cypress.$(el).attr('fill-opacity'); + return parseFloat(fillOpacity || '0') > 0; + }); + const lastIndex = visiblePaths.length - 1; + + incidentsPage.hoverOverIncidentBarById('ESCALATING-14d-monitoring-severity', lastIndex); + incidentsPage.getTooltipSeverity().should('equal', 'Critical'); + incidentsPage.getTooltipStartDate().then(d => { + expect(d).to.equal(criticalSegment.start); + cy.log(`Critical segment start in 7d view: ${d} (matches 15d view: ${criticalSegment.start})`); + }); + }); + + cy.log('2.11 Hover warning segment and verify tooltip (start may differ due to hidden boundary)'); + incidentsPage.elements.incidentsChartBar('ESCALATING-14d-monitoring-severity') + .find('path[role="presentation"]') + .then(($paths) => { + const visiblePaths = $paths.filter((i, el) => { + const fillOpacity = Cypress.$(el).css('fill-opacity') || Cypress.$(el).attr('fill-opacity'); + return parseFloat(fillOpacity || '0') > 0; + }); + const warningIndex = visiblePaths.length - 2; + if (warningIndex < 0) { + cy.log('Warning segment not visible in 7d view (entirely clipped)'); + return; + } + + incidentsPage.hoverOverIncidentBarById('ESCALATING-14d-monitoring-severity', warningIndex); + incidentsPage.getTooltipSeverity().should('equal', 'Warning'); + incidentsPage.getTooltipStartDate().then(d => { + expect(d).to.not.be.empty; + // The warning segment's info→warning boundary (10d ago) is before the 7-day window. + // The tooltip may show either the segment start or the overall incident start — both are acceptable. + cy.log(`Warning segment start in 7d: ${d} (was ${warningSegment.start} in 15d, incident starts at ${infoSegment.start})`); + }); + }); + + cy.log('Verified: Escalating severity segments show segment-specific boundaries in tooltips'); + }); +}); diff --git a/web/cypress/fixtures/coo/monitoring-ui-plugin.yaml b/web/cypress/fixtures/coo/monitoring-ui-plugin.yaml index 3d27d5649..1a3d91b06 100644 --- a/web/cypress/fixtures/coo/monitoring-ui-plugin.yaml +++ b/web/cypress/fixtures/coo/monitoring-ui-plugin.yaml @@ -12,6 +12,6 @@ spec: thanosQuerier: url: 'https://rbac-query-proxy.open-cluster-management-observability.svc:8443' perses: - enabled: true + enabled: false # TEMPORARILY DISABLED incidents: enabled: true \ No newline at end of file diff --git a/web/cypress/fixtures/incident-scenarios/19-15-day-data-loading.yaml b/web/cypress/fixtures/incident-scenarios/19-15-day-data-loading.yaml new file mode 100644 index 000000000..372abb688 --- /dev/null +++ b/web/cypress/fixtures/incident-scenarios/19-15-day-data-loading.yaml @@ -0,0 +1,83 @@ +name: "14-Day Data Loading Test" +description: "Tests absolute start date display when incidents span beyond the selected 'Last N Days' filter window. Uses 14d durations to guarantee all data fits within the 15-day fetch window. Baseline is collected at 15d filter (widest), then narrowed to 7d and 3d." +incidents: + # 14-day ongoing incident: fully within 15d fetch window, extends beyond 7d filter + - id: "LONG-14d-monitoring-ongoing" + component: "monitoring" + layer: "core" + timeline: + start: "14d" + alerts: + - name: "AlertLong14DayCritical" + namespace: "openshift-monitoring" + severity: "critical" + firing: true + timeline: + start: "14d" + + # 10-day resolved incident: start beyond 7-day window, ended 2 days ago + - id: "MEDIUM-10d-storage-resolved" + component: "storage" + layer: "core" + timeline: + start: "10d" + end: "2d" + alerts: + - name: "AlertMedium10DayStorageWarning" + namespace: "openshift-storage" + severity: "warning" + firing: false + timeline: + start: "10d" + end: "2d" + + # Escalating severity incident: info (14d-10d) → warning (10d-5d) → critical (5d-now) + # The severityChange at "14d" anchors the initial severity so that all 3 alerts + # produce identical health metric values (getSeverityAtTime never falls back to default). + - id: "ESCALATING-14d-monitoring-severity" + component: "monitoring" + layer: "core" + timeline: + start: "14d" + severityChanges: + - time: "14d" + severity: "info" + - time: "10d" + severity: "warning" + - time: "5d" + severity: "critical" + alerts: + - name: "AlertEscalatingInfo" + namespace: "openshift-monitoring" + severity: "info" + firing: false + timeline: + start: "14d" + end: "10d" + - name: "AlertEscalatingWarning" + namespace: "openshift-monitoring" + severity: "warning" + firing: false + timeline: + start: "10d" + end: "5d" + - name: "AlertEscalatingCritical" + namespace: "openshift-monitoring" + severity: "critical" + firing: true + timeline: + start: "5d" + + # 2-day recent incident: fully within all filter windows + - id: "SHORT-2d-network-recent" + component: "network" + layer: "core" + timeline: + start: "2d" + alerts: + - name: "AlertShortRecentNetworkWarning" + namespace: "openshift-network" + severity: "warning" + firing: true + timeline: + start: "2d" diff --git a/web/cypress/support/commands/operator-commands.ts b/web/cypress/support/commands/operator-commands.ts index 927ace76d..b6324a16c 100644 --- a/web/cypress/support/commands/operator-commands.ts +++ b/web/cypress/support/commands/operator-commands.ts @@ -303,11 +303,8 @@ const operatorUtils = { }); }); - cy.get('#page-sidebar').then(($sidebar) => { - const section = $sidebar.text().includes('Ecosystem') ? 'Ecosystem' : 'Operators'; - nav.sidenav.clickNavLink([section, 'Installed Operators']); - }); - + // Navigate to Installed Operators in the COO namespace + cy.visit(`/k8s/ns/${MCP.namespace}/operators.coreos.com~v1alpha1~ClusterServiceVersion`); cy.byTestID('name-filter-input').should('be.visible').type('Observability{enter}'); cy.get('[data-test="status-text"]', { timeout: installTimeoutMilliseconds }).eq(0).should('contain.text', 'Succeeded', { timeout: installTimeoutMilliseconds }); }, @@ -372,48 +369,49 @@ const operatorUtils = { setupDashboardsAndPlugins(MCP: { namespace: string }): void { - cy.log('Create perses-dev namespace.'); - cy.exec(`oc new-project perses-dev --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // TEMPORARILY DISABLED - Dashboard setup failing, not needed for current tests + // cy.log('Create perses-dev namespace.'); + // cy.exec(`oc new-project perses-dev --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - /** - * TODO: When COO1.4.0 is released, points COO_UI_INSTALL to install dashboards on COO1.4.0 folder - */ - if (Cypress.env('COO_UI_INSTALL')) { - cy.log('COO_UI_INSTALL is set. Installing dashboards on COO1.2.0 folder'); + // /** + // * TODO: When COO1.4.0 is released, points COO_UI_INSTALL to install dashboards on COO1.4.0 folder + // */ + // if (Cypress.env('COO_UI_INSTALL')) { + // cy.log('COO_UI_INSTALL is set. Installing dashboards on COO1.2.0 folder'); - cy.log('Create openshift-cluster-sample-dashboard instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/openshift-cluster-sample-dashboard.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // // cy.log('Create openshift-cluster-sample-dashboard instance.'); + // // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/openshift-cluster-sample-dashboard.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create perses-dashboard-sample instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/perses-dashboard-sample.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // // cy.log('Create perses-dashboard-sample instance.'); + // // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/perses-dashboard-sample.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create prometheus-overview-variables instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/prometheus-overview-variables.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Create prometheus-overview-variables instance.'); + // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/prometheus-overview-variables.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create thanos-compact-overview-1var instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-compact-overview-1var.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Create thanos-compact-overview-1var instance.'); + // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-compact-overview-1var.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create Thanos Querier instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-querier-datasource.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - } else { - cy.log('COO_UI_INSTALL is not set. Installing dashboards on COO1.4.0 folder'); + // cy.log('Create Thanos Querier instance.'); + // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-querier-datasource.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // } else { + // cy.log('COO_UI_INSTALL is not set. Installing dashboards on COO1.4.0 folder'); - cy.log('Create openshift-cluster-sample-dashboard instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // // cy.log('Create openshift-cluster-sample-dashboard instance.'); + // // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create perses-dashboard-sample instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // // cy.log('Create perses-dashboard-sample instance.'); + // // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create prometheus-overview-variables instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // // cy.log('Create prometheus-overview-variables instance.'); + // // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create thanos-compact-overview-1var instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // // cy.log('Create thanos-compact-overview-1var instance.'); + // // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create Thanos Querier instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Create Thanos Querier instance.'); + // cy.exec(`oc apply -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - } + // } cy.exec( `oc label namespace ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, @@ -421,69 +419,74 @@ const operatorUtils = { cy.log('Create Monitoring UI Plugin instance.'); cy.exec(`oc apply -f ./cypress/fixtures/coo/monitoring-ui-plugin.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + // COO deploys monitoring-plugin pods to openshift-monitoring, not the COO namespace cy.exec( - `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=monitoring -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=monitoring -n openshift-monitoring --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, { timeout: readyTimeoutMilliseconds, failOnNonZeroExit: true } ).then((result) => { expect(result.code).to.eq(0); - cy.log(`Monitoring plugin pod is now running in namespace: ${MCP.namespace}`); + cy.log('Monitoring plugin pod is now running in namespace: openshift-monitoring'); }); - cy.exec( - `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=perses -n ${MCP.namespace} --timeout=600s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, - { - timeout: installTimeoutMilliseconds, - failOnNonZeroExit: true - } - ).then((result) => { - expect(result.code).to.eq(0); - cy.log(`Perses-0 pod is now running in namespace: ${MCP.namespace}`); - }); - - cy.exec( - `sleep 15 && oc wait --for=jsonpath='{.metadata.name}'=health-analyzer --timeout=60s servicemonitor/health-analyzer -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, - { - timeout: readyTimeoutMilliseconds, - failOnNonZeroExit: true - } - ).then((result) => { - expect(result.code).to.eq(0); - cy.log(`Health-analyzer service monitor is now running in namespace: ${MCP.namespace}`); - }); + // TEMPORARILY DISABLED - Dashboard/Perses setup not needed for current tests + // COO deploys operand pods to openshift-monitoring, not the COO namespace + // cy.exec( + // `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=perses -n openshift-monitoring --timeout=600s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + // { + // timeout: installTimeoutMilliseconds, + // failOnNonZeroExit: true + // } + // ).then((result) => { + // expect(result.code).to.eq(0); + // cy.log('Perses-0 pod is now running in namespace: openshift-monitoring'); + // }); + + // cy.exec( + // `sleep 15 && oc wait --for=jsonpath='{.metadata.name}'=health-analyzer --timeout=60s servicemonitor/health-analyzer -n openshift-monitoring --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + // { + // timeout: readyTimeoutMilliseconds, + // failOnNonZeroExit: true + // } + // ).then((result) => { + // expect(result.code).to.eq(0); + // cy.log('Health-analyzer service monitor is now running in namespace: openshift-monitoring'); + // }); cy.reload(true); - cy.visit('/monitoring/v2/dashboards'); - cy.url().should('include', '/monitoring/v2/dashboards'); + // cy.visit('/monitoring/v2/dashboards'); + // cy.url().should('include', '/monitoring/v2/dashboards'); }, setupTroubleshootingPanel(MCP: { namespace: string }): void { cy.log('Create troubleshooting panel instance.'); cy.exec(`oc apply -f ./cypress/fixtures/coo/troubleshooting-panel-ui-plugin.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // COO deploys operand pods to openshift-monitoring, not the COO namespace cy.log('Troubleshooting panel instance created. Waiting for pods to be ready.'); cy.exec( - `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=troubleshooting-panel -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=troubleshooting-panel -n openshift-monitoring --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, { timeout: readyTimeoutMilliseconds, failOnNonZeroExit: true } ).then((result) => { expect(result.code).to.eq(0); - cy.log(`Troubleshooting panel pod is now running in namespace: ${MCP.namespace}`); + cy.log('Troubleshooting panel pod is now running in namespace: openshift-monitoring'); }); cy.exec( - `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=korrel8r -n ${MCP.namespace} --timeout=600s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=korrel8r -n openshift-monitoring --timeout=600s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, { timeout: installTimeoutMilliseconds, failOnNonZeroExit: true } ).then((result) => { expect(result.code).to.eq(0); - cy.log(`Korrel8r pod is now running in namespace: ${MCP.namespace}`); + cy.log('Korrel8r pod is now running in namespace: openshift-monitoring'); }); cy.wait(30000); @@ -550,41 +553,42 @@ const operatorUtils = { `oc delete ${config.kind} ${config.name} --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); - if (Cypress.env('COO_UI_INSTALL')) { - cy.log('Remove openshift-cluster-sample-dashboard instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/openshift-cluster-sample-dashboard.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // TEMPORARILY DISABLED - Dashboard/Perses cleanup not needed + // if (Cypress.env('COO_UI_INSTALL')) { + // cy.log('Remove openshift-cluster-sample-dashboard instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/openshift-cluster-sample-dashboard.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove perses-dashboard-sample instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/perses-dashboard-sample.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Remove perses-dashboard-sample instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/perses-dashboard-sample.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove prometheus-overview-variables instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/prometheus-overview-variables.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Remove prometheus-overview-variables instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/prometheus-overview-variables.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove thanos-compact-overview-1var instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-compact-overview-1var.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Remove thanos-compact-overview-1var instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-compact-overview-1var.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove Thanos Querier instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-querier-datasource.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - } else { - cy.log('COO_UI_INSTALL is not set. Removing dashboards on COO1.4.0 folder'); + // cy.log('Remove Thanos Querier instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo121_perses_dashboards/thanos-querier-datasource.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // } else { + // cy.log('COO_UI_INSTALL is not set. Removing dashboards on COO1.4.0 folder'); - cy.log('Remove openshift-cluster-sample-dashboard instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Remove openshift-cluster-sample-dashboard instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/openshift-cluster-sample-dashboard.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove perses-dashboard-sample instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Remove perses-dashboard-sample instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/perses-dashboard-sample.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove prometheus-overview-variables instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Remove prometheus-overview-variables instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/prometheus-overview-variables.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove thanos-compact-overview-1var instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Remove thanos-compact-overview-1var instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-compact-overview-1var.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Remove Thanos Querier instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - } - cy.log('Remove perses-dev namespace'); - cy.executeAndDelete(`oc delete namespace perses-dev --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // cy.log('Remove Thanos Querier instance.'); + // cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/coo141_perses_dashboards/thanos-querier-datasource.yaml --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + // } + // cy.log('Remove perses-dev namespace'); + // cy.executeAndDelete(`oc delete namespace perses-dev --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); // Additional cleanup only when COO is installed if (!Cypress.env('SKIP_COO_INSTALL')) { @@ -699,7 +703,8 @@ const operatorUtils = { checkStatus(); cy.then(() => { - operatorUtils.waitForPodsDeleted(MCP.namespace); + // COO deploys operand pods to openshift-monitoring, not the COO namespace + operatorUtils.waitForPodsDeleted('openshift-monitoring'); }); } else { @@ -770,7 +775,8 @@ const operatorUtils = { cy.aboutModal(); cy.podImage('monitoring-plugin', MP.namespace); if (MCP && MCP.namespace) { - cy.podImage('monitoring', MCP.namespace); + // COO deploys monitoring pods to openshift-monitoring, not the COO namespace + cy.podImage('monitoring', 'openshift-monitoring'); } } }; @@ -840,9 +846,9 @@ Cypress.Commands.add('beforeBlock', (MP: { namespace: string, operatorName: stri cacheAcrossSpecs: true, validate() { cy.validateLogin(); - // Additional validation for COO setup - cy.visit('/monitoring/v2/dashboards'); - cy.url().should('include', '/monitoring/v2/dashboards'); + // TEMPORARILY DISABLED - Dashboard validation not needed for current tests + // cy.visit('/monitoring/v2/dashboards'); + // cy.url().should('include', '/monitoring/v2/dashboards'); }, }, ); diff --git a/web/cypress/support/e2e.js b/web/cypress/support/e2e.js index 56bfe492c..c938e2c11 100644 --- a/web/cypress/support/e2e.js +++ b/web/cypress/support/e2e.js @@ -1,4 +1,13 @@ import './commands'; import registerCypressGrep from '@cypress/grep'; -registerCypressGrep(); \ No newline at end of file +registerCypressGrep(); + +// Hide fetch/XHR request entries from the Cypress command log to reduce noise +const app = window.top; +if (app && !app.document.head.querySelector('[data-hide-command-log-request]')) { + const style = app.document.createElement('style'); + style.innerHTML = '.command-name-request, .command-name-xhr { display: none }'; + style.setAttribute('data-hide-command-log-request', ''); + app.document.head.appendChild(style); +} \ No newline at end of file diff --git a/web/cypress/support/incidents_prometheus_query_mocks/mock-generators.ts b/web/cypress/support/incidents_prometheus_query_mocks/mock-generators.ts index 28a728c32..81ad3cd4d 100644 --- a/web/cypress/support/incidents_prometheus_query_mocks/mock-generators.ts +++ b/web/cypress/support/incidents_prometheus_query_mocks/mock-generators.ts @@ -1,5 +1,5 @@ // Mock data generators for Prometheus mocking system -import { IncidentDefinition, PrometheusResult, AlertDefinition } from './types'; +import { IncidentDefinition, PrometheusResult, AlertDefinition, PrometheusInstantResult } from './types'; import { severityToValue, parseQueryLabels } from './utils'; import { nowInClusterTimezone } from './utils'; import { NEW_METRIC_NAME, OLD_METRIC_NAME } from './prometheus-mocks'; @@ -10,26 +10,26 @@ import { NEW_METRIC_NAME, OLD_METRIC_NAME } from './prometheus-mocks'; * For longer durations, uses 5-minute intervals */ function generateIntervalTimestamps(startTime: number, endTime: number): number[] { - const duration = endTime - startTime; const fiveMinutes = 300; + + // Align to 5-minute grid (matches real Prometheus query_range step=300 behavior) + const alignedStart = Math.ceil(startTime / fiveMinutes) * fiveMinutes; + const alignedEnd = Math.floor(endTime / fiveMinutes) * fiveMinutes; + + const duration = alignedEnd - alignedStart; const timestamps: number[] = []; - if (duration < fiveMinutes) { - timestamps.push(startTime); + if (duration < fiveMinutes) { + timestamps.push(alignedStart); return timestamps; } - let currentTime = startTime; - while (currentTime <= endTime) { + let currentTime = alignedStart; + while (currentTime <= alignedEnd) { timestamps.push(currentTime); currentTime += fiveMinutes; } - // Ensure we have the end timestamp if it doesn't align with 5-minute intervals - if (timestamps[timestamps.length - 1] !== endTime) { - timestamps.push(endTime); - } - return timestamps; } @@ -273,4 +273,82 @@ export function createAlertDetailsMock( console.log(`Final ALERTS results array length: ${results.length}`); return results; +} +/** + * Creates mock instant query response for incident start timestamps + * Used for: min_over_time(timestamp(cluster_health_components_map)[15d:5m]) + */ +export function createIncidentTimestampMock( + incidents: IncidentDefinition[] +): PrometheusInstantResult[] { + const now = nowInClusterTimezone(); + const results: PrometheusInstantResult[] = []; + + incidents.forEach(incident => { + incident.alerts.forEach(alert => { + + const timeline = alert.timeline || { start: 0}; + // Use rounded version to see edge case for Tooltip Dates + // const startTimestamp = Math.floor(timeline.start / 300) * 300; + const startTimestamp = timeline.start; + + results.push({ + metric: { + component: alert.component || incident.component, + container: 'cluster-health-analyzer', + endpoint: 'metrics', + group_id: incident.id, + instance: '10.128.0.134:8443', + job: 'cluster-health-analyzer', + layer: incident.layer, + namespace: alert.namespace, + severity: alert.severity, + src_alertname: alert.name, + src_namespace: alert.namespace, + src_severity: alert.severity, + type: 'alert' + }, + value: [now, startTimestamp.toString()] + }); + }); + }); + + console.log(`Created ${results.length} incident timestamp mocks`); + return results; +} + +/** + * Creates mock instant query response for alert start timestamps + * Used for: min_over_time(timestamp(ALERTS{alertstate="firing"})[15d:5m]) + */ +export function createAlertTimestampMock( + incidents: IncidentDefinition[] +): PrometheusInstantResult[] { + const now = nowInClusterTimezone(); + const results: PrometheusInstantResult[] = []; + + incidents.forEach(incident => { + incident.alerts + .filter(alert => alert.firing !== false) + .forEach(alert => { + const timeline = alert.timeline || { start: 0}; + // Use rounded version to see edge case for Tooltip Dates + // const startTimestamp = Math.floor(timeline.start / 300) * 300; + const startTimestamp = timeline.start; + + results.push({ + metric: { + alertname: alert.name, + alertstate: 'firing', + namespace: alert.namespace, + prometheus: 'openshift-monitoring/k8s', + severity: alert.severity + }, + value: [now, startTimestamp.toString()] + }); + }); + }); + + console.log(`Created ${results.length} alert timestamp mocks`); + return results; } \ No newline at end of file diff --git a/web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts b/web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts index bf35bb09d..47f087ea2 100644 --- a/web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts +++ b/web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts @@ -1,5 +1,5 @@ -import { IncidentDefinition, PrometheusResponse, IncidentScenarioFixture } from './types'; -import { createIncidentMock, createAlertDetailsMock } from './mock-generators'; +import { IncidentDefinition, PrometheusResponse, PrometheusInstantResponse, IncidentScenarioFixture } from './types'; +import { createIncidentMock, createAlertDetailsMock, createIncidentTimestampMock, createAlertTimestampMock } from './mock-generators'; import { convertFixtureToIncidents, parseYamlFixture } from './schema/fixture-converter'; declare global { @@ -14,7 +14,8 @@ declare global { export const NEW_METRIC_NAME = 'cluster_health_components_map'; export const OLD_METRIC_NAME = 'cluster:health:components:map'; -const MOCK_QUERY = '/api/prometheus/api/v1/query_range*'; +const MOCK_QUERY_RANGE = '/api/prometheus/api/v1/query_range*'; +const MOCK_QUERY_INSTANT = /\/api\/prometheus\/api\/v1\/query\?.*/; /** * Main mocking function - sets up cy.intercept for Prometheus query_range API @@ -25,7 +26,7 @@ const MOCK_QUERY = '/api/prometheus/api/v1/query_range*'; * @param incidents - Array of incident definitions to mock */ export function mockPrometheusQueryRange(incidents: IncidentDefinition[]): void { - cy.intercept('GET', MOCK_QUERY, (req) => { + cy.intercept('GET', MOCK_QUERY_RANGE, (req) => { const url = new URL(req.url, window.location.origin); const query = url.searchParams.get('query') || ''; const startTime = url.searchParams.get('start'); @@ -66,6 +67,49 @@ export function mockPrometheusQueryRange(incidents: IncidentDefinition[]): void }); } +/** + * Mocks instant queries for min_over_time timestamp lookups + * Used for absolute start timestamps of incidents and alerts + */ +export function mockPrometheusInstantQuery(incidents: IncidentDefinition[]): void { + cy.intercept('GET', MOCK_QUERY_INSTANT, (req) => { + const url = new URL(req.url, window.location.origin); + const query = url.searchParams.get('query') || ''; + + console.log(`INTERCEPTED INSTANT: ${req.method} ${req.url}`); + console.log(`Query: ${query}`); + + if (query.includes('min_over_time') && query.includes('timestamp')) { + let results: any[]; + + if (query.includes(NEW_METRIC_NAME) || query.includes(OLD_METRIC_NAME)) { + results = createIncidentTimestampMock(incidents); + } else if (query.includes('ALERTS')) { + results = createAlertTimestampMock(incidents); + } else { + console.log('Passing through non-mocked instant query'); + req.continue(); + return; + } + + const response: PrometheusInstantResponse = { + status: 'success', + data: { + resultType: 'vector', + result: results + } + }; + + console.log(`Responding with ${results.length} timestamp results`); + req.reply(response); + return; + } + + console.log('Passing through non-mocked instant query'); + req.continue(); + }); +} + Cypress.Commands.add('mockIncidents', (incidents: IncidentDefinition[]) => { cy.log(`=== SETTING UP INCIDENT MOCKING (${incidents.length} incidents) ===`); if (!Array.isArray(incidents)) { @@ -81,6 +125,7 @@ Cypress.Commands.add('mockIncidents', (incidents: IncidentDefinition[]) => { cy.log(`=== SETTING UP INCIDENT MOCKING (${incidents.length} incidents) ===`); mockPrometheusQueryRange(incidents); + mockPrometheusInstantQuery(incidents); // The mocking is not applied until the page is reloaded and the components fetch the new data cy.reload(); }); @@ -92,12 +137,14 @@ Cypress.Commands.add('mockIncidentFixture', (fixturePath: string) => { const incidents = convertFixtureToIncidents(fixture); cy.log(`=== SETTING UP YAML FIXTURE: ${fixture.name} (${incidents.length} incidents) ===`); mockPrometheusQueryRange(incidents); + mockPrometheusInstantQuery(incidents); }); } else { cy.fixture(fixturePath).then((fixture: IncidentScenarioFixture) => { const incidents = convertFixtureToIncidents(fixture); cy.log(`=== SETTING UP JSON FIXTURE: ${fixture.name} (${incidents.length} incidents) ===`); mockPrometheusQueryRange(incidents); + mockPrometheusInstantQuery(incidents); }); } @@ -118,7 +165,7 @@ Cypress.Commands.add('transformMetrics', () => { cy.log('Transforming old metric queries to new format'); - cy.intercept('GET', MOCK_QUERY, (req) => { + cy.intercept('GET', MOCK_QUERY_RANGE, (req) => { const url = new URL(req.url, window.location.origin); const query = url.searchParams.get('query') || ''; const hasNewMetric = query.includes(NEW_METRIC_NAME); diff --git a/web/cypress/support/incidents_prometheus_query_mocks/types.ts b/web/cypress/support/incidents_prometheus_query_mocks/types.ts index 1812a49b8..056ef72ca 100644 --- a/web/cypress/support/incidents_prometheus_query_mocks/types.ts +++ b/web/cypress/support/incidents_prometheus_query_mocks/types.ts @@ -13,6 +13,19 @@ export interface PrometheusResponse { }; } +export interface PrometheusInstantResult { + metric: Record; + value: [number, string]; +} + +export interface PrometheusInstantResponse { + status: 'success' | 'error'; + data: { + resultType: 'vector'; + result: PrometheusInstantResult[]; + }; +} + // Declarative interface for defining incidents with alerts export interface IncidentDefinition { id: string; // Used as group_id diff --git a/web/cypress/views/incidents-page.ts b/web/cypress/views/incidents-page.ts index 92cd8ca92..12567c7d6 100644 --- a/web/cypress/views/incidents-page.ts +++ b/web/cypress/views/incidents-page.ts @@ -119,6 +119,11 @@ export const incidentsPage = { incidentsTableComponentCell: (index: number) => cy.byTestID(`${DataTestIDs.IncidentsTable.ComponentCell}-${index}`), incidentsTableSeverityCell: (index: number) => cy.byTestID(`${DataTestIDs.IncidentsTable.SeverityCell}-${index}`), incidentsTableStateCell: (index: number) => cy.byTestID(`${DataTestIDs.IncidentsTable.StateCell}-${index}`), + incidentsDetailsStartCell: (index: number) => incidentsPage.elements.incidentsTable() + .find('td[data-label="Start"]').eq(index).find('span[data-test="timestamp"]'), + incidentsDetailsEndCell: (index: number) => incidentsPage.elements.incidentsTable() + .find('td[data-label="End"]').eq(index).find('span[data-test="timestamp"]'), + // Details table (expanded row) incidentsDetailsTable: () => cy.byTestID(DataTestIDs.IncidentsDetailsTable.Table), @@ -129,9 +134,11 @@ export const incidentsPage = { incidentsDetailsNamespaceCell: (index: number) => cy.byTestID(`${DataTestIDs.IncidentsDetailsTable.NamespaceCell}-${index}`), incidentsDetailsSeverityCell: (index: number) => cy.byTestID(`${DataTestIDs.IncidentsDetailsTable.SeverityCell}-${index}`), incidentsDetailsStateCell: (index: number) => cy.byTestID(`${DataTestIDs.IncidentsDetailsTable.StateCell}-${index}`), - incidentsDetailsStartCell: (index: number) => cy.byTestID(`${DataTestIDs.IncidentsDetailsTable.StartCell}-${index}`), - incidentsDetailsEndCell: (index: number) => cy.byTestID(`${DataTestIDs.IncidentsDetailsTable.EndCell}-${index}`), - + incidentsDetailsFiringStartCell: (index: number) => incidentsPage.elements.incidentsDetailsTable() + .find('td[data-label="expanded-details-firingstart"]').eq(index).find('span[data-test="timestamp"]'), + incidentsDetailsFiringEndCell: (index: number) => incidentsPage.elements.incidentsDetailsTable() + .find('td[data-label="expanded-details-firingend"]').eq(index).find('span[data-test="timestamp"]'), + // Generic selectors for incident table rows and details table rows incidentsTableRows: () => incidentsPage.elements.incidentsTable().find(`tbody[data-test*="${DataTestIDs.IncidentsTable.Row}-"]`), incidentsDetailsTableRows: () => incidentsPage.elements.incidentsDetailsTable().find('tbody tr'), @@ -350,6 +357,32 @@ export const incidentsPage = { return incidentsPage.waitForTooltip(); }, + hoverOverIncidentBarById: (incidentId: string, segmentIndex = 0) => { + cy.log(`incidentsPage.hoverOverIncidentBarById: ${incidentId}, segment: ${segmentIndex}`); + incidentsPage.elements.incidentsChartBar(incidentId) + .find('path[role="presentation"]') + .then(($paths) => { + const visiblePaths = $paths.filter((i, el) => { + const fillOpacity = Cypress.$(el).css('fill-opacity') || Cypress.$(el).attr('fill-opacity'); + return parseFloat(fillOpacity || '0') > 0; + }); + + if (segmentIndex >= visiblePaths.length) { + throw new Error(`Segment ${segmentIndex} not found for incident ${incidentId} (${visiblePaths.length} visible)`); + } + + const rect = visiblePaths[segmentIndex].getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + + incidentsPage.elements.incidentsChartSvg() + .first() + .trigger('mousemove', { clientX: x, clientY: y, force: true }) + .wait(100); + }); + return incidentsPage.waitForTooltip(); + }, + getIncidentBarRect: (index: number) => { cy.log(`incidentsPage.getIncidentBarRect: ${index}`); return incidentsPage.elements.incidentsChartBarsGroups() @@ -391,12 +424,20 @@ export const incidentsPage = { hoverOverAlertBar: (index: number) => { cy.log(`incidentsPage.hoverOverAlertBar: ${index}`); incidentsPage.elements.alertsChartBarsPaths() - .eq(index) - .then(($bar) => { - const rect = $bar[0].getBoundingClientRect(); + .then(($paths) => { + const visiblePaths = $paths.filter((i, el) => { + const fillOpacity = Cypress.$(el).css('fill-opacity') || Cypress.$(el).attr('fill-opacity'); + return parseFloat(fillOpacity || '0') > 0; + }); + + if (index >= visiblePaths.length) { + throw new Error(`Alert bar ${index} not found (${visiblePaths.length} visible)`); + } + + const rect = visiblePaths[index].getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; - + incidentsPage.elements.alertsChartSvg() .first() .trigger('mousemove', { clientX: x, clientY: y, force: true }) @@ -405,6 +446,54 @@ export const incidentsPage = { return incidentsPage.waitForTooltip(); }, + getTooltipStartDate: (): Cypress.Chainable => { + cy.log('incidentsPage.getTooltipStartDate'); + return incidentsPage.elements.tooltip() + .find('p') + .contains('Start:') + .invoke('text') + .then((text) => { + const match = text.match(/Start\s*:\s*(.*)/); + return cy.wrap(match ? match[1].trim() : ''); + }); + }, + + getTooltipSeverity: (): Cypress.Chainable => { + cy.log('incidentsPage.getTooltipSeverity'); + return incidentsPage.elements.tooltip() + .find('p') + .contains('Severity:') + .invoke('text') + .then((text) => { + const match = text.match(/Severity\s*:\s*(.*)/); + return cy.wrap(match ? match[1].trim() : ''); + }); + }, + + getTooltipEndDate: (): Cypress.Chainable => { + cy.log('incidentsPage.getTooltipEndDate'); + return incidentsPage.elements.tooltip() + .find('p') + .contains('End:') + .invoke('text') + .then((text) => { + const match = text.match(/End\s*:\s*(.*)/); + return cy.wrap(match ? match[1].trim() : ''); + }); + }, + + getAlertsTooltipStartDate: (): Cypress.Chainable => { + cy.log('incidentsPage.getAlertsTooltipStartDate'); + return incidentsPage.elements.alertsChartTooltip() + .find('p') + .contains('Start:') + .invoke('text') + .then((text) => { + const match = text.match(/Start\s*:\s*(.*)/); + return cy.wrap(match ? match[1].trim() : ''); + }); + }, + // Constants for search configuration SEARCH_CONFIG: { CHART_LOAD_WAIT: 1000,