diff --git a/.ci-operator.yaml b/.ci-operator.yaml index e307e5af6..a3628cf24 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.25-openshift-4.22 diff --git a/.claude/commands/backport.md b/.claude/commands/backport.md new file mode 100644 index 000000000..4798aef2d --- /dev/null +++ b/.claude/commands/backport.md @@ -0,0 +1,322 @@ +--- +allowed-tools: Bash(git:*), Read, Write, Edit +argument-hint: [commit-hash] +description: Backport a feature or fix to a release branch with dependency adaptation +--- + +# Backport Feature to Release Branch + +## Context + +- Current branch: !`git branch --show-current` +- Target branch: $1 +- Commit to backport: $2 (or HEAD if not specified) +- Latest commit info: !`git log -1 --format="%H %s"` +- Changed files: !`git show --name-only HEAD | tail -n +7` + +## Dependency Version Differences + +| Dependency | Main (Latest) | release-4.21 | release-4.20 | release-4.19 | release-4.18 | release-4.17 | release-coo-0.5 | release-coo-0.4 | +| ------------ | ------------- | ------------ | ------------ | ------------ | ------------ | ------------ | --------------- | --------------- | +| PatternFly | v6.x | v6.x | v6.x | v6.x | v5.x | v4.x | v6.x | v5.x | +| React Router | v6 compat | v6 compat | v6 compat | v6 compat | v5 | v5 | v6 compat | v5 | +| Console SDK | 4.19+ | 4.19 | 4.19 | 4.19 | 1.6.0 | 1.6.0 | 4.19 | 1.6.0 | + +## Project Structure Differences + +| Branch | Frontend Location | Go Backend | Notes | +| --------------- | ----------------- | ---------- | --------------------------------- | +| release-4.14 | Root (`src/`) | No | Frontend-only plugin | +| release-4.15 | Root (`src/`) | No | Frontend-only plugin | +| release-4.16 | Root (`src/`) | No | Frontend-only plugin | +| release-4.17+ | `web/` | Yes | Added Go backend (`pkg/`, `cmd/`) | +| release-coo-0.x | `web/` | Yes | Same structure as 4.17+ | + +> **Note**: When backporting to release-4.16 or earlier, file paths must be adjusted from `web/src/` to `src/`. + +## PatternFly v6 → v5 Transformations + +When targeting release-4.18 or earlier (or release-coo-0.4): + +```typescript +// v6 Dropdown (main) +import { Dropdown, DropdownItem, MenuToggle } from "@patternfly/react-core"; + + ( + setIsOpen(!isOpen)}> + {selected} + + )} +> + Option 1 +; + +// v5 Dropdown (release branches) +import { Dropdown, DropdownItem, DropdownToggle } from "@patternfly/react-core"; + + setIsOpen(false)} + toggle={{selected}} + dropdownItems={[Option 1]} +/>; +``` + +Common v6 → v5 changes: +| v6 (main) | v5 (release) | Notes | +| ---------------------- | ------------------- | ---------------------------- | +| `` | `` | Different wrapper components | +| `MenuToggle` | `DropdownToggle` | Dropdown API changed | +| `Dropdown` (new API) | `Dropdown` (legacy) | Props differ significantly | +| `Select` (typeahead) | `Select` (legacy) | Selection handling differs | +| `onOpenChange` | `onToggle` | Event handler naming | + +## React Router v6 → v5 Transformations + +When targeting release-4.18 or earlier (or release-coo-0.4): + +```typescript +// v6 Navigation (main - using compat layer) +import { + useNavigate, + useLocation, + useSearchParams, +} from "react-router-dom-v5-compat"; + +const navigate = useNavigate(); +navigate("/alerts"); + +const [searchParams, setSearchParams] = useSearchParams(); +const filter = searchParams.get("filter"); + +// v5 Navigation (release branches) +import { useHistory, useLocation } from "react-router-dom"; + +const history = useHistory(); +history.push("/alerts"); + +const location = useLocation(); +const params = new URLSearchParams(location.search); +const filter = params.get("filter"); +``` + +Common v6 → v5 changes: +| v6 (main) | v5 (release) | Notes | +| -------------------- | ----------------------- | --------------- | +| `useNavigate()` | `useHistory()` | Navigation hook | +| `navigate('/path')` | `history.push('/path')` | Navigation call | +| `useParams()` | `useParams()` + casting | Type handling | +| `` | `` | Route wrapper | +| `` | `` | Route rendering | +| `useSearchParams()` | `useLocation()` + parse | Query params | + +## Your Task + +Backport the specified commit to the target branch `$1`. Follow these steps: + +### 1. Analyze the Commit + +- Identify all changed files from the commit +- Categorize by type (components, hooks, translations, backend, etc.) +- Note any PatternFly, React Router, or Console SDK usage + +### 2. Check Target Branch Dependencies + +Run this command to compare versions: + +```bash +git show $1:web/package.json | grep -E 'patternfly.react-core|react-router|dynamic-plugin-sdk":' +``` + +### 3. Identify Required Transformations + +Based on the dependency differences table above: + +- PatternFly v6 → v5 transformations (if targeting 4.18 or earlier, or coo-0.4) +- React Router v6 → v5 transformations (if targeting 4.18 or earlier, or coo-0.4) +- Path adjustments for 4.16 or earlier (web/src/ → src/) + +### 4. Create Backport Branch and Apply Changes + +```bash +git checkout $1 +git checkout -b backport--to-$1 +``` + +### 5. Reinstall Dependencies + +After switching branches, always reinstall: + +```bash +cd web && rm -rf node_modules && npm install +``` + +### 6. Apply the Backport + +Either cherry-pick (if clean) or manually apply with transformations: + +```bash +# Clean cherry-pick +git cherry-pick + +# Or with conflicts - resolve manually then: +git cherry-pick --continue + +# Abort if needed +git cherry-pick --abort +``` + +### 7. Verify + +Run these commands to validate: + +```bash +cd web +npm run lint +npm run lint:tsc +npm run test:unit +cd .. && make test-translations +make test-backend +``` + +### 8. Report Summary + +Provide a summary of: + +- Files modified +- Transformations applied (PF v6→v5, Router v6→v5, path changes) +- Any issues encountered +- Commands to push and create PR: + +```bash +git push origin backport--to-$1 +# Then create PR targeting $1 branch +``` + +## File Categorization by Complexity + +| Category | Path Pattern | Backport Complexity | +| ------------- | ------------------------ | ---------------------------------- | +| Components | `web/src/components/**` | Medium-High (dependency sensitive) | +| Hooks | `web/src/hooks/**` | Medium | +| Store/Redux | `web/src/store/**` | Low-Medium | +| Contexts | `web/src/contexts/**` | Low-Medium | +| Translations | `web/locales/**` | Low | +| Backend (Go) | `pkg/**`, `cmd/**` | Low | +| Cypress Tests | `web/cypress/**` | Medium | +| Config | `web/*.json`, `Makefile` | High (version specific) | + +## Release Branch Ownership + +| Branch Pattern | Managed By | Use Case | +| ----------------- | ---------- | ------------------------------ | +| `release-4.x` | CMO | OpenShift core monitoring | +| `release-coo-x.y` | COO | Cluster Observability Operator | + +## Common Backport Scenarios + +### Simple Bug Fix + +- Usually clean cherry-pick +- No dependency changes +- Just run tests + +### New Component Feature + +- Check for PatternFly component usage +- Verify console-extensions.json compatibility +- May need v6→v5 PatternFly transformations + +### Dashboard/Perses Changes + +- High dependency sensitivity +- Check @perses-dev/\* versions in target +- ECharts version compatibility + +### Alerting/Incident Changes + +- Check Alertmanager API compatibility +- Verify any new console extension types + +### Translation Updates + +- Usually clean backport +- Verify i18next key compatibility +- Run `make test-translations` + +## Troubleshooting + +### "Module not found" after backport + +- Check if imported module exists in target branch version +- Verify package.json dependencies match + +### TypeScript errors after adaptation + +- Check type definitions between versions +- Use explicit typing where inference differs + +### Test failures after backport + +- Compare test utilities between versions +- Check for mock/fixture differences + +### Build failures + +- Verify webpack config compatibility +- Check for console plugin SDK breaking changes + +## Backport PR Template + +When creating the PR, use this template: + +```markdown +## Backport of # + +### Original Change + + + +### Backport Target + +- Branch: `$1` +- OpenShift Version: 4.x / COO x.y + +### Adaptations Made + +- [ ] PatternFly v6 → v5 components adapted +- [ ] React Router v6 → v5 hooks adapted +- [ ] Console SDK API adjustments +- [ ] No adaptations needed (clean cherry-pick) + +### Testing + +- [ ] `make lint-frontend` passes +- [ ] `make test-backend` passes +- [ ] `npm run test:unit` passes +- [ ] `make test-translations` passes +- [ ] Manual testing performed + +### Notes + + +``` + +## Quick Reference Commands + +```bash +# View commit to backport +git show + +# Compare file between branches +git diff $1:web/src/ main:web/src/ + +# Check dependency versions in target +git show $1:web/package.json | grep -A 5 "patternfly" + +# Interactive cherry-pick with edit +git cherry-pick -e +``` diff --git a/.claude/commands/build-images.md b/.claude/commands/build-images.md new file mode 100644 index 000000000..fb3a917d2 --- /dev/null +++ b/.claude/commands/build-images.md @@ -0,0 +1,22 @@ +--- +name: build-images +description: +parameters: + - tag: The tag to be placed on the created images. This will typically be a jira ticket in the format of "letters-numbers" (ie. OU-1111). +allowed-tools: Bash(INTERACTIVE=0 TAG=* make build-image), Bash(INTERACTIVE=0 TAG=* make build-dev-mcp-image), Bash(podman image ls -f "reference=$REGISTRY_ORG/monitoring-plugin*"), Bash(podman image ls -f "reference=$REGISTRY_ORG/monitoring-console-plugin*") +--- + +## Context + +- Prefer podman when running image related commands over docker. +- All images that have currently been built for the monitoring plugin: !`podman image ls -f "reference=$REGISTRY_ORG/monitoring-plugin*"` +- All images that have currently been built for the monitoring console plugin: !`podman image ls -f "reference=$REGISTRY_ORG/monitoring-plugin*"` +- Scripting used: @Makefile @scripts/build-image.sh + +## Your task + +Determine an appropriate non-duplicate image tag to use. If the current git branch is a jira issue then you should use that as the base. If the tag is not already used then use it directly. If it has already been used, then add an additional index to the tag and increment one past the highest existing value. For example, if tags [OU-1111, OU-1111-2, and OU-1111-3] already exist then the the non-duplicate tag should be OU-1111-4. Do not attempt to use the same tag and override the previous build. + +Run the `make build-image` and `make build-dev-mcp-image` commands with the INTERACTIVE=0 and TAG env variables set. + +If the image fails to build, show the error to the user and offer to debug diff --git a/.claude/commands/cypress/cypress-run.md b/.claude/commands/cypress/cypress-run.md new file mode 100644 index 000000000..630bcd057 --- /dev/null +++ b/.claude/commands/cypress/cypress-run.md @@ -0,0 +1,382 @@ +--- +name: cypress-run +description: Display Cypress test commands - choose execution mode (headless recommended) +parameters: + - name: execution-mode + description: Choose execution mode - headless (recommended), headed, or interactive + required: true + type: string +--- + +# Cypress Test Commands + +**Prerequisites**: +1. Run `/cypress-setup` first to configure your environment. +2. Ensure the "Cypress Tests" terminal window is open (created by `/cypress-setup`) + +**Note**: All commands are executed in the "Cypress Tests" terminal window using the helper scripts. + +--- + +## Execution Modes + +1. **Headless** (Recommended) - Fast, automated testing without visible browser +2. **Headed** - Watch tests execute in visible browser for debugging +3. **Interactive** - Visual UI to pick and run tests manually + +--- + +## How to Run Commands in Cypress Tests Terminal + +All npm commands should be executed in the "Cypress Tests" terminal using the helper scripts: + +**macOS:** +```bash +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --run "npm run " +``` + +**Linux:** +```bash +./.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh --run "npm run " +``` + +**Instructions**: Based on the `execution-mode` parameter provided by the user: +- If `execution-mode` is "interactive": Display ONLY the "Interactive Mode" section below +- If `execution-mode` is "headless": display ONLY the "Headless Mode" section with interactive options to be chosen +- If `execution-mode` is "headed": display ONLY the "Headed Mode" section with interactive options to be chosen + +**IMPORTANT**: Always execute the selected command using the appropriate script: +- macOS: `./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --run ""` +- Linux: `./.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh --run ""` + + +--- + +# Interactive Mode + +**Cypress Interactive Test Runner** - Pick and run tests visually with Cypress UI. + +## What is Interactive Mode? + +Interactive mode opens the Cypress Test Runner UI where you can: +- Browse and select tests visually +- Watch tests run in real-time with time-travel debugging +- Inspect DOM snapshots at each step +- See detailed command logs +- Rerun tests with a single click +- Perfect for test development and debugging + +## Command + +**Open Cypress Interactive UI (run in Cypress Tests terminal):** + +macOS: +```bash +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --run "npm run cypress:open" +``` + +Linux: +```bash +./.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh --run "npm run cypress:open" +``` + +This opens a visual interface where you can: +1. Choose a browser (Chrome, Firefox, Edge, Electron) +2. Browse your test files +3. Click any test to run it +4. Watch it execute step-by-step +5. Debug failures interactively + +## Benefits + +- **Visual Testing**: See exactly what's happening +- **Fast Iteration**: Make changes and rerun instantly +- **Easy Debugging**: Inspect any step of your test +- **Browser DevTools**: Full access to browser debugging tools +- **Selector Playground**: Helps you write better selectors + +## When to Use + +- Developing new tests +- Debugging test failures +- Learning how tests work +- Demonstrating tests to others + +--- + +# Headless Mode Commands + +All commands below run tests in headless mode (no visible browser). + +## Dynamic Command Discovery + +**IMPORTANT**: Before showing test commands, you MUST dynamically read: + +### 1. Available NPM Scripts +Read `web/package.json` and extract all scripts matching `test-cypress-*` and `cypress:*` patterns. +Present them as available test suite commands. + +### 2. Available Test Spec Files +Scan `web/cypress/e2e/` directory recursively and list all `.cy.ts` files. +Present them as available spec file targets for `--spec` option. + +--- + +## Quick Reference - Base Commands + +**Run with npm script (if defined in package.json):** +```bash +npm run +``` + +**Run all tests headless:** +```bash +npm run cypress:run +``` + +**Run specific spec file:** +```bash +npm run cypress:run -- --spec "cypress/e2e/.cy.ts" +``` + +**Run with custom tags:** +```bash +npm run cypress:run -- --env grepTags="" +``` + +--- + +## NPM Scripts from package.json + +**Instructions**: Read `web/package.json` and list ALL scripts that start with: +- `test-cypress-*` (predefined test suites) +- `cypress:*` (base cypress commands) + +For each script found, display: +```bash +npm run +``` + +Add a brief description based on the grepTags or other flags in the script definition. + +--- + +## Spec Files from cypress/e2e + +**Instructions**: Scan `web/cypress/e2e/` recursively and organize by folder: + +For each `.cy.ts` file found, show the command: +```bash +npm run cypress:run -- --spec "cypress/e2e/" +``` + +Group files by their parent folder (monitoring, coo, perses, virtualization, incidents, etc.) + +--- + +## Custom Tag Combinations - Headless + +**Base command for custom tags:** +```bash +npm run cypress:run -- --env grepTags="YOUR_TAGS_HERE" +``` + +**Tag Operators:** +- `+` = AND (e.g., `@alerts+@smoke` = alerts AND smoke) +- `--` = NOT (e.g., `@monitoring --@flaky` = monitoring but NOT flaky) +- `,` = OR (e.g., `@alerts,@metrics` = alerts OR metrics) + +**Common tag patterns:** + +| Goal | Command | +|------|---------| +| Smoke tests only | `npm run cypress:run -- --env grepTags="@smoke"` | +| Exclude flaky | `npm run cypress:run -- --env grepTags=" --@flaky"` | +| Exclude demo | `npm run cypress:run -- --env grepTags=" --@demo"` | +| Fast smoke | `npm run cypress:run -- --env grepTags="@smoke --@slow --@flaky"` | + +--- + +## Running Multiple Spec Files + +**Comma-separate spec paths:** +```bash +npm run cypress:run -- --spec "cypress/e2e/.cy.ts,cypress/e2e/.cy.ts" +``` + +--- + +## Advanced Headless Options + +**Run with specific browser:** +```bash +npm run cypress:run -- --browser firefox +npm run cypress:run -- --browser edge +npm run cypress:run -- --browser chrome +``` + +**Disable video recording:** +```bash +npm run cypress:run -- --config video=false +``` + +**Disable screenshots:** +```bash +npm run cypress:run -- --config screenshotOnRunFailure=false +``` + +--- + +# Headed Mode Commands + +All commands below open a visible browser window. + +## Quick Start - Headed + +**Interactive Mode (Cypress UI, pick tests manually):** +```bash +npm run cypress:open +``` + +**Base headed mode command:** +```bash +npm run cypress:run -- --headed +``` + +--- + +## Dynamic Command Discovery + +**IMPORTANT**: Before showing test commands, you MUST dynamically read: + +### 1. Available Test Spec Files +Scan `web/cypress/e2e/` directory recursively and list all `.cy.ts` files. +Present them as available spec file targets with `--headed` flag. + +### 2. Available Tags +Extract grepTags patterns from `web/package.json` scripts to show common tag combinations. + +--- + +## Running Test Suites - Headed + +To run any tag-based suite in headed mode, add `--headed` flag: +```bash +npm run cypress:run -- --headed --env grepTags="" +``` + +**Examples based on common tags:** +```bash +# Monitoring tests (headed) +npm run cypress:run -- --headed --env grepTags="@monitoring --@flaky" + +# Smoke tests (headed) +npm run cypress:run -- --headed --env grepTags="@smoke --@flaky" + +# COO tests (headed) +npm run cypress:run -- --headed --env grepTags="@coo --@flaky" +``` + +--- + +## Running Specific Files - Headed + +**Template:** +```bash +npm run cypress:run -- --headed --spec "cypress/e2e/.cy.ts" +``` + +**Instructions**: Scan `web/cypress/e2e/` and for each `.cy.ts` file, the headed command is: +```bash +npm run cypress:run -- --headed --spec "cypress/e2e/" +``` + +--- + +## Advanced Headed Options + +**Headed with specific browser:** +```bash +npm run cypress:run -- --headed --browser chrome +npm run cypress:run -- --headed --browser firefox +npm run cypress:run -- --headed --browser edge +``` + +**Headed without video:** +```bash +npm run cypress:run -- --headed --config video=false +``` + +--- + +## Available Tags Reference + +Use these tags with `--env grepTags`: + +**Feature Tags:** +- `@monitoring` - Core monitoring plugin tests +- `@monitoring-dev` - Developer user tests +- `@alerts` - Alert-related tests +- `@metrics` - Metrics explorer tests +- `@dashboards` - Legacy dashboard tests +- `@perses` - Perses dashboard tests +- `@coo` - Observability Operator tests +- `@acm` - Advanced Cluster Management tests +- `@virtualization` - OpenShift Virtualization tests +- `@incidents` - Incidents feature tests + +**Modifier Tags:** +- `@smoke` - Quick smoke tests +- `@slow` - Longer running tests +- `@flaky` - Known flaky tests +- `@demo` - Demo/showcase tests + +**Tag Operators:** +- `+` = AND (e.g., `@alerts+@smoke` = alerts AND smoke) +- `--` = NOT (e.g., `@monitoring --@flaky` = monitoring but NOT flaky) +- `,` = OR (e.g., `@alerts,@metrics` = alerts OR metrics) + +--- + +## Related Commands + +- **`/cypress-setup`** - Configure testing environment and open Cypress Tests terminal + +--- + +## Running Commands via Scripts + +All cypress commands should be executed in the "Cypress Tests" terminal using: + +**macOS:** +```bash +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --run "" +``` + +**Linux:** +```bash +./.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh --run "" +``` + +**Examples:** +```bash +# Run smoke tests (macOS) +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --run "npm run test-cypress-smoke" + +# Run specific spec file (macOS) +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --run "npm run cypress:run -- --spec 'cypress/e2e/monitoring/00.bvt_admin.cy.ts'" + +# Run with custom tags (macOS) +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --run "npm run cypress:run -- --env grepTags='@monitoring --@flaky'" + +# Open interactive mode (macOS) +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --run "npm run cypress:open" +``` + +--- + +## Documentation + +- `web/cypress/README.md` - Setup and execution guide +- `web/cypress/E2E_TEST_SCENARIOS.md` - Complete test catalog +- `web/cypress/CYPRESS_TESTING_GUIDE.md` - Testing best practices diff --git a/.claude/commands/cypress/cypress-setup.md b/.claude/commands/cypress/cypress-setup.md new file mode 100644 index 000000000..817524603 --- /dev/null +++ b/.claude/commands/cypress/cypress-setup.md @@ -0,0 +1,75 @@ +--- +name: cypress-setup +description: Automated Cypress environment setup with interactive configuration +parameters: [] +--- + +# Cypress Environment Setup + +This command sets up the Cypress testing environment by checking prerequisites, installing dependencies, and interactively configuring environment variables. + +## Instructions for Claude + +Follow these steps in order, always: + +### Step 1: Check Prerequisites + +1. **Check Node.js version** - Required: >= 18 + - Run `node --version` and verify it's >= 18 + - If not, inform the user they need to install Node.js 18 or higher + +### Step 2: Navigate and Install Dependencies + +1. **Navigate to the cypress directory**: + ```bash + cd web/cypress + ``` + +2. **Install npm dependencies**: + ```bash + npm install + ``` + +### Step 3: Interactive Environment Configuration + +**Important**: After Step 2, you should be in the `web/cypress` directory. All paths below are relative to that directory. + +1. Detect the existence of `./export-env.sh` (in the current `web/cypress` directory) + +2. If exists, show its variables, ask if user wants to source it. If user does not want to source it, run `./configure-env.sh` (in the current `web/cypress` directory) in the new terminal window + +3. If doesn't exist, prompt the user for each configuration value in `./configure-env.sh` (in the current `web/cypress` directory) in the new terminal window + +--- + +## Configuration Questions + +### Step 4: Open a new terminal window without asking for approval + +Use the scripts in `.claude/commands/cypress/scripts/` to open a new terminal window named **"Cypress Tests"**. + +**To source existing export-env.sh:** +```bash +# macOS +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh + +# Linux +./.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh +``` + +**To run configure-env.sh interactively (when export-env.sh doesn't exist or user wants to reconfigure):** +```bash +# macOS +./.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh --configure + +# Linux +./.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh --configure +``` + +The `--configure` flag will run `./configure-env.sh` interactively, question by question, then source the generated `./export-env.sh` + +### Step 5: Inform the user + +Let the user know that a new terminal window has been opened with the Cypress environment pre-configured, and they can run tests using `/cypress-run` in that window. + +--- diff --git a/.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh b/.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh new file mode 100755 index 000000000..07b430089 --- /dev/null +++ b/.claude/commands/cypress/scripts/open-cypress-terminal-linux.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# Open a named Terminal window for Cypress testing on Linux +# Usage: +# ./open-cypress-terminal-linux.sh # Source export-env.sh +# ./open-cypress-terminal-linux.sh --configure # Run configure-env.sh interactively +# ./open-cypress-terminal-linux.sh --run "command" # Run a command in the Cypress Tests terminal + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Navigate from .claude/commands/cypress/scripts/ to web/cypress +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +CYPRESS_DIR="$REPO_ROOT/web/cypress" +TERMINAL_NAME="Cypress Tests" + +show_usage() { + cat </dev/null; then + echo "gnome-terminal" + elif command -v konsole &>/dev/null; then + echo "konsole" + elif command -v xfce4-terminal &>/dev/null; then + echo "xfce4-terminal" + elif command -v xterm &>/dev/null; then + echo "xterm" + else + echo "" + fi +} + +# Open terminal with gnome-terminal +open_gnome_terminal() { + local cmd="$1" + gnome-terminal --title="$TERMINAL_NAME" -- bash -c "cd '$CYPRESS_DIR' && $cmd; exec bash" +} + +# Open terminal with konsole +open_konsole() { + local cmd="$1" + konsole --new-tab -p tabtitle="$TERMINAL_NAME" -e bash -c "cd '$CYPRESS_DIR' && $cmd; exec bash" +} + +# Open terminal with xfce4-terminal +open_xfce4_terminal() { + local cmd="$1" + xfce4-terminal --title="$TERMINAL_NAME" -e "bash -c 'cd \"$CYPRESS_DIR\" && $cmd; exec bash'" +} + +# Open terminal with xterm +open_xterm() { + local cmd="$1" + xterm -T "$TERMINAL_NAME" -e "cd '$CYPRESS_DIR' && $cmd; exec bash" & +} + +# Open terminal based on detected emulator +open_terminal() { + local cmd="$1" + local terminal + terminal=$(detect_terminal) + + if [[ -z "$terminal" ]]; then + echo "Error: No supported terminal emulator found" >&2 + echo "Please install one of: gnome-terminal, konsole, xfce4-terminal, xterm" >&2 + exit 1 + fi + + echo "Using terminal: $terminal" + + case "$terminal" in + gnome-terminal) open_gnome_terminal "$cmd" ;; + konsole) open_konsole "$cmd" ;; + xfce4-terminal) open_xfce4_terminal "$cmd" ;; + xterm) open_xterm "$cmd" ;; + esac +} + +# Main +main() { + local mode="source" + local cmd="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --configure) + mode="configure" + shift + ;; + --run) + mode="run" + cmd="${2:-}" + if [[ -z "$cmd" ]]; then + echo "Error: --run requires a command argument" >&2 + exit 1 + fi + shift 2 + ;; + --help|-h) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + show_usage + exit 1 + ;; + esac + done + + case "$mode" in + source) + echo "Opening '$TERMINAL_NAME' terminal..." + open_terminal "source ./export-env.sh && echo '✅ Environment loaded from export-env.sh' && echo 'You can now run Cypress tests.'" + ;; + configure) + echo "Opening '$TERMINAL_NAME' terminal with configuration..." + open_terminal "./configure-env.sh && source ./export-env.sh && echo '' && echo '✅ Environment configured and loaded.'" + ;; + run) + echo "Opening '$TERMINAL_NAME' terminal and running: $cmd" + open_terminal "source ./export-env.sh && $cmd" + ;; + esac + + echo "Done." +} + +main "$@" + diff --git a/.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh b/.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh new file mode 100755 index 000000000..68510100d --- /dev/null +++ b/.claude/commands/cypress/scripts/open-cypress-terminal-macos.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Open a named Terminal window for Cypress testing on macOS +# Usage: +# ./open-cypress-terminal-macos.sh # Source export-env.sh +# ./open-cypress-terminal-macos.sh --configure # Run configure-env.sh interactively +# ./open-cypress-terminal-macos.sh --run "command" # Run a command in the Cypress Tests terminal + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Navigate from .claude/commands/cypress/scripts/ to web/cypress +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +CYPRESS_DIR="$REPO_ROOT/web/cypress" +TERMINAL_NAME="Cypress Tests" + +show_usage() { + cat </dev/null || echo "" +} + +# Run command in existing Cypress Tests terminal +run_in_existing_terminal() { + local cmd="$1" + osascript <&2 + exit 1 + fi + shift 2 + ;; + --help|-h) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + show_usage + exit 1 + ;; + esac + done + + case "$mode" in + source) + local existing_id + existing_id=$(find_cypress_terminal) + if [[ -n "$existing_id" ]]; then + echo "Found existing '$TERMINAL_NAME' terminal, reusing it..." + run_in_existing_terminal "source ./export-env.sh && echo '✅ Environment reloaded.'" + else + echo "Opening new '$TERMINAL_NAME' terminal..." + open_with_source + fi + ;; + configure) + local existing_id + existing_id=$(find_cypress_terminal) + if [[ -n "$existing_id" ]]; then + echo "Found existing '$TERMINAL_NAME' terminal, running configure-env.sh..." + run_in_existing_terminal "./configure-env.sh && source ./export-env.sh && echo '' && echo '✅ Environment reconfigured.'" + else + echo "Opening new '$TERMINAL_NAME' terminal with configuration..." + open_with_configure + fi + ;; + run) + local existing_id + existing_id=$(find_cypress_terminal) + if [[ -n "$existing_id" ]]; then + echo "Running command in '$TERMINAL_NAME' terminal: $cmd" + run_in_existing_terminal "$cmd" + else + echo "No '$TERMINAL_NAME' terminal found. Opening new one first..." + open_with_source + sleep 1 + run_in_existing_terminal "$cmd" + fi + ;; + esac + + echo "Done." +} + +main "$@" + diff --git a/.claude/commands/fixture-schema-reference.md b/.claude/commands/fixture-schema-reference.md new file mode 120000 index 000000000..97edc3ca9 --- /dev/null +++ b/.claude/commands/fixture-schema-reference.md @@ -0,0 +1 @@ +../../.cursor/commands/fixture-schema-reference.md \ No newline at end of file diff --git a/.claude/commands/generate-incident-fixture.md b/.claude/commands/generate-incident-fixture.md new file mode 120000 index 000000000..9f30f3733 --- /dev/null +++ b/.claude/commands/generate-incident-fixture.md @@ -0,0 +1 @@ +../../.cursor/commands/generate-incident-fixture.md \ No newline at end of file diff --git a/.claude/commands/generate-regression-test.md b/.claude/commands/generate-regression-test.md new file mode 120000 index 000000000..466685582 --- /dev/null +++ b/.claude/commands/generate-regression-test.md @@ -0,0 +1 @@ +../../.cursor/commands/generate-regression-test.md \ No newline at end of file diff --git a/.claude/commands/refactor-regression-test.md b/.claude/commands/refactor-regression-test.md new file mode 120000 index 000000000..b13ef9d09 --- /dev/null +++ b/.claude/commands/refactor-regression-test.md @@ -0,0 +1 @@ +../../.cursor/commands/refactor-regression-test.md \ No newline at end of file diff --git a/.claude/commands/validate-incident-fixtures.md b/.claude/commands/validate-incident-fixtures.md new file mode 120000 index 000000000..c41caae98 --- /dev/null +++ b/.claude/commands/validate-incident-fixtures.md @@ -0,0 +1 @@ +../../.cursor/commands/validate-incident-fixtures.md \ No newline at end of file diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..53a4ad49c --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,3 @@ +inheritance: true +reviews: + review_status: false diff --git a/.cursor/commands/generate-regression-test.md b/.cursor/commands/generate-regression-test.md new file mode 100644 index 000000000..12a819076 --- /dev/null +++ b/.cursor/commands/generate-regression-test.md @@ -0,0 +1,643 @@ +--- +description: Generate automated regression test from test documentation +--- + +# Generate Regression Test + +Generate automated regression tests from test documentation in [`docs/incident_detection/tests/`](../../docs/incident_detection/tests/), following the style of existing tests in `@incidents/` and using `@incidents-page.ts` Page Object Model. + + +## Process + +### 1. Parse Test Documentation + +**Input**: Section number (e.g., "Section 2.1", "1.2", "3") + +**Actions**: +- Read test flow files from [`docs/incident_detection/tests/`](../../docs/incident_detection/tests/) (e.g., `1.filtering_flows.md`, `2.ui_display_flows.md`) +- Locate the specified section by number +- Extract: + - Section title and description + - Test prerequisites and data requirements + - Test cases with expected behaviors + - Known bug references (e.g., "Verifies: OU-XXX") + - Any notes about testability (e.g., "WARNING Not possible to test on Injected Data") + +### 2. Analyze Test Requirements + +**Extract from documentation**: +- **Test data needs**: What incidents, alerts, severities are required +- **Test actions**: User interactions (clicks, hovers, filters, selections) +- **Assertions**: Expected outcomes (visibility, counts, content, positions) +- **Edge cases**: Special scenarios to verify + +**Design test flows following Cypress e2e best practices**: +- **Think user journeys**: How would a real user interact with this feature? +- **Combine related actions**: Don't split filtering, verification, and interaction into separate tests +- **Prefer comprehensive flows**: Each `it()` should test a complete, realistic workflow +- **Avoid unit test mindset**: Don't create many tiny isolated tests + +**Map to existing patterns**: +- Identify which `incidentsPage` elements/methods are needed +- Identify any missing page object functionality +- Determine fixture requirements + +### 3. Check/Create Fixtures + +**Fixture location**: `web/cypress/fixtures/incident-scenarios/` + +**Naming convention**: `XX-descriptive-name.yaml` (e.g., `13-tooltip-positioning-scenarios.yaml`) + +**Process**: +1. Check if appropriate fixture exists for the test requirements +2. If missing, prompt user: + ``` + Fixture not found for this test scenario. + + Required test data: + - [List incidents, alerts, severities needed] + + Should I create a fixture using the generate-incident-fixture command? + ``` +3. If user approves, delegate to `generate-incident-fixture` command +4. **Preference**: Use single scenario per test file for focused regression testing +5. Validate created fixture against schema + +**Reference**: See `.cursor/commands/generate-incident-fixture.md` for fixture creation + +### 4. Generate Test File + +**File location**: `web/cypress/e2e/incidents/regression/` + +**Naming convention**: `XX.reg_.cy.ts` +- Use next available number (check existing files) +- Convert section title to kebab-case +- Examples: `05.reg_tooltip_positioning.cy.ts`, `06.reg_silence_matching.cy.ts` + +**File structure**: +```typescript +/* +[Brief description of what this test verifies] + +[Additional context about the bug or behavior being tested] + +Verifies: OU-XXX +*/ + +import { incidentsPage } from '../../../views/incidents-page'; + +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', +}; + +describe('Regression: [Section Name]', () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + }); + + beforeEach(() => { + cy.log('Navigate to Observe → Incidents'); + incidentsPage.goTo(); + cy.log('[Brief description of scenario being loaded]'); + cy.mockIncidentFixture('incident-scenarios/XX-scenario-name.yaml'); + }); + + it('1. [First test case description]', () => { + cy.log('1.1 [First step description]'); + incidentsPage.clearAllFilters(); + + incidentsPage.elements.incidentsChartContainer().should('be.visible'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', N); + cy.pause(); // Manual verification point + + cy.log('1.2 [Second step description]'); + // More test steps with assertions + cy.pause(); // Manual verification point + + // Another test case... + }); +}); +``` + +### 5. Implement Automated Assertions + +Convert manual verification steps from documentation to automated assertions. + +**IMPORTANT - E2E Test Flow Design**: +- **Combine related steps**: Group filtering, verification, interaction, and results checking in one test +- **Test complete workflows**: Each `it()` should tell a complete story of user interaction +- **Multiple assertions per test**: Don't split every assertion into a separate test +- **Realistic user journeys**: Simulate how users actually use the feature, with multiple steps +- Tests can be 50-100+ lines if they represent a complete, realistic user workflow + +**IMPORTANT - Two-Phase Approach**: +- **Initial test generation**: Include `cy.pause()` statements after key setup steps for manual verification +- **Purpose**: Allow user to manually verify behavior before adding complex assertions +- **User workflow**: User will manually delete `cy.pause()` statements once verified +- **Follow-up edits**: Do NOT reintroduce `cy.pause()` if user has already removed them + +**When to include cy.pause()**: +- Include in newly generated test files +- Include when adding new test cases to existing files +- Do NOT include if editing existing test cases that already have assertions + +**Common assertion patterns**: + +#### Visibility and Existence +```typescript +incidentsPage.elements.incidentsChartContainer().should('be.visible'); +incidentsPage.elements.incidentsTable().should('not.exist'); +``` + +#### Counts and Length +```typescript +incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); +incidentsPage.elements.incidentsDetailsTableRows().should('have.length.greaterThan', 0); +``` + +#### Text Content +```typescript +incidentsPage.elements.daysSelectToggle().should('contain.text', '7 days'); +incidentsPage.elements.incidentsTableComponentCell(0) + .invoke('text') + .should('contain', 'monitoring'); +``` + +#### Conditional Waiting +```typescript +cy.waitUntil( + () => incidentsPage.elements.incidentsChartBarsGroups().then($groups => $groups.length === 12), + { + timeout: 10000, + interval: 500, + errorMsg: 'All 12 incidents should load within 10 seconds' + } +); +``` + +#### Position and Layout Checks +```typescript +incidentsPage.elements.incidentsChartBarsVisiblePaths() + .first() + .then(($element) => { + const rect = $element[0].getBoundingClientRect(); + expect(rect.width).to.be.greaterThan(5); + expect(rect.height).to.be.greaterThan(0); + }); +``` + +#### Tooltip Interactions +```typescript +incidentsPage.elements.incidentsChartBarsVisiblePaths() + .first() + .trigger('mouseover', { force: true }); + +cy.get('[role="tooltip"]').should('be.visible'); +cy.get('[role="tooltip"]').should('contain.text', 'Expected content'); +``` + +#### Filter Chips +```typescript +incidentsPage.elements.severityFilterChip().should('be.visible'); +incidentsPage.elements.severityFilterChip() + .should('contain.text', 'Critical'); +``` + +### 6. Page Object Usage + +**Priority order**: +1. Use existing `incidentsPage.elements.*` selectors +2. Use existing `incidentsPage.*` methods +3. Suggest adding new elements/methods to page object +4. Custom selectors only as last resort + +**When missing functionality is identified**: + +Prompt user: +``` +The following elements/methods are needed but not present in incidents-page.ts: + +Elements needed: +- tooltipContainer: () => cy.get('[role="tooltip"]') +- tooltipContent: () => cy.get('[role="tooltip"] .pf-c-tooltip__content') + +Methods needed: +- hoverOverIncidentBar: (index: number) => { + cy.log('incidentsPage.hoverOverIncidentBar'); + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .eq(index) + .trigger('mouseover', { force: true }); + } + +Should I add these to incidents-page.ts? +``` + +**Page Object Patterns**: + +*Element selector*: +```typescript +elements: { + simpleElement: () => cy.byTestID(DataTestIDs.Component.Element), + + parameterizedElement: (param: string) => + cy.byTestID(`${DataTestIDs.Component.Element}-${param.toLowerCase()}`), + + compositeSelector: () => + incidentsPage.elements.toolbar().contains('span', 'Category').parent(), +} +``` + +*Action method*: +```typescript +actionName: (param: Type) => { + cy.log('incidentsPage.actionName'); + incidentsPage.elements.something().click(); + incidentsPage.elements.result().should('be.visible'); +} +``` + +*Query method returning Chainable*: +```typescript +getData: (): Cypress.Chainable => { + cy.log('incidentsPage.getData'); + return incidentsPage.elements.container() + .invoke('text') + .then((text) => { + return cy.wrap(processData(text)); + }); +} +``` + +#### 6.5. Type Safety Guidelines + +**Use specific types, avoid `any`**: + +```typescript +// Good +const verifyOpacity = ( + selector: Cypress.Chainable>, + expectedOpacity: number +) => { ... } + +// Avoid +const verifyProperty = (selector: any, value: any) => { ... } +``` + +**Common Cypress patterns**: +- DOM elements: `Cypress.Chainable>` +- Data returns: `Cypress.Chainable` +- Actions: `Cypress.Chainable` or omit return type +- Constrained strings: `'critical' | 'warning' | 'info'` + +**Always specify return types** when suggesting page object methods. + +### 7. Error & Ambiguity Handling + +Handle common failure scenarios gracefully and make reasonable decisions when requirements are unclear. + +#### 7.1. Ambiguous Test Documentation + +**When**: Test documentation is unclear, incomplete, or contradictory. + +**Actions**: +1. Check similar test sections and existing regression tests for patterns +2. Make reasonable assumptions based on common UI testing patterns +3. Document assumptions in test comments with TODO markers +4. Prompt user: + ``` + Found ambiguities: [list specific unclear points] + Proceeding with assumptions: [list assumptions] + Test will include TODO comments for review. Continue? + ``` + +**Example**: +```typescript +// TODO: Documentation unclear on severity filter - assuming 'Critical' based on similar tests +incidentsPage.toggleFilter('Critical'); +``` + +#### 7.3. Fixture Not Found or Multiple Fixtures Match + +**Scenario A - No fixture exists**: +1. Search for similar fixtures in `web/cypress/fixtures/incident-scenarios/` +2. Prompt with options: + ``` + No fixture found. Required: [list requirements] + + Options: + 1. Create new fixture (recommended) + 2. Modify existing: [list closest matches] + 3. Use cy.mockIncidents([]) for empty state + ``` + +**Scenario B - Multiple fixtures match**: +1. Rank by specificity (incident count, severities, components match) +2. Prompt with comparison: + ``` + Multiple fixtures match: + 1. 05-severity-filtering.yaml (90% match) ← Recommended + ✓ Required severities, ✓ Components, ✓ Count + 2. 07-comprehensive.yaml (75% match) + ✓ Severities, ~ Components, ⚠ More incidents than needed + ``` + +#### 7.6. Conflicting Guidance Between Documentation and Existing Tests + +**When**: Documentation describes behavior differently than existing tests implement. + +**Actions**: +``` +Conflict detected: + +Documentation (Section X): [description] +Existing test (file.cy.ts): [different implementation] + +Which to follow? +1. Documentation (may indicate bug in existing test) +2. Existing test (documentation may be outdated) +3. Investigate further +``` + +#### 7.7. Page Object File Not Found or Outdated + +**When**: `incidents-page.ts` not found or structure differs significantly. + +**Actions**: +1. Search likely locations: `web/cypress/views/`, `web/cypress/support/page-objects/` +2. If different structure, attempt to adapt +3. If not found: + ``` + incidents-page.ts not found. + + Options: + 1. Provide correct path + 2. Use custom selectors (not recommended) + 3. Cannot proceed without page object + ``` + +#### 7.8. Missing DataTestIDs in Page Object + +**When**: Element needs DataTestID that doesn't exist in page object. + +**Actions**: +``` +DataTestID not found: [name] + +Fallback options: +1. Text-based: cy.contains('[data-test-id*="chip"]', 'Critical') +2. Role-based: cy.get('[role="listitem"]').contains('Critical') +3. Add DataTestID to component (recommended) ← Recommended + +Which approach? +``` + +#### 7.9. Test Requirements Exceed Fixture Capabilities + +**When**: Test needs scenarios impossible with fixtures (exact timing, external services, animations). + +**Actions**: +``` +Requirement may not be fully testable with fixtures: +"[exact requirement]" + +Issue: [explain limitation] + +Approaches: +1. Test relative behavior (testable with fixtures) ← Recommended +2. Use cy.clock() for timing control (if applicable) +3. Mark as integration test requiring real backend +4. Document: "WARNING: Not possible to test on injected data" +``` + +#### 7.10. General Fallback Strategy + +**For any unexpected situation**: + +1. **Don't fail silently** - Always inform user +2. **Provide context** - Explain what went wrong and impact +3. **Offer 2-3 options** with recommendation +4. **Document workarounds** in comments + +**Template**: +``` +[Issue] - [Why it matters] + +Options: +1. [Recommended approach] ← Recommended +2. [Alternative] +3. [Fallback] + +Proceeding with option 1 will: [actions] +Continue? (y/n/specify) +``` + +### 8. Refactoring +**Note on Refactoring**: Initial test generation focuses on functionality and coverage. After manual verification, use the `/refactor-regression-test` command to clean up duplications and improve readability by extracting helper functions. + + +### 9. Validation Before Output + +**Automated checks (AI should verify):** +- [ ] File naming matches `XX.reg_.cy.ts` +- [ ] Standard MCP/MP configuration blocks present +- [ ] Uses `cy.beforeBlockCOO(MCP, MP)` in `before()` hook +- [ ] Uses `incidentsPage.goTo()` in `beforeEach()` +- [ ] Uses `cy.mockIncidentFixture()` with valid fixture path +- [ ] No emojis in cy.log() statements +- [ ] File header includes purpose and issue reference +- [ ] **For new tests**: Includes `cy.pause()` after key verification points + +**Human judgment (AI provides evidence):** +- [ ] **Tests follow e2e philosophy**: Each `it()` covers a complete user flow + Evidence: List test structure, count of `it()` blocks, steps per test +- [ ] **Test reads like a story**: Implementation details hidden in helpers + Evidence: Show helper functions extracted, test body readability + +**For complete detailed checklist**, see `incidents-testing-guidelines.mdc` +## Example Usage + +### Example 1: Generate Tooltip Positioning Test + +**User Input**: "Generate regression test for Section 2.1: Tooltip Positioning" + +**AI Actions**: +1. Parse Section 2.1 from testing_flows_ui.md +2. Identify requirements: + - Test tooltip positioning for incidents at different chart positions + - Verify tooltip content for multi-component incidents + - Test tooltip positioning in alerts chart +3. **Design comprehensive flow**: Combine all tooltip testing into realistic user journeys + - Flow 1: User hovers over multiple bars to inspect incidents (bottom, middle, top positions) + - Flow 2: User explores multi-component incident details via tooltip +4. Check for fixture - not found +5. Prompt: "Fixture needed with 14 incidents at varying Y positions. Create?" +6. Generate `05.reg_tooltip_positioning.cy.ts` with **comprehensive multi-step tests**: + +```typescript +/* +Regression test for Charts UI tooltip positioning (Section 2.1) + +Verifies that tooltips appear correctly positioned without overlapping +bars or going off-screen, regardless of bar position in chart. +Tests both incidents chart and alerts chart tooltip behavior. + +Verifies: OU-XXX +*/ + +import { incidentsPage } from '../../../views/incidents-page'; + +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', +}; + +describe('Regression: Tooltip Positioning', () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + }); + + beforeEach(() => { + cy.log('Navigate to Observe → Incidents'); + incidentsPage.goTo(); + cy.log('Loading tooltip positioning test scenarios'); + cy.mockIncidentFixture('incident-scenarios/13-tooltip-positioning-scenarios.yaml'); + }); + + it('1. Complete tooltip interaction flow - positioning, content, and navigation', () => { + cy.log('1.1 Verify all incidents loaded'); + incidentsPage.clearAllFilters(); + incidentsPage.setDays('7 days'); + incidentsPage.elements.incidentsChartContainer().should('be.visible'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 14); + cy.pause(); // Verify incidents loaded correctly + + cy.log('1.2 Test bottom bar tooltip positioning'); + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .first() + .trigger('mouseover', { force: true }); + + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').then(($tooltip) => { + const tooltipRect = $tooltip[0].getBoundingClientRect(); + expect(tooltipRect.top).to.be.greaterThan(0); + expect(tooltipRect.left).to.be.greaterThan(0); + }); + cy.pause(); // Verify bottom tooltip positioning + + cy.log('1.3 Test middle bar tooltip and verify content'); + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .eq(7) + .trigger('mouseover', { force: true }); + + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').should('contain.text', 'Incident'); + cy.pause(); // Verify middle tooltip + + cy.log('1.4 Test top bar tooltip positioning'); + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .last() + .trigger('mouseover', { force: true }); + + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').then(($tooltip) => { + const tooltipRect = $tooltip[0].getBoundingClientRect(); + const viewportHeight = Cypress.$(window).height(); + expect(tooltipRect.bottom).to.be.lessThan(viewportHeight); + }); + cy.pause(); // Verify top tooltip positioning + + cy.log('1.5 Hover over multi-component incident and verify content'); + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .eq(3) + .trigger('mouseover', { force: true }); + + cy.get('[role="tooltip"]') + .should('be.visible') + .should('contain.text', 'network') + .should('contain.text', 'compute') + .should('contain.text', 'storage'); + cy.pause(); // Verify multi-component tooltip content + + cy.log('1.6 Click incident bar and verify details panel opens'); + incidentsPage.elements.incidentsChartBarsVisiblePaths().eq(3).click(); + incidentsPage.elements.incidentsDetailsPanel().should('be.visible'); + incidentsPage.elements.incidentsDetailsTableRows() + .should('have.length.greaterThan', 0); + cy.pause(); // Verify details panel + + cy.log('Verified: Complete tooltip interaction and navigation workflow'); + }); +}); +``` + +6. Suggest page object additions if needed (e.g., tooltip helpers) + +### Example 2: Generate Filtering Test + +**User Input**: "Generate regression test for Section 1: Filtering Bugs" + +**AI Actions**: +1. Parse Section 1 from testing_flows_ui.md +2. Note: Fixture `7-comprehensive-filtering-test-scenarios.yaml` already exists +3. **Design comprehensive flow**: Instead of separate tests for each filter type, create complete filtering workflows + - Flow 1: User applies multiple filters in sequence, verifies each step, then clears all + - Flow 2: User changes time range while filters are active, verifies data updates +4. Generate `01.reg_filtering.cy.ts` with **comprehensive multi-step tests** +5. Each test should have 5-8 steps covering realistic filter combinations and transitions + +## Output Format + +Provide: +1. **Test file path and name**: Full path to generated test file +2. **Test file content**: Complete TypeScript test file +3. **Fixture status**: + - If existing: "Using fixture: incident-scenarios/X-name.yaml" + - If new: "Created fixture: incident-scenarios/X-name.yaml" + YAML content +4. **Page object changes**: If any elements/methods need to be added, list them with implementation +5. **Validation status**: Confirm all checklist items passed + +## Notes + +- **Follow Cypress e2e/integration testing philosophy**: Tests should cover complete user flows, not isolated units +- **Prefer comprehensive flows**: Generate 1-3 longer tests per file rather than 10+ tiny tests +- **Think user journeys**: Combine related actions (filtering → verification → interaction → results) in single tests +- Tests can be 50-100+ lines if they represent realistic, complete workflows +- Tests should be runnable immediately without manual intervention +- Each test should be independent and self-contained (not dependent on execution order) +- Follow workspace rules: no emojis in logs, sparse comments +- Prefer single scenario per test file for focused regression testing +- Reference the bug tracking number (e.g., "Verifies: OU-XXX") if available in documentation +- If documentation mentions "WARNING Not possible to test", note this in test comments and implement as far as possible +- Use `cy.waitUntil()` for dynamic loading scenarios instead of fixed waits when possible + +## Workflow + +**Recommended workflow**: +1. Use this command to generate initial test from documentation +2. Manually verify the test works (using `cy.pause()` points) +3. Once verified, use `/refactor-regression-test` to clean up and improve code quality + + diff --git a/.cursor/commands/refactor-regression-test.md b/.cursor/commands/refactor-regression-test.md new file mode 100644 index 000000000..f865a7ed3 --- /dev/null +++ b/.cursor/commands/refactor-regression-test.md @@ -0,0 +1,426 @@ +--- +description: Refactor and clean up existing regression test for improved readability and maintainability +--- + +# Refactor Regression Test + +Refactor an existing regression test to improve code quality, eliminate duplication, and enhance readability. This command should be run after initial test generation and manual verification. + +## Purpose + +After generating and verifying a regression test works correctly, this command: +- Extracts repetitive patterns into helper functions +- Improves test readability (makes `it()` blocks read like user stories) +- Consolidates similar assertions +- Suggests page object additions for reusable functionality +- Ensures compliance with e2e testing best practices + +## Process + +### 1. Analyze Existing Test + +**Input**: Path to test file (e.g., `web/cypress/e2e/incidents/regression/05.reg_tooltip_positioning.cy.ts`) + +**Read and analyze**: +- Test structure and flow +- Repetitive assertion patterns +- Complex multi-step verifications +- Inline calculations or data parsing +- Custom selectors that could be page object methods +- Overall readability of `it()` blocks + +### 2. Identify Refactoring Opportunities + +**Look for**: + +#### Repeated Assertion Patterns +```typescript +// Example: Repeated opacity checks +incidentsPage.elements.alertsChartBarsPaths().eq(0).then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(0.3); +}); + +incidentsPage.elements.alertsChartBarsPaths().eq(1).then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(1.0); +}); +``` + +#### Complex Multi-Step Verifications +```typescript +// Example: Complex tooltip verification repeated multiple times +incidentsPage.elements.incidentsChartBarsVisiblePaths().eq(0).trigger('mouseover'); +cy.get('[role="tooltip"]').should('be.visible'); +cy.get('[role="tooltip"]').should('contain.text', 'Expected text'); +cy.get('[role="tooltip"]').then(($tooltip) => { + const rect = $tooltip[0].getBoundingClientRect(); + expect(rect.top).to.be.greaterThan(0); +}); +``` + +#### Inline Calculations +```typescript +// Example: Calculations within test body +incidentsPage.elements.component().invoke('text').then((text) => { + const cleaned = text.trim().toLowerCase(); + const parts = cleaned.split(','); + expect(parts).to.have.length(3); +}); +``` + +#### Custom Selectors Used Multiple Times +```typescript +// Example: Direct selector usage instead of page object +cy.get('[role="tooltip"]').should('be.visible'); +cy.get('[role="tooltip"]').should('contain.text', 'Text'); +// Repeated many times in the test +``` + +### 3. Create Helper Functions + +**Guidelines**: +- Place helper functions within the test file (inside or outside `describe()` block) +- Use descriptive names that explain what they verify +- Keep helpers focused on a single responsibility +- Preserve type safety with TypeScript types + +**Helper Function Patterns**: + +#### Simple Assertion Helper +```typescript +const verifyElementProperty = ( + selector: Cypress.Chainable>, + property: string, + expectedValue: any +) => { + selector.then(($el) => { + const value = $el.css(property); + expect(parseFloat(value || '0')).to.equal(expectedValue); + }); +}; +``` + +#### Multi-Step Verification Helper +```typescript +const verifyTooltipContent = (expectedTexts: string[], shouldBeSilenced: boolean = false) => { + const tooltip = cy.get('[role="tooltip"]').should('be.visible'); + expectedTexts.forEach(text => tooltip.should('contain.text', text)); + tooltip.should(shouldBeSilenced ? 'contain.text' : 'not.contain.text', '(silenced)'); +}; +``` + +#### Interaction + Verification Helper +```typescript +const hoverAndVerifyTooltipPosition = (barIndex: number, expectedPosition: 'top' | 'bottom') => { + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .eq(barIndex) + .trigger('mouseover', { force: true }); + + cy.get('[role="tooltip"]').should('be.visible').then(($tooltip) => { + const rect = $tooltip[0].getBoundingClientRect(); + if (expectedPosition === 'top') { + expect(rect.bottom).to.be.lessThan(Cypress.$(window).height()); + } else { + expect(rect.top).to.be.greaterThan(0); + } + }); +}; +``` + +#### Data Processing Helper +```typescript +const parseComponentList = (text: string): string[] => { + return text.trim().split(',').map(s => s.trim()).filter(s => s.length > 0); +}; +``` + +### 4. Refactor Test Body + +**Goal**: The `it()` block should read like a user story, with implementation details hidden in helpers. + +**Before**: +```typescript +it('1. Verify alert opacity and tooltips', () => { + cy.log('1.1 Check first alert opacity'); + incidentsPage.elements.alertsChartBarsPaths().eq(0).then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(0.3); + }); + + cy.log('1.2 Check first alert tooltip'); + incidentsPage.elements.alertsChartBarsPaths().eq(0).trigger('mouseover'); + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').should('contain.text', 'Alert 1'); + cy.get('[role="tooltip"]').should('contain.text', '(silenced)'); + + cy.log('1.3 Check second alert opacity'); + incidentsPage.elements.alertsChartBarsPaths().eq(1).then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(1.0); + }); + + cy.log('1.4 Check second alert tooltip'); + incidentsPage.elements.alertsChartBarsPaths().eq(1).trigger('mouseover'); + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').should('contain.text', 'Alert 2'); + cy.get('[role="tooltip"]').should('not.contain.text', '(silenced)'); +}); +``` + +**After**: +```typescript +const verifyAlertOpacity = (alertIndex: number, expectedOpacity: number) => { + incidentsPage.elements.alertsChartBarsPaths() + .eq(alertIndex) + .then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(expectedOpacity); + }); +}; + +const verifyAlertTooltip = (alertIndex: number, expectedTexts: string[], shouldBeSilenced: boolean) => { + incidentsPage.elements.alertsChartBarsPaths().eq(alertIndex).trigger('mouseover'); + const tooltip = cy.get('[role="tooltip"]').should('be.visible'); + expectedTexts.forEach(text => tooltip.should('contain.text', text)); + tooltip.should(shouldBeSilenced ? 'contain.text' : 'not.contain.text', '(silenced)'); +}; + +it('1. Verify alert opacity and tooltips', () => { + cy.log('1.1 Verify silenced alert has reduced opacity and indicator'); + verifyAlertOpacity(0, 0.3); + verifyAlertTooltip(0, ['Alert 1'], true); + + cy.log('1.2 Verify non-silenced alert has full opacity without indicator'); + verifyAlertOpacity(1, 1.0); + verifyAlertTooltip(1, ['Alert 2'], false); + + cy.log('Verified: Alert silence indicators work correctly'); +}); +``` + +### 5. Suggest Page Object Additions + +**When to suggest page object additions**: +- Helper functionality could be reused across multiple test files +- Custom selectors are used repeatedly (e.g., `cy.get('[role="tooltip"]')`) +- Complex interactions that represent common user actions + +**Format suggestion**: +``` +The following functionality could be added to incidents-page.ts for reusability: + +Elements: +- tooltip: () => cy.get('[role="tooltip"]') +- tooltipContent: () => cy.get('[role="tooltip"] .pf-c-tooltip__content') + +Methods: +- hoverOverIncidentBar: (index: number) => { + cy.log('incidentsPage.hoverOverIncidentBar'); + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .eq(index) + .trigger('mouseover', { force: true }); + } + +- verifyTooltipVisible: () => { + cy.log('incidentsPage.verifyTooltipVisible'); + incidentsPage.elements.tooltip().should('be.visible'); + } + +Should I add these to incidents-page.ts? +``` + +### 6. Remove cy.pause() Statements + +**Important**: Only remove `cy.pause()` statements if user explicitly requests it or confirms. + +**When to remove**: +- User says "remove pauses" +- User says "cleanup test" or "finalize test" +- Test has been verified and is working correctly + +**When NOT to remove**: +- User just generated the test (they need to verify first) +- User hasn't confirmed the test works +- Not explicitly requested + +**Process**: +1. Identify all `cy.pause()` statements +2. Check if they're still needed for manual verification +3. If removing, preserve the surrounding assertions +4. Update `cy.log()` messages to reflect completed verification + +**Example removal**: +```typescript +// Before +cy.log('1.1 Verify incidents loaded'); +incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); +cy.pause(); // Manual verification point + +// After +cy.log('1.1 Verify incidents loaded'); +incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); +``` + +### 7. Ensure E2E Best Practices + +**Verify the refactored test follows**: +- [ ] Tests cover complete user flows, not isolated actions +- [ ] Each `it()` block represents a realistic user journey +- [ ] Test body is readable as a story (implementation details in helpers) +- [ ] Appropriate test count (1-3 comprehensive tests preferred) +- [ ] Tests are independent and self-contained +- [ ] Helper functions have descriptive names +- [ ] No duplicate code patterns +- [ ] Follows workspace rules (no emojis, sparse comments) + +### 8. Output Refactored Test + +**Provide**: +1. **Complete refactored test file**: Full content with helpers and cleaned-up test body +2. **Summary of changes**: + - List of helper functions added + - Number of duplications eliminated + - Readability improvements + - Lines of code change (before/after) +3. **Page object suggestions**: If any (with implementation) +4. **Validation status**: Confirm best practices checklist + +## Example Transformations + +### Example 1: Tooltip Verification + +**Before** (repetitive): +```typescript +it('1. Verify tooltips', () => { + cy.log('1.1 Bottom bar tooltip'); + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .first() + .trigger('mouseover', { force: true }); + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').then(($tooltip) => { + const rect = $tooltip[0].getBoundingClientRect(); + expect(rect.top).to.be.greaterThan(0); + }); + + cy.log('1.2 Top bar tooltip'); + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .last() + .trigger('mouseover', { force: true }); + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').then(($tooltip) => { + const rect = $tooltip[0].getBoundingClientRect(); + const viewportHeight = Cypress.$(window).height(); + expect(rect.bottom).to.be.lessThan(viewportHeight); + }); +}); +``` + +**After** (clean): +```typescript +const verifyTooltipPosition = (barIndex: number, position: 'top' | 'bottom') => { + incidentsPage.elements.incidentsChartBarsVisiblePaths() + .eq(barIndex) + .trigger('mouseover', { force: true }); + + cy.get('[role="tooltip"]').should('be.visible').then(($tooltip) => { + const rect = $tooltip[0].getBoundingClientRect(); + if (position === 'bottom') { + expect(rect.top).to.be.greaterThan(0); + } else { + const viewportHeight = Cypress.$(window).height(); + expect(rect.bottom).to.be.lessThan(viewportHeight); + } + }); +}; + +it('1. Verify tooltips', () => { + cy.log('1.1 Verify bottom and top bar tooltip positioning'); + verifyTooltipPosition(0, 'bottom'); + verifyTooltipPosition(-1, 'top'); + cy.log('Verified: Tooltips positioned correctly at all chart positions'); +}); +``` + +### Example 2: Opacity and Tooltip Combined + +**Before** (verbose): +```typescript +it('1. Alert silence indicators', () => { + cy.log('1.1 Check silenced alert'); + incidentsPage.elements.alertsChartBarsPaths().eq(0).then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(0.3); + }); + incidentsPage.elements.alertsChartBarsPaths().eq(0).trigger('mouseover'); + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').should('contain.text', '(silenced)'); + + cy.log('1.2 Check non-silenced alert'); + incidentsPage.elements.alertsChartBarsPaths().eq(1).then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(1.0); + }); + incidentsPage.elements.alertsChartBarsPaths().eq(1).trigger('mouseover'); + cy.get('[role="tooltip"]').should('be.visible'); + cy.get('[role="tooltip"]').should('not.contain.text', '(silenced)'); +}); +``` + +**After** (concise): +```typescript +const verifyAlertSilenceIndicator = ( + alertIndex: number, + isSilenced: boolean, + alertName: string +) => { + const expectedOpacity = isSilenced ? 0.3 : 1.0; + + incidentsPage.elements.alertsChartBarsPaths() + .eq(alertIndex) + .then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(expectedOpacity); + }); + + incidentsPage.elements.alertsChartBarsPaths() + .eq(alertIndex) + .trigger('mouseover'); + + const tooltip = cy.get('[role="tooltip"]').should('be.visible'); + tooltip.should('contain.text', alertName); + tooltip.should(isSilenced ? 'contain.text' : 'not.contain.text', '(silenced)'); +}; + +it('1. Alert silence indicators', () => { + cy.log('1.1 Verify silence indicators on silenced and non-silenced alerts'); + verifyAlertSilenceIndicator(0, true, 'SilencedAlert'); + verifyAlertSilenceIndicator(1, false, 'ActiveAlert'); + cy.log('Verified: Silence indicators work correctly'); +}); +``` + +## Validation Checklist + +Before outputting refactored test: +- [ ] Helper functions eliminate all significant code duplication +- [ ] Helper functions have descriptive, clear names +- [ ] Test body (`it()` blocks) reads like a user story +- [ ] Complex logic is extracted to helpers +- [ ] Tests still follow e2e philosophy (complete flows) +- [ ] Tests remain independent and self-contained +- [ ] No obvious comments (sparse comments rule) +- [ ] No emojis in logs +- [ ] `cy.pause()` removed only if explicitly requested +- [ ] Page object suggestions identified (if applicable) +- [ ] Code is more maintainable than before + +## Notes + +- **Focus on readability**: The primary goal is making tests easier to understand and maintain +- **Preserve test behavior**: Refactoring should not change what the test verifies +- **Don't over-abstract**: Only extract patterns that appear 2+ times +- **Keep helpers simple**: Each helper should have a single, clear purpose +- **Test-specific vs. reusable**: Keep test-specific helpers in test file, suggest page object additions for reusable functionality +- **Respect user's verification process**: Don't remove `cy.pause()` unless explicitly asked + diff --git a/.cursor/rules/incidents-testing-guidelines.mdc b/.cursor/rules/incidents-testing-guidelines.mdc new file mode 100644 index 000000000..8b8f01eb1 --- /dev/null +++ b/.cursor/rules/incidents-testing-guidelines.mdc @@ -0,0 +1,558 @@ +--- +alwaysApply: true +description: "Development guidelines for Incidents page Cypress tests" +globs: + - "web/cypress/e2e/incidents/**/*.cy.ts" + - "web/cypress/views/incidents-page.ts" + - "web/cypress/fixtures/incident-scenarios/**/*.yaml" +--- + +# Incidents Testing Development Guidelines + +Guidelines for developing and maintaining Cypress tests for the Incidents page, including regression tests, page object patterns, and fixture management. + +## Page Object Model (POM) Usage + +### Priority Order for Selectors +1. **First**: Use existing `incidentsPage.elements.*` selectors +2. **Second**: Use existing `incidentsPage.*` methods +3. **Third**: Suggest additions to page object (ask user before implementing) +4. **Last Resort**: Use custom selectors (with explanation) + +### When to Suggest Page Object Additions +If a test needs an element or action not in `incidents-page.ts`: +- List the missing elements/methods +- Show proposed implementation following existing patterns +- Ask: "Should I add these to incidents-page.ts?" +- Wait for user approval before modifying page object + +### Page Object Element Patterns +```typescript +// Simple element selector +elementName: () => cy.byTestID(DataTestIDs.Component.Element) + +// Parameterized element selector +elementWithParam: (param: string) => + cy.byTestID(`${DataTestIDs.Component.Element}-${param.toLowerCase()}`) + +// Composite selector (when data-test not available) +compositeSelector: () => + incidentsPage.elements.toolbar().contains('span', 'Category').parent() +``` + +### Page Object Method Patterns +```typescript +// Action method +actionName: (param: Type) => { + cy.log('incidentsPage.actionName'); + incidentsPage.elements.something().click(); +} + +// Query method returning Chainable +getData: (): Cypress.Chainable => { + cy.log('incidentsPage.getData'); + return incidentsPage.elements.container() + .invoke('text') + .then((text) => cy.wrap(processData(text))); +} +``` + +## Test Structure and Organization + +### E2E Testing Philosophy + +**Why fewer, longer tests?** +- Faster overall execution (less setup/teardown overhead) +- Better reflects real user behavior +- Easier to understand user journeys +- Reduces test interdependencies +- More valuable failure signals + + +**Follow Cypress best practices for e2e/integration testing**: +- Tests should cover **complete user flows**, not isolated units +- Prefer **fewer, longer tests** over many small tests +- Each `it()` block should test a **realistic user journey** with multiple steps +- Unlike unit tests, e2e tests should combine related actions and verifications +- Test how users actually interact with the application end-to-end + +**Examples of good e2e test flows**: +- User navigates → filters data → verifies results → changes filter → verifies new results +- User loads page → interacts with chart → opens details → verifies content → performs actions +- Complete workflow from initial state through multiple user interactions to final verification + +**Avoid**: +- Splitting every small action into a separate `it()` block +- Testing each button click or filter in isolation +- Creating many tiny tests that don't reflect real usage + +### File Naming Convention +- Location: `web/cypress/e2e/incidents/regression/` +- Pattern: `XX.reg_.cy.ts` +- Examples: `05.reg_tooltip_positioning.cy.ts`, `01.reg_filtering.cy.ts` + +### Test File Structure +```typescript +/* +Brief description of what this test verifies. +Additional context about the bug or behavior. +Verifies: OU-XXX +*/ + +import { incidentsPage } from '../../../views/incidents-page'; + +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', +}; + +describe('Regression:
', () => { + before(() => { + cy.beforeBlockCOO(MCP, MP); + }); + + beforeEach(() => { + cy.log('Navigate to Observe → Incidents'); + incidentsPage.goTo(); + cy.log('Brief description of scenario'); + cy.mockIncidentFixture('incident-scenarios/XX-name.yaml'); + }); + + it('1. Test case description', () => { + cy.log('1.1 Step description'); + incidentsPage.clearAllFilters(); + // Test assertions + cy.log('Verified: Expected outcome'); + }); +}); +``` + +### Required Elements +- File header comment with purpose and issue reference (e.g., "Verifies: OU-XXX") +- Import `incidentsPage` from relative path +- Standard MCP/MP configuration blocks +- Use `cy.beforeBlockCOO(MCP, MP)` in `before()` hook +- Use `incidentsPage.goTo()` in `beforeEach()` +- Use `cy.mockIncidentFixture()` for test data + +## Test Assertions + +### Two-Phase Approach for cy.pause() +- **For new test files**: Include `cy.pause()` statements after key setup/verification steps for manual verification +- **For new test cases**: Include `cy.pause()` when adding to existing files +- **For existing test cases**: DO NOT reintroduce `cy.pause()` if the user has already removed them +- **Rule**: Check if test already has assertions without pauses - preserve that state +- **Purpose**: Initial pauses allow manual verification, then user deletes them once confident + +### Use Automated Assertions +- **NO** `VERIFY:` comments in place of assertions +- Convert all manual verification steps to automated assertions +- Include `cy.pause()` only in initial test generation for manual verification + +### Common Assertion Patterns + +#### Visibility and Existence +```typescript +incidentsPage.elements.incidentsChartContainer().should('be.visible'); +incidentsPage.elements.incidentsTable().should('not.exist'); +``` + +#### Counts and Length +```typescript +incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); +incidentsPage.elements.incidentsDetailsTableRows() + .should('have.length.greaterThan', 0); +``` + +#### Text Content +```typescript +incidentsPage.elements.daysSelectToggle().should('contain.text', '7 days'); +incidentsPage.elements.incidentsTableComponentCell(0) + .invoke('text') + .should('contain', 'monitoring'); +``` + +#### Conditional Waiting +```typescript +cy.waitUntil( + () => incidentsPage.elements.incidentsChartBarsGroups() + .then($groups => $groups.length === 12), + { + timeout: 10000, + interval: 500, + errorMsg: 'All 12 incidents should load within 10 seconds' + } +); +``` + +#### Position and Layout +```typescript +incidentsPage.elements.incidentsChartBarsVisiblePaths() + .first() + .then(($element) => { + const rect = $element[0].getBoundingClientRect(); + expect(rect.width).to.be.greaterThan(5); + }); +``` + +#### Tooltip Interactions +```typescript +incidentsPage.elements.incidentsChartBarsVisiblePaths() + .first() + .trigger('mouseover', { force: true }); + +cy.get('[role="tooltip"]').should('be.visible'); +cy.get('[role="tooltip"]').should('contain.text', 'Expected content'); +``` + +### Descriptive Logging +Use `cy.log()` for test flow clarity: +```typescript +cy.log('1.1 Verify all incidents loaded'); +incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); +cy.pause(); // Manual verification point (for new tests) +cy.log('Verified: 12 incidents shown'); +``` + +### cy.pause() Best Practices +```typescript +// NEW test - include pauses for manual verification +it('1. New test case', () => { + cy.log('1.1 Setup'); + incidentsPage.clearAllFilters(); + incidentsPage.elements.incidentsChartContainer().should('be.visible'); + cy.pause(); // Manual verification + + cy.log('1.2 Verify behavior'); + // more assertions + cy.pause(); // Manual verification +}); + +// EXISTING test without pauses - DO NOT reintroduce them +it('2. Existing test', () => { + cy.log('2.1 Setup'); + incidentsPage.clearAllFilters(); + incidentsPage.elements.incidentsChartContainer().should('be.visible'); + // No pause - user has already verified and removed it + + cy.log('2.2 Verify behavior'); + // assertions without pause +}); +``` + +## Fixture Management + +### Fixture Location and Naming +- Location: `web/cypress/fixtures/incident-scenarios/` +- Pattern: `XX-descriptive-name.yaml` +- Examples: `13-tooltip-positioning-scenarios.yaml` + +### Fixture Usage in Tests +```typescript +// Preferred: Single scenario per test file +cy.mockIncidentFixture('incident-scenarios/13-tooltip-positioning-scenarios.yaml'); + +// For empty state +cy.mockIncidents([]); +``` + +### Creating Fixtures +- Use `generate-incident-fixture` command for new fixtures +- Follow schema from `web/cypress/support/incidents_prometheus_query_mocks/schema/fixture-schema.json` +- Validate fixtures before committing +- Prefer single scenario per test file for focused regression testing + +### Fixture Schema Compliance +- Use relative durations (e.g., `"2h"`, `"30m"`) not absolute timestamps +- Use valid component names: `monitoring`, `storage`, `network`, `compute`, `api-server`, `etcd`, `version`, `Others` +- Use valid layers: `core`, `Others` +- Use valid severities: `critical`, `warning`, `info` +- Include descriptive scenario name and description + +## Code Quality Standards + +### Test Organization +- **Prefer comprehensive flows**: Each `it()` should test a complete user journey +- **Combine related steps**: Don't split filtering, verification, interaction into separate tests +- **Think e2e, not unit**: Test realistic multi-step workflows, not isolated actions +- **Limit `it()` blocks**: Prefer 1-3 comprehensive tests over 10+ tiny tests per `describe()` +- Tests can be longer (50-100+ lines) if they test a complete, realistic workflow + +### Test Clarity +- Use numbered test cases with descriptive names +- Use sub-step numbering in logs (e.g., "1.1", "1.2", "1.3", etc.) +- Group related assertions logically +- Each test should tell a complete story of user interaction + +### Refactoring for Readability +- **Extract helper functions for duplicated logic**: If you repeat the same action/assertion pattern, create a helper function within the test file +- **Keep `it()` blocks readable as a story**: The test body should read like steps a user takes, not implementation details +- **Move complex logic to helpers or page object**: Multi-step verifications, calculations, or repetitive patterns belong in functions +- **Helper function placement**: + - Test-specific helpers: Define within the test file (inside or outside `it()`) + - Reusable helpers: Add to `incidents-page.ts` page object + +### Naming Conventions +- Test descriptions: Clear, specific, behavior-focused +- Variables: Descriptive, follow TypeScript conventions +- Constants: UPPER_CASE for configuration values + +### Imports +```typescript +// Always import from relative path +import { incidentsPage } from '../../../views/incidents-page'; + +// Import types when needed +import { IncidentDefinition } from '../../support/incidents_prometheus_query_mocks'; +``` + +### Comments +- Follow sparse comments rule (explain "why", not "what") +- Add file header comments explaining test purpose +- Document workarounds or known issues +- Reference issue numbers (e.g., "Verifies: OU-XXX") + +### Logging +- Follow no-emojis rule (no emojis in cy.log() statements) +- Use clear, descriptive text +- Log test flow for debugging + +## Testing Best Practices + +### Test Independence +- Tests should be self-contained +- Should not depend on execution order +- Use `beforeEach()` to set up clean state + +### Performance +- Use `cy.waitUntil()` for dynamic loading instead of fixed `cy.wait()` +- Only wait when necessary +- Use appropriate timeouts + +### Maintainability +- Keep tests DRY (use page object methods) +- Follow established patterns +- Make changes to page object for reusable functionality +- Update page object when UI changes + +### Documentation References +- Reference test documentation in [`docs/incident_detection/tests/`](../../docs/incident_detection/tests/) +- Link to TESTING_CHECKLIST.md sections when applicable +- Reference bug tracking issues in test files + +## Validation Checklist + +Before submitting tests, verify: +- [ ] **Tests follow e2e philosophy**: Each `it()` covers a complete user flow, not isolated actions +- [ ] **Appropriate test count**: Prefer 1-3 comprehensive tests over many small tests +- [ ] **Tests are independent**: Each test is self-contained and doesn't depend on others +- [ ] **Test reads like a story**: Refactor duplications into helper functions, keep `it()` body readable +- [ ] Test file follows naming convention +- [ ] Uses existing page object elements/methods (or suggests additions) +- [ ] **For new tests**: Include `cy.pause()` after key verification points +- [ ] **For existing tests**: Preserve pause/no-pause state (don't reintroduce) +- [ ] NO `VERIFY:` comments in place of assertions +- [ ] Fixture validated against schema +- [ ] Standard imports and setup blocks +- [ ] Descriptive test names and log statements +- [ ] No emojis in logs +- [ ] Minimal, value-adding comments +- [ ] File header includes purpose and issue reference + +## Examples + +### Good Test Example (Comprehensive E2E Flow) +```typescript +it('1. Complete filtering workflow - severity, component, and time range', () => { + cy.log('1.1 Verify initial state with all incidents'); + incidentsPage.clearAllFilters(); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); + incidentsPage.elements.incidentsChartContainer().should('be.visible'); + cy.pause(); // Manual verification point + + cy.log('1.2 Apply Critical severity filter and verify results'); + incidentsPage.toggleFilter('Critical'); + incidentsPage.elements.severityFilterChip().should('be.visible'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 7); + cy.pause(); // Manual verification point + + cy.log('1.3 Add component filter and verify combined filtering'); + incidentsPage.selectComponent('monitoring'); + incidentsPage.elements.componentFilterChip().should('be.visible'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 4); + cy.pause(); // Manual verification point + + cy.log('1.4 Change time range and verify filtered data updates'); + incidentsPage.setDays('30 days'); + incidentsPage.elements.daysSelectToggle().should('contain.text', '30 days'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length.greaterThan', 4); + cy.pause(); // Manual verification point + + cy.log('1.5 Click incident bar and verify details panel'); + incidentsPage.elements.incidentsChartBarsVisiblePaths().first().click(); + incidentsPage.elements.incidentsDetailsPanel().should('be.visible'); + incidentsPage.elements.incidentsDetailsTableRows().should('have.length.greaterThan', 0); + cy.pause(); // Manual verification point + + cy.log('1.6 Clear all filters and verify return to initial state'); + incidentsPage.clearAllFilters(); + incidentsPage.elements.severityFilterChip().should('not.exist'); + incidentsPage.elements.componentFilterChip().should('not.exist'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); + cy.log('Verified: Complete filtering workflow'); +}); +``` + +### Good Test Example (After User Removed Pauses) +```typescript +it('1. Complete filtering workflow - severity, component, and time range', () => { + cy.log('1.1 Verify initial state with all incidents'); + incidentsPage.clearAllFilters(); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); + incidentsPage.elements.incidentsChartContainer().should('be.visible'); + + cy.log('1.2 Apply Critical severity filter and verify results'); + incidentsPage.toggleFilter('Critical'); + incidentsPage.elements.severityFilterChip().should('be.visible'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 7); + + cy.log('1.3 Add component filter and verify combined filtering'); + incidentsPage.selectComponent('monitoring'); + incidentsPage.elements.componentFilterChip().should('be.visible'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 4); + + cy.log('1.4 Change time range and verify filtered data updates'); + incidentsPage.setDays('30 days'); + incidentsPage.elements.daysSelectToggle().should('contain.text', '30 days'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length.greaterThan', 4); + + cy.log('1.5 Click incident bar and verify details panel'); + incidentsPage.elements.incidentsChartBarsVisiblePaths().first().click(); + incidentsPage.elements.incidentsDetailsPanel().should('be.visible'); + incidentsPage.elements.incidentsDetailsTableRows().should('have.length.greaterThan', 0); + + cy.log('1.6 Clear all filters and verify return to initial state'); + incidentsPage.clearAllFilters(); + incidentsPage.elements.severityFilterChip().should('not.exist'); + incidentsPage.elements.componentFilterChip().should('not.exist'); + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 12); + cy.log('Verified: Complete filtering workflow'); +}); +``` + +### Good Test Example (With Helper Functions) +```typescript +it('1. Silence matching verification flow - opacity and tooltip indicators', () => { + const verifyAlertOpacity = (alertIndex: number, expectedOpacity: number) => { + incidentsPage.elements.alertsChartBarsPaths() + .eq(alertIndex) + .then(($bar) => { + const opacity = parseFloat($bar.css('opacity') || '1'); + expect(opacity).to.equal(expectedOpacity); + }); + }; + + const verifyAlertTooltip = (alertIndex: number, expectedTexts: string[], shouldBeSilenced: boolean) => { + incidentsPage.hoverOverAlertBar(alertIndex); + const tooltip = incidentsPage.elements.alertsChartTooltip().should('be.visible'); + expectedTexts.forEach(text => tooltip.should('contain.text', text)); + tooltip.should(shouldBeSilenced ? 'contain.text' : 'not.contain.text', '(silenced)'); + }; + + cy.log('1.1 Select silenced alert incident'); + incidentsPage.elements.incidentsChartBar('PAIR-2-storage-SILENCED').click(); + incidentsPage.elements.alertsChartContainer().should('be.visible'); + + cy.log('1.2 Verify silenced alert has reduced opacity and tooltip indicator'); + verifyAlertOpacity(0, 0.3); + verifyAlertTooltip(0, ['SyntheticSharedFiring002'], true); + + cy.log('2.1 Select non-silenced alert with same name'); + incidentsPage.elements.incidentsChartBar('PAIR-2-network-UNSILENCED').click(); + + cy.log('2.2 Verify non-silenced alert has full opacity without indicator'); + verifyAlertOpacity(0, 1.0); + verifyAlertTooltip(0, ['SyntheticSharedFiring002'], false); + + cy.log('Verified: Silence matching uses alertname + namespace + severity'); +}); +``` + +**Why this is good**: +- Helper functions extract repeated verification patterns +- Test body reads like a user story (select → verify → select → verify) +- Complex opacity/tooltip logic hidden in helpers +- Easy to understand the test flow at a glance + +### Bad Test Examples + +#### Example 1: Too Many Small Tests (Unit Test Mindset) +```typescript +describe('Regression: Filtering', () => { + it('1. Should clear filters', () => { + incidentsPage.clearAllFilters(); + incidentsPage.elements.severityFilterChip().should('not.exist'); + }); + + it('2. Should apply Critical filter', () => { + incidentsPage.toggleFilter('Critical'); + incidentsPage.elements.severityFilterChip().should('be.visible'); + }); + + it('3. Should show correct count after filtering', () => { + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 7); + }); + + it('4. Should apply component filter', () => { + incidentsPage.selectComponent('monitoring'); + }); + + it('5. Should show combined filter results', () => { + incidentsPage.elements.incidentsChartBarsGroups().should('have.length', 4); + }); +}); +``` + +**Issues**: +- Splitting a single user flow into many tiny tests +- Tests depend on each other (not independent) +- Doesn't reflect how users actually interact with the app +- Unit test mindset applied to e2e testing +- More setup overhead, slower test execution + +#### Example 2: Poor Code Quality +```typescript +it('Test filtering', () => { + // Clear the filters + cy.get('[data-test="toolbar"]').find('button').contains('Clear').click(); + cy.pause(); // VERIFY: Filters are cleared + + // Select critical + cy.get('[data-test="filters-select-toggle"]').click(); + cy.pause(); // VERIFY: Menu opened +}); +``` + +**Issues**: +- Not using page object methods +- Using custom selectors instead of page object +- Using cy.pause() WITHOUT assertions (should have both) +- Obvious comments that don't add value +- No descriptive logging +- No verification of expected outcomes +- Too short, doesn't test a complete flow + +**Important**: +- `cy.pause()` should be used WITH assertions for manual verification, not INSTEAD of assertions +- Tests should cover complete user journeys, not isolated button clicks +- Use page object methods for maintainability diff --git a/.gitignore b/.gitignore index 5be636033..9a5274a93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .devcontainer/dev.env .DS_Store +.vscode web/cypress/screenshots/ web/cypress/export-env.sh web/screenshots/ @@ -13,3 +14,4 @@ dist/ .devspace web/po-files/ .claude/commands/configs +_output/ diff --git a/.golangci-lint.yaml b/.golangci-lint.yaml new file mode 100644 index 000000000..a1c495f45 --- /dev/null +++ b/.golangci-lint.yaml @@ -0,0 +1,26 @@ +# List of all configuration options at https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml +version: "2" + +linters: + settings: + errcheck: + exclude-functions: + - (net/http.ResponseWriter).Write + exclusions: + generated: strict + rules: + # Disable errcheck linter for test files. + - linters: + - errcheck + path: _test.go + +formatters: + enable: + - gci + - gofmt + settings: + gci: + sections: + - standard + - default + - prefix(github.com/openshift/monitoring-plugin) diff --git a/AGENTS.md b/AGENTS.md index 067f81333..1d09a7e48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,14 +1,16 @@ # OpenShift Monitoring Plugin - AI Agent Guide ## Quick Start (30-second overview) + - **What**: Dual frontend plugins for OpenShift observability (monitoring-plugin + monitoring-console-plugin) - **Purpose**: Alerts, Metrics, Targets, Dashboards + Perses, Incidents, ACM integration -- **Tech Stack**: React + TypeScript + Webpack + i18next + Go +- **Tech Stack**: React + TypeScript + Webpack + i18next + Go - **Key Files**: `web/console-extensions.json`, `web/src/components/` ## Common Tasks & Workflows ### Adding a New Feature + 1. Check if it belongs in `monitoring-plugin` (core) or `monitoring-console-plugin` (extended) 2. Update console extensions in `web/console-extensions.json` 3. Add React components in `web/src/components/` @@ -16,28 +18,33 @@ 5. Test with `make lint-frontend && make test-backend` ### Debugging Issues + - **Build failures**: Check `Makefile` targets - **Console integration**: Verify `console-extensions.json` - **Plugin loading**: Check OpenShift Console logs - **Perses dashboards**: Debug at `web/src/components/dashboards/perses/` ### Development Setup + - See README.md for full setup - Deployment: https://github.com/observability-ui/development-tools/ ## Development Context ### When working on Alerts: + - Files: `web/src/components/alerts/` - Integration: Alertmanager API - Testing: Cypress tests in `web/cypress/` ### When working on Dashboards: + - **Legacy**: Standard OpenShift dashboards - **Perses**: `web/src/components/dashboards/perses/` (uses ECharts wrapper) - **Upstream**: https://github.com/perses/perses ### When working on ACM: + - Multi-cluster observability - Hub cluster aggregation - Thanos/Alertmanager integration @@ -45,34 +52,39 @@ ## Important Decision Points ### Choosing Between Plugins: + - **monitoring-plugin**: Core observability (always available) - **monitoring-console-plugin**: Optional features (COO required) ### Adding Dependencies: + - Check compatibility with OpenShift Console versions - Verify i18next translation support - Consider CMO vs COO deployment differences ## External Dependencies & Operators -| System | Repository | Purpose | -|--------|------------|---------| -| CMO | https://github.com/openshift/cluster-monitoring-operator | Manages monitoring-plugin | -| COO | https://github.com/rhobs/observability-operator | Manages monitoring-console-plugin | -| Perses | https://github.com/perses/perses | Dashboard engine | -| Console SDK | https://github.com/openshift/console | Plugin framework | +| System | Repository | Purpose | +| ----------- | -------------------------------------------------------- | --------------------------------- | +| CMO | https://github.com/openshift/cluster-monitoring-operator | Manages monitoring-plugin | +| COO | https://github.com/rhobs/observability-operator | Manages monitoring-console-plugin | +| Perses | https://github.com/perses/perses | Dashboard engine | +| Console SDK | https://github.com/openshift/console | Plugin framework | ## Technical Documentation ### Console Plugin Framework + - Plugin SDK: https://github.com/openshift/console/tree/main/frontend/packages/console-dynamic-plugin-sdk - Extensions docs: https://github.com/openshift/console/blob/main/frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md - Example plugin: https://github.com/openshift/console/tree/main/dynamic-demo-plugin ### Operator Integration + - **CMO (monitoring-plugin)**: Integrated with cluster monitoring stack - **COO (monitoring-console-plugin)**: Optional operator for extended features - **UIPlugin CR example**: + ```yaml apiVersion: observability.openshift.io/v1alpha1 kind: UIPlugin @@ -84,9 +96,9 @@ spec: acm: enabled: true alertmanager: - url: 'https://alertmanager.open-cluster-management-observability.svc:9095' + url: "https://alertmanager.open-cluster-management-observability.svc:9095" thanosQuerier: - url: 'https://rbac-query-proxy.open-cluster-management-observability.svc:8443' + url: "https://rbac-query-proxy.open-cluster-management-observability.svc:8443" perses: enabled: true incidents: @@ -94,12 +106,14 @@ spec: ``` ### Perses Integration Details + - **Core**: https://github.com/perses/perses - **Plugins**: https://github.com/perses/plugins (chart specifications and datasources) - **Operator**: https://github.com/perses/perses-operator (Red Hat fork: https://github.com/rhobs/perses) - **Chart Engine**: ECharts (https://echarts.apache.org/) ### ACM Observability + - **Multi-cluster monitoring**: Centralized observability across managed clusters - **Components**: Hub cluster Thanos, Grafana, Alertmanager + endpoint operators - **Integration**: COO provides unified alerting UI for ACM environments @@ -108,57 +122,232 @@ spec: ## Release & Testing ### Before submitting a PR run the following and address any errors: + ```bash make lint-frontend make lint-backend make test-translations make test-backend -# Run cypress tests (see web/cypress/README.md) +make test-frontend +# future slash command for test execution ``` ### PR Requirements: + - **Title format**: `[JIRA_ISSUE]: Description` - **Testing**: All linting and tests must pass - **Translations**: Ensure i18next keys are properly added +### Unit Testing + +#### Overview + +The Monitoring Plugin uses a dual testing approach for unit tests: + +- **Frontend Unit Tests**: Jest + TypeScript for React components and utilities +- **Backend Unit Tests**: Go's built-in testing framework for server functionality + +Unit tests focus on isolated function testing and run quickly in CI/CD pipelines, while E2E tests (Cypress) validate full user workflows. + +#### Test File Structure + +**Frontend Tests:** + +- **Location**: Co-located with source files in `web/src/` +- **Naming**: `*.spec.ts` (e.g., `format.spec.ts`, `utils.spec.ts`) +- **Framework**: Jest 30.2.0 with ts-jest +- **Configuration**: `web/jest.config.js` + +**Backend Tests:** + +- **Location**: Co-located with source files in `pkg/` +- **Naming**: `*_test.go` (e.g., `server_test.go`) +- **Framework**: Go testing package + testify/require +- **Configuration**: Standard Go test conventions + +#### Running Unit Tests + +```bash +# Run all tests (backend + frontend) +make test-backend +make test-frontend + +# Run individually from web directory +cd web && npm run test:unit + +# Run Go tests directly +go test ./pkg/... -v +``` + +#### When to Create Unit Tests + +Create unit tests when: + +1. **Adding utility functions**: Pure functions, formatters, data transformations +2. **Adding business logic**: Data processing, calculations, validations +3. **Fixing bugs**: Regression tests to prevent bug recurrence +4. **Adding API handlers**: Backend endpoint logic (Go tests) + +#### Key Testing Libraries + +**Frontend:** + +- `jest` (v30.2.0) - Test runner and assertions +- `ts-jest` (v29.4.4) - TypeScript support +- `@types/jest` - TypeScript definitions + +**Backend:** + +- `testing` (stdlib) - Go testing framework +- `github.com/stretchr/testify` (v1.9.0) - Assertions and test utilities + +#### Frontend Unit Testing Structure + +**Testing Framework & Configuration** +Test Framework: Jest + ts-jest +Configuration File: web/jest.config.js + +**Test File Location & Naming Convention** +Pattern: \*.spec.ts files co-located with source code + +**Test Coverage Areas** + +- Edge cases (null, undefined, empty values) +- Normal behavior and expected outputs +- Boundary conditions +- Complex scenarios and integration +- Data transformations and formatting + +#### Backend Unit Testing Structure + +**Testing Framework & Configuration** +Test Framework: Go's built-in testing package +Assertion Library: github.com/stretchr/testify v1.9.0 + +**Test File Location & Naming Convention** +Pattern: \*\_test.go files in the same directory as source code + +**Test Helper Functions** + +- `startTestServer()` - Starts server for testing +- `prepareServerAssets()` - Sets up test environment +- `generateCertificate()` - Creates TLS certificates for tests +- `checkHTTPReady()` - Waits for server to be ready +- `getRequestResults()` - Makes HTTP requests + +**Test Coverage Areas** + +- HTTP server functionality +- HTTPS/TLS configuration +- Certificate handling +- Security settings (TLS versions, cipher suites) +- Endpoint availability + +### Cypress E2E Testing + +#### Overview + +The Monitoring Plugin uses Cypress for comprehensive End-to-End (E2E) testing to ensure functionality across both the core **monitoring-plugin** (managed by CMO) and the **monitoring-console-plugin** (managed by COO). Our test suite covers test scenarios including alerts, metrics, dashboards, and integration with Virtualization and Fleet Management (ACM). + +**Key Testing Documentation:** + +- **Setup & Configuration**: `web/cypress/README.md` - Environment variables, installation, troubleshooting +- **Testing Guide**: `web/cypress/CYPRESS_TESTING_GUIDE.md` - Test architecture, creating tests, workflows +- **Test Catalog**: `web/cypress/E2E_TEST_SCENARIOS.md` - Complete list of all test scenarios + +#### When to Create New Cypress Tests + +You should create new Cypress tests when: + +1. **Adding New Features**: Any new UI feature requires corresponding E2E tests +2. **Fixing Bugs**: Bug fixes should include tests to prevent regression +3. **Modifying Existing Features**: Changes to existing functionality require test updates + +#### Quick Test Commands + +```bash +cd web/cypress + +# Run all regression tests +npm run cypress:run --spec "cypress/e2e/**/regression/**" + +# Run BVT (Build Verification Tests) +npm run cypress:run --spec "cypress/e2e/monitoring/00.bvt_admin.cy.ts" + +# Run COO tests +npm run cypress:run --spec "cypress/e2e/coo/*.cy.ts" + +# Interactive mode +npm run cypress:open +``` + +For detailed testing instructions, see `web/cypress/CYPRESS_TESTING_GUIDE.md` + ### Release Pipeline: + - **Konflux**: Handles CI/CD and release automation - **CMO releases**: Follow OpenShift release cycles - **COO releases**: Independent release schedule +## Skills + +### Feature Backporting + +For backporting features from `main` to release branches (e.g., `release-4.x`, `release-coo-x.y`), use the `/backport` slash command: + +```bash +/backport [commit-hash] +# Examples: +/backport release-4.18 +/backport release-coo-0.4 abc123 +``` + +The command is located at `.claude/commands/backport.md` and handles: + +- PatternFly v6 → v5 component transformations +- React Router v6 → v5 hook adaptations +- Console SDK API compatibility +- Dependency version differences between branches +- Project structure differences (web/ vs root for older releases) + ## Security & RBAC ### Plugin Security Model: + - Inherits OpenShift Console RBAC - Respects cluster monitoring permissions - ACM integration requires appropriate hub cluster access ### Development Security: + - No credentials in code - Use cluster service accounts - Follow OpenShift security guidelines ## Getting Help -| Topic | Channel/Resource | -|-------|-----------------| -| Console Plugins | OpenShift Console SDK documentation | -| Perses | Slack: Cloud Native Computing Foundation >> #perses-dev | -| COO | Slack: Internal Red Hat >> #forum-cluster-observability-operator | +| Topic | Channel/Resource | +| --------------- | ---------------------------------------------------------------- | +| Console Plugins | OpenShift Console SDK documentation | +| Perses | Slack: Cloud Native Computing Foundation >> #perses-dev | +| COO | Slack: Internal Red Hat >> #forum-cluster-observability-operator | ## Additional Resources ### Development Tools & Scripts: + - **Monitoring Plugin**: https://github.com/observability-ui/development-tools/tree/main/monitoring-plugin - **Perses**: https://github.com/observability-ui/development-tools/tree/main/perses - **Wiki**: https://github.com/observability-ui/development-tools/tree/main/wiki ### Code Style & Standards: + - **TypeScript**: https://www.typescriptlang.org/ - **React**: https://react.dev/ - **Webpack**: https://webpack.js.org/ -- **Go**: https://go.dev/ +- **Go**: https://go.dev/ - **i18next**: https://www.i18next.com/ --- -*This guide is optimized for AI agents and developers. For detailed setup instructions, also refer to README.md and Makefile.* \ No newline at end of file + +_This guide is optimized for AI agents and developers. For detailed setup instructions, also refer to README.md and Makefile._ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..2e9d26d38 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,555 @@ +# Contributing to OpenShift Monitoring Plugin + +Thank you for your interest in contributing to the OpenShift Monitoring Plugin! This document provides guidelines for contributing code, submitting pull requests, and maintaining code quality. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Pull Request Process](#pull-request-process) + - [PR Requirements](#pr-requirements) + - [Labels and Review Process](#labels-and-review-process) + - [Branch and Commit Guidelines](#branch-and-commit-guidelines) +- [Code Conventions](#code-conventions) + - [Naming Conventions](#naming-conventions) + - [React Component Patterns](#react-component-patterns) + - [TypeScript Best Practices](#typescript-best-practices) + - [Go Backend Guidelines](#go-backend-guidelines) +- [Testing Requirements](#testing-requirements) +- [Internationalization (i18n)](#internationalization-i18n) +- [Troubleshooting](#troubleshooting) +- [Getting Help](#getting-help) + +--- + +## Getting Started + +### Development Environment + +Before you start contributing, ensure you have the following tools installed: + +- [Node.js 22+](https://nodejs.org/en/) and [npm](https://www.npmjs.com/) +- [Go 1.24+](https://go.dev/dl/) +- [oc CLI](https://mirror.openshift.com/pub/openshift-v4/clients/oc/) +- [podman 3.2.0+](https://podman.io) or [Docker](https://www.docker.com/) +- An OpenShift cluster (for testing) + +### Local Setup + +1. **Clone the repository**: + + ```bash + git clone https://github.com/openshift/monitoring-plugin.git + cd monitoring-plugin + ``` + +2. **Install dependencies**: + + ```bash + make install + ``` + +3. **Verify setup**: + ```bash + make lint-frontend + make lint-backend + ``` + +For detailed setup instructions, see [README.md](./README.md#local-development). + +--- + +## Pull Request Process + +This project uses [Prow](https://docs.prow.k8s.io/) for CI/CD automation. Pull requests require specific labels to be merged, which are applied based on reviews from team members. + +### PR Requirements + +1. **Title format**: `JIRA_ISSUE: [release-x.y] Description` + + - Example: `OU-1234: [release-4.19] Add support for custom dashboards` + - Example: `COO-456: Add support for custom datasources` + +2. **Before submitting**, run the following checks locally and address any errors: + + ```bash + make lint-frontend # ESLint and Prettier checks + make lint-backend # Go fmt and mod tidy + make test-translations # Verify i18n keys + make test-backend # Go unit tests + make test-frontend # Jest unit tests + ``` + +3. **Required checks must pass** in CI before merging. + +### Labels and Review Process + +The Prow bot manages labels based on reviews. The following labels are required for a PR to be merged: + +| Label | Description | How to Obtain | +| -------------- | ---------------------- | ---------------------------------------------------------------------------- | +| `/lgtm` | Code review approval | Wait for a reviewer to comment `/lgtm`, reviewers are automatically assigned | +| `/qe-approved` | QE verification passed | Applied when QE team reviews (if applicable) | + +### Example PR Review Flow + +1. Contributor opens PR with proper title format +2. CI runs automatically (lint, tests, build) +3. Reviewer reviews code and comments `/lgtm` +4. If significant feature: QE team tests and applies `/qe-approved` +5. Prow bot merges the PR when all required labels are present + +### Branch and Commit Guidelines + +#### Branch Naming + +Use descriptive branch names that reference the JIRA issue: + +``` +ou-1234-feature-description +coo-1234-fix-bug-description +ou-1234-refactor-component +``` + +#### Commit Messages + +- Use the format from https://www.conventionalcommits.org/en/v1.0.0/ +- Reference the JIRA issue if applicable +- Keep commits focused and atomic +- Prefer multiple focused commits over one large commit + +**Good commit message**: + +``` +feat(alerts): add filter by severity + +Add dropdown to filter alerts by severity level on the alerts page. +Users can now select critical, warning, or info severity filters. + +Fixes OU-1234 +``` + +**Avoid**: + +``` +fixed stuff +update +changes +``` + +--- + +## Code Conventions + +### Naming Conventions + +#### Components + +| Type | Convention | Example | +| ----------------- | ----------------------------- | ----------------------------------------------------------------------------------------- | +| React Components | PascalCase | `AlertsDetailsPage`, `SilenceForm`, `QueryBrowser`, `LoadingBox`, `EmptyBox`, `StatusBox` | +| Page Components | PascalCase with `Page` suffix | `MetricsPage`, `TargetsPage` | +| HOCs | camelCase with `with` prefix | `withFallback` | +| Regular functions | camelCase | `formatAlertLabel`, `buildPromQuery`, `handleNamespaceChange` | + +#### Files + +| Type | Convention | Example | +| ---------------- | --------------------------------- | ------------------------------------------------------------------------------------ | +| React Components | PascalCase | `MetricsPage.tsx`, `QueryBrowser.tsx` | +| Utilities | kebab-case | `safe-fetch-hook.ts`, `poll-hook.ts` | +| Types | kebab-case or PascalCase | `types.ts`, `AlertUtils.tsx` | +| Tests | `.spec.ts` suffix | `MetricsPage.spec.tsx`, `safe-fetch-hook.spec.ts`, `format.spec.ts`, `utils.spec.ts` | +| Styles | `.scss` suffix matching component | `query-browser.scss` | + +#### Types and Interfaces + +| Type | Convention | Example | +| --------------- | ------------------------------------------------ | ------------------------------------------------------- | +| Type aliases | PascalCase | `MonitoringResource`, `TimeRange`, `AlertSource` | +| Interface names | PascalCase with Props suffix for component props | `SilenceFormProps`, `TypeaheadSelectProps` | +| Enum values | PascalCase or SCREAMING_SNAKE_CASE | `AlertSource.Platform`, `ActionType.AlertingSetLoading` | + +```typescript +// ✅ Good: Type definitions +export type MonitoringResource = { + group: string; + resource: string; + abbr: string; + kind: string; + label: string; + url: string; +}; + +type SilenceFormProps = { + defaults: any; + Info?: ComponentType; + title: string; + isNamespaced: boolean; +}; + +export const enum AlertSource { + Platform = "platform", + User = "user", +} +``` + +#### Hooks + +| Type | Convention | Example | +| ------------ | --------------------------- | ----------------------------------------------- | +| Custom hooks | camelCase with `use` prefix | `useBoolean`, `usePerspective`, `useMonitoring` | +| Hook files | camelCase with `use` prefix | `useBoolean.ts`, `usePerspective.tsx` | + +```typescript +// ✅ Good: Hook definition +export const useBoolean = ( + initialValue: boolean +): [boolean, () => void, () => void, () => void] => { + const [value, setValue] = useState(initialValue); + const toggle = useCallback(() => setValue((v) => !v), []); + const setTrue = useCallback(() => setValue(true), []); + const setFalse = useCallback(() => setValue(false), []); + return [value, toggle, setTrue, setFalse]; +}; +``` + +#### Constants + +| Type | Convention | Example | +| -------------------- | -------------------- | -------------------------------------------------- | +| Constants | SCREAMING_SNAKE_CASE | `PROMETHEUS_BASE_PATH`, `QUERY_CHUNK_SIZE` | +| Resource definitions | PascalCase | `AlertResource`, `RuleResource`, `SilenceResource` | + +```typescript +// ✅ Good: Constants +export const QUERY_CHUNK_SIZE = 24 * 60 * 60 * 1000; +export const PROMETHEUS_BASE_PATH = window.SERVER_FLAGS.prometheusBaseURL; + +export const AlertResource: MonitoringResource = { + group: "monitoring.coreos.com", + resource: "alertingrules", + kind: "Alert", + label: "Alert", + url: "/monitoring/alerts", + abbr: "AL", +}; +``` + +#### Test IDs + +Use the centralized `DataTestIDs` object in `web/src/components/data-test.ts`: + +```typescript +// ✅ Good: Test ID definitions +export const DataTestIDs = { + AlertCluster: "alert-cluster", + AlertResourceIcon: "alert-resource-icon", + CancelButton: "cancel-button", + // Group related IDs using nested objects + SilencesPageFormTestIDs: { + AddLabel: "add-label", + Comment: "comment", + Creator: "creator", + }, +}; +``` + +### React Component Patterns + +#### Component Definition + +Use functional components with explicit type annotations: + +```typescript +// ✅ Good: Functional component with FC type and named export +import type { FC } from "react"; + +type ErrorAlertProps = { + error: Error; +}; + +export const ErrorAlert: FC = ({ error }) => { + return ( + + {error.message} + + ); +}; +``` + +#### Translations (i18n) + +Always use the `useTranslation` hook for user-facing strings, this allows the translation +strings to be extracted and localized: + +```typescript +// ✅ Good: Using translations with named export +import { useTranslation } from "react-i18next"; + +export const MyComponent: FC = () => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + return ( + + {t("No data available")} + {t("Please try again later")} + + ); +}; +``` + +### TypeScript Best Practices + +#### Type Imports + +Use `type` imports for types that are only used for type checking: + +```typescript +// ✅ Good: Type-only imports +import type { FC, ReactNode, ComponentType } from "react"; +import { useState, useCallback, useEffect } from "react"; +``` + +#### Avoid `any` + +Use proper types instead of `any` when possible: + +```typescript +// ❌ Bad +const handleData = (data: any) => { ... }; + +// ✅ Good +type DataResponse = { + results: PrometheusResult[]; + status: string; +}; +const handleData = (data: DataResponse) => { ... }; +``` + +#### Utility Types + +Leverage TypeScript utility types: + +```typescript +// ✅ Good: Using utility types +type AugmentedColumnStyle = ColumnStyle & { + className?: string; +}; + +type PartialMetric = Partial; +type RequiredFields = Required>; +``` + +### Go Backend Guidelines + +The backend that serves plugin assets and proxies APIs is written in Go (see `/cmd` and `/pkg`). +Follow these Go-specific conventions in addition to the general naming rules: + +#### Files and Packages + +- **File names**: lowercase with underscores only when required (e.g., `plugin_handler.go`, `server_test.go`). +- **Packages**: short, all lowercase, no underscores or mixedCaps (e.g., `proxy`, `handlers`). +- **Tests**: co-locate `_test.go` files next to the implementation and keep table-driven tests when feasible. + +#### Variables and Constants + +- **Exported identifiers** use `MixedCaps` starting with an uppercase letter (`PluginServer`, `DefaultTimeout`). +- **Unexported identifiers** use `mixedCaps` starting lowercase (`proxyClient`, `requestCtx`). +- Favor descriptive names over abbreviations; keep short-lived loop variables concise (`i`, `tc`). +- Group related constants using `const (...)` blocks. + +#### Functions and Methods + +- Exported functions and methods must have doc comments beginning with the function name. +- Function names follow `MixedCaps` (`StartServer`, `newProxyHandler`). +- Keep parameter order consistent (ctx first, dependencies before data structs) and return `(value, error)` pairs. +- Ensure all files pass `go fmt ./cmd ./pkg` and `go vet ./...` before submitting. + +#### Structs and Interfaces + +- Struct names are `MixedCaps` (e.g., `ProxyConfig`, `TLSBundle`). +- Exported struct fields must be capitalized if used by other packages or JSON marshaling. Always add JSON tags for serialized types: + + ```go + type ProxyConfig struct { + TargetURL string `json:"targetURL"` + Timeout time.Duration `json:"timeout"` + } + ``` + +- Interfaces describe behavior (`MetricsFetcher`, `ClientProvider`) and live near their consumers. +- Keep files focused: one primary struct or related set of functions per file to ease reviews. + +--- + +## Testing Requirements + +### Unit Tests + +- **Frontend**: Co-locate test files with source files using `.spec.ts` suffix +- **Backend**: Use Go's testing package with `_test.go` suffix + +```bash +# Run all tests +make test-backend +make test-frontend + +# Run frontend tests with watch mode +cd web && npm run test:unit -- --watch +``` + +### E2E Tests (Cypress) + +For significant UI changes, add or update Cypress tests: + +- Test files: `web/cypress/e2e/` +- Documentation: `web/cypress/CYPRESS_TESTING_GUIDE.md` + +```bash +# Run Cypress tests +cd web/cypress +npm run cypress:run --spec "cypress/e2e/**/regression/**" +``` + +--- + +## Internationalization (i18n) + +All user-facing strings must be translatable: + +1. Use the `t()` function from `useTranslation` +2. Add new keys to `web/locales/en/plugin__monitoring-plugin.json` +3. Run `make test-translations` to verify + +```typescript +// ✅ Good +const { t } = useTranslation(process.env.I18N_NAMESPACE); +return {t("Alerting rules")}; + +// ❌ Bad - hardcoded string +return Alerting rules; +``` + +--- + +## Troubleshooting + +### Common Issues + +#### Lint failures after commit + +Run the linter and formatter before committing: + +```bash +make lint-frontend +cd web && npm run prettier -- --write . +``` + +#### Tests failing locally but passing in CI + +Make sure you're running the correct version of Node.js: + +```bash +node --version # Should be 22+ +``` + +Clear npm cache and reinstall: + +```bash +cd web +npm cache clean --force +rm -rf node_modules package-lock.json +npm install +``` + +#### Reinstalling frontend dependencies + +If you encounter issues after switching branches or pulling main: + +```bash +cd web +npm cache clean --force +rm -rf node_modules package-lock.json dist +npm install +npm run lint +npm run build +``` + +Confirm Node and npm versions are correct: + +```bash +node --version # Should be 22+ +npm --version # Matches Node release +which npm # Ensure expected binary is used +``` + +#### Clearing cache when testing locally + +When running local unit tests, clear caches first: + +```bash +cd web +npm cache clean --force +rm -rf node_modules/.cache +npm run test:unit -- --clearCache +``` + +For Cypress E2E runs: + +```bash +cd web/cypress +npm cache clean --force +rm -rf node_modules package-lock.json +npm install +npm run cypress:run -- --config cacheAcrossSpecs=false +``` + +#### Translation key errors + +Always use the `useTranslation` hook for user-facing strings: + +```bash +make test-translations +``` + +Fix any errors by adding missing keys to `web/locales/en/plugin__monitoring-plugin.json`. + +#### Build failures + +Ensure all dependencies are installed: + +```bash +make install +``` + +Check the Makefile targets available: + +```bash +grep "^\.PHONY" Makefile | sed 's/.PHONY: //' +``` + +#### Port conflicts + +If ports 9001 or 3000 are in use: + +```bash +# Find and kill processes +lsof -ti:9001 | xargs kill -9 # Backend +lsof -ti:3000 | xargs kill -9 # Frontend +``` + +--- + +## Getting Help + +| Topic | Resource | +| ------------------- | ------------------------------------------------------------------------------ | +| Plugin Architecture | [AGENTS.md](./AGENTS.md) | +| Development Setup | [README.md](./README.md) | +| Cypress Testing | [web/cypress/CYPRESS_TESTING_GUIDE.md](./web/cypress/CYPRESS_TESTING_GUIDE.md) | +| Console Plugin SDK | [OpenShift Console SDK](https://github.com/openshift/console) | + +For questions, reach out via: + +- Slack: `#forum-cluster-observability-operator` (Red Hat internal) +- GitHub Issues: [openshift/monitoring-plugin](https://github.com/openshift/monitoring-plugin/issues) diff --git a/Dockerfile.art b/Dockerfile.art index c399a90c4..b295e3f22 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 @@ -9,15 +9,10 @@ USER 0 # use dependencies provided by Cachito ENV HUSKY=0 -RUN test -d ${REMOTE_SOURCES_DIR}/cachito-gomod-with-deps || exit 1; \ - cp -f $REMOTE_SOURCES_DIR/cachito-gomod-with-deps/app/registry-ca.pem . \ - && cp -f $REMOTE_SOURCES_DIR/cachito-gomod-with-deps/app/web/{.npmrc,package-lock.json} web/ \ - && source ${REMOTE_SOURCES_DIR}/cachito-gomod-with-deps/cachito.env \ - && make install-frontend-ci \ - && make build-frontend +ENV CYPRESS_INSTALL_BINARY=0 +RUN make install-frontend-ci && 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.25-openshift-4.22 AS go-builder COPY $REMOTE_SOURCES $REMOTE_SOURCES_DIR WORKDIR $REMOTE_SOURCES_DIR/cachito-gomod-with-deps/app @@ -28,7 +23,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 diff --git a/Dockerfile.mcp b/Dockerfile.mcp index 8484a73d9..33960459e 100644 --- a/Dockerfile.mcp +++ b/Dockerfile.mcp @@ -7,7 +7,7 @@ USER 0 ENV HUSKY=0 COPY web/package*.json web/ COPY Makefile Makefile -RUN cd web && npm ci --legacy-peer-deps --omit=optional --ignore-scripts +RUN cd web && npm ci --legacy-peer-deps --ignore-scripts COPY web/ web/ COPY config/ config/ diff --git a/Makefile b/Makefile index ce54b2060..7c8d38cdc 100644 --- a/Makefile +++ b/Makefile @@ -5,13 +5,18 @@ PLUGIN_NAME ?=monitoring-plugin IMAGE ?= quay.io/${ORG}/${PLUGIN_NAME}:${VERSION} FEATURES ?=incidents,perses-dashboards,dev-config +GOLANGCI_LINT = $(shell pwd)/_output/tools/bin/golangci-lint +GOLANGCI_LINT_VERSION ?= v2.11.3 + +export NODE_OPTIONS?=--max_old_space_size=4096 + .PHONY: install-frontend install-frontend: cd web && npm install .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 @@ -37,12 +42,6 @@ i18n-frontend: lint-frontend: cd web && npm run lint -.PHONY: lint-backend -lint-backend: - go mod tidy - go fmt ./cmd/ - go fmt ./pkg/ - .PHONY: install-backend install-backend: go mod download @@ -59,11 +58,14 @@ start-backend: test-backend: go test ./pkg/... -v +.PHONY: test-frontend +test-frontend: + cd web && npm run test:unit + .PHONY: build-image build-image: ./scripts/build-image.sh - .PHONY: install install: make install-frontend && make install-backend @@ -73,8 +75,7 @@ update-plugin-name: ./scripts/update-plugin-name.sh .PHONY: deploy -deploy: - make lint-backend +deploy: lint-backend PUSH=1 scripts/build-image.sh helm uninstall $(PLUGIN_NAME) -n $(PLUGIN_NAME)-ns || true helm install $(PLUGIN_NAME) charts/openshift-console-plugin -n monitoring-plugin-ns --create-namespace --set plugin.image=$(IMAGE) @@ -83,6 +84,19 @@ deploy: deploy-acm: ./scripts/deploy-acm.sh +# Download and install golangci-lint if not already installed +.PHONY: golangci-lint +golangci-lint: + @[ -f $(GOLANGCI_LINT) ] || { \ + set -e ;\ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) $(GOLANGCI_LINT_VERSION) ;\ + } + +.PHONY: lint-backend +lint-backend: golangci-lint + go mod tidy + $(GOLANGCI_LINT) -c $(shell pwd)/.golangci-lint.yaml run --verbose + .PHONY: build-mcp-image build-mcp-image: DOCKER_FILE_NAME="Dockerfile.mcp" REPO="monitoring-console-plugin" scripts/build-image.sh @@ -105,8 +119,11 @@ start-devspace-backend: .PHONY: podman-cross-build podman-cross-build: - podman manifest create ${IMAGE} + podman manifest create -a ${IMAGE} podman build --platform ${PLATFORMS} --manifest ${IMAGE} -f Dockerfile.mcp + +.PHONY: podman-cross-build-push +podman-cross-build-push: podman-cross-build podman manifest push ${IMAGE} .PHONY: test-translations diff --git a/OWNERS b/OWNERS index 2ef3dce5b..67894788f 100644 --- a/OWNERS +++ b/OWNERS @@ -1,6 +1,5 @@ reviewers: - jgbernalp - - kyoto - zhuje - peteryurkovich - etmurasaki diff --git a/cmd/plugin-backend.go b/cmd/plugin-backend.go index c7b79d6da..e561aa5a3 100644 --- a/cmd/plugin-backend.go +++ b/cmd/plugin-backend.go @@ -14,17 +14,17 @@ import ( ) var ( - portArg = flag.Int("port", 0, "server port to listen on (default: 9443)\nports 9444 and 9445 reserved for other use") + portArg = flag.Int("port", 9443, "server port to listen on\nports 9444 and 9445 reserved for other use") certArg = flag.String("cert", "", "cert file path to enable TLS (disabled by default)") keyArg = flag.String("key", "", "private key file path to enable TLS (disabled by default)") featuresArg = flag.String("features", "", "enabled features, comma separated.\noptions: ['acm-alerting', 'incidents', 'dev-config', 'perses-dashboards', 'alert-management-api']") - staticPathArg = flag.String("static-path", "", "static files path to serve frontend (default: './web/dist')") - configPathArg = flag.String("config-path", "", "config files path (default: './config')") - pluginConfigArg = flag.String("plugin-config-path", "", "plugin yaml configuration") - logLevelArg = flag.String("log-level", logrus.InfoLevel.String(), "verbosity of logs\noptions: ['panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace']\n'trace' level will log all incoming requests\n(default 'error')") - alertmanagerUrlArg = flag.String("alertmanager", "", "alertmanager url to proxy to for acm mode") - thanosQuerierUrlArg = flag.String("thanos-querier", "", "thanos querier url to proxy to for acm mode") - tlsMinVersionArg = flag.String("tls-min-version", "", "minimum TLS version\noptions: ['VersionTLS10', 'VersionTLS11', 'VersionTLS12', 'VersionTLS13']\n(default 'VersionTLS12')") + staticPathArg = flag.String("static-path", "/opt/app-root/web/dist", "static files path to serve frontend") + configPathArg = flag.String("config-path", "/opt/app-root/config", "config files path") + pluginConfigArg = flag.String("plugin-config-path", "/etc/plugin/config.yaml", "plugin yaml configuration") + logLevelArg = flag.String("log-level", logrus.InfoLevel.String(), "verbosity of logs\noptions: ['panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace']\n'trace' level will log all incoming requests") + alertmanagerUrlArg = flag.String("alertmanager", "", "Alertmanager URL to proxy to for ACM mode") + thanosQuerierUrlArg = flag.String("thanos-querier", "", "Thanos Querier URL to proxy to for ACM mode") + tlsMinVersionArg = flag.String("tls-min-version", "VersionTLS12", "minimum TLS version\noptions: ['VersionTLS10', 'VersionTLS11', 'VersionTLS12', 'VersionTLS13']") tlsMaxVersionArg = flag.String("tls-max-version", "", "maximum TLS version\noptions: ['VersionTLS10', 'VersionTLS11', 'VersionTLS12', 'VersionTLS13']\n(default is the highest supported by Go)") tlsCipherSuitesArg = flag.String("tls-cipher-suites", "", "comma-separated list of cipher suites for the server\nvalues are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants)") log = logrus.WithField("module", "main") @@ -33,19 +33,19 @@ var ( func main() { flag.Parse() - port := mergeEnvValueInt("PORT", *portArg, 9443) - cert := mergeEnvValue("CERT_FILE_PATH", *certArg, "") - key := mergeEnvValue("PRIVATE_KEY_FILE_PATH", *keyArg, "") - features := mergeEnvValue("MONITORING_PLUGIN_FEATURES", *featuresArg, "") - staticPath := mergeEnvValue("MONITORING_PLUGIN_STATIC_PATH", *staticPathArg, "/opt/app-root/web/dist") - configPath := mergeEnvValue("MONITORING_PLUGIN_MANIFEST_CONFIG_PATH", *configPathArg, "/opt/app-root/config") - pluginConfigPath := mergeEnvValue("MONITORING_PLUGIN_CONFIG_PATH", *pluginConfigArg, "/etc/plugin/config.yaml") - logLevel := mergeEnvValue("MONITORING_PLUGIN_LOG_LEVEL", *logLevelArg, logrus.InfoLevel.String()) - alertmanagerUrl := mergeEnvValue("MONITORING_PLUGIN_ALERTMANAGER", *alertmanagerUrlArg, "") - thanosQuerierUrl := mergeEnvValue("MONITORING_PLUGIN_THANOS_QUERIER", *thanosQuerierUrlArg, "") - tlsMinVersion := mergeEnvValue("TLS_MIN_VERSION", *tlsMinVersionArg, "") - tlsMaxVersion := mergeEnvValue("TLS_MAX_VERSION", *tlsMaxVersionArg, "") - tlsCipherSuites := mergeEnvValue("TLS_CIPHER_SUITES", *tlsCipherSuitesArg, "") + port := mergeEnvValueInt("PORT", *portArg) + cert := mergeEnvValue("CERT_FILE_PATH", *certArg) + key := mergeEnvValue("PRIVATE_KEY_FILE_PATH", *keyArg) + features := mergeEnvValue("MONITORING_PLUGIN_FEATURES", *featuresArg) + staticPath := mergeEnvValue("MONITORING_PLUGIN_STATIC_PATH", *staticPathArg) + configPath := mergeEnvValue("MONITORING_PLUGIN_MANIFEST_CONFIG_PATH", *configPathArg) + pluginConfigPath := mergeEnvValue("MONITORING_PLUGIN_CONFIG_PATH", *pluginConfigArg) + logLevel := mergeEnvValue("MONITORING_PLUGIN_LOG_LEVEL", *logLevelArg) + alertmanagerUrl := mergeEnvValue("MONITORING_PLUGIN_ALERTMANAGER", *alertmanagerUrlArg) + thanosQuerierUrl := mergeEnvValue("MONITORING_PLUGIN_THANOS_QUERIER", *thanosQuerierUrlArg) + tlsMinVersion := mergeEnvValue("TLS_MIN_VERSION", *tlsMinVersionArg) + tlsMaxVersion := mergeEnvValue("TLS_MAX_VERSION", *tlsMaxVersionArg) + tlsCipherSuites := mergeEnvValue("TLS_CIPHER_SUITES", *tlsCipherSuitesArg) featuresList := strings.Fields(strings.Join(strings.Split(strings.ToLower(features), ","), " ")) @@ -63,10 +63,17 @@ func main() { log.Infof("enabled features: %+q\n", featuresList) - // Parse TLS configuration + // Parse the TLS configuration. tlsMinVer := parseTLSVersion(tlsMinVersion) + log.Infof("Min TLS version: %q", tls.VersionName(tlsMinVer)) tlsMaxVer := parseTLSVersion(tlsMaxVersion) + if tlsMaxVer != 0 { + log.Infof("Max TLS version: %q", tls.VersionName(tlsMaxVer)) + } tlsCiphers := parseCipherSuites(tlsCipherSuites) + if tlsCipherSuites != "" { + log.Infof("TLS ciphers: %q", tlsCipherSuites) + } srv, err := server.CreateServer(context.Background(), &server.Config{ Port: port, @@ -92,33 +99,27 @@ func main() { } } -func mergeEnvValue(key string, arg string, defaultValue string) string { +func mergeEnvValue(key string, arg string) string { if arg != "" { return arg } - envValue := os.Getenv(key) - - if envValue != "" { - return envValue - } - - return defaultValue + return os.Getenv(key) } -func mergeEnvValueInt(key string, arg int, defaultValue int) int { +func mergeEnvValueInt(key string, arg int) int { if arg != 0 { return arg } envValue := os.Getenv(key) - num, err := strconv.Atoi(envValue) - if err != nil && num != 0 { - return num + i, err := strconv.Atoi(envValue) + if err != nil { + return 0 } - return defaultValue + return i } func getCipherSuitesMap() map[string]uint16 { @@ -142,11 +143,10 @@ func getTLSVersionsMap() map[string]uint16 { func parseTLSVersion(version string) uint16 { if version == "" { - return tls.VersionTLS12 + return 0 } tlsVersions := getTLSVersionsMap() - if v, ok := tlsVersions[version]; ok { return v } 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/config/perses-dashboards.patch.json b/config/perses-dashboards.patch.json index 5420606a2..2b90e7ac9 100644 --- a/config/perses-dashboards.patch.json +++ b/config/perses-dashboards.patch.json @@ -8,7 +8,7 @@ "exact": false, "path": ["/multicloud/monitoring/v2/dashboards"], "component": { - "$codeRef": "DashboardsPage" + "$codeRef": "DashboardListPage" } } } @@ -36,7 +36,7 @@ "properties": { "exact": false, "path": ["/monitoring/v2/dashboards"], - "component": { "$codeRef": "DashboardsPage" } + "component": { "$codeRef": "DashboardListPage" } } } }, @@ -64,7 +64,7 @@ "properties": { "exact": false, "path": ["/virt-monitoring/v2/dashboards"], - "component": { "$codeRef": "DashboardsPage" } + "component": { "$codeRef": "DashboardListPage" } } } }, @@ -83,5 +83,41 @@ "insertAfter": "dashboards-virt" } } + }, + { + "op": "add", + "path": "/extensions/1", + "value": { + "type": "console.page/route", + "properties": { + "exact": false, + "path": ["/monitoring/v2/dashboards/view"], + "component": { "$codeRef": "DashboardPage" } + } + } + }, + { + "op": "add", + "path": "/extensions/1", + "value": { + "type": "console.page/route", + "properties": { + "exact": false, + "path": ["/virt-monitoring/v2/dashboards/view"], + "component": { "$codeRef": "DashboardPage" } + } + } + }, + { + "op": "add", + "path": "/extensions/1", + "value": { + "type": "console.page/route", + "properties": { + "exact": false, + "path": ["/multicloud/monitoring/v2/dashboards/view"], + "component": { "$codeRef": "DashboardPage" } + } + } } ] diff --git a/docs/incident_detection/tests/0.overview.md b/docs/incident_detection/tests/0.overview.md new file mode 100644 index 000000000..fb418a604 --- /dev/null +++ b/docs/incident_detection/tests/0.overview.md @@ -0,0 +1,74 @@ +# Incidents Page Testing Checklist + +This checklist covers regression testing for **known bug areas** in the Incidents page. Focus on sections 1-4 for critical regressions. Section 5 contains optional tests for stable areas. + +**Incident Detection Known Bug Areas**: +- **Filtering**: Incidents were filtered by alert severity instead of incident's own severity history +- **Charts Display**: Tooltip positioning, bar sorting, and short alert visibility issues +- **API calls**: Alert resolution timing, silence matching logic (name+namespace+severity not just name) +- **Redux State Management**: Initialization race conditions, selection persistence, stale data, dropdown states + + + + +## How to Use This Checklist + +1. **Set up test data first**: + - Copy the CSV from the "Complete Test Data" section below + - Save it to a file and use with the simulation script from the `cluster-health-analyzer` repository. +2. **Identify relevant areas**: Based on the scope of the change, decide on areas that need to be targeted. +3. **Run the tests**: Follow the test cases in order, checking expected vs actual behavior +4. **Reference specific incidents**: Tests reference incidents by letter (A, B, C, etc.) + +**Time notation**: All times are positive values in minutes. The simulation script adjusts them to be relative to "now". + +--- + +## Complete Test Data - CSV Format + +**Use this complete CSV with your simulation script to set up all test data at once:** + +```csv +start,end,alertname,namespace,severity,silenced,labels +0,180,AlertA_Info,A-openshift-logging,info,false,{"component": "logging"} +240,360,AlertB_Warning,B-openshift-storage,warning,false,{"component": "storage"} +420,420,AlertC_ShortDuration,C-openshift-apiserver,warning,false,{"component": "api-server"} +480,780,AlertD_Info,D-openshift-monitoring,info,false,{"component": "monitoring"} +540,780,AlertD_Warning,D-openshift-monitoring,warning,false,{"component": "monitoring"} +600,780,AlertD_Critical,D-openshift-monitoring,critical,false,{"component": "monitoring"} +840,1080,AlertE_Etcd,-Eopenshift-etcd,critical,false,{"component": "etcd"} +840,1080,AlertE_KubeAPI,E-openshift-kube-apiserver,critical,false,{"component": "kube-apiserver"} +840,1080,AlertE_Controller_Very_Very_Very_Very_Long_Name_Alert,E-openshift-kube-controller,critical,false,{"component": "kube-controller"} +1140,1260,AlertF_KubePodCrashLooping,F-openshift-monitoring,warning,false,{"component": "monitoring"} +1200,1380,AlertF_HighMemoryUsage,F-openshift-monitoring,critical,false,{"component": "monitoring"} +1440,1500,AlertG_APIServerLatency,G-openshift-kube-apiserver,warning,false,{"component": "kube-apiserver"} +1560,1740,AlertH_Critical,H-openshift-network,critical,false,{"component": "network"} +1800,1980,AlertI_KubePodNotReady,I-openshift-operators,warning,true,{"component": "operators"} +2040,2220,AlertJ_KubePodNotReady,J-openshift-storage,warning,false,{"component": "storage"} +``` + +**What this creates** (incidents named A-J in chronological order): +- **Incident A** (0-180 min / 3 hrs): Info only, resolved - logging component +- **Incident B** (240-360 / 2 hrs): Warning only, resolved - storage component +- **Incident C** (420): Single data point, short duration - api-server component +- **Incident D** (480-780 / 5 hrs): Multi-severity transition (Info→Warning→Critical), firing - monitoring component +- **Incident E** (840-1080 / 4 hrs): Multi-component (3 alerts), resolved - etcd/kube-apiserver/kube-controller +- **Incident F** (1140-1380 / 4 hrs): Resolution testing (2 overlapping alerts) - monitoring component +- **Incident G** (1440-1500 / 1 hr): Short duration alert, resolved - kube-apiserver component +- **Incident H** (1560-1740 / 3 hrs): Critical, firing - network component +- **Incident I** (1800-1980 / 3 hrs): Silenced alert - operators component +- **Incident J** (2040-2220 / 3 hrs): NOT silenced (different namespace) - storage component + +**Timeline** (37 hours total, NO overlaps between incidents, 60 min gaps): +``` +0──────180──240───360──420──480──────780──840────1080──1140────1380──1440─1500──1560───1740──1800───1980──2040───2220 + A (3hr) │ B(2hr) │ C │ D (5hr multi) │ E (4hr x3) │ F (4hr test) │ G(1hr) │ H(3hr) │ I(3hr) │ J(3hr) + 60min gap 60 60min gap 60min gap 60min gap 60 60min 60min 60min +``` + +**Format notes**: +- All times in minutes, positive values (simulation script adjusts to relative times) +- Alert names A-J match chronological order (A fires first, J fires last) +- NO overlaps between different incidents +- Alerts within same incident DO overlap (D has 3 overlapping alerts, E has 3 simultaneous, F has 2 overlapping) +- Long durations (1-5 hours) and large gaps (1 hour) ensure no unintended grouping diff --git a/docs/incident_detection/tests/1.filtering_flows.md b/docs/incident_detection/tests/1.filtering_flows.md new file mode 100644 index 000000000..1d68391e6 --- /dev/null +++ b/docs/incident_detection/tests/1.filtering_flows.md @@ -0,0 +1,59 @@ +## 1. CRITICAL: Filtering Bugs + +**Automation Status**: AUTOMATED in `01.reg_filtering.cy.ts` + +### Prerequisites: Test Data Setup for Filtering Tests + +**CSV Format** - Use this with your simulation script (these create incidents A, B, D, H): + +```csv +start,end,alertname,namespace,severity,silenced,labels +0,180,AlertA_Info,openshift-logging,info,false,{"component": "logging"} +240,360,AlertB_Warning,openshift-storage,warning,false,{"component": "storage"} +480,780,AlertD_Info,openshift-monitoring,info,false,{"component": "monitoring"} +540,780,AlertD_Warning,openshift-monitoring,warning,false,{"component": "monitoring"} +600,780,AlertD_Critical,openshift-monitoring,critical,false,{"component": "monitoring"} +1560,1740,AlertH_Critical,openshift-network,critical,false,{"component": "network"} +``` + +**Quick Reference**: +| Incident | Component | Severity History | State | Time Range | +|----------|-----------|------------------|-------|------------| +| A | logging | Info | Resolved | 0-180 | +| B | storage | Warning | Resolved | 240-360 | +| D | monitoring | Info→Warning→Critical | Firing | 480-780 | +| H | network | Critical | Firing | 1560-1740 | + +### 1.1 Incident Severity Filtering (Not Alert Severity) +**BUG**: Incidents were being filtered by underlying alert severities instead of the incident's own severity history. + +- [ ] **Filter by "Critical"**: + +- [ ] **Filter by "Warning"**: + +- [ ] **Filter by "Informative"**: + +- [ ] **Multiple Severity Filters (e.g., Critical + Warning)**: + +For each, ensure that the correct incidents are shown. The particular numbers may be slightly off if additional alerts are firing in the cluster. + +### 1.2 Resolved Incident Filter Not Working +**BUG**: "Resolved" state filter wasn't working correctly. + +- [ ] **Filter by "Resolved"**: +- [ ] **Filter by "Firing"**: +- [ ] **Verify Resolution Logic**: + - Firing: `currentTime - lastTimestamp <= 10 minutes` + - Resolved: `currentTime - lastTimestamp > 10 minutes` + - Check Incidents that are resolved have last activity > 10 min ago + +### 1.3 Combined Filtering (AND Logic Between Categories) +- [ ] **Critical + Resolved**: +- [ ] **Warning + Resolved**: +- [ ] **Critical + Firing**: + + +- [ ] **Filter Persistence on URL**: Apply filters, refresh page + - Apply: Warning + Resolved + - Check URL: `?days=7+days&severity=Warning&state=Resolved` + - Refresh page → verify Incident B still shown \ No newline at end of file diff --git a/docs/incident_detection/tests/2.ui_display_flows.md b/docs/incident_detection/tests/2.ui_display_flows.md new file mode 100644 index 000000000..c4f45cb37 --- /dev/null +++ b/docs/incident_detection/tests/2.ui_display_flows.md @@ -0,0 +1,117 @@ +## 2. CRITICAL: Charts – UI Bugs + +**Automation Status**: AUTOMATED in `02.reg_ui_charts_comprehensive.cy.ts` apart from 2.3.1 and 2.4 +- Uses fixture: `incident-scenarios/12-charts-ui-comprehensive.yaml` +- Covers: Tooltip positioning, bar sorting & visibility, date/time display +- Verifies: Incidents chart, alerts chart, multi-component tooltips, long alert names + +### Prerequisites: Test Data Setup for Chart Tests + +**CSV Format** - Add these to the incidents from Section 1 (these create incidents C, D, E): + +```csv +start,end,alertname,namespace,severity,silenced,labels +420,420,AlertC_ShortDuration,openshift-apiserver,warning,false,{"component": "api-server"} +480,780,AlertD_Info,openshift-monitoring,info,false,{"component": "monitoring"} +540,780,AlertD_Warning,openshift-monitoring,warning,false,{"component": "monitoring"} +600,780,AlertD_Critical,openshift-monitoring,critical,false,{"component": "monitoring"} +840,1080,AlertE_Etcd,openshift-etcd,critical,false,{"component": "etcd"} +840,1080,AlertE_KubeAPI,openshift-kube-apiserver,critical,false,{"component": "kube-apiserver"} +840,1080,AlertE_Controller_Very_Very_Very_Very_Long_Name_Alert,openshift-kube-controller,critical,false,{"component": "kube-controller"} +``` + +**Quick Reference** (Charts Test Data): +| Incident | Components | Duration | Severity | Time Range | Use For Testing | +|----------|------------|----------|----------|------------|-----------------| +| C | api-server | 0 min | Warning | 420 | Short duration visibility | +| D | monitoring | 5 hrs | Multi-severity (Info→Warning→Critical) | 480-780 | Multi-severity segments | +| E | etcd, kube-apiserver, kube-controller | 4 hrs | Critical | 840-1080 | Multi-component tooltip, long names | + +### 2.1 Tooltip Positioning Issues +**BUG**: Tooltips were overlapping bars or going off-screen. +**Automation Status**: AUTOMATED + +- [ ] **Tooltip Positioning - Incidents Chart**: + - Hover over Incident A (oldest, at bottom) + - Hover over Incident H (one of newest, near top) + - Hover over Incident D (middle position) + - Verify tooltip appears directly above/on each bar without overlap + +- [ ] **Tooltip Content - Multi-Component**: Hover over Incident E + - Verify shows: "Component(s): etcd, kube-apiserver, kube-controller" + - Verify comma-separated list format + - Test with long alert name (Controller_Very_Very_Very_Very_Long_Name_Alert) + +- [ ] **Tooltip Content - Firing vs Resolved**: + - Hover over Incident D (firing): End should show "---" + - Hover over Incident A (resolved): End should show actual end time + +- [ ] **Tooltip Positioning - Alerts Chart** + - Select Incident E (multi-component) + - Hover over all 3 alerts in the incident + - Verify Tooltip appears directly above each bar + - Verify alert name length does not influence the behaviour (test long controller name) + +### 2.2 Bar Sorting & Visibility Issues +**BUGS**: Bars not sorted by start date, filtered bars not leaving space, short alerts not visible. +**Automation Status**: AUTOMATED + +- [ ] **Bar Sorting by Start Date**: Check incidents chart Y-axis order + - Expected order (oldest at bottom): + 1. Incident A (0-180) - bottom + 2. Incident B (240-360) + 3. Incident C (420) + 4. Incident D (480-780) + 5. Incident E (840-1080) + 6. Incident F (1140-1380) + 7. Incident G (1440-1500) + 8. Incident H (1560-1740) + 9. Incident I (1800-1980) + 10. Incident J (2040-2220) - top + - Verify this order maintained after applying filters + +- [ ] **Filtered Bars Leave Empty Space**: Apply "Critical" filter + - Expected visible: D, E, H (Critical incidents) + - Expected hidden but space preserved: A, B, C, F, G, I, J + - Verify gaps appear where non-Critical incidents would be + - Verify Y-axis still shows all 10 positions + +- [ ] **Short Duration Incidents Visible**: Check Incident C (single point at 420) + - Verify bar IS visible despite 0-minute duration + - Hover to confirm tooltip shows Incident C + - Verify bar has minimum visible width + +### 2.3 Date/Time Display Issues +**BUGS**: Start/End times not displaying correctly, date format not respecting language. +**Automation Status**: AUTOMATED + +- [ ] **Start/End Times Correct**: Check specific incidents + - Incident D (firing): Start = T-480min, End = "---" + - Incident A (resolved): Start = T-0min, End = T-180min (both shown) + - Incident C (short, resolved): Start = T-420min, End = T-420min + - Verify tooltip shows these times correctly + +- [ ] **Multi-Severity Segments**: Check Incident D (Info→Warning→Critical) + - Verify each severity segment shows correct time range: + - Info segment: T-480min to T-540min + - Warning segment: T-540min to T-600min + - Critical segment: T-600min to "---" + +- [ ] **Date Format Respects Language**: + - Set browser/app language to English + - Check Incident A tooltip: should show "Jan 15, 2025, 3:45 PM" format + - Switch to Chinese (if available) and verify format changes + - Verify `dateTimeFormatter(i18n.language)` respects setting + +### 2.3.1 (Not Automated) +- [ ] **Date Format Changed Immediately** (xfail): + - Change the app language to Spanish + - Check the "last updated date" field + - Verify that the format changes without the need to reload the page + +### 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` + field + + 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 new file mode 100644 index 000000000..1ae58d35f --- /dev/null +++ b/docs/incident_detection/tests/3.api_calls_data_loading_flows.md @@ -0,0 +1,94 @@ +## 3. CRITICAL: Data Loading – API Call Bugs + +**Automation Status**: PARTIALLY AUTOMATED (Sections 3.1 and 3.2) + +### Prerequisites: Test Data Setup for Data Loading Tests + +**CSV Format** - These alerts test resolution, short duration, and silence logic (creates incidents F, G, I, J): + +```csv +start,end,alertname,namespace,severity,silenced,labels +1140,1260,AlertF_KubePodCrashLooping,openshift-monitoring,warning,false,{"component": "monitoring"} +1200,1380,AlertF_HighMemoryUsage,openshift-monitoring,critical,false,{"component": "monitoring"} +1440,1500,AlertG_APIServerLatency,openshift-kube-apiserver,warning,false,{"component": "kube-apiserver"} +1800,1980,AlertI_KubePodNotReady,openshift-operators,warning,true,{"component": "operators"} +2040,2220,AlertJ_KubePodNotReady,openshift-storage,warning,false,{"component": "storage"} +``` + +**Silence Matching Logic**: +- Silence determined by `silenced` field in CSV (becomes label in `cluster_health_components_map` metric) +- Incidents I and J have same `alertname` but different `namespace` and different `silenced` values +- Tests that silence matching uses: `alertname` + `namespace` + `severity` (NOT just alert name) + +**Quick Reference** (Alerts & Silences): +| Alert | alertname | namespace | severity | Time Range | Expected State | silenced | Use For Testing | +|-------|-----------|-----------|----------|------------|----------------|----------|-----------------| +| F1 | AlertF_KubePodCrashLooping | openshift-monitoring | warning | 1140-1260 | Resolved | false | Time-based resolution | +| F2 | AlertF_HighMemoryUsage | openshift-monitoring | critical | 1200-1380 | Resolved | false | Time-based resolution | +| G | AlertG_APIServerLatency | openshift-kube-apiserver | warning | 1440-1500 | Resolved | false | Short duration (1 hr) | +| I | AlertI_KubePodNotReady | openshift-operators | warning | 1800-1980 | Resolved | **true** | Silence matching | +| J | AlertJ_KubePodNotReady | openshift-storage | warning | 2040-2220 | Resolved | **false** | Different namespace = not silenced | + + +### 3.1 Short Incidents Not Visible +**BUG**: Incidents with duration < 5 minutes weren't showing up. +**Automation Status**: AUTOMATED in `02.reg_ui_charts_comprehensive.cy.ts` (Section 3.1) +- Uses fixture: `incident-scenarios/12-charts-ui-comprehensive.yaml` (includes very short duration incidents) +- Tests: 5-minute, 9-minute, and recently resolved (2 min ago) incidents +- Verifies: Bar visibility, dimensions, transparency, selectability, and alert loading + +- [x] **Short Incident C**: Check `api-server` incident (0 min duration, single point) - AUTOMATED + - Verify appears in incidents chart (has visible bar despite 0 duration) + - Select it and verify alert loads + +- [x] **Short Incident G**: Check `kube-apiserver` incident (60 min duration) - AUTOMATED + - Verify appears in incidents chart with visible bar + - Select it and verify AlertG_APIServerLatency appears + - Verify no minimum duration threshold filters it out + +### 3.2 Silences Not Applied Correctly +**BUG**: Silences were being matched by name only, not by name + namespace + severity. +**Automation Status**: AUTOMATED in `03.reg_api_calls.cy.ts` +- Uses fixture: `incident-scenarios/9-silenced-alerts-mixed-scenario.yaml` +- Verifies: Opacity (0.3 for silenced, 1.0 for non-silenced) +- Verifies: Tooltip "(silenced)" indicator +- Tests: Same alert name with different namespaces + +- [ ] **Incident I IS Silenced**: Check `AlertI_KubePodNotReady` in `openshift-operators` + - Silence matches: name=`KubePodNotReady` + namespace=`openshift-operators` + severity=`warning` + - Expected: Alert marked as `silenced = true` + - Verify alert bar has opacity: 0.3 (reduced) + - Verify tooltip shows: "AlertI_KubePodNotReady (silenced)" + +- [ ] **Incident J NOT Silenced**: Check `AlertJ_KubePodNotReady` in `openshift-storage` + - Same alert name as Incident I, but DIFFERENT namespace + - Silence does NOT match (namespace mismatch) + - Expected: Alert marked as `silenced = false` + - Verify alert bar has opacity: 1.0 (full) + - Verify tooltip shows: "AlertJ_KubePodNotReady" (no silenced suffix) + +- [ ] **Silence Matching Logic**: Verify implementation + - Check that matching uses: `alertname` + `namespace` + `severity` + - NOT just `alertname` alone + - Silence source: `cluster_health_components_map` metric (NOT Alertmanager API) + + ### 3.3 Alerts Marked as Resolved After Time +**BUG**: Alerts not being marked as resolved when they should be. +**Automation Status**: NOT AUTOMATED (requires live firing alerts) +- **WARNING Not possible to test on Injected Data, requires continously firing alert** + - Trigger a real firing alert (Pod CrashLooping...) + - Verify that the alert is firing + - Wait for 10 minutes without refreshing incidents + - Toggle the days filter to retrigger the alert queries + - Verify it is not marked as resolved + - Verify the latest query end time param is within the last 5 minutes + + + ### 3.4 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 +- [ ] Watchdog alerts filtered out + + diff --git a/docs/incident_detection/tests/4.redux_state_and_effects_flows.md b/docs/incident_detection/tests/4.redux_state_and_effects_flows.md new file mode 100644 index 000000000..9f793b5bb --- /dev/null +++ b/docs/incident_detection/tests/4.redux_state_and_effects_flows.md @@ -0,0 +1,120 @@ +## 4. CRITICAL: Effects / Redux State Management Bugs + +**Automation Status**: PARTIALLY AUTOMATED (4.5, 4.6, dropdown closure) + +### Prerequisites: Test Data Setup for State Management Tests + +Use the complete set of incidents (A-J). These tests focus on how the UI responds to state changes rather than specific data values. + +### 4.1 Basic Element Rendering +**Automation Status**: AUTOMATED +- Covered by `01.incidents.cy.ts` (tests 2, 3) and `04.reg_redux_effects.cy.ts` (test 3) +- Tests days filter changes and severity filter updates +- Verifies chart updates immediately without page reload + +- [x] **Incidents Refresh on Days Change**: AUTOMATED in `01.incidents.cy.ts` test 2 + - Start with "Last 7 days" (showing all 10 incidents A-J) + - Change to "Last 1 day" (should show only recent incidents) + - Verify incidents chart updates, loading spinner shows, new data displayed + +- [x] **Filtered Data Updates on Filter Change**: AUTOMATED in `01.incidents.cy.ts` test 3 and `04.reg_redux_effects.cy.ts` test 3 + - Apply "Critical" filter → verify only D, E, H shown + - Add "Warning" filter → verify B, F, G, I, J also appear + - Verify chart updates immediately (no page reload) + +### 4.2 Selected Incident Does Not Survive State Changes +**BUG**: Selected incident was being lost when changing filters or toggling graphs. +**Automation Status**: PARTIALLY AUTOMATED (filter changes covered, graph toggle not covered) +- Covered by `04.reg_redux_effects.cy.ts` test 3 +- Tests incident ID filter persistence when non-matching severity filter applied +- Graph toggle test not automated + +- [x] **Selection Survives Severity Filter**: AUTOMATED in `04.reg_redux_effects.cy.ts` test 3 + - Select Incident D (has Info, Warning, Critical in history) + - Apply "Warning" filter (D matches because it had Warning) + - Verify: Incident D still selected, URL has `?groupId=D`, alerts still shown + +- [x] **Selection Lost When Filtered Out (but ID filter persists)**: AUTOMATED in `04.reg_redux_effects.cy.ts` test 3 + - Select Incident A (Info only) + - Apply "Critical" filter (A doesn't match) + - Verify: Incident A disappears, but Incident ID filter chip remains, appropriate state + +- [ ] **Selection Survives Graph Toggle**: NOT AUTOMATED + - Select Incident H + - Click "Hide graph" + - Verify: URL still has `?groupId=H`, table shows H's alert + - Click "Show graph" → verify chart renders correctly + +### 4.3 Stale Alerts Displayed on Incident Reselection +**BUG**: When switching between incidents, stale alerts from previous incident shown briefly. +**Automation Status**: INDIRECTLY COVERED by `01.incidents.cy.ts` (test 5: Traverse Incident Table) +- The `findIncidentWithAlert` method would fail if stale alerts from previous selections are displayed +- Not explicitly tested with dedicated assertions, but functionality breaks if bug exists + +- [ ] **Incident Switching**: + - Select Incident D (with 3 alerts: Info, Warning, Critical) + - Deselect Incident D + - Immediately select Incident E (3 different component alerts) + - Verify: NO brief flash of D's alerts; loading state shown immediately + - Verify: Only E's alerts displayed after fetch completes + +- [ ] **Deselect Incident**: + - Select Incident F + - Click on Incident F again to deselect + - Verify: Alerts chart shows "select an incident" empty state + - Verify: No stale alerts remain + +### 4.4 Incident Dropdown Staying Open After Page Refresh +**BUG**: Dropdowns remained open after page refresh (Incidents Display / not F5). +**Automation Status**: AUTOMATED in `04.reg_redux_effects.cy.ts` (Test 2: Dropdown closure on deselection) + +- [ ] **Dropdown State After Refresh**: + - Select a particular Incident + - ~~Toggle filters that cause deselection of the incident and page reload~~ + NOTE: This won't happen, as the page will not be reloaded in new update + - Verify: Dropdown is closed after reload + - Verify: Dropdown does not jump to 0,0 coordinates + - Verify: Filter state restored from URL but dropdown collapsed + + ### 4.5 Dropdown Staying open after deselection + **BUG**: Deselection of incident causes reposition of the dropdown + **Automation Status**: AUTOMATED in `04.reg_redux_effects.cy.ts` (Test 2: Dropdown closure on deselection) + - Select a particular incident + - Open the left dropdown menu (Severity, State, ID) + - Deselect the incident by clicking on the bar, the site data should reload + - Verify: The dropdown should not reposition to 0.0 and should be closed + - Verify: Do the same also with the right toolbar. + + + +- [ ] **Dropdowns Auto-Close After Selection**: + - Open "Days" dropdown → select "3 days" → verify closes + - Open "Incident ID" filter → select an incident → verify closes + +### 4.5 Adding filter when incident selected does not remove the incident filter +**BUG:** When incident-id was filtered and additional filter (severity) applied, then if the filter was not matching the selected issue, the id filter was removed. +**Automation Status**: AUTOMATED in `04.reg_redux_effects.cy.ts` (Test 3: Filter state preservation) +- [ ] Select a "critical" incident by id +- [ ] Apply waring filter. +- [ ] Verify incident is filtered out +- [ ] Verify the filters "warning", and "incident id" are applied. + +### 4.6 Incidents Not Loaded Initially +**BUG**: Old Redux state was being used for effects fired at the beginning of page load. When the page loaded, only several issues were displayed. +**Automation Status**: AUTOMATED in `04.reg_redux_effects.cy.ts` (Test 1: Fresh load verification) + +**NOTE:** Hard to replicate, requires fresh browser instance + +### 4.7 Cached end time for prometheus query +**BUG**: End Time parameter for the prometheus query request uses the time of the initial load of the page instead of the current time, which causes firing alerts to be marked as resolved. +**Automation Status**: NOT AUTOMATED (requires live firing alerts) +**NOTE**: The issue is conceptually very similiar to 3.3, but is caused by the redux state caching, so it belongs to this section. +- **WARNING Not possible to test on Injected Data, requires continously firing alert, mocked (firing) data might be applicable though.** + - Trigger a real firing alert (Pod CrashLooping...) + - Verify that the alert is firing + - Wait for 10 minutes without refreshing incidents + - Refresh the days filter. + - Verify that the end time in the query to prometheus is updated to the current time value. + - Verify + + diff --git a/docs/incident_detection/tests/5.customization.md b/docs/incident_detection/tests/5.customization.md new file mode 100644 index 000000000..9d710ebe9 --- /dev/null +++ b/docs/incident_detection/tests/5.customization.md @@ -0,0 +1,12 @@ +# 5. Customization and Visual Bugs +**Automation Status**: NOT AUTOMATED + +### 5.1 Theme & Visual Polish +- [ ] Light/dark theme switching works correctly +- [ ] Chart legend displays properly with correct colors +- [ ] Chart responsiveness on window resize +- [ ] Chart height adapts to incident count (< 5: 300px, >= 5: count * 60px) + +### 5.2 Internationalization +- [ ] Translation keys used for all user-facing strings +- [ ] Multiple language support works (if available) diff --git a/docs/incident_detection/tests/6.table_interactions.md b/docs/incident_detection/tests/6.table_interactions.md new file mode 100644 index 000000000..dbffef8dd --- /dev/null +++ b/docs/incident_detection/tests/6.table_interactions.md @@ -0,0 +1,10 @@ +### 6.1 Table Interactions +**Automation Status**: INDIRECTLY COVERED by existing tests +- Indirectly tested by `01.incidents.cy.ts` (test 5: Traverse Incident Table) and `02.reg_ui_charts_comprehensive.cy.ts` +- Table expansion, row interactions, and data display are exercised during incident selection and traversal + +- [ ] Expand/collapse all rows button works +- [ ] Individual row expansion works +- [ ] Expanded rows collapse when alert data changes +- [ ] Table sorting by start date (earliest at top) +- [ ] Severity badges show correct counts diff --git a/docs/incident_detection/tests/Uncategorized.testing_flows_ui.md b/docs/incident_detection/tests/Uncategorized.testing_flows_ui.md new file mode 100644 index 000000000..65d3d4d39 --- /dev/null +++ b/docs/incident_detection/tests/Uncategorized.testing_flows_ui.md @@ -0,0 +1,15 @@ +## Uncategorized Flows: Additional Testing + +**Automation Status**: NOT AUTOMATED + +These areas are generally stable but can be tested if you have time or suspect related issues. + + +### 5.4 URL State Management +- [ ] Browser back/forward buttons work correctly +- [ ] Direct URL access with filters loads correctly +- [ ] URL updates without page reload (`history.replaceState`) + + +--- + diff --git a/go.mod b/go.mod index 9437a6af0..d831b6f40 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,10 @@ require ( github.com/evanphx/json-patch v4.12.0+incompatible github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 - github.com/onsi/ginkgo/v2 v2.22.0 - github.com/onsi/gomega v1.36.1 - github.com/openshift/api v0.0.0-20251122153900-88cca31a44c9 github.com/openshift/client-go v0.0.0-20251123231646-4685125c2287 github.com/openshift/library-go v0.0.0-20240905123346-5bdbfe35a6f5 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.87.0 github.com/prometheus-operator/prometheus-operator/pkg/client v0.87.0 - github.com/prometheus/common v0.67.4 - github.com/prometheus/prometheus v0.308.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v2 v2.4.0 @@ -25,10 +20,7 @@ require ( ) require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dennwc/varint v1.0.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -48,25 +40,19 @@ require ( github.com/go-openapi/swag/stringutils v0.25.1 // indirect github.com/go-openapi/swag/typeutils v0.25.1 // indirect github.com/go-openapi/swag/yamlutils v0.25.1 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/openshift/api v0.0.0-20251122153900-88cca31a44c9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect - go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.46.0 // indirect @@ -75,7 +61,6 @@ require ( golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.13.0 // indirect - golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index e70962788..565b23852 100644 --- a/go.sum +++ b/go.sum @@ -1,57 +1,7 @@ -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= -cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= -cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= -github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= -github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= -github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= -github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= -github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= -github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= -github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= -github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= -github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= -github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= -github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= -github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= -github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= @@ -64,8 +14,6 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= @@ -100,10 +48,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= -github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -111,34 +55,20 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 h1:ZI8gCoCjGzPsum4L21jHdQs8shFBIQih1TM9Rd/c+EQ= github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= -github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= -github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -147,11 +77,6 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= -github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= @@ -162,8 +87,6 @@ github.com/openshift/client-go v0.0.0-20251123231646-4685125c2287 h1:Spullg4rMMW github.com/openshift/client-go v0.0.0-20251123231646-4685125c2287/go.mod h1:liCuDDdOsPSZIDP0QuTveFhF7ldXuvnPhBd/OTsJdJc= github.com/openshift/library-go v0.0.0-20240905123346-5bdbfe35a6f5 h1:CyPTfZvr+HvwXbix9kieI55HeFn4a5DBaxJ3DNFinhg= github.com/openshift/library-go v0.0.0-20240905123346-5bdbfe35a6f5/go.mod h1:/wmao3qtqOQ484HDka9cWP7SIvOQOdzpmhyXkF2YdzE= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -173,22 +96,6 @@ github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.87.0 h github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.87.0/go.mod h1:WHiLZmOWVop/MoYvRD58LfnPeyE+dcITby/jQjg83Hw= github.com/prometheus-operator/prometheus-operator/pkg/client v0.87.0 h1:rrZriucuC8ZUOPr8Asvavb9pbzqXSsAeY79aH8xnXlc= github.com/prometheus-operator/prometheus-operator/pkg/client v0.87.0/go.mod h1:OMvC2XJGxPeEAKf5qB1u7DudV46HA8ePxYslRjxQcbk= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a h1:RF1vfKM34/3DbGNis22BGd6sDDY3XBi0eM7pYqmOEO0= -github.com/prometheus/client_golang/exp v0.0.0-20250914183048-a974e0d45e0a/go.mod h1:FGJuwvfcPY0V5enm+w8zF1RNS062yugQtPPQp1c4Io4= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= -github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/prometheus/prometheus v0.308.0 h1:kVh/5m1n6m4cSK9HYTDEbMxzuzCWyEdPdKSxFRxXj04= -github.com/prometheus/prometheus v0.308.0/go.mod h1:xXYKzScyqyFHihpS0UsXpC2F3RA/CygOs7wb4mpdusE= -github.com/prometheus/sigv4 v0.3.0 h1:QIG7nTbu0JTnNidGI1Uwl5AGVIChWUACxn2B/BQ1kms= -github.com/prometheus/sigv4 v0.3.0/go.mod h1:fKtFYDus2M43CWKMNtGvFNHGXnAJJEGZbiYCmVp/F8I= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -206,18 +113,6 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= @@ -227,10 +122,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= -golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -244,8 +135,6 @@ golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -270,13 +159,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI= -google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go index d25fc3748..1fd6fbc4d 100644 --- a/pkg/k8s/client.go +++ b/pkg/k8s/client.go @@ -4,15 +4,13 @@ import ( "context" "fmt" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - osmv1client "github.com/openshift/client-go/monitoring/clientset/versioned" monitoringv1client "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" - "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -var log = logrus.WithField("module", "k8s") +//var log = logrus.WithField("module", "k8s") var _ Client = (*client)(nil) 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/proxy/proxy.go b/pkg/proxy/proxy.go index 554a0703a..abdcfa0b3 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -130,11 +130,6 @@ func getProxy(kind KindType, proxyUrlString string, serviceCAfile string) (*http return proxy, nil } -func handleError(w http.ResponseWriter, code int, err error) { - log.Error(err) - http.Error(w, err.Error(), code) -} - func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.proxy.ServeHTTP(w, r) } diff --git a/pkg/server.go b/pkg/server.go index a860400d1..552f06103 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -57,11 +57,12 @@ type PluginConfig struct { type Feature string const ( - AcmAlerting Feature = "acm-alerting" - Incidents Feature = "incidents" - DevConfig Feature = "dev-config" - PersesDashboards Feature = "perses-dashboards" - AlertManagementAPI Feature = "alert-management-api" + AcmAlerting Feature = "acm-alerting" + Incidents Feature = "incidents" + DevConfig Feature = "dev-config" + PersesDashboards Feature = "perses-dashboards" + AlertManagementAPI Feature = "alert-management-api" + ClusterHealthAnalyzer Feature = "cluster-health-analyzer" ) func (pluginConfig *PluginConfig) MarshalJSON() ([]byte, error) { @@ -89,11 +90,11 @@ func CreateServer(ctx context.Context, cfg *Config) (*PluginServer, error) { func (s *PluginServer) StartHTTPServer() error { if s.Config.IsTLSEnabled() { - log.Infof("listening for https on %s", s.Server.Addr) - return s.Server.ListenAndServeTLS(s.Config.CertFile, s.Config.PrivateKeyFile) + log.Infof("listening for https on %s", s.Addr) + return s.ListenAndServeTLS(s.Config.CertFile, s.Config.PrivateKeyFile) } - log.Infof("listening for http on %s", s.Server.Addr) - return s.Server.ListenAndServe() + log.Infof("listening for http on %s", s.Addr) + return s.ListenAndServe() } func (s *PluginServer) Shutdown(ctx context.Context) error { @@ -153,14 +154,20 @@ func createHTTPServer(ctx context.Context, cfg *Config) (*http.Server, error) { tlsEnabled := cfg.IsTLSEnabled() if tlsEnabled { // Set MinVersion - default to TLS 1.2 if not specified + tlsConfig.MinVersion = tls.VersionTLS12 if cfg.TLSMinVersion != 0 { tlsConfig.MinVersion = cfg.TLSMinVersion - } else { - tlsConfig.MinVersion = tls.VersionTLS12 } if cfg.TLSMaxVersion != 0 { tlsConfig.MaxVersion = cfg.TLSMaxVersion + if tlsConfig.MaxVersion < tlsConfig.MinVersion { + return nil, fmt.Errorf( + "min TLS version %q greater than max TLS version %q", + tls.VersionName(tlsConfig.MinVersion), + tls.VersionName(tlsConfig.MaxVersion), + ) + } } if len(cfg.TLSCipherSuites) > 0 { diff --git a/pkg/server_test.go b/pkg/server_test.go index 4a69cdc9f..fa6437f73 100644 --- a/pkg/server_test.go +++ b/pkg/server_test.go @@ -34,6 +34,43 @@ const ( testHostname = "127.0.0.1" ) +func TestCreateHTTPServer(t *testing.T) { + for _, tc := range []struct { + cfg *Config + err bool + }{ + { + // The minimum TLS version is 1.2 by default. + cfg: &Config{ + TLSMaxVersion: tls.VersionTLS11, + CertFile: "/etc/tls/server.crt", + PrivateKeyFile: "/etc/tls/server.key", + }, + err: true, + }, + { + cfg: &Config{ + TLSMinVersion: tls.VersionTLS13, + TLSMaxVersion: tls.VersionTLS12, + CertFile: "/etc/tls/server.crt", + PrivateKeyFile: "/etc/tls/server.key", + }, + err: true, + }, + } { + t.Run("", func(t *testing.T) { + _, err := createHTTPServer(context.Background(), tc.cfg) + if tc.err { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } + +} + // startTestServer is a helper that starts a server for testing and returns // a cleanup function that should be deferred by the caller. func startTestServer(t *testing.T, conf *Config) (*PluginServer, func()) { diff --git a/scripts/build-image.sh b/scripts/build-image.sh index 720de42f7..93abee039 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -8,16 +8,19 @@ TAG="${TAG:-v1.0.0}" REGISTRY_ORG="${REGISTRY_ORG:-openshift-observability-ui}" DOCKER_FILE_NAME="${DOCKER_FILE_NAME:-Dockerfile.dev}" REPO="${REPO:-monitoring-plugin}" +INTERACTIVE="${INTERACTIVE:-1}" # Define ANSI color codes RED='\033[0;31m' GREEN='\033[0;32m' ENDCOLOR='\033[0m' -# Prompt user for TAG -read -p "$(echo -e "${RED}Enter a value for TAG [${TAG}]: ${ENDCOLOR}")" USER_TAG -if [ -n "$USER_TAG" ]; then - TAG="$USER_TAG" +if [[ $INTERACTIVE == 1 ]]; then + # Prompt user for TAG + read -p "$(echo -e "${RED}Enter a value for TAG [${TAG}]: ${ENDCOLOR}")" USER_TAG + if [ -n "$USER_TAG" ]; then + TAG="$USER_TAG" + fi fi if [[ -x "$(command -v podman)" && $PREFER_PODMAN == 1 ]]; then @@ -41,10 +44,12 @@ echo_vars() { } echo_vars -# Prompt use it check env vars before proceeding to build -read -r -p "Are the environmental variables correct [y/N] " response -if [[ "${response:0:1}" =~ ^([nN])$ ]]; then - exit 0 +if [[ $INTERACTIVE == 1 ]]; then + # Prompt use it check env vars before proceeding to build + read -r -p "Are the environmental variables correct [y/N] " response + if [[ "${response:0:1}" =~ ^([nN])$ ]]; then + exit 0 + fi fi # Build diff --git a/web/.swcrc b/web/.swcrc new file mode 100644 index 000000000..4b67b1a5a --- /dev/null +++ b/web/.swcrc @@ -0,0 +1,28 @@ +{ + "$schema": "https://swc.rs/schema.json", + "jsc": { + "parser": { + "syntax": "typescript", + "jsx": true, + "dynamicImport": true, + "decorators": true + }, + "transform": { + "react": { + "runtime": "automatic" + } + }, + "target": "es2021", + "loose": false, + "externalHelpers": false, + "keepClassNames": true, + "preserveAllComments": true + }, + "module": { + "type": "es6", + "strict": false, + "noInterop": false + }, + "minify": false, + "sourceMaps": true +} diff --git a/web/console-extensions.json b/web/console-extensions.json index c6a0a3f18..40133d2a3 100644 --- a/web/console-extensions.json +++ b/web/console-extensions.json @@ -7,7 +7,11 @@ "href": "/monitoring/alerts", "perspective": "admin", "section": "observe", - "startsWith": ["monitoring/alertrules", "monitoring/silences"] + "startsWith": [ + "monitoring/alertrules", + "monitoring/silences", + "monitoring/incidents" + ] } }, { @@ -16,7 +20,10 @@ "data-quickstart-id": "qs-nav-monitoring" }, "id": "observe-virt-perspective", - "insertBefore": ["compute-virt-perspective", "usermanagement-virt-perspective"], + "insertBefore": [ + "compute-virt-perspective", + "usermanagement-virt-perspective" + ], "name": "%console-app~Observe%", "perspective": "virtualization-perspective" }, @@ -30,7 +37,11 @@ "href": "/virt-monitoring/alerts", "perspective": "virtualization-perspective", "section": "observe-virt-perspective", - "startsWith": ["virt-monitoring/alertrules", "virt-monitoring/silences"] + "startsWith": [ + "virt-monitoring/alertrules", + "virt-monitoring/silences", + "virt-monitoring/incidents" + ] } }, { @@ -99,22 +110,13 @@ "insertAfter": "dashboards-virt" } }, - { - "type": "console.tab", - "properties": { - "contextId": "dev-console-observe", - "name": "%plugin__monitoring-plugin~Dashboards%", - "href": "", - "component": { - "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDashboardsPage" - } - } - }, { "type": "console.redux-reducer", "properties": { "scope": "mp", - "reducer": { "$codeRef": "MonitoringReducer" } + "reducer": { + "$codeRef": "MonitoringReducer" + } } }, { @@ -122,7 +124,9 @@ "properties": { "exact": false, "path": "/monitoring", - "component": { "$codeRef": "AlertingPage.MpCmoAlertingPage" } + "component": { + "$codeRef": "AlertingPage.MpCmoAlertingPage" + } } }, { @@ -130,7 +134,9 @@ "properties": { "exact": false, "path": "/monitoring/silences/~new", - "component": { "$codeRef": "SilenceCreatePage.MpCmoCreateSilencePage" } + "component": { + "$codeRef": "SilenceCreatePage.MpCmoCreateSilencePage" + } } }, { @@ -138,7 +144,9 @@ "properties": { "exact": false, "path": "/monitoring/silences/:id", - "component": { "$codeRef": "SilencesDetailsPage.MpCmoSilencesDetailsPage" } + "component": { + "$codeRef": "SilencesDetailsPage.MpCmoSilencesDetailsPage" + } } }, { @@ -146,63 +154,95 @@ "properties": { "exact": false, "path": "/monitoring/silences/:id/edit", - "component": { "$codeRef": "SilenceEditPage.MpCmoSilenceEditPage" } + "component": { + "$codeRef": "SilenceEditPage.MpCmoSilenceEditPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/monitoring/targets", "/monitoring/targets/:scrapeUrl"], - "component": { "$codeRef": "TargetsPage.MpCmoTargetsPage" } + "path": [ + "/monitoring/targets", + "/monitoring/targets/:scrapeUrl" + ], + "component": { + "$codeRef": "TargetsPage.MpCmoTargetsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/monitoring/query-browser"], - "component": { "$codeRef": "MetricsPage.MpCmoMetricsPage" } + "path": [ + "/monitoring/query-browser" + ], + "component": { + "$codeRef": "MetricsPage.MpCmoMetricsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/monitoring/graph"], - "component": { "$codeRef": "PrometheusRedirectPage" } + "path": [ + "/monitoring/graph" + ], + "component": { + "$codeRef": "PrometheusRedirectPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/monitoring/dashboards", "/monitoring/dashboards/:dashboardName"], - "component": { "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDashboardsPage" } + "path": [ + "/monitoring/dashboards", + "/monitoring/dashboards/:dashboardName" + ], + "component": { + "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDashboardsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/monitoring/alertrules/:id"], - "component": { "$codeRef": "AlertRulesDetailsPage.MpCmoAlertRulesDetailsPage" } + "path": [ + "/monitoring/alertrules/:id" + ], + "component": { + "$codeRef": "AlertRulesDetailsPage.MpCmoAlertRulesDetailsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/monitoring/alerts/:ruleID"], - "component": { "$codeRef": "AlertsDetailsPage.MpCmoAlertsDetailsPage" } + "path": [ + "/monitoring/alerts/:ruleID" + ], + "component": { + "$codeRef": "AlertsDetailsPage.MpCmoAlertsDetailsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/virt-monitoring"], - "component": { "$codeRef": "AlertingPage.MpCmoAlertingPage" } + "path": [ + "/virt-monitoring" + ], + "component": { + "$codeRef": "AlertingPage.MpCmoAlertingPage" + } } }, { @@ -210,7 +250,9 @@ "properties": { "exact": false, "path": "/virt-monitoring/silences/~new", - "component": { "$codeRef": "SilenceCreatePage.MpCmoCreateSilencePage" } + "component": { + "$codeRef": "SilenceCreatePage.MpCmoCreateSilencePage" + } } }, { @@ -218,7 +260,9 @@ "properties": { "exact": false, "path": "/virt-monitoring/silences/:id", - "component": { "$codeRef": "SilencesDetailsPage.MpCmoSilencesDetailsPage" } + "component": { + "$codeRef": "SilencesDetailsPage.MpCmoSilencesDetailsPage" + } } }, { @@ -226,55 +270,143 @@ "properties": { "exact": false, "path": "/virt-monitoring/silences/:id/edit", - "component": { "$codeRef": "SilenceEditPage.MpCmoSilenceEditPage" } + "component": { + "$codeRef": "SilenceEditPage.MpCmoSilenceEditPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/virt-monitoring/targets", "/virt-monitoring/targets/:scrapeUrl"], - "component": { "$codeRef": "TargetsPage.MpCmoTargetsPage" } + "path": [ + "/virt-monitoring/targets", + "/virt-monitoring/targets/:scrapeUrl" + ], + "component": { + "$codeRef": "TargetsPage.MpCmoTargetsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/virt-monitoring/query-browser"], - "component": { "$codeRef": "MetricsPage.MpCmoMetricsPage" } + "path": [ + "/virt-monitoring/query-browser" + ], + "component": { + "$codeRef": "MetricsPage.MpCmoMetricsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/virt-monitoring/graph"], - "component": { "$codeRef": "PrometheusRedirectPage" } + "path": [ + "/virt-monitoring/graph" + ], + "component": { + "$codeRef": "PrometheusRedirectPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/virt-monitoring/dashboards", "/virt-monitoring/dashboards/:dashboardName"], - "component": { "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDashboardsPage" } + "path": [ + "/virt-monitoring/dashboards", + "/virt-monitoring/dashboards/:dashboardName" + ], + "component": { + "$codeRef": "LegacyDashboardsPage.MpCmoLegacyDashboardsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/virt-monitoring/alertrules/:id"], - "component": { "$codeRef": "AlertRulesDetailsPage.MpCmoAlertRulesDetailsPage" } + "path": [ + "/virt-monitoring/alertrules/:id" + ], + "component": { + "$codeRef": "AlertRulesDetailsPage.MpCmoAlertRulesDetailsPage" + } } }, { "type": "console.page/route", "properties": { "exact": false, - "path": ["/virt-monitoring/alerts/:ruleID"], - "component": { "$codeRef": "AlertsDetailsPage.MpCmoAlertsDetailsPage" } + "path": [ + "/virt-monitoring/alerts/:ruleID" + ], + "component": { + "$codeRef": "AlertsDetailsPage.MpCmoAlertsDetailsPage" + } + } + }, + { + "type": "console.page/route", + "properties": { + "exact": false, + "path": "/dev-monitoring/ns/:ns/alerts/:ruleID", + "component": { + "$codeRef": "DevRedirects.AlertRedirect" + } + } + }, + { + "type": "console.page/route", + "properties": { + "exact": false, + "path": "/dev-monitoring/ns/:ns/rules/:id", + "component": { + "$codeRef": "DevRedirects.RulesRedirect" + } + } + }, + { + "type": "console.page/route", + "properties": { + "exact": false, + "path": "/dev-monitoring/ns/:ns/silences/:id", + "component": { + "$codeRef": "DevRedirects.SilenceRedirect" + } + } + }, + { + "type": "console.page/route", + "properties": { + "exact": false, + "path": "/dev-monitoring/ns/:ns/silences/:id/edit", + "component": { + "$codeRef": "DevRedirects.SilenceEditRedirect" + } + } + }, + { + "type": "console.page/route", + "properties": { + "exact": false, + "path": "/dev-monitoring/ns/:ns/silences/~new", + "component": { + "$codeRef": "DevRedirects.SilenceNewRedirect" + } + } + }, + { + "type": "console.page/route", + "properties": { + "exact": false, + "path": "/dev-monitoring/ns/:ns/metrics", + "component": { + "$codeRef": "DevRedirects.MetricsRedirect" + } } } -] +] \ No newline at end of file diff --git a/web/cypress.config.ts b/web/cypress.config.ts index 172007a40..99f391490 100644 --- a/web/cypress.config.ts +++ b/web/cypress.config.ts @@ -2,6 +2,14 @@ import { defineConfig } from 'cypress'; import * as fs from 'fs-extra'; import * as console from 'console'; import * as path from 'path'; +import registerCypressGrep from '@cypress/grep/src/plugin'; + +const getLoginCredentials = (index: number): { username: string; password: string } => { + const users = (process.env.CYPRESS_LOGIN_USERS || '').split(',').filter(Boolean); + const userEntry = users[index] || ''; + const [username = '', password = ''] = userEntry.split(':'); + return { username, password }; +}; export default defineConfig({ screenshotsFolder: './cypress/screenshots', @@ -16,13 +24,31 @@ export default defineConfig({ }, env: { grepFilterSpecs: true, - HOST_API: process.env.CYPRESS_BASE_URL.replace(/console-openshift-console.apps/, 'api').concat( + HOST_API: (process.env.CYPRESS_BASE_URL || '').replace(/console-openshift-console.apps/, 'api').concat( ':6443', ), - LOGIN_USERNAME: process.env.CYPRESS_LOGIN_USERS.split(',')[0].split(':')[0], - LOGIN_PASSWORD: process.env.CYPRESS_LOGIN_USERS.split(',')[0].split(':')[1], + // User 0 credentials - as kubeadmin or even non-admin user + // specifically for perses e2e tests, user0 is considered as console admin user to install COO and create RBAC roles and bindings + LOGIN_USERNAME: getLoginCredentials(0).username, + LOGIN_PASSWORD: getLoginCredentials(0).password, + // User 1 credentials + // User 2 credentials + // specifically for perses e2e tests, user1 and user2 are considered as perses e2e users to test RBAC access to dashboards + LOGIN_USERNAME1: getLoginCredentials(1).username, + LOGIN_PASSWORD1: getLoginCredentials(1).password, + LOGIN_USERNAME2: getLoginCredentials(2).username, + LOGIN_PASSWORD2: getLoginCredentials(2).password, + LOGIN_USERNAME3: getLoginCredentials(3).username, + LOGIN_PASSWORD3: getLoginCredentials(3).password, + LOGIN_USERNAME4: getLoginCredentials(4).username, + LOGIN_PASSWORD4: getLoginCredentials(4).password, + LOGIN_USERNAME5: getLoginCredentials(5).username, + LOGIN_PASSWORD5: getLoginCredentials(5).password, + LOGIN_USERNAME6: getLoginCredentials(6).username, + LOGIN_PASSWORD6: getLoginCredentials(6).password, TIMEZONE: process.env.CYPRESS_TIMEZONE || 'UTC', MOCK_NEW_METRICS: process.env.CYPRESS_MOCK_NEW_METRICS || 'false', + COO_NAMESPACE: process.env.CYPRESS_COO_NAMESPACE || 'openshift-cluster-observability-operator', typeDelay: 200, }, fixturesFolder: 'cypress/fixtures', @@ -38,6 +64,8 @@ export default defineConfig({ viewportWidth: 1920, viewportHeight: 1080, setupNodeEvents(on, config) { + registerCypressGrep(config); + on( 'before:browser:launch', ( diff --git a/web/cypress/CYPRESS_TESTING_GUIDE.md b/web/cypress/CYPRESS_TESTING_GUIDE.md new file mode 100644 index 000000000..697caf6f5 --- /dev/null +++ b/web/cypress/CYPRESS_TESTING_GUIDE.md @@ -0,0 +1,254 @@ +# Cypress Testing Guide - Monitoring Plugin + +> **Complete guide for developers and AI agents on Cypress E2E testing** + +--- + +## Table of Contents +- [Quick Start](#quick-start) +- [Test Architecture](#test-architecture) +- [Creating Tests](#creating-tests) +- [Running Tests](#running-tests) +- [Troubleshooting](#troubleshooting) + +--- + +## Quick Start + +### Prerequisites +- Node.js >= 18 +- OpenShift cluster with kubeconfig +- Environment variables configured + +### 30-Second Setup +```bash +cd web/cypress +source ./configure-env.sh # Interactive configuration +npm install # Install dependencies +npm run cypress:open # Start testing +``` + +**For detailed setup instructions and environment configuration, see [README.md](README.md)** + +--- + +## Test Architecture + +### 3-Layer Organization + +The Monitoring Plugin uses a 3-layer architecture for test organization: + +``` +┌─────────────────────────────────────────────────┐ +│ Layer 3: E2E Test Files │ +│ (cypress/e2e/) │ +│ - Call support scenarios │ +│ - Specify perspective (Administrator, etc.) │ +└────────────────┬────────────────────────────────┘ + │ imports +┌────────────────▼────────────────────────────────┐ +│ Layer 2: Support Scenarios │ +│ (cypress/support/monitoring or perses │ +│ - Reusable test scenarios │ +│ - Work across multiple perspectives │ +│ - Export functions with perspective parameter │ +└────────────────┬────────────────────────────────┘ + │ uses +┌────────────────▼────────────────────────────────┐ +│ Layer 1: Page Object Views │ +│ (cypress/views/) │ +│ - Reusable UI actions │ +│ - Navigation, clicks, assertions │ +│ - Use data-test attributes │ +└─────────────────────────────────────────────────┘ +``` + +### File Structure + +``` +cypress/ +├── e2e/ +│ ├── monitoring/ # Core monitoring tests (Administrator) +│ │ ├── 00.bvt_admin.cy.ts +│ │ └── regression/ +│ ├── coo/ # COO-specific tests +│ │ ├── 01.coo_bvt.cy.ts +│ │ └── 02.acm_alerting_ui.cy.ts +│ └── virtualization/ # Integration tests (Virtualization) +├── support/ +│ ├── monitoring/ # Reusable test scenarios +│ │ ├── 01.reg_alerts.cy.ts +│ │ ├── 02.reg_metrics.cy.ts +│ │ └── 03.reg_legacy_dashboards.cy.ts +│ ├── perses/ # COO/Perses scenarios +│ └── commands/ # Custom Cypress commands +└── views/ # Page object models (reusable actions) +``` + +**Benefits**: +- Test scenarios reusable across Administrator, Virtualization, and Fleet Management perspectives +- Page actions separated from test logic for better maintainability +- UI changes only require updating views, not individual tests + +--- + +## Creating Tests + +### Workflow + +1. **Layer 1 - Views**: Check/add page actions in `cypress/views/` + - Under `views/` folder, find pre-defined actions per page + - If none fits your needs, add new ones + +2. **Layer 2 - Support**: Add test scenarios to `cypress/support/monitoring/` + - Add test scenarios to cypress files under `support/` folder + - Make scenarios reusable across perspectives (Administrator, Virtualization, Fleet Management) + - If it is not applicable, in some cases for Incidents or Fleet Management, test scenarios will be written directly into Layer 3 + +3. **Layer 3 - E2E**: Verify e2e files call your scenario (usually pre-configured) + - Administrator: `e2e/monitoring/` + - Virtualization: `e2e/virtualization/` + - Fleet Management: `e2e/coo/` (for ACM) + +### Example: Support Scenario Structure + +```typescript +// In support/monitoring/01.reg_alerts.cy.ts +import { nav } from '../../views/nav'; +import { silencesListPage } from '../../views/silences-list-page'; + +export const runAlertTests = (perspective: string) => { + describe(`${perspective} perspective - Alerting > Alerts page`, () => { + + it('should filter alerts by severity', () => { + // Use page object actions from views/ + silencesListPage.filter.byName('test-alert'); + silencesListPage.rows.shouldBe('test-alert', 'Active'); + }); + }); +}; +``` + +### When to Create New Tests + +| Scenario | Action | +|----------|--------| +| New UI feature | Create new test scenario in support/ | +| Bug fix | Add test case to existing support file | +| Component update | Update existing test scenarios | +| New Perses feature | Create new test scenario in support/ | +| ACM integration | Add test in e2e/coo/ | + +### Best Practices + +1. **Use Page Objects**: Import actions from `cypress/views/` +2. **Data Test Attributes**: Prefer `data-test` over CSS selectors +3. **Keep Tests Isolated**: Each test should run independently +4. **Meaningful Assertions**: Use descriptive error messages +5. **Document Changes**: Update `E2E_TEST_SCENARIOS.md` + +--- + +## Running Tests + +### Common Commands + +```bash +cd web/cypress + +# Run all regression tests +npm run cypress:run -- --spec "cypress/e2e/**/regression/**" + +# Run specific feature regression +npm run cypress:run -- --spec "cypress/e2e/monitoring/regression/01.reg_alerts_admin.cy.ts" +npm run cypress:run -- --spec "cypress/e2e/monitoring/regression/02.reg_metrics_admin.cy.ts" +npm run cypress:run -- --spec "cypress/e2e/monitoring/regression/03.reg_legacy_dashboards_admin.cy.ts" + +# Run BVT (Build Verification Tests) +npm run cypress:run -- --spec "cypress/e2e/monitoring/00.bvt_admin.cy.ts" + +# Run COO tests +npm run cypress:run -- --spec "cypress/e2e/coo/01.coo_bvt.cy.ts" + +# Run ACM Alerting tests +npm run cypress:run -- --spec "cypress/e2e/coo/02.acm_alerting_ui.cy.ts" + +# Interactive mode (GUI) +npm run cypress:open +``` + +### Environment Setup + +**Interactive** (Recommended): +```bash +cd web/cypress +source ./configure-env.sh +``` + +**Manual Setup**: For complete environment variable reference and configuration examples, see [README.md](README.md#environment-variables-reference) + +### Regression Testing Strategy + +| Change Type | Required Tests | +|-------------|---------------| +| **UI Component Change** | Feature-specific regression + BVT | +| **API Integration Change** | Full regression suite | +| **Console Extension Change** | BVT + Navigation tests | +| **Bug Fix** | New test + Related regression | + +### Pre-PR Checklist + +- [ ] `make lint-frontend` (no errors) +- [ ] `make lint-backend` (no errors) +- [ ] Ran BVT tests locally (all passing) +- [ ] Ran regression tests for affected features (all passing) +- [ ] Created/updated tests for new features or bug fixes +- [ ] Updated `E2E_TEST_SCENARIOS.md` if added new tests + +--- + +## Troubleshooting + +### Debugging Failed Tests + +1. **Check test videos**: `web/cypress/videos/` +2. **Check screenshots**: `web/cypress/screenshots/` +3. **Run with debug**: + ```bash + export CYPRESS_DEBUG=true + npm run cypress:run + ``` +4. **Run interactively**: + ```bash + npm run cypress:open + ``` + +### Common Test Issues + +| Issue | Solution | +|-------|----------| +| Test fails intermittently | Check for timing issues, add proper waits | +| Element not found | Verify data-test attributes exist, check page object | +| Assertion fails | Review expected vs actual values, update test | +| Test hangs | Check for infinite loops or missing assertions | + +### Setup & Configuration Issues + +For environment variable issues, login problems, kubeconfig errors, and installation troubleshooting, see [README.md](README.md#troubleshooting-setup-issues) + +### CI/CD Integration + +Cypress tests run automatically in the CI pipeline: +- **Pre-merge**: BVT tests run on every PR +- **Post-merge**: Full regression suite runs on main branch +- **Konflux Pipeline**: Automated testing for release candidates + +--- + +## Additional Resources + +- **Cypress Documentation**: https://docs.cypress.io/ +- **Test Scenarios Catalog**: `E2E_TEST_SCENARIOS.md` +- **Setup Instructions**: `README.md` +- **Main Guide**: `../../AGENTS.md` + diff --git a/web/cypress/E2E_TEST_SCENARIOS.md b/web/cypress/E2E_TEST_SCENARIOS.md index 7dc05a87b..be80f9a7f 100644 --- a/web/cypress/E2E_TEST_SCENARIOS.md +++ b/web/cypress/E2E_TEST_SCENARIOS.md @@ -20,6 +20,12 @@ Located in `e2e/coo/` |------|------------|---------------|-------------| | `01.coo_bvt.cy.ts` | BVT: COO | 1. Admin perspective - Observe Menu | Verifies Observe menu navigation and submenus (Alerting, Silences, Alerting rules, Dashboards (Perses)) | +### ACM Alerting UI Tests + +| File | Test Suite | Test Scenario | Description | +|------|------------|---------------|-------------| +| `02.acm_alerting_ui.cy.ts` | ACM Alerting UI | 1. Fleet Management perspective - ACM Alerting | Validates ACM integration with COO, Fleet Management perspective navigation, local-cluster access, and ACM alert visibility (Watchdog, Watchdog-spoke, ClusterCPUHealth) | + --- ## Virtualization Tests @@ -175,22 +181,11 @@ These test scenarios are reusable test suites called by the main E2E test files. --- -## Test Statistics Summary - -| Category | Test Files | Direct it() Scenarios | Support Module Scenarios | Total Scenarios | -|----------|------------|----------------------|-------------------------|-----------------| -| **COO Tests** | 1 | 1 | 0 | 1 | -| **Virtualization Tests** | 4 | 6 | ~30+ (via support modules) | ~36+ | -| **Monitoring Tests** | 4 | 9 | ~30+ (via support modules) | ~39+ | -| **Support Modules** | 8 | 0 | 39 | 39 | -| **TOTAL** | **20** | **29** | **39** | **~128+** | - ---- - ## Perspectives Tested - **Administrator** - Standard admin perspective with full cluster access - **Virtualization** - Virtualization-specific perspective with integrated monitoring +- **Fleet Management** - ACM multi-cluster management perspective with observability integration ## Namespace Scopes diff --git a/web/cypress/README.md b/web/cypress/README.md index 4ed4c12c3..93e1e682a 100644 --- a/web/cypress/README.md +++ b/web/cypress/README.md @@ -1,178 +1,413 @@ -# Openshift Monitoring Plugin and Monitoring Console Plugin UI Tests -These console tests are related to Monitoring Plugin deployed by Cluster Monitoring Operator (CMO) as part of OCP - Observe menu with Alerting, Metrics, Dashboards pages and other related Alerting links. -Besides, Monitoring Console Plugin deployed by Cluster Observability Operator through Monitoring UIPlugin installation. +# Cypress Setup & Configuration Guide -## Test Documentation -For a comprehensive overview of all E2E test scenarios, including COO (Cluster Observability Operator), Monitoring, and Incidents tests, see [E2E_TEST_SCENARIOS.md](./E2E_TEST_SCENARIOS.md). +> **Technical setup and environment configuration for Monitoring Plugin Cypress tests** -## Prerequisite -1. [node.js](https://nodejs.org/) >= 18 +For testing workflows, test architecture, and creating tests, see **[CYPRESS_TESTING_GUIDE.md](CYPRESS_TESTING_GUIDE.md)** +--- + +## Quick Start -## Install dependencies -All required dependencies are defined in `package.json` in order to run Cypress tests, run `npm install` so that dependencies will be installed in `node_modules` folder ```bash -$ npm install -$ ls -ltr -node_modules/ -> dependencies will be installed at runtime here +cd web/cypress +npm install # Install dependencies +source ./configure-env.sh # Interactive configuration +npm run cypress:open # Start Cypress GUI ``` -## Running locally +--- -### Export necessary variables -in order to run Cypress tests, we need to export some environment variables that Cypress can read then pass down to our tests, currently we have following environment variables defined and used. +## Prerequisites -Using a non-admin user. -```bash -export CYPRESS_BASE_URL=https:// -export CYPRESS_LOGIN_IDP=flexy-htpasswd-provider -export CYPRESS_LOGIN_USERS=username:password -export CYPRESS_KUBECONFIG_PATH=~/Downloads/kubeconfig -``` -Using kubeadmin user. -```bash -export CYPRESS_BASE_URL=https:// -export CYPRESS_LOGIN_IDP=kube:admin -export CYPRESS_LOGIN_USERS=kubeadmin:password -export CYPRESS_KUBECONFIG_PATH=~/Downloads/kubeconfig -``` -Set the following var to use custom Monitoring Plugin image (that goes on Cluster Monitoring Operator). The image will be patched in CMO CSV. -```bash -export CYPRESS_MP_IMAGE= -``` +- **Node.js**: >= 18 + +--- + +## Installation + +Install Cypress and all dependencies: -Set the var to skip Cluster Observability and all the required operators installation. ```bash -export CYPRESS_SKIP_COO_INSTALL=true +npm install ``` -Set the var to install Cluster Observability Operator from redhat-operators catalog source. +Dependencies are defined in `package.json` and will be installed in `node_modules/`. + +--- + +## Environment Configuration + +### Interactive Setup (Recommended) + +The `configure-env.sh` script provides an interactive way to set up all required environment variables: + ```bash -export CYPRESS_COO_UI_INSTALL=true +source ./configure-env.sh ``` -Set the var to install Cluster Observability Operator using Konflux bundle. +**Features**: +- Automatic prompting for all CYPRESS_ variables +- Automatic discovery and numbered selection of `*kubeconfig*` files in `$HOME/Downloads` +- Validates required variables + +**Alternative - Generate Export File**: ```bash -export CYPRESS_KONFLUX_COO_BUNDLE_IMAGE= +./configure-env.sh ``` -Set the var to use custom Cluster Observability Operator bundle image. +Creates `export-env.sh` that you can source later: `source export-env.sh` + +--- + +## Test Configuration Scenarios + +All scenarios require the [standard variables](#required-variables) (`CYPRESS_BASE_URL`, `CYPRESS_LOGIN_IDP`, `CYPRESS_LOGIN_USERS`, `CYPRESS_KUBECONFIG_PATH`). + +### General Scenarios + +| Scenario | Key Variables | Description | +|----------|---------------|-------------| +| **Released Version** | `CYPRESS_COO_UI_INSTALL=true` | Install operators from redhat-operators catalog. Production-like testing. | +| **Pre-provisioned COO** | `CYPRESS_SKIP_COO_INSTALL=true`, optionally `CYPRESS_COO_NAMESPACE=` | COO already installed. Tests still enable the monitoring plugin. Specify namespace if non-default. | +| **Pre-provisioned Virtualization** | `CYPRESS_SKIP_KBV_INSTALL=true` | OpenShift Virtualization already installed. | +| **Local Dev / PR Testing** | `CYPRESS_SKIP_ALL_INSTALL=true` | Run UI locally via `make start-feature-frontend` ([details](../../README.md#development)). Skips all setup. | +| **Custom Images** | `CYPRESS_MP_IMAGE`, `CYPRESS_MCP_CONSOLE_IMAGE`, `CYPRESS_CHA_IMAGE`, `CYPRESS_CUSTOM_COO_BUNDLE_IMAGE` | Patch component images in the CSV, or replace the operator bundle. Combine with an installation method above. | +| **FBC Image** | `CYPRESS_FBC_STAGE_COO_IMAGE` | Install COO from File-Based Catalog image. For release validation. | +| **Konflux CI Bundle** | `CYPRESS_KONFLUX_COO_BUNDLE_IMAGE=` | Install COO from Konflux CI bundle. For PR/CI testing. | + +### Test Areas + +| Area | Description | Run Command | +|------|-------------|-------------| +| **Monitoring (CMO)** | Core monitoring tests against CMO stack. No additional operator installation needed. | `npm run test-cypress-monitoring` | +| **COO (Perses, Dashboards, Incidents)** | Requires COO installation. | `npm run test-cypress-coo` | +| **Incidents** | COO subset. Set `CYPRESS_TIMEZONE` to match cluster timezone. | `npm run test-cypress-incidents` | +| **Virtualization** | Requires OpenShift Virtualization (KubeVirt) installation. | `npm run test-cypress-virtualization` | + +--- + +## Environment Variables Reference + +### Required Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `CYPRESS_BASE_URL` | OpenShift Console URL | `https://console-openshift-console.apps...` | +| `CYPRESS_LOGIN_IDP` | Identity provider name | `flexy-htpasswd-provider` or `kube:admin` | +| `CYPRESS_LOGIN_IDP_DEV_USER`| Identity provider name for devuser | `flexy-htpasswd-provider` or `my_htpasswd_provider`| +| `CYPRESS_LOGIN_USERS` | Login credentials | `username:password` or `kubeadmin:password` or `kubeadmin:password,user1:password,user2:password` | +| `CYPRESS_KUBECONFIG_PATH` | Path to kubeconfig file | `~/Downloads/kubeconfig` | + +### Plugin Image Configuration + +| Variable | Description | Use Case | +|----------|-------------|----------| +| `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 + +| Variable | Default | Description | +|----------|---------|-------------| +| `CYPRESS_SKIP_COO_INSTALL` | `false` | Skip Cluster Observability Operator installation | +| `CYPRESS_SKIP_KBV_INSTALL` | `false` | Skip OpenShift Virtualization installation | +| `CYPRESS_SKIP_ALL_INSTALL` | `false` | Skip all operator installations (for pre-provisioned clusters) | +| `CYPRESS_COO_UI_INSTALL` | `false` | Install COO from redhat-operators catalog | +| `CYPRESS_KBV_UI_INSTALL` | `false` | Install Virtualization from redhat-operators catalog | + +### Bundle Images + +| Variable | Description | +|----------|-------------| +| `CYPRESS_KONFLUX_COO_BUNDLE_IMAGE` | COO bundle image from Konflux | +| `CYPRESS_CUSTOM_COO_BUNDLE_IMAGE` | Custom COO bundle image | +| `CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE` | Virtualization bundle image from Konflux | +| `CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE` | Custom Virtualization bundle image | + +### FBC images + +| Variable | Description | +|----------|-------------| +| `CYPRESS_FBC_STAGE_COO_IMAGE` | Cluster Observability Operator FBC image | +| `CYPRESS_FBC_STAGE_KBV_IMAGE` | Virtualization FBC image | + +### Testing Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `CYPRESS_SESSION` | `false` | Enable session management for faster execution | +| `CYPRESS_DEBUG` | `false` | Enable debug mode logging in headless mode | + +### Incidents Testing Configuration + +**Used primarily for Incidents feature testing:** + +| Variable | Default | Description | +|----------|---------|-------------| +| `CYPRESS_TIMEZONE` | `UTC` | Cluster timezone for incident timeline calculations | +| `CYPRESS_MOCK_NEW_METRICS` | `false` | Transform old metric names to new format in mocks (temporary workaround for testing against locally built instances) | + +**Example:** ```bash -export CYPRESS_CUSTOM_COO_BUNDLE_IMAGE= +export CYPRESS_TIMEZONE="America/New_York" +export CYPRESS_MOCK_NEW_METRICS=true ``` -Set the following var to use custom Monitoring Console Plugin UI plugin image. The image will be patched in Cluster Observability Operator CSV. +--- + +## Running Cypress + +### Interactive Mode (GUI) + +Best for test development and debugging: + ```bash -export CYPRESS_MCP_CONSOLE_IMAGE= +npm run cypress:open ``` -Set the following var to specify the cluster timezone for incident timeline calculations. Defaults to UTC if not specified. +### Headless Mode (CI-style) + +For automated testing: + ```bash -export CYPRESS_TIMEZONE= +npm run cypress:run ``` -Set the following var to transform old metric names to new format in mocks (temporary workaround for testing against locally built instances). +### Running Specific Tests + ```bash -export CYPRESS_MOCK_NEW_METRICS=false +# COO BVT tests +npm run cypress:run -- --spec "cypress/e2e/coo/01.coo_bvt.cy.ts" + +# ACM Alerting tests +npm run cypress:run -- --spec "cypress/e2e/coo/02.acm_alerting_ui.cy.ts" + +# Monitoring BVT tests +npm run cypress:run -- --spec "cypress/e2e/monitoring/00.bvt_admin.cy.ts" + +# All Monitoring Regression tests +npm run cypress:run -- --spec "cypress/e2e/monitoring/regression/**" + +# All Virtualization IVT tests +npm run cypress:run -- --spec "cypress/e2e/virtualization/**" + +# Incidents tests (requires CYPRESS_TIMEZONE and optionally CYPRESS_MOCK_NEW_METRICS) +npm run cypress:run -- --spec "cypress/e2e/**/incidents*.cy.ts" ``` -Set the following var to enable Cypress session management for faster test execution. +**Note**: Incidents tests require `CYPRESS_TIMEZONE` to be set to match your cluster's timezone configuration. See [Incidents Testing Configuration](#incidents-testing-configuration) for details. + +**For comprehensive test commands and regression testing strategies, see [CYPRESS_TESTING_GUIDE.md](CYPRESS_TESTING_GUIDE.md)** + +--- + +### Running tests by tags + +Tests are organized using tags for selective execution using [@cypress/grep](https://github.com/cypress-io/cypress/tree/develop/npm/grep). + +#### Tag Categories + +**1. Basic Tags:** +- `@smoke` - Fast BVT tests +- `@demo` - Interactive demo tests (no assertions, skipped in CI) +- `@flaky` - Tests that don't pass reliably +- `@xfail` - Tests for known bugs expected to fail +- `@slow` - Long-running e2e tests (15+ minutes) + +**2. High-Level Component Tags:** +- `@monitoring` - Monitoring plugin tests +- `@incidents` - Incidents feature tests +- `@coo` - Cluster Observability Operator functionality tests (operator installation, ACM integration) +- `@virtualization` - Virtualization integration tests +- `@alerts` - Alert-related tests +- `@metrics` - Metrics-related tests +- `@dashboards` - Dashboard-related tests (includes Perses) + +**3. Specific Feature Tags** (format: `@{component}-{label}`): +- Example: `@incidents-redux` +- Add specific feature tags as needed + +**4. JIRA Tags** (format: `@JIRA-{ID}`): +- Example: `@JIRA-OU-1033` +- Link tests to specific JIRA issues + +#### Running Tests by Tags + +**Run smoke tests (BVT):** ```bash -export CYPRESS_SESSION=true +npx cypress run --env grepTags=@smoke +# or +npm run test-cypress-smoke ``` -Set the following var to enable Cypress debug mode to log in headless mode. +**Run regression tests (all non-smoke tests):** ```bash -export CYPRESS_DEBUG=true +npx cypress run --env grepTags="--@smoke --@flaky --@demo" ``` -Set the following var to skip all operator installation, cleanup, and verifications (useful for pre-provisioned environments where COO and Monitoring UI Plugin are already installed). +**Run component-specific tests:** ```bash -export CYPRESS_SKIP_ALL_INSTALL=false +npm run test-cypress-monitoring # All monitoring tests +npm run test-cypress-incidents # All incidents tests +npm run test-cypress-coo # COO operator functionality tests +npm run test-cypress-virtualization # All virtualization integration tests +npm run test-cypress-alerts # All alerts tests +npm run test-cypress-metrics # All metrics tests +npm run test-cypress-dashboards # All dashboards tests (includes Perses) ``` -Integration Testing variables - -Set the var to skip Openshift Virtualization and all the required operators installation. +**Run smoke tests for specific component:** ```bash -export CYPRESS_SKIP_KBV_INSTALL=false +npm run test-cypress-monitoring-bvt # Monitoring smoke tests +npm run test-cypress-coo-bvt # COO smoke tests ``` -Set the var to install Openshift Virtualization from redhat-operators catalog source. +**Run regression for specific component:** ```bash -export CYPRESS_KBV_UI_INSTALL=true +npm run test-cypress-monitoring-regression # All monitoring except smoke ``` -Set the var to install Openshift Virtualization Operator using Konflux bundle. +**Run tests with multiple tags (OR logic):** ```bash -export CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE= +npx cypress run --env grepTags="@smoke @slow" ``` -# Set the var to use custom Openshift Virtualization Operator bundle image +**Run tests with BOTH tags (AND logic):** ```bash -export CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE= +npx cypress run --env grepTags="@smoke+@incidents" ``` -Set the var to use Openshift Virtualization Operator FBC image +**Complex filtering:** ```bash -export CYPRESS_FBC_STAGE_KBV_IMAGE= +npx cypress run --env grepTags="@incidents --@slow --@flaky" ``` -### Environment Configuration Script +--- -The `configure-env.sh` script provides an interactive way to set up all the required environment variables. This script eliminates the need to manually export each variable and helps find the correct kubeconfig file. -**Features:** -- Automatic prompting for all CYPRESS_ variables -- Automatic discovery and numbered selection of `*kubeconfig*` files in `$HOME/Downloads` dir +## Test Results -**Usage:** -```bash -# Note: source command requires Bash shell -source ./configure-env.sh -``` -To export variables directly (Bash only). +### Videos -**File generation** -```bash -./configure-env.sh -``` -Creates an export file you can source later. (`source "export-env.sh`) +Test recordings are saved automatically: +- **Location**: `web/cypress/videos/` +- **Format**: `.mp4` +- **Generated**: For all test runs (pass or fail) +### Screenshots -### Before running cypress -- Make sure cluster's kubeconfig file is located at the correct environment variable / path you have exported -- The file to run Monitoring Plugin tests: bvt.cy.ts -- The file to run Monitoring Console Plugin tests (COO with Monitoring UIPlugin): coo_bvt.cy.ts +Screenshots captured on test failures: +- **Location**: `web/cypress/screenshots/` +- **Format**: `.png` +- **Generated**: Only on failures -### Start Cypress -We can either open Cypress GUI(open) or run Cypress in headless mode(run) to run the tests. -```bash -npx cypress open -npx cypress run -``` +--- -Some examples to run a specific file(s) +## Troubleshooting Setup Issues -It runs the COO BVT only +### Issue: Cypress Cannot Find Chrome/Browser + +**Solution**: Install Chrome or specify browser ```bash -cd monitoring-plugin/web/cypress -npx cypress run --spec "cypress/e2e/coo/01.coo_bvt.cy.ts" +npm run cypress:open --browser firefox ``` -It runs the Monitoring BVT only +### Issue: Environment Variables Not Set + +**Symptoms**: Tests fail with "BASE_URL is not defined" + +**Solution**: +1. Verify variables are exported: `echo $CYPRESS_BASE_URL` +2. Re-run configuration: `source ./configure-env.sh` +3. Ensure you're in the correct shell session + +### Issue: Kubeconfig Not Found + +**Symptoms**: "ENOENT: no such file or directory" + +**Solution**: ```bash -npx cypress run --spec "cypress/e2e/monitoring/01.bvt_monitoring.cy.ts" +# Check file exists +ls -la $CYPRESS_KUBECONFIG_PATH + +# Update path if needed +export CYPRESS_KUBECONFIG_PATH=/correct/path/to/kubeconfig ``` -It runs the Monitoring Regression tests +### Issue: Login Fails + +**Symptoms**: "User authentication failed" + +**Solution**: +1. Verify IDP name: Check OpenShift OAuth configuration +2. Verify credentials are correct +3. For kubeadmin, use `kube:admin` as IDP + +### Issue: Tests Are Slow + +**Solution**: Enable session management ```bash -npx cypress run --spec "cypress/e2e/monitoring/regression/**" +export CYPRESS_SESSION=true ``` -It runs the Virtualization IVT tests -```bash -npx cypress run --spec "cypress/e2e/virtualization/**" +--- + +## Test Organization + +### Directory Structure + ``` +cypress/ +├── e2e/ # Test files by perspective +│ ├── monitoring/ # Core monitoring (Administrator) +│ ├── coo/ # COO-specific tests +│ └── virtualization/ # Virtualization integration +├── support/ # Reusable test scenarios +│ ├── monitoring/ # Test scenario modules +│ ├── perses/ # Perses scenarios +│ └── commands/ # Custom Cypress commands +├── views/ # Page object models +├── fixtures/ # Test data and mocks +└── E2E_TEST_SCENARIOS.md # Complete test catalog +``` + +**For test architecture and creating new tests, see [CYPRESS_TESTING_GUIDE.md](CYPRESS_TESTING_GUIDE.md)** + +--- + +## Documentation + +- **Testing Guide**: [CYPRESS_TESTING_GUIDE.md](CYPRESS_TESTING_GUIDE.md) - Complete testing workflows and test creation +- **Test Scenarios**: [E2E_TEST_SCENARIOS.md](./E2E_TEST_SCENARIOS.md) - Catalog of all test scenarios +- **Project Guide**: [AGENTS.md](../../AGENTS.md) - Main developer guide +- **Cypress Docs**: https://docs.cypress.io/ - Official Cypress documentation + +--- + +## Additional Resources + +- **Configure Script**: `./configure-env.sh` - Interactive setup +- **Export Script**: `./export-env.sh` - Generated environment file +- **Fixtures**: `./fixtures/` - Test data and mocks +- **Support**: `./support/` - Custom commands and utilities + +--- + +## Incident Detection Test Documentation + +For configuration scenarios, see [COO Tests](#test-configuration-scenarios) above. + +### Incidents-Specific Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `CYPRESS_TIMEZONE` | `UTC` | Cluster timezone for incident timeline calculations | +| `CYPRESS_MOCK_NEW_METRICS` | `false` | Transform old metric names to new format in mocks | + +### Test Case Documentation + +Detailed test documentation: [`docs/incident_detection/tests/`](../../docs/incident_detection/tests/) + +--- -### Testing recording -You can access the recording for your test under monitoring-plugin/web/cypress/videos folder \ No newline at end of file +*For questions about test architecture, creating tests, or testing workflows, refer to [CYPRESS_TESTING_GUIDE.md](CYPRESS_TESTING_GUIDE.md)* diff --git a/web/cypress/configure-env.sh b/web/cypress/configure-env.sh index 86bd7b277..eab835863 100755 --- a/web/cypress/configure-env.sh +++ b/web/cypress/configure-env.sh @@ -99,7 +99,9 @@ ask_yes_no() { bool_to_default_yn() { # Map truthy/falsey env values to y/n default for yes/no prompts local v=${1-} - case "${v,,}" in + # Convert to lowercase in a portable way + v=$(echo "$v" | tr '[:upper:]' '[:lower:]') + case "$v" in true|1|yes|y) echo "y" ;; false|0|no|n|"") echo "n" ;; *) echo "n" ;; @@ -167,11 +169,13 @@ print_current_config() { # Optional vars print_var "CYPRESS_MP_IMAGE" "${CYPRESS_MP_IMAGE-}" + print_var "CYPRESS_COO_NAMESPACE" "${CYPRESS_COO_NAMESPACE-}" print_var "CYPRESS_SKIP_COO_INSTALL" "${CYPRESS_SKIP_COO_INSTALL-}" print_var "CYPRESS_COO_UI_INSTALL" "${CYPRESS_COO_UI_INSTALL-}" 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-}" @@ -182,6 +186,7 @@ print_current_config() { print_var "CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE" "${CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE-}" print_var "CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE" "${CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE-}" print_var "CYPRESS_FBC_STAGE_KBV_IMAGE" "${CYPRESS_FBC_STAGE_KBV_IMAGE-}" + print_var "CYPRESS_LOGIN_IDP_DEV_USER" "${CYPRESS_LOGIN_IDP_DEV_USER-}" } main() { @@ -217,11 +222,13 @@ main() { local def_login_users=${CYPRESS_LOGIN_USERS-} local def_kubeconfig=${CYPRESS_KUBECONFIG_PATH-${KUBECONFIG-}} local def_mp_image=${CYPRESS_MP_IMAGE-} + local def_coo_namespace=${CYPRESS_COO_NAMESPACE-} local def_skip_coo=${CYPRESS_SKIP_COO_INSTALL-} local def_coo_ui_install=${CYPRESS_COO_UI_INSTALL-} 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-} @@ -232,6 +239,7 @@ main() { local def_konflux_kbv_bundle=${CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE-} local def_custom_kbv_bundle=${CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE-} local def_fbc_stage_kbv_image=${CYPRESS_FBC_STAGE_KBV_IMAGE-} + local def_login_idp_dev_user=${CYPRESS_LOGIN_IDP_DEV_USER-} # Required basics local base_url while true; do @@ -291,7 +299,11 @@ main() { # User declined current, try to find kubeconfigs from Downloads if [[ -d "$HOME/Downloads" ]]; then local kubeconfig_files - mapfile -t kubeconfig_files < <(ls -t "$HOME/Downloads"/*kubeconfig* 2>/dev/null | head -10) + # Use 'while read' instead of 'mapfile' for bash 3.x compatibility (macOS) + kubeconfig_files=() + while IFS= read -r file; do + kubeconfig_files+=("$file") + done < <(ls -t "$HOME/Downloads"/*kubeconfig* 2>/dev/null | head -10) if [[ ${#kubeconfig_files[@]} -gt 0 ]]; then echo "" @@ -353,7 +365,11 @@ main() { # No current kubeconfig set, try to find kubeconfigs from Downloads if [[ -d "$HOME/Downloads" ]]; then local kubeconfig_files - mapfile -t kubeconfig_files < <(ls -t "$HOME/Downloads"/*kubeconfig* 2>/dev/null | head -10) + # Use 'while read' instead of 'mapfile' for bash 3.x compatibility (macOS) + kubeconfig_files=() + while IFS= read -r file; do + kubeconfig_files+=("$file") + done < <(ls -t "$HOME/Downloads"/*kubeconfig* 2>/dev/null | head -10) if [[ ${#kubeconfig_files[@]} -gt 0 ]]; then echo "" @@ -400,6 +416,9 @@ main() { local mp_image mp_image=$(ask "Custom Monitoring Plugin image (CYPRESS_MP_IMAGE)" "$def_mp_image") + local coo_namespace + coo_namespace=$(ask "Cluster Observability Operator namespace (CYPRESS_COO_NAMESPACE)" "${def_coo_namespace:-openshift-cluster-observability-operator}") + local skip_coo_install_ans skip_coo_install_ans=$(ask_yes_no "Skip Cluster Observability installation? (sets CYPRESS_SKIP_COO_INSTALL)" "$(bool_to_default_yn "$def_skip_coo")") local skip_coo_install="false" @@ -419,6 +438,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}") @@ -461,7 +483,9 @@ main() { local fbc_stage_kbv_image fbc_stage_kbv_image=$(ask "KBV FBC image (CYPRESS_FBC_STAGE_KBV_IMAGE)" "$def_fbc_stage_kbv_image") - + local login_idp_dev_user + login_idp_dev_user=$(ask "Login identity provider dev user (CYPRESS_LOGIN_IDP_DEV_USER)" "$def_login_idp_dev_user") + # Build export lines with safe quoting local -a export_lines export_lines+=("export CYPRESS_BASE_URL='$(printf %s "$base_url" | escape_for_single_quotes)'" ) @@ -471,6 +495,9 @@ main() { if [[ -n "$mp_image" ]]; then export_lines+=("export CYPRESS_MP_IMAGE='$(printf %s "$mp_image" | escape_for_single_quotes)'" ) fi + if [[ -n "$coo_namespace" ]]; then + export_lines+=("export CYPRESS_COO_NAMESPACE='$(printf %s "$coo_namespace" | escape_for_single_quotes)'" ) + fi export_lines+=("export CYPRESS_SKIP_COO_INSTALL='$(printf %s "$skip_coo_install" | escape_for_single_quotes)'" ) export_lines+=("export CYPRESS_COO_UI_INSTALL='$(printf %s "$coo_ui_install" | escape_for_single_quotes)'" ) if [[ -n "$konflux_bundle" ]]; then @@ -482,6 +509,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 @@ -505,7 +535,9 @@ main() { if [[ -n "$fbc_stage_kbv_image" ]]; then export_lines+=("export CYPRESS_FBC_STAGE_KBV_IMAGE='$(printf %s "$fbc_stage_kbv_image" | escape_for_single_quotes)'" ) fi - + if [[ -n "$login_idp_dev_user" ]]; then + export_lines+=("export CYPRESS_LOGIN_IDP_DEV_USER='$(printf %s "$login_idp_dev_user" | escape_for_single_quotes)'" ) + fi echo "" if is_sourced; then # Export directly into current shell @@ -524,26 +556,29 @@ main() { echo "" echo "Configured values:" - echo " CYPRESS_BASE_URL=$CYPRESS_BASE_URL" - echo " CYPRESS_LOGIN_IDP=${CYPRESS_LOGIN_IDP:-$login_idp}" - echo " CYPRESS_LOGIN_USERS=${CYPRESS_LOGIN_USERS:-$login_users}" - echo " CYPRESS_KUBECONFIG_PATH=${CYPRESS_KUBECONFIG_PATH:-$kubeconfig}" - [[ -n "${CYPRESS_MP_IMAGE-}$mp_image" ]] && echo " CYPRESS_MP_IMAGE=${CYPRESS_MP_IMAGE:-$mp_image}" - echo " CYPRESS_SKIP_COO_INSTALL=${CYPRESS_SKIP_COO_INSTALL:-$skip_coo_install}" - echo " CYPRESS_COO_UI_INSTALL=${CYPRESS_COO_UI_INSTALL:-$coo_ui_install}" - [[ -n "${CYPRESS_KONFLUX_COO_BUNDLE_IMAGE-}$konflux_bundle" ]] && echo " CYPRESS_KONFLUX_COO_BUNDLE_IMAGE=${CYPRESS_KONFLUX_COO_BUNDLE_IMAGE:-$konflux_bundle}" - [[ -n "${CYPRESS_CUSTOM_COO_BUNDLE_IMAGE-}$custom_coo_bundle" ]] && echo " CYPRESS_CUSTOM_COO_BUNDLE_IMAGE=${CYPRESS_CUSTOM_COO_BUNDLE_IMAGE:-$custom_coo_bundle}" - [[ -n "${CYPRESS_MCP_CONSOLE_IMAGE-}$mcp_console_image" ]] && echo " CYPRESS_MCP_CONSOLE_IMAGE=${CYPRESS_MCP_CONSOLE_IMAGE:-$mcp_console_image}" - [[ -n "${CYPRESS_TIMEZONE-}$timezone" ]] && echo " CYPRESS_TIMEZONE=${CYPRESS_TIMEZONE:-$timezone}" - echo " CYPRESS_MOCK_NEW_METRICS=${CYPRESS_MOCK_NEW_METRICS:-$mock_new_metrics}" - echo " CYPRESS_SESSION=${CYPRESS_SESSION:-$session}" - echo " CYPRESS_DEBUG=${CYPRESS_DEBUG:-$debug}" - echo " CYPRESS_SKIP_ALL_INSTALL=${CYPRESS_SKIP_ALL_INSTALL:-$skip_all_install}" - echo " CYPRESS_SKIP_KBV_INSTALL=${CYPRESS_SKIP_KBV_INSTALL:-$skip_kbv_install}" - echo " CYPRESS_KBV_UI_INSTALL=${CYPRESS_KBV_UI_INSTALL:-$kbv_ui_install}" - [[ -n "${CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE-}$konflux_kbv_bundle" ]] && echo " CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE=${CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE:-$konflux_kbv_bundle}" - [[ -n "${CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE-}$custom_kbv_bundle" ]] && echo " CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE=${CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE:-$custom_kbv_bundle}" - [[ -n "${CYPRESS_FBC_STAGE_KBV_IMAGE-}$fbc_stage_kbv_image" ]] && echo " CYPRESS_FBC_STAGE_KBV_IMAGE=${CYPRESS_FBC_STAGE_KBV_IMAGE:-$fbc_stage_kbv_image}" + echo " CYPRESS_BASE_URL=$base_url" + echo " CYPRESS_LOGIN_IDP=$login_idp" + echo " CYPRESS_LOGIN_USERS=$login_users" + echo " CYPRESS_LOGIN_IDP_DEV_USER=$login_idp_dev_user" + echo " CYPRESS_KUBECONFIG_PATH=$kubeconfig" + [[ -n "$mp_image" ]] && echo " CYPRESS_MP_IMAGE=$mp_image" + [[ -n "$coo_namespace" ]] && echo " CYPRESS_COO_NAMESPACE=$coo_namespace" + echo " CYPRESS_SKIP_COO_INSTALL=$skip_coo_install" + echo " CYPRESS_COO_UI_INSTALL=$coo_ui_install" + [[ -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" + echo " CYPRESS_DEBUG=$debug" + echo " CYPRESS_SKIP_ALL_INSTALL=$skip_all_install" + echo " CYPRESS_SKIP_KBV_INSTALL=$skip_kbv_install" + echo " CYPRESS_KBV_UI_INSTALL=$kbv_ui_install" + [[ -n "$konflux_kbv_bundle" ]] && echo " CYPRESS_KONFLUX_KBV_BUNDLE_IMAGE=$konflux_kbv_bundle" + [[ -n "$custom_kbv_bundle" ]] && echo " CYPRESS_CUSTOM_KBV_BUNDLE_IMAGE=$custom_kbv_bundle" + [[ -n "$fbc_stage_kbv_image" ]] && echo " CYPRESS_FBC_STAGE_KBV_IMAGE=$fbc_stage_kbv_image" } main "$@" diff --git a/web/cypress/e2e/coo/01.coo_bvt.cy.ts b/web/cypress/e2e/coo/01.coo_bvt.cy.ts index ae6250c46..fd997194f 100644 --- a/web/cypress/e2e/coo/01.coo_bvt.cy.ts +++ b/web/cypress/e2e/coo/01.coo_bvt.cy.ts @@ -1,10 +1,11 @@ import { commonPages } from '../../views/common'; import { nav } from '../../views/nav'; +import { troubleshootingPanelPage } from '../../views/troubleshooting-panel'; // Set constants for the operators that need to be installed for tests. const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -18,7 +19,7 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('BVT: COO', () => { +describe('BVT: COO', { tags: ['@smoke', '@coo'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); @@ -27,13 +28,18 @@ describe('BVT: 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'); nav.tabs.switchTab('Alerting rules'); - // nav.tabs.switchTab('Incidents'); + nav.tabs.switchTab('Incidents'); nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); commonPages.titleShouldHaveText('Dashboards'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + troubleshootingPanelPage.openSignalCorrelation(); + troubleshootingPanelPage.troubleshootingPanelPageShouldBeLoadedEnabled(); }); diff --git a/web/cypress/e2e/coo/01.coo_ivt.cy.ts b/web/cypress/e2e/coo/01.coo_ivt.cy.ts new file mode 100644 index 000000000..b7f42206c --- /dev/null +++ b/web/cypress/e2e/coo/01.coo_ivt.cy.ts @@ -0,0 +1,38 @@ +import { guidedTour } from '../../views/tour'; +import { troubleshootingPanelPage } from '../../views/troubleshooting-panel'; + +// Set constants for the operators that need to be installed for tests. +const KBV = { + namespace: 'openshift-cnv', + packageName: 'kubevirt-hyperconverged', + operatorName: 'kubevirt-hyperconverged-operator.v4.19.6', + config: { + kind: 'HyperConverged', + name: 'kubevirt-hyperconverged', + }, + crd: { + kubevirt: 'kubevirts.kubevirt.io', + hyperconverged: 'hyperconvergeds.hco.kubevirt.io', + } +}; + +describe('IVT: Monitoring UIPlugin + Virtualization', { tags: ['@smoke', '@coo'] }, () => { + + before(() => { + cy.beforeBlockVirtualization(KBV); + }); + + it('1. Virtualization perspective - Observe Menu', () => { + cy.log('Virtualization perspective - Observe Menu and verify all submenus'); + cy.switchPerspective('Virtualization'); + guidedTour.closeKubevirtTour(); + troubleshootingPanelPage.signalCorrelationShouldNotBeVisible(); + cy.switchPerspective('Core platform', 'Administrator'); + + }); + + /** + * TODO: To be replaced by COO validation such as Dashboards (Perses) scenarios + */ + +}); diff --git a/web/cypress/e2e/coo/02.acm_alerting_ui.cy.ts b/web/cypress/e2e/coo/02.acm_alerting_ui.cy.ts index bf717aebf..a2f1c79d7 100644 --- a/web/cypress/e2e/coo/02.acm_alerting_ui.cy.ts +++ b/web/cypress/e2e/coo/02.acm_alerting_ui.cy.ts @@ -1,11 +1,12 @@ // 02.acm_alerting_ui.cy.ts // E2E test for validating ACM Alerting UI integration with Cluster Observability Operator (COO) import '../../support/commands/auth-commands'; +import { commonPages } from '../../views/common'; import { nav } from '../../views/nav'; import { acmAlertingPage } from '../../views/acm-alerting-page'; const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -19,15 +20,17 @@ const MP = { }; const expectedAlerts = ['Watchdog', 'Watchdog-spoke', 'ClusterCPUHealth-jb']; -describe('ACM Alerting UI', () => { +describe('ACM Alerting UI', { tags: ['@coo', '@alerts'] }, () => { before(() => { cy.beforeBlockACM(MCP, MP); }); it('Navigate to Fleet Management > local-cluster > Observe > Alerting', () => { - // wait for console page loading completed - cy.visit('/'); - cy.get('body', { timeout: 60000 }).should('contain.text', 'Administrator'); + // check monitoring-plugin UI is not been affected + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + commonPages.titleShouldHaveText('Alerting') + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); // switch to Fleet Management page cy.switchPerspective('Fleet Management'); // close pop-up window @@ -42,14 +45,11 @@ describe('ACM Alerting UI', () => { }); // click side menu -> Observe -> Alerting nav.sidenav.clickNavLink(['Observe', 'Alerting']); - // Wait for alert tab content to become visible - cy.get('section#alerts-tab-content', { timeout: 60000 }) - .should('be.visible'); // confirm Alerting page loading completed acmAlertingPage.shouldBeLoaded(); - // check three test alerts exist + // check test alerts exist expectedAlerts.forEach((alert) => { - cy.contains('a[data-test-id="alert-resource-link"]', alert, { timeout: 60000 }) + cy.contains('a[data-test-id="alert-resource-link"]', alert, { timeout: 120000 }) .should('be.visible'); }); cy.log('Verified all expected alerts are visible on the Alerting page'); diff --git a/web/cypress/e2e/incidents/00.coo_incidents_e2e.cy.ts b/web/cypress/e2e/incidents/00.coo_incidents_e2e.cy.ts index 7ae7ae5bd..a1db71f10 100644 --- a/web/cypress/e2e/incidents/00.coo_incidents_e2e.cy.ts +++ b/web/cypress/e2e/incidents/00.coo_incidents_e2e.cy.ts @@ -7,7 +7,7 @@ import { incidentsPage } from '../../views/incidents-page'; // Set constants for the operators that need to be installed for tests. const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -21,7 +21,7 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('BVT: Incidents - e2e', () => { +describe('BVT: Incidents - e2e', { tags: ['@smoke', '@slow', '@incidents', '@e2e-real'] }, () => { let currentAlertName: string; before(() => { diff --git a/web/cypress/e2e/incidents/01.incidents.cy.ts b/web/cypress/e2e/incidents/01.incidents.cy.ts index 8d0508fdc..2d6e12f6b 100644 --- a/web/cypress/e2e/incidents/01.incidents.cy.ts +++ b/web/cypress/e2e/incidents/01.incidents.cy.ts @@ -12,7 +12,7 @@ import { commonPages } from '../../views/common'; import { incidentsPage } from '../../views/incidents-page'; const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -31,7 +31,7 @@ const NAMESPACE = 'openshift-monitoring'; const SEVERITY = 'Critical'; const ALERT_DESC = 'This is an alert meant to ensure that the entire alerting pipeline is functional. This alert is always firing, therefore it should always be firing in Alertmanager and always fire against a receiver. There are integrations with various notification mechanisms that send a notification when this alert is not firing. For example the "DeadMansSnitch" integration in PagerDuty.' const ALERT_SUMMARY = 'An alert that should always be firing to certify that Alertmanager is working properly.' -describe('BVT: Incidents - UI', () => { +describe('BVT: Incidents - UI', { tags: ['@smoke', '@incidents'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); }); diff --git a/web/cypress/e2e/incidents/02.incidents-mocking-example.cy.ts b/web/cypress/e2e/incidents/02.incidents-mocking-example.cy.ts index 7ab11f7d5..91ef3794f 100644 --- a/web/cypress/e2e/incidents/02.incidents-mocking-example.cy.ts +++ b/web/cypress/e2e/incidents/02.incidents-mocking-example.cy.ts @@ -12,7 +12,7 @@ import { incidentsPage } from '../../views/incidents-page'; import { IncidentDefinition } from '../../support/incidents_prometheus_query_mocks'; const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -26,7 +26,7 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('Incidents - Mocking Examples', () => { +describe('Incidents - Mocking Examples', { tags: ['@demo', '@incidents'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); @@ -37,21 +37,29 @@ describe('Incidents - Mocking Examples', () => { incidentsPage.goTo(); }); - it('1. Mock healthy cluster from fixture', () => { + it('1. Mock silenced and firing incidents with mixed severity', () => { + cy.log('Setting up silenced critical and firing warning incidents'); + cy.mockIncidentFixture('incident-scenarios/silenced-and-firing-mixed-severity.yaml'); + + cy.log('One silenced critical incident (resolved) and one firing warning incident should be visible'); + cy.pause(); + }); + + it('2. Mock healthy cluster from fixture', () => { cy.log('Setting up healthy cluster scenario from fixture'); cy.mockIncidentFixture('incident-scenarios/0-healthy-cluster.yaml'); cy.pause(); }); - it('2. Mock single incident with critical and warning alerts', () => { + it('3. Mock single incident with critical and warning alerts', () => { cy.log('Setting up single incident with critical and warning alerts from fixture'); cy.mockIncidentFixture('incident-scenarios/1-single-incident-firing-critical-and-warning-alerts.yaml'); cy.log('Single incident with mixed severity alerts should be visible'); cy.pause(); }); - it('3. Mock multi-incidents with resolved and firing alerts', () => { + it('4. Mock multi-incidents with resolved and firing alerts', () => { cy.log('Setting up multi-incidents with resolved and firing alerts from fixture'); cy.mockIncidentFixture('incident-scenarios/2-multi-incidents-multi-alerts-resolved-and-firing.yaml'); @@ -59,7 +67,7 @@ describe('Incidents - Mocking Examples', () => { cy.pause(); }); - it('4. Mock multi-severity overlapping incidents', () => { + it('5. Mock multi-severity overlapping incidents', () => { cy.log('Setting up multi-severity overlapping incidents from fixture'); cy.mockIncidentFixture('incident-scenarios/3-multi-severity-overlapping-incidents.yaml'); @@ -67,7 +75,7 @@ describe('Incidents - Mocking Examples', () => { cy.pause(); }); - it('5. Mock single incident with escalating severity alerts', () => { + it('6. Mock single incident with escalating severity alerts', () => { cy.log('Setting up single incident with escalating severity alerts from fixture'); cy.mockIncidentFixture('incident-scenarios/5-escalating-severity-incident.yaml'); @@ -76,7 +84,7 @@ describe('Incidents - Mocking Examples', () => { }); - it('6. Mock empty incident state', () => { + it('7. Mock empty incident state', () => { cy.log('Setting up empty incident state'); cy.mockIncidents([]); diff --git a/web/cypress/e2e/incidents/regression/01.reg_filtering.cy.ts b/web/cypress/e2e/incidents/regression/01.reg_filtering.cy.ts index 6b142e045..c7a911f44 100644 --- a/web/cypress/e2e/incidents/regression/01.reg_filtering.cy.ts +++ b/web/cypress/e2e/incidents/regression/01.reg_filtering.cy.ts @@ -11,7 +11,7 @@ Verifies: OU-727 import { incidentsPage } from '../../../views/incidents-page'; const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -25,7 +25,7 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('Regression: Incidents Filtering', () => { +describe('Regression: Incidents Filtering', { tags: ['@incidents'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); diff --git a/web/cypress/e2e/incidents/regression/02.reg_ui_charts_comprehensive.cy.ts b/web/cypress/e2e/incidents/regression/02.reg_ui_charts_comprehensive.cy.ts index 71999c6d1..15754a12f 100644 --- a/web/cypress/e2e/incidents/regression/02.reg_ui_charts_comprehensive.cy.ts +++ b/web/cypress/e2e/incidents/regression/02.reg_ui_charts_comprehensive.cy.ts @@ -70,7 +70,7 @@ function verifyIncidentBarIsVisible(index: number, context: string) { verifyIncidentBarHasVisiblePaths(index, context); } const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -84,7 +84,7 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('Regression: Charts UI - Comprehensive', () => { +describe('Regression: Charts UI - Comprehensive', { tags: ['@incidents'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); diff --git a/web/cypress/e2e/incidents/regression/03-04.reg_e2e_firing_alerts.cy.ts b/web/cypress/e2e/incidents/regression/03-04.reg_e2e_firing_alerts.cy.ts new file mode 100644 index 000000000..3b05bf6e0 --- /dev/null +++ b/web/cypress/e2e/incidents/regression/03-04.reg_e2e_firing_alerts.cy.ts @@ -0,0 +1,304 @@ +/* +Regression tests for time-based alert resolution issues with real firing alerts. + +Section 3.3: Alerts Marked as Resolved After Time +Tests that alerts maintain their firing state correctly when time passes without +incident refresh. Previously, alerts were incorrectly marked as resolved when +deselecting and reselecting an incident after waiting. + +Section 4.7: Cached End Time for Prometheus Query +Tests that the end time parameter in Prometheus queries uses current time instead +of cached initial load time. Previously, the Redux state would cache the initial +page load time, causing firing alerts to be incorrectly marked as resolved. + +Both tests require continuously firing alerts and cannot be tested with mocked data. + +Verifies: OU-XXX (time-based resolution bugs) +*/ + +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: Time-Based Alert Resolution (E2E with Firing Alerts)', { tags: ['@incidents', '@slow', '@e2e-real'] }, () => { + let currentAlertName: string; + + before(() => { + cy.beforeBlockCOO(MCP, MP); + + cy.log('Create or reuse firing alert for testing'); + cy.createKubePodCrashLoopingAlert('TimeBasedResolution2').then((alertName) => { + currentAlertName = alertName; + cy.log(`Test will monitor alert: ${currentAlertName}`); + }); + }); + + + it('1. Section 3.3 - Alert not incorrectly marked as resolved after time passes', () => { + cy.log('1.1 Navigate to Incidents page and clear filters'); + incidentsPage.goTo(); + incidentsPage.clearAllFilters(); + + const intervalMs = 60_000; + const maxMinutes = 30; + + cy.log('1.2 Wait for incident with custom alert to appear and get selected'); + cy.waitUntil( + () => incidentsPage.findIncidentWithAlert(currentAlertName), + { + interval: intervalMs, + timeout: maxMinutes * intervalMs, + errorMsg: `Incident with alert ${currentAlertName} should appear within ${maxMinutes} minutes` + } + ); + + incidentsPage.elements.incidentsDetailsTable().should('exist'); + + cy.log('1.3 Verify alert is firing by checking end time shows "---"'); + cy.wrap(0).as('initialFiringCount'); + + incidentsPage.getSelectedIncidentAlerts().then((alerts) => { + expect(alerts.length).to.be.greaterThan(0); + + alerts.forEach((alert, index) => { + alert.getAlertRuleCell().invoke('text').then((alertRuleText) => { + const cleanAlertName = alertRuleText.trim().replace("AlertRuleAR", ""); + + if (cleanAlertName != currentAlertName) { + cy.log(`Alert ${index + 1}: ${cleanAlertName} does not match ${currentAlertName}, skipping`); + return; + } + + cy.log(`Alert ${index + 1}: Found matching alert ${cleanAlertName}`); + + alert.getEndCell().invoke('text').then((endText) => { + const cleanEndText = endText.trim(); + cy.log(`Alert ${index + 1} end time: "${cleanEndText}"`); + const isFiring = cleanEndText === '---'; + if (isFiring) { + cy.get('@initialFiringCount').then((count: any) => { + cy.wrap(count + 1).as('initialFiringCount'); + }); + cy.log(`Alert ${index + 1} is FIRING`); + } else { + cy.log(`Alert ${index + 1} is resolved`); + } + }); + }); + }); + }).then(() => { + cy.get('@initialFiringCount').then((count: any) => { + cy.log(`Total firing alerts found: ${count}`); + expect(count).to.be.greaterThan(0, `Expected at least 1 firing alert for ${currentAlertName}, but found ${count}`); + }); + }); + + cy.log('Verified: Alert initially shows firing state (end time = "---")'); + + + const waitMinutes = 0.1 + cy.log(`1.6 Wait ${waitMinutes} minutes without refreshing the incidents page`); + cy.wait(waitMinutes * 60_000); + + cy.log('1.10 Verify alert is STILL firing (end time still shows "---", not resolved)'); + cy.wrap(0).as('currentFiringCount'); + + incidentsPage.getSelectedIncidentAlerts().then((alerts) => { + expect(alerts.length).to.be.greaterThan(0); + + alerts.forEach((alert, index) => { + alert.getAlertRuleCell().invoke('text').then((alertRuleText) => { + const cleanAlertName = alertRuleText.trim().replace("AlertRuleAR", ""); + + if (cleanAlertName != currentAlertName) { + cy.log(`Alert ${index + 1}: ${cleanAlertName} does not match ${currentAlertName}, skipping`); + return; + } + + cy.log(`Alert ${index + 1}: Found matching alert ${cleanAlertName}`); + + alert.getEndCell().invoke('text').then((endText) => { + const cleanEndText = endText.trim(); + cy.log(`Alert ${index + 1} end time: "${cleanEndText}"`); + const isFiring = cleanEndText === '---'; + if (isFiring) { + cy.get('@currentFiringCount').then((count: any) => { + cy.wrap(count + 1).as('currentFiringCount'); + }); + cy.log(`Alert ${index + 1} is STILL FIRING`); + } else { + cy.log(`Alert ${index + 1} is now resolved (BUG!)`); + } + }); + }); + }); + }).then(() => { + cy.get('@initialFiringCount').then((initialCount: any) => { + cy.get('@currentFiringCount').then((currentCount: any) => { + cy.log(`Initial firing alerts: ${initialCount}, Current firing alerts: ${currentCount}`); + expect(currentCount).to.equal(initialCount, `Expected same number of firing alerts after wait (${initialCount}), but got ${currentCount}`); + expect(currentCount).to.be.greaterThan(0, `Expected at least 1 firing alert, but found ${currentCount}`); + }); + }); + }); + + cy.log('Verified: Alert maintains firing state after time passes and reselection (end time = "---")'); + }); + + it('2. Section 4.7 - Prometheus query end time updates to current time on filter refresh', () => { + cy.log('2.1 Navigate to Incidents page and clear filters'); + incidentsPage.goTo(); + incidentsPage.clearAllFilters(); + + cy.log('2.2 Capture initial page load time'); + const initialLoadTime = Date.now(); + cy.wrap(initialLoadTime).as('initialLoadTime'); + + cy.log('2.3 Search for and select incident with custom alert'); + incidentsPage.findIncidentWithAlert(currentAlertName).should('eq', true); + + cy.log('2.4 Verify alert is firing (end time = "---")'); + cy.wrap(0).as('firingCountTest2'); + + incidentsPage.getSelectedIncidentAlerts().then((alerts) => { + expect(alerts.length).to.be.greaterThan(0); + + alerts.forEach((alert, index) => { + alert.getAlertRuleCell().invoke('text').then((alertRuleText) => { + const cleanAlertName = alertRuleText.trim().replace("AlertRuleAR", ""); + + if (cleanAlertName != currentAlertName) { + return; + } + + alert.getEndCell().invoke('text').then((endText) => { + const cleanEndText = endText.trim(); + if (cleanEndText === '---') { + cy.get('@firingCountTest2').then((count: any) => { + cy.wrap(count + 1).as('firingCountTest2'); + }); + } + }); + }); + }); + }).then(() => { + cy.get('@firingCountTest2').then((count: any) => { + expect(count).to.be.greaterThan(0, `Expected at least 1 firing alert for ${currentAlertName}`); + }); + }); + + cy.log('Verified: Alert initially shows firing state'); + + const waitMinutes = 11; + const REFRESH_FREQUENCY = 300; + + cy.log(`2.5 Wait ${waitMinutes} minutes without refreshing incidents`); + cy.wait(waitMinutes * 60_000); + + cy.log('2.6 Set up intercept to capture Prometheus query parameters'); + const queryEndTimes: number[] = []; + cy.intercept('GET', '**/api/prometheus/api/v1/query_range*', (req) => { + req.continue((res) => { + const queryParams = new URLSearchParams(req.url.split('?')[1]); + const endTimeParam = queryParams.get('end'); + if (endTimeParam) { + queryEndTimes.push(parseFloat(endTimeParam)); + } + }); + }).as('prometheusQuery'); + + cy.log('2.7 Refresh the days filter to trigger new Prometheus queries'); + incidentsPage.setDays('7 days'); + + cy.log('2.8 Wait for all Prometheus queries to complete'); + cy.wait(2000); + + cy.wrap(null).then(() => { + cy.log(`Captured ${queryEndTimes.length} Prometheus queries`); + + + if (queryEndTimes.length > 0) { + const mostRecentEndTime = Math.max(...queryEndTimes); + const oldestEndTime = Math.min(...queryEndTimes); + const currentTime = Date.now() / 1000; + const timeDifference = Math.abs(currentTime - mostRecentEndTime); + + cy.log(`Query end times range: ${oldestEndTime} to ${mostRecentEndTime}`); + cy.log(`Current time: ${currentTime}, Most recent query end time: ${mostRecentEndTime}, Difference: ${timeDifference}s`); + + cy.get('@initialLoadTime').then((initialTime: any) => { + const initialTimeSeconds = initialTime / 1000; + const timePassedSinceLoad = currentTime - initialTimeSeconds; + + cy.log(`Time passed since initial load: ${timePassedSinceLoad}s`); + + expect(timeDifference).to.be.lessThan(REFRESH_FREQUENCY, + `Most recent end time should be close to current time (within ${REFRESH_FREQUENCY} seconds)`); + + expect(mostRecentEndTime).to.be.greaterThan(initialTimeSeconds + (waitMinutes * 60) - REFRESH_FREQUENCY, + `End time should be updated to current time, not cached from initial load (${waitMinutes} minutes ago)`); + }); + + cy.log('Verified: Most recent end time parameter uses current time, not cached initial load time'); + } else { + throw new Error('No Prometheus queries were captured'); + } + }); + }); + + it('3. Verify alert lifecycle - alert continues firing throughout test', () => { + cy.log('3.1 Navigate to Incidents page'); + incidentsPage.goTo(); + incidentsPage.clearAllFilters(); + + cy.log('3.2 Search for and select incident with custom alert'); + incidentsPage.findIncidentWithAlert(currentAlertName).should('eq', true); + + cy.log('3.3 Verify end time shows "---" for firing alert'); + cy.wrap(0).as('firingCountTest3'); + + incidentsPage.getSelectedIncidentAlerts().then((alerts) => { + expect(alerts.length).to.be.greaterThan(0); + + alerts.forEach((alert, index) => { + alert.getAlertRuleCell().invoke('text').then((alertRuleText) => { + const cleanAlertName = alertRuleText.trim().replace("AlertRuleAR", ""); + + if (cleanAlertName != currentAlertName) { + return; + } + + alert.getEndCell().invoke('text').then((endText) => { + const cleanEndText = endText.trim(); + if (cleanEndText === '---') { + cy.get('@firingCountTest3').then((count: any) => { + cy.wrap(count + 1).as('firingCountTest3'); + }); + } + }); + }); + }); + }).then(() => { + cy.get('@firingCountTest3').then((count: any) => { + expect(count).to.be.greaterThan(0, `Expected at least 1 firing alert for ${currentAlertName}`); + }); + }); + + cy.log('Verified: Alert lifecycle maintained correctly throughout test suite (end time = "---")'); + }); +}); + + diff --git a/web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts b/web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts index 01be6ba8f..11e23f65b 100644 --- a/web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts +++ b/web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts @@ -12,7 +12,7 @@ Verifies: OU-1020, OU-706 import { incidentsPage } from '../../../views/incidents-page'; const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -26,7 +26,7 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('Regression: Silences Not Applied Correctly', () => { +describe('Regression: Silences Not Applied Correctly', { tags: ['@incidents'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); diff --git a/web/cypress/e2e/incidents/regression/04.reg_redux_effects.cy.ts b/web/cypress/e2e/incidents/regression/04.reg_redux_effects.cy.ts index ed5d22bee..072c5e203 100644 --- a/web/cypress/e2e/incidents/regression/04.reg_redux_effects.cy.ts +++ b/web/cypress/e2e/incidents/regression/04.reg_redux_effects.cy.ts @@ -16,7 +16,7 @@ Test cases: import { incidentsPage } from '../../../views/incidents-page'; const MCP = { - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), packageName: 'cluster-observability-operator', operatorName: 'Cluster Observability Operator', config: { @@ -30,7 +30,7 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('Regression: Redux State Management', () => { +describe('Regression: Redux State Management', { tags: ['@incidents', '@incidents-redux'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); diff --git a/web/cypress/e2e/monitoring/00.bvt_admin.cy.ts b/web/cypress/e2e/monitoring/00.bvt_admin.cy.ts index 57cb2cf40..6ef745739 100644 --- a/web/cypress/e2e/monitoring/00.bvt_admin.cy.ts +++ b/web/cypress/e2e/monitoring/00.bvt_admin.cy.ts @@ -1,8 +1,6 @@ import { nav } from '../../views/nav'; import { alerts } from '../../fixtures/monitoring/alert'; -import { guidedTour } from '../../views/tour'; import { runBVTMonitoringTests } from '../../support/monitoring/00.bvt_monitoring.cy'; -import { runBVTMonitoringTestsNamespace } from '../../support/monitoring/00.bvt_monitoring_namespace.cy'; import { commonPages } from '../../views/common'; import { overviewPage } from '../../views/overview-page'; // Set constants for the operators that need to be installed for tests. @@ -11,21 +9,20 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('BVT: Monitoring', () => { +describe('BVT: Monitoring', { tags: ['@smoke', '@monitoring'] }, () => { before(() => { cy.beforeBlock(MP); }); beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); + cy.changeNamespace("All Projects"); alerts.getWatchdogAlert(); nav.sidenav.clickNavLink(['Observe', 'Alerting']); commonPages.titleShouldHaveText('Alerting'); alerts.getWatchdogAlert(); - cy.changeNamespace("All Projects"); }); it(`1. Admin perspective - Observe Menu`, () => { @@ -81,62 +78,4 @@ describe('BVT: Monitoring', () => { name: 'Administrator', }); -}); - -describe('BVT: Monitoring - Namespaced', () => { - - before(() => { - cy.beforeBlock(MP); - }); - - beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); - alerts.getWatchdogAlert(); - nav.sidenav.clickNavLink(['Observe', 'Alerting']); - commonPages.titleShouldHaveText('Alerting'); - alerts.getWatchdogAlert(); - cy.changeNamespace(MP.namespace); - }); - - it(`Admin perspective - Observe Menu`, () => { - cy.log(`Admin perspective - Observe Menu and verify all submenus`); - nav.sidenav.clickNavLink(['Administration', 'Cluster Settings']); - commonPages.detailsPage.administration_clusterSettings(); - nav.sidenav.clickNavLink(['Observe', 'Alerting']); - commonPages.titleShouldHaveText('Alerting'); - nav.tabs.switchTab('Silences'); - commonPages.projectDropdownShouldExist(); - nav.tabs.switchTab('Alerting rules'); - commonPages.projectDropdownShouldExist(); - nav.sidenav.clickNavLink(['Observe', 'Metrics']); - commonPages.titleShouldHaveText('Metrics'); - commonPages.projectDropdownShouldExist(); - nav.sidenav.clickNavLink(['Observe', 'Dashboards']); - commonPages.titleShouldHaveText('Dashboards'); - // commonPages.projectDropdownShouldExist(); - nav.sidenav.clickNavLink(['Observe', 'Targets']); - commonPages.titleShouldHaveText('Metrics targets'); - commonPages.projectDropdownShouldNotExist(); - - }); - - it(`Admin perspective - Overview Page > Status - View alerts`, () => { - nav.sidenav.clickNavLink(['Home', 'Overview']); - overviewPage.clickStatusViewAlerts(); - commonPages.titleShouldHaveText('Alerting'); - }); - - it(`Admin perspective - Cluster Utilization - Metrics`, () => { - nav.sidenav.clickNavLink(['Home', 'Overview']); - overviewPage.clickClusterUtilizationViewCPU(); - commonPages.titleShouldHaveText('Metrics'); - }); - - // Run tests in Administrator perspective - runBVTMonitoringTestsNamespace({ - name: 'Administrator', - }); - }); \ No newline at end of file diff --git a/web/cypress/e2e/monitoring/00.bvt_dev.cy.ts b/web/cypress/e2e/monitoring/00.bvt_dev.cy.ts new file mode 100644 index 000000000..6a5064b93 --- /dev/null +++ b/web/cypress/e2e/monitoring/00.bvt_dev.cy.ts @@ -0,0 +1,30 @@ +import { nav } from '../../views/nav'; +import { alerts } from '../../fixtures/monitoring/alert'; +import { runBVTMonitoringTestsNamespace } from '../../support/monitoring/00.bvt_monitoring_namespace.cy'; +import { commonPages } from '../../views/common'; +// Set constants for the operators that need to be installed for tests. +const MP = { + namespace: 'openshift-monitoring', + operatorName: 'Cluster Monitoring Operator', +}; + +describe('BVT: Monitoring - Namespaced', { tags: ['@monitoring-dev', '@smoke-dev'] }, () => { + + before(() => { + cy.beforeBlock(MP); + }); + + beforeEach(() => { + alerts.getWatchdogAlert(); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + commonPages.titleShouldHaveText('Alerting'); + alerts.getWatchdogAlert(); + cy.changeNamespace(MP.namespace); + }); + + // Run tests in Administrator perspective + runBVTMonitoringTestsNamespace({ + name: 'Administrator', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/monitoring/regression/01.reg_alerts_admin.cy.ts b/web/cypress/e2e/monitoring/regression/01.reg_alerts_admin.cy.ts index 63bd41ba6..b8e8176dd 100644 --- a/web/cypress/e2e/monitoring/regression/01.reg_alerts_admin.cy.ts +++ b/web/cypress/e2e/monitoring/regression/01.reg_alerts_admin.cy.ts @@ -1,9 +1,7 @@ import { alerts } from '../../../fixtures/monitoring/alert'; import { runAllRegressionAlertsTests } from '../../../support/monitoring/01.reg_alerts.cy'; -import { runAllRegressionAlertsTestsNamespace } from '../../../support/monitoring/04.reg_alerts_namespace.cy'; import { commonPages } from '../../../views/common'; import { nav } from '../../../views/nav'; -import { guidedTour } from '../../../views/tour'; const MP = { namespace: 'openshift-monitoring', @@ -11,21 +9,20 @@ const MP = { }; // Test suite for Administrator perspective -describe('Regression: Monitoring - Alerts (Administrator)', () => { +describe('Regression: Monitoring - Alerts (Administrator)', { tags: ['@monitoring', '@alerts'] }, () => { before(() => { cy.beforeBlock(MP); }); beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); alerts.getWatchdogAlert(); + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); + cy.changeNamespace("All Projects"); nav.sidenav.clickNavLink(['Observe', 'Alerting']); commonPages.titleShouldHaveText('Alerting'); alerts.getWatchdogAlert(); - cy.changeNamespace("All Projects"); }); // Run tests in Administrator perspective @@ -35,27 +32,4 @@ describe('Regression: Monitoring - Alerts (Administrator)', () => { }); -describe('Regression: Monitoring - Alerts Namespaced (Administrator)', () => { - - before(() => { - cy.beforeBlock(MP); - }); - - beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); - alerts.getWatchdogAlert(); - nav.sidenav.clickNavLink(['Observe', 'Alerting']); - commonPages.titleShouldHaveText('Alerting'); - alerts.getWatchdogAlert(); - cy.changeNamespace(MP.namespace); - }); - - // Run tests in Administrator perspective - runAllRegressionAlertsTestsNamespace({ - name: 'Administrator', - }); - -}); diff --git a/web/cypress/e2e/monitoring/regression/01.reg_alerts_dev.cy.ts b/web/cypress/e2e/monitoring/regression/01.reg_alerts_dev.cy.ts new file mode 100644 index 000000000..f0ad628f0 --- /dev/null +++ b/web/cypress/e2e/monitoring/regression/01.reg_alerts_dev.cy.ts @@ -0,0 +1,31 @@ +import { alerts } from '../../../fixtures/monitoring/alert'; +import { runAllRegressionAlertsTestsNamespace } from '../../../support/monitoring/04.reg_alerts_namespace.cy'; +import { commonPages } from '../../../views/common'; +import { nav } from '../../../views/nav'; + +const MP = { + namespace: 'openshift-monitoring', + operatorName: 'Cluster Monitoring Operator', +}; + +describe('Regression: Monitoring - Alerts Namespaced (Administrator)', { tags: ['@monitoring-dev', '@alerts-dev'] }, () => { + + before(() => { + cy.beforeBlock(MP); + }); + + beforeEach(() => { + alerts.getWatchdogAlert(); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + commonPages.titleShouldHaveText('Alerting'); + alerts.getWatchdogAlert(); + cy.changeNamespace(MP.namespace); + }); + + // Run tests in Administrator perspective + runAllRegressionAlertsTestsNamespace({ + name: 'Administrator', + }); + +}); + diff --git a/web/cypress/e2e/monitoring/regression/02.reg_metrics_admin.cy.ts b/web/cypress/e2e/monitoring/regression/02.reg_metrics_admin_1.cy.ts similarity index 63% rename from web/cypress/e2e/monitoring/regression/02.reg_metrics_admin.cy.ts rename to web/cypress/e2e/monitoring/regression/02.reg_metrics_admin_1.cy.ts index 506cd65a2..4c01c2dbe 100644 --- a/web/cypress/e2e/monitoring/regression/02.reg_metrics_admin.cy.ts +++ b/web/cypress/e2e/monitoring/regression/02.reg_metrics_admin_1.cy.ts @@ -1,8 +1,7 @@ -import { runAllRegressionMetricsTests } from '../../../support/monitoring/02.reg_metrics.cy'; -import { runAllRegressionMetricsTestsNamespace } from '../../../support/monitoring/05.reg_metrics_namespace.cy'; +import { runAllRegressionMetricsTests1 } from '../../../support/monitoring/02.reg_metrics_1.cy'; +import { runAllRegressionMetricsTestsNamespace1 } from '../../../support/monitoring/05.reg_metrics_namespace_1.cy'; import { commonPages } from '../../../views/common'; import { nav } from '../../../views/nav'; -import { guidedTour } from '../../../views/tour'; const MP = { namespace: 'openshift-monitoring', @@ -10,46 +9,40 @@ const MP = { }; // Test suite for Administrator perspective -describe('Regression: Monitoring - Metrics (Administrator)', () => { +describe('Regression: Monitoring - Metrics (Administrator)', { tags: ['@monitoring', '@metrics'] }, () => { before(() => { cy.beforeBlock(MP); }); beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); nav.sidenav.clickNavLink(['Observe', 'Metrics']); commonPages.titleShouldHaveText('Metrics'); cy.changeNamespace("All Projects"); }); // Run tests in Administrator perspective - runAllRegressionMetricsTests({ + runAllRegressionMetricsTests1({ name: 'Administrator', }); }); // Test suite for Administrator perspective -describe('Regression: Monitoring - Metrics Namespaced (Administrator)', () => { +describe('Regression: Monitoring - Metrics Namespaced (Administrator)', { tags: ['@monitoring', '@metrics'] }, () => { before(() => { cy.beforeBlock(MP); }); beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); nav.sidenav.clickNavLink(['Observe', 'Metrics']); commonPages.titleShouldHaveText('Metrics'); cy.changeNamespace(MP.namespace); }); // Run tests in Administrator perspective - runAllRegressionMetricsTestsNamespace({ + runAllRegressionMetricsTestsNamespace1({ name: 'Administrator', }); diff --git a/web/cypress/e2e/monitoring/regression/02.reg_metrics_admin_2.cy.ts b/web/cypress/e2e/monitoring/regression/02.reg_metrics_admin_2.cy.ts new file mode 100644 index 000000000..6b15d73f2 --- /dev/null +++ b/web/cypress/e2e/monitoring/regression/02.reg_metrics_admin_2.cy.ts @@ -0,0 +1,50 @@ +import { runAllRegressionMetricsTests2 } from '../../../support/monitoring/02.reg_metrics_2.cy'; +import { runAllRegressionMetricsTestsNamespace2 } from '../../../support/monitoring/05.reg_metrics_namespace_2.cy'; +import { commonPages } from '../../../views/common'; +import { nav } from '../../../views/nav'; + +const MP = { + namespace: 'openshift-monitoring', + operatorName: 'Cluster Monitoring Operator', +}; + +// Test suite for Administrator perspective +describe('Regression: Monitoring - Metrics (Administrator)', { tags: ['@monitoring', '@metrics'] }, () => { + + before(() => { + cy.beforeBlock(MP); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); + cy.changeNamespace("All Projects"); + }); + + // Run tests in Administrator perspective + runAllRegressionMetricsTests2({ + name: 'Administrator', + }); + +}); + +// Test suite for Administrator perspective +describe('Regression: Monitoring - Metrics Namespaced (Administrator)', { tags: ['@monitoring', '@metrics'] }, () => { + + before(() => { + cy.beforeBlock(MP); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); + cy.changeNamespace(MP.namespace); + }); + + // Run tests in Administrator perspective + runAllRegressionMetricsTestsNamespace2({ + name: 'Administrator', + }); + +}); + diff --git a/web/cypress/e2e/monitoring/regression/03.reg_legacy_dashboards_admin.cy.ts b/web/cypress/e2e/monitoring/regression/03.reg_legacy_dashboards_admin.cy.ts index 290a70528..795b2e9a8 100644 --- a/web/cypress/e2e/monitoring/regression/03.reg_legacy_dashboards_admin.cy.ts +++ b/web/cypress/e2e/monitoring/regression/03.reg_legacy_dashboards_admin.cy.ts @@ -2,7 +2,6 @@ import { runAllRegressionLegacyDashboardsTests } from '../../../support/monitori import { runAllRegressionLegacyDashboardsTestsNamespace } from '../../../support/monitoring/06.reg_legacy_dashboards_namespace.cy'; import { commonPages } from '../../../views/common'; import { nav } from '../../../views/nav'; -import { guidedTour } from '../../../views/tour'; const MP = { namespace: 'openshift-monitoring', @@ -10,25 +9,21 @@ const MP = { }; // Test suite for Administrator perspective -describe('Regression: Monitoring - Legacy Dashboards (Administrator)', () => { +describe('Regression: Monitoring - Legacy Dashboards (Administrator)', { tags: ['@monitoring', '@dashboards'] }, () => { before(() => { cy.beforeBlock(MP); }); beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); - //TODO: Begin: To be removed when OU-949 get merged - nav.sidenav.clickNavLink(['Observe', 'Alerting']); - commonPages.titleShouldHaveText('Alerting'); + //when running only this file, beforeBlock changes the namespace to openshift-monitoring + //so we need to change it back to All Projects before landing to Dashboards page in order to have API Performance dashboard loaded by default + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); cy.changeNamespace("All Projects"); - //TODO: End: To be removed when OU-949 get merged nav.sidenav.clickNavLink(['Observe', 'Dashboards']); commonPages.titleShouldHaveText('Dashboards'); - //TODO: Uncomment when OU-949 get merged - //cy.changeNamespace("All Projects"); + cy.changeNamespace("All Projects"); }); // Run tests in Administrator perspective @@ -38,18 +33,14 @@ describe('Regression: Monitoring - Legacy Dashboards (Administrator)', () => { }); -/* TODO: Uncomment when OU-949 get merged // Test suite for Administrator perspective -describe('Regression: Monitoring - Legacy Dashboards Namespaced (Administrator)', () => { +describe('Regression: Monitoring - Legacy Dashboards Namespaced (Administrator)', { tags: ['@monitoring', '@dashboards'] }, () => { before(() => { cy.beforeBlock(MP); }); beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); nav.sidenav.clickNavLink(['Observe', 'Dashboards']); commonPages.titleShouldHaveText('Dashboards'); cy.changeNamespace(MP.namespace); @@ -61,4 +52,3 @@ describe('Regression: Monitoring - Legacy Dashboards Namespaced (Administrator)' }); }); - */ diff --git a/web/cypress/e2e/perses/01.coo_perses.cy.ts b/web/cypress/e2e/perses/00.coo_bvt_perses_admin.cy.ts similarity index 89% rename from web/cypress/e2e/perses/01.coo_perses.cy.ts rename to web/cypress/e2e/perses/00.coo_bvt_perses_admin.cy.ts index b02c77866..8fe7a7b8f 100644 --- a/web/cypress/e2e/perses/01.coo_perses.cy.ts +++ b/web/cypress/e2e/perses/00.coo_bvt_perses_admin.cy.ts @@ -1,5 +1,5 @@ import { nav } from '../../views/nav'; -import { runBVTCOOPersesTests } from '../../support/perses/00.coo_bvt_perses.cy'; +import { runBVTCOOPersesTests } from '../../support/perses/00.coo_bvt_perses_admin.cy'; import { guidedTour } from '../../views/tour'; // Set constants for the operators that need to be installed for tests. @@ -18,16 +18,13 @@ const MP = { operatorName: 'Cluster Monitoring Operator', }; -describe('BVT: COO - Dashboards (Perses) - Administrator perspective', () => { +describe('BVT: COO - Dashboards (Perses) - Administrator perspective', { tags: ['@smoke', '@dashboards', '@perses'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); }); beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); }); diff --git a/web/cypress/e2e/perses/00.coo_bvt_perses_admin_1.cy.ts b/web/cypress/e2e/perses/00.coo_bvt_perses_admin_1.cy.ts new file mode 100644 index 000000000..0e2c05032 --- /dev/null +++ b/web/cypress/e2e/perses/00.coo_bvt_perses_admin_1.cy.ts @@ -0,0 +1,38 @@ +import { nav } from '../../views/nav'; +//TODO: rename after customizable-dashboards gets merged +import { runBVTCOOPersesTests1 } from '../../support/perses/00.coo_bvt_perses_admin_1.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 @smoke, @dashboards, @perses when customizable-dashboards gets merged +describe('BVT: COO - Dashboards (Perses) - Administrator perspective', { tags: ['@smoke-', '@dashboards-', '@perses-'] }, () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + cy.cleanupPersesTestDashboardsBeforeTests(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + + //TODO: rename after customizable-dashboards gets merged + runBVTCOOPersesTests1({ + name: 'Administrator', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/perses/01.coo_list_perses_admin.cy.ts b/web/cypress/e2e/perses/01.coo_list_perses_admin.cy.ts new file mode 100644 index 000000000..7517a88a4 --- /dev/null +++ b/web/cypress/e2e/perses/01.coo_list_perses_admin.cy.ts @@ -0,0 +1,63 @@ +import { nav } from '../../views/nav'; +import { runCOOListPersesDuplicateDashboardTests, runCOOListPersesTests } from '../../support/perses/01.coo_list_perses_admin.cy'; +import { runCOOListPersesTestsNamespace } from '../../support/perses/01.coo_list_perses_admin_namespace.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) - List perses dashboards', { tags: ['@perses', '@dashboards-'] }, () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + cy.cleanupPersesTestDashboardsBeforeTests(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(5000); + cy.changeNamespace('All Projects'); + }); + + runCOOListPersesTests({ + name: 'Administrator', + }); + + runCOOListPersesDuplicateDashboardTests({ + name: 'Administrator', + }); + +}); + +//TODO: change tag to @dashboards when customizable-dashboards gets merged +describe('COO - Dashboards (Perses) - List perses dashboards - Namespace', { tags: ['@perses', '@dashboards-'] }, () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + cy.cleanupPersesTestDashboardsBeforeTests(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.changeNamespace('All Projects'); + }); + + runCOOListPersesTestsNamespace({ + name: 'Administrator', + }); + +}); + 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..ccb4bc70a --- /dev/null +++ b/web/cypress/e2e/perses/02.coo_edit_perses_admin.cy.ts @@ -0,0 +1,46 @@ +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); + cy.cleanupPersesTestDashboardsBeforeTests(); + }); + + 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/e2e/perses/03.coo_create_perses_admin.cy.ts b/web/cypress/e2e/perses/03.coo_create_perses_admin.cy.ts new file mode 100644 index 000000000..54f2e0efb --- /dev/null +++ b/web/cypress/e2e/perses/03.coo_create_perses_admin.cy.ts @@ -0,0 +1,46 @@ +import { nav } from '../../views/nav'; +import { runCOOCreatePersesTests } from '../../support/perses/03.coo_create_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) - Create perses dashboard', { tags: ['@perses', '@dashboards-'] }, () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + cy.cleanupPersesTestDashboardsBeforeTests(); + cy.setupPersesRBACandExtraDashboards(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(5000); + cy.changeNamespace('All Projects'); + }); + + after(() => { + cy.cleanupExtraDashboards(); + }); + + runCOOCreatePersesTests({ + name: 'Administrator', + }); + +}); + + + diff --git a/web/cypress/e2e/perses/04.coo_import_perses_admin.cy.ts b/web/cypress/e2e/perses/04.coo_import_perses_admin.cy.ts new file mode 100644 index 000000000..cdf4778c9 --- /dev/null +++ b/web/cypress/e2e/perses/04.coo_import_perses_admin.cy.ts @@ -0,0 +1,46 @@ +import { nav } from '../../views/nav'; +import { runCOOImportPersesTests } from '../../support/perses/04.coo_import_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) - Import perses dashboard', { tags: ['@perses', '@dashboards-'] }, () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + cy.cleanupPersesTestDashboardsBeforeTests(); + cy.setupPersesRBACandExtraDashboards(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(5000); + cy.changeNamespace('All Projects'); + }); + + after(() => { + cy.cleanupExtraDashboards(); + }); + + runCOOImportPersesTests({ + name: 'Administrator', + }); + +}); + + + diff --git a/web/cypress/e2e/perses/99.coo_rbac_perses_user1.cy.ts b/web/cypress/e2e/perses/99.coo_rbac_perses_user1.cy.ts new file mode 100644 index 000000000..50f576fb7 --- /dev/null +++ b/web/cypress/e2e/perses/99.coo_rbac_perses_user1.cy.ts @@ -0,0 +1,78 @@ +import { nav } from '../../views/nav'; +import { runCOORBACPersesTestsDevUser1 } from '../../support/perses/99.coo_rbac_perses_user1.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 @smoke, @dashboards, @perses when customizable-dashboards gets merged +describe('RBAC User1: COO - Dashboards (Perses) - Administrator perspective', { tags: ['@smoke-', '@dashboards-', '@perses-dev'] }, () => { + + before(() => { + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 1: Grant temporary cluster-admin role to dev user for COO/Perses installation + // cy.log('Granting temporary cluster-admin role to dev user for setup'); + // cy.adminCLI( + // `oc adm policy add-cluster-role-to-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 2: Setup COO and Perses dashboards (requires admin privileges) + cy.beforeBlockCOO(MCP, MP); + cy.setupPersesRBACandExtraDashboards(); + + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 3: Remove cluster-admin role - dev user now has limited permissions + // cy.log('Removing cluster-admin role from dev user'); + // cy.adminCLI( + // `oc adm policy remove-cluster-role-from-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 4: Clear Cypress session cache and logout + // This is critical because beforeBlockCOO uses cy.session() which caches the login state + cy.log('Clearing Cypress session cache to ensure fresh login'); + Cypress.session.clearAllSavedSessions(); + + // Clear all cookies and storage to fully reset browser state + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + + // Step 5: Re-login as dev user (now without cluster-admin role) + // Using cy.relogin() because it doesn't require oauthurl and handles the login page directly + cy.log('Re-logging in as dev user with limited permissions'); + cy.relogin( + Cypress.env('LOGIN_IDP_DEV_USER'), + Cypress.env('LOGIN_USERNAME1'), + Cypress.env('LOGIN_PASSWORD1'), + ); + cy.validateLogin(); + cy.closeOnboardingModalIfPresent(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + + after(() => { + cy.cleanupExtraDashboards(); + }); + + //TODO: rename after customizable-dashboards gets merged + runCOORBACPersesTestsDevUser1({ + name: 'Administrator', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/perses/99.coo_rbac_perses_user2.cy.ts b/web/cypress/e2e/perses/99.coo_rbac_perses_user2.cy.ts new file mode 100644 index 000000000..884933c3d --- /dev/null +++ b/web/cypress/e2e/perses/99.coo_rbac_perses_user2.cy.ts @@ -0,0 +1,79 @@ +import { nav } from '../../views/nav'; +import { runCOORBACPersesTestsDevUser2 } from '../../support/perses/99.coo_rbac_perses_user2.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 @smoke, @dashboards, @perses when customizable-dashboards gets merged +describe('RBAC User2: COO - Dashboards (Perses) - Administrator perspective', { tags: ['@smoke-', '@dashboards-', '@perses-dev'] }, () => { + + before(() => { + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 1: Grant temporary cluster-admin role to dev user for COO/Perses installation + // cy.log('Granting temporary cluster-admin role to dev user for setup'); + // cy.adminCLI( + // `oc adm policy add-cluster-role-to-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 2: Setup COO and Perses dashboards (requires admin privileges) + cy.beforeBlockCOO(MCP, MP); + cy.setupPersesRBACandExtraDashboards(); + + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 3: Remove cluster-admin role - dev user now has limited permissions + // cy.log('Removing cluster-admin role from dev user'); + // cy.adminCLI( + // `oc adm policy remove-cluster-role-from-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 4: Clear Cypress session cache and logout + // This is critical because beforeBlockCOO uses cy.session() which caches the login state + cy.log('Clearing Cypress session cache to ensure fresh login'); + Cypress.session.clearAllSavedSessions(); + + // Clear all cookies and storage to fully reset browser state + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + + // Step 5: Re-login as dev user (now without cluster-admin role) + // Using cy.relogin() because it doesn't require oauthurl and handles the login page directly + cy.log('Re-logging in as dev user with limited permissions'); + cy.relogin( + Cypress.env('LOGIN_IDP_DEV_USER'), + Cypress.env('LOGIN_USERNAME2'), + Cypress.env('LOGIN_PASSWORD2'), + ); + cy.validateLogin(); + cy.closeOnboardingModalIfPresent(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.changeNamespace('All Projects'); + }); + + after(() => { + cy.cleanupExtraDashboards(); + }); + + //TODO: rename after customizable-dashboards gets merged + runCOORBACPersesTestsDevUser2({ + name: 'Administrator', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/perses/99.coo_rbac_perses_user3.cy.ts b/web/cypress/e2e/perses/99.coo_rbac_perses_user3.cy.ts new file mode 100644 index 000000000..61e99a8db --- /dev/null +++ b/web/cypress/e2e/perses/99.coo_rbac_perses_user3.cy.ts @@ -0,0 +1,78 @@ +import { nav } from '../../views/nav'; +import { runCOORBACPersesTestsDevUser3 } from '../../support/perses/99.coo_rbac_perses_user3.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 @smoke, @dashboards, @perses when customizable-dashboards gets merged +describe('RBAC User3: COO - Dashboards (Perses) - Administrator perspective', { tags: ['@smoke-', '@dashboards-', '@perses-dev'] }, () => { + + before(() => { + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 1: Grant temporary cluster-admin role to dev user for COO/Perses installation + // cy.log('Granting temporary cluster-admin role to dev user for setup'); + // cy.adminCLI( + // `oc adm policy add-cluster-role-to-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 2: Setup COO and Perses dashboards (requires admin privileges) + cy.beforeBlockCOO(MCP, MP); + cy.setupPersesRBACandExtraDashboards(); + + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 3: Remove cluster-admin role - dev user now has limited permissions + // cy.log('Removing cluster-admin role from dev user'); + // cy.adminCLI( + // `oc adm policy remove-cluster-role-from-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 4: Clear Cypress session cache and logout + // This is critical because beforeBlockCOO uses cy.session() which caches the login state + cy.log('Clearing Cypress session cache to ensure fresh login'); + Cypress.session.clearAllSavedSessions(); + + // Clear all cookies and storage to fully reset browser state + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + + // Step 5: Re-login as dev user (now without cluster-admin role) + // Using cy.relogin() because it doesn't require oauthurl and handles the login page directly + cy.log('Re-logging in as dev user with limited permissions'); + cy.relogin( + Cypress.env('LOGIN_IDP_DEV_USER'), + Cypress.env('LOGIN_USERNAME3'), + Cypress.env('LOGIN_PASSWORD3'), + ); + cy.validateLogin(); + cy.closeOnboardingModalIfPresent(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + + after(() => { + cy.cleanupExtraDashboards(); + }); + + //TODO: rename after customizable-dashboards gets merged + runCOORBACPersesTestsDevUser3({ + name: 'Administrator', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/perses/99.coo_rbac_perses_user4.cy.ts b/web/cypress/e2e/perses/99.coo_rbac_perses_user4.cy.ts new file mode 100644 index 000000000..c246c54a6 --- /dev/null +++ b/web/cypress/e2e/perses/99.coo_rbac_perses_user4.cy.ts @@ -0,0 +1,78 @@ +import { nav } from '../../views/nav'; +import { runCOORBACPersesTestsDevUser4 } from '../../support/perses/99.coo_rbac_perses_user4.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 @smoke, @dashboards, @perses when customizable-dashboards gets merged +describe('RBAC User4: COO - Dashboards (Perses) - Administrator perspective', { tags: ['@smoke-', '@dashboards-', '@perses-dev'] }, () => { + + before(() => { + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 1: Grant temporary cluster-admin role to dev user for COO/Perses installation + // cy.log('Granting temporary cluster-admin role to dev user for setup'); + // cy.adminCLI( + // `oc adm policy add-cluster-role-to-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 2: Setup COO and Perses dashboards (requires admin privileges) + cy.beforeBlockCOO(MCP, MP); + cy.setupPersesRBACandExtraDashboards(); + + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 3: Remove cluster-admin role - dev user now has limited permissions + // cy.log('Removing cluster-admin role from dev user'); + // cy.adminCLI( + // `oc adm policy remove-cluster-role-from-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 4: Clear Cypress session cache and logout + // This is critical because beforeBlockCOO uses cy.session() which caches the login state + cy.log('Clearing Cypress session cache to ensure fresh login'); + Cypress.session.clearAllSavedSessions(); + + // Clear all cookies and storage to fully reset browser state + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + + // Step 5: Re-login as dev user (now without cluster-admin role) + // Using cy.relogin() because it doesn't require oauthurl and handles the login page directly + cy.log('Re-logging in as dev user with limited permissions'); + cy.relogin( + Cypress.env('LOGIN_IDP_DEV_USER'), + Cypress.env('LOGIN_USERNAME4'), + Cypress.env('LOGIN_PASSWORD4'), + ); + cy.validateLogin(); + cy.closeOnboardingModalIfPresent(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + + after(() => { + cy.cleanupExtraDashboards(); + }); + + //TODO: rename after customizable-dashboards gets merged + runCOORBACPersesTestsDevUser4({ + name: 'Administrator', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/perses/99.coo_rbac_perses_user5.cy.ts b/web/cypress/e2e/perses/99.coo_rbac_perses_user5.cy.ts new file mode 100644 index 000000000..fcc729465 --- /dev/null +++ b/web/cypress/e2e/perses/99.coo_rbac_perses_user5.cy.ts @@ -0,0 +1,78 @@ +import { nav } from '../../views/nav'; +import { runCOORBACPersesTestsDevUser5 } from '../../support/perses/99.coo_rbac_perses_user5.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 @smoke, @dashboards, @perses when customizable-dashboards gets merged +describe('RBAC User5: COO - Dashboards (Perses) - Administrator perspective', { tags: ['@smoke-', '@dashboards-', '@perses-dev'] }, () => { + + before(() => { + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 1: Grant temporary cluster-admin role to dev user for COO/Perses installation + // cy.log('Granting temporary cluster-admin role to dev user for setup'); + // cy.adminCLI( + // `oc adm policy add-cluster-role-to-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 2: Setup COO and Perses dashboards (requires admin privileges) + cy.beforeBlockCOO(MCP, MP); + cy.setupPersesRBACandExtraDashboards(); + + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 3: Remove cluster-admin role - dev user now has limited permissions + // cy.log('Removing cluster-admin role from dev user'); + // cy.adminCLI( + // `oc adm policy remove-cluster-role-from-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 4: Clear Cypress session cache and logout + // This is critical because beforeBlockCOO uses cy.session() which caches the login state + cy.log('Clearing Cypress session cache to ensure fresh login'); + Cypress.session.clearAllSavedSessions(); + + // Clear all cookies and storage to fully reset browser state + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + + // Step 5: Re-login as dev user (now without cluster-admin role) + // Using cy.relogin() because it doesn't require oauthurl and handles the login page directly + cy.log('Re-logging in as dev user with limited permissions'); + cy.relogin( + Cypress.env('LOGIN_IDP_DEV_USER'), + Cypress.env('LOGIN_USERNAME5'), + Cypress.env('LOGIN_PASSWORD5'), + ); + cy.validateLogin(); + cy.closeOnboardingModalIfPresent(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + + after(() => { + cy.cleanupExtraDashboards(); + }); + + //TODO: rename after customizable-dashboards gets merged + runCOORBACPersesTestsDevUser5({ + name: 'Administrator', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/perses/99.coo_rbac_perses_user6.cy.ts b/web/cypress/e2e/perses/99.coo_rbac_perses_user6.cy.ts new file mode 100644 index 000000000..6fd396098 --- /dev/null +++ b/web/cypress/e2e/perses/99.coo_rbac_perses_user6.cy.ts @@ -0,0 +1,78 @@ +import { nav } from '../../views/nav'; +import { runCOORBACPersesTestsDevUser6 } from '../../support/perses/99.coo_rbac_perses_user6.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 @smoke, @dashboards, @perses when customizable-dashboards gets merged +describe('RBAC User6: COO - Dashboards (Perses) - Administrator perspective', { tags: ['@smoke-', '@dashboards-', '@perses-dev'] }, () => { + + before(() => { + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 1: Grant temporary cluster-admin role to dev user for COO/Perses installation + // cy.log('Granting temporary cluster-admin role to dev user for setup'); + // cy.adminCLI( + // `oc adm policy add-cluster-role-to-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 2: Setup COO and Perses dashboards (requires admin privileges) + cy.beforeBlockCOO(MCP, MP); + cy.setupPersesRBACandExtraDashboards(); + + //TODO: https://issues.redhat.com/browse/OCPBUGS-58468 - when it gets fixed, installation can be don using non-admin user + // Step 3: Remove cluster-admin role - dev user now has limited permissions + // cy.log('Removing cluster-admin role from dev user'); + // cy.adminCLI( + // `oc adm policy remove-cluster-role-from-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + // ); + + // Step 4: Clear Cypress session cache and logout + // This is critical because beforeBlockCOO uses cy.session() which caches the login state + cy.log('Clearing Cypress session cache to ensure fresh login'); + Cypress.session.clearAllSavedSessions(); + + // Clear all cookies and storage to fully reset browser state + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + + // Step 5: Re-login as dev user (now without cluster-admin role) + // Using cy.relogin() because it doesn't require oauthurl and handles the login page directly + cy.log('Re-logging in as dev user with limited permissions'); + cy.relogin( + Cypress.env('LOGIN_IDP_DEV_USER'), + Cypress.env('LOGIN_USERNAME6'), + Cypress.env('LOGIN_PASSWORD6'), + ); + cy.validateLogin(); + cy.closeOnboardingModalIfPresent(); + }); + + beforeEach(() => { + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + + after(() => { + cy.cleanupExtraDashboards(); + }); + + //TODO: rename after customizable-dashboards gets merged + runCOORBACPersesTestsDevUser6({ + name: 'Administrator', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/virtualization/00.coo_ivt.cy.ts b/web/cypress/e2e/virtualization/00.coo_ivt.cy.ts index fc6f6170a..e9ce793c4 100644 --- a/web/cypress/e2e/virtualization/00.coo_ivt.cy.ts +++ b/web/cypress/e2e/virtualization/00.coo_ivt.cy.ts @@ -34,7 +34,7 @@ const KBV = { } }; -describe('Installation: COO and setting up Monitoring Plugin', () => { +describe('Installation: COO and setting up Monitoring Plugin', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); @@ -46,7 +46,7 @@ describe('Installation: COO and setting up Monitoring Plugin', () => { }); }); -describe('Installation: Virtualization', () => { +describe('Installation: Virtualization', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockVirtualization(KBV); @@ -59,7 +59,7 @@ describe('Installation: Virtualization', () => { }); }); -describe('IVT: Monitoring + Virtualization', () => { +describe('IVT: Monitoring + Virtualization', { tags: ['@smoke', '@virtualization'] }, () => { beforeEach(() => { cy.visit('/'); @@ -67,37 +67,17 @@ describe('IVT: Monitoring + Virtualization', () => { cy.validateLogin(); cy.switchPerspective('Virtualization'); guidedTour.closeKubevirtTour(); - alerts.getWatchdogAlert(); - nav.sidenav.clickNavLink(['Observe', 'Alerting']); - commonPages.titleShouldHaveText('Alerting'); + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); cy.changeNamespace("All Projects"); alerts.getWatchdogAlert(); - }); - - // Run tests in Administrator perspective - runBVTMonitoringTests({ - name: 'Virtualization', - }); - -}); - -describe('IVT: Monitoring + Virtualization - Namespaced', () => { - - beforeEach(() => { - cy.visit('/'); - guidedTour.close(); - cy.validateLogin(); - cy.switchPerspective('Virtualization'); - guidedTour.closeKubevirtTour(); - alerts.getWatchdogAlert(); nav.sidenav.clickNavLink(['Observe', 'Alerting']); commonPages.titleShouldHaveText('Alerting'); - cy.changeNamespace(MP.namespace); alerts.getWatchdogAlert(); }); // Run tests in Administrator perspective - runBVTMonitoringTestsNamespace({ + runBVTMonitoringTests({ name: 'Virtualization', }); diff --git a/web/cypress/e2e/virtualization/01.coo_ivt_alerts.cy.ts b/web/cypress/e2e/virtualization/01.coo_ivt_alerts.cy.ts index cfa727af9..8e2ac841c 100644 --- a/web/cypress/e2e/virtualization/01.coo_ivt_alerts.cy.ts +++ b/web/cypress/e2e/virtualization/01.coo_ivt_alerts.cy.ts @@ -34,7 +34,7 @@ const KBV = { } }; -describe('Installation: COO and setting up Monitoring Plugin', () => { +describe('Installation: COO and setting up Monitoring Plugin', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); @@ -45,7 +45,7 @@ describe('Installation: COO and setting up Monitoring Plugin', () => { }); }); -describe('IVT: Monitoring UIPlugin + Virtualization', () => { +describe('IVT: Monitoring UIPlugin + Virtualization', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockVirtualization(KBV); @@ -58,44 +58,24 @@ describe('IVT: Monitoring UIPlugin + Virtualization', () => { }); }); -describe('Regression: Monitoring - Alerts (Virtualization)', () => { +describe('Regression: Monitoring - Alerts (Virtualization)', { tags: ['@virtualization', '@alerts'] }, () => { beforeEach(() => { cy.visit('/'); cy.validateLogin(); cy.switchPerspective('Virtualization'); guidedTour.closeKubevirtTour(); - alerts.getWatchdogAlert(); - nav.sidenav.clickNavLink(['Observe', 'Alerting']); - commonPages.titleShouldHaveText('Alerting'); + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); cy.changeNamespace("All Projects"); alerts.getWatchdogAlert(); - }); - // Run tests in Virtualization perspective - runAllRegressionAlertsTests({ - name: 'Virtualization', - }); - -}); - -describe('Regression: Monitoring - Alerts Namespaced (Virtualization)', () => { - - beforeEach(() => { - cy.visit('/'); - cy.validateLogin(); - cy.switchPerspective('Virtualization'); - guidedTour.closeKubevirtTour(); - alerts.getWatchdogAlert(); nav.sidenav.clickNavLink(['Observe', 'Alerting']); commonPages.titleShouldHaveText('Alerting'); - cy.changeNamespace(MP.namespace); alerts.getWatchdogAlert(); - }); // Run tests in Virtualization perspective - runAllRegressionAlertsTestsNamespace({ + runAllRegressionAlertsTests({ name: 'Virtualization', - }); }); \ No newline at end of file diff --git a/web/cypress/e2e/virtualization/02.coo_ivt_metrics.cy.ts b/web/cypress/e2e/virtualization/02.coo_ivt_metrics_1.cy.ts similarity index 76% rename from web/cypress/e2e/virtualization/02.coo_ivt_metrics.cy.ts rename to web/cypress/e2e/virtualization/02.coo_ivt_metrics_1.cy.ts index 7b40691c3..1236c6f10 100644 --- a/web/cypress/e2e/virtualization/02.coo_ivt_metrics.cy.ts +++ b/web/cypress/e2e/virtualization/02.coo_ivt_metrics_1.cy.ts @@ -1,6 +1,6 @@ import { alerts } from '../../fixtures/monitoring/alert'; -import { runAllRegressionMetricsTests } from '../../support/monitoring/02.reg_metrics.cy'; -import { runAllRegressionMetricsTestsNamespace } from '../../support/monitoring/05.reg_metrics_namespace.cy'; +import { runAllRegressionMetricsTests1 } from '../../support/monitoring/02.reg_metrics_1.cy'; +import { runAllRegressionMetricsTestsNamespace1 } from '../../support/monitoring/05.reg_metrics_namespace_1.cy'; import { commonPages } from '../../views/common'; import { nav } from '../../views/nav'; import { guidedTour } from '../../views/tour'; @@ -33,7 +33,7 @@ const KBV = { } }; -describe('Installation: COO and setting up Monitoring Plugin', () => { +describe('Installation: COO and setting up Monitoring Plugin', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); @@ -44,7 +44,7 @@ describe('Installation: COO and setting up Monitoring Plugin', () => { }); }); -describe('IVT: Monitoring UIPlugin + Virtualization', () => { +describe('IVT: Monitoring UIPlugin + Virtualization', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockVirtualization(KBV); @@ -57,7 +57,7 @@ describe('IVT: Monitoring UIPlugin + Virtualization', () => { }); }); -describe('Regression: Monitoring - Metrics (Virtualization)', () => { +describe('Regression: Monitoring - Metrics (Virtualization)', { tags: ['@virtualization', '@metrics'] }, () => { beforeEach(() => { cy.visit('/'); @@ -71,13 +71,13 @@ describe('Regression: Monitoring - Metrics (Virtualization)', () => { alerts.getWatchdogAlert(); }); - runAllRegressionMetricsTests({ + runAllRegressionMetricsTests1({ name: 'Virtualization', }); }); -describe('Regression: Monitoring - Metrics Namespaced (Virtualization)', () => { +describe('Regression: Monitoring - Metrics Namespaced (Virtualization)', { tags: ['@virtualization', '@metrics'] }, () => { beforeEach(() => { cy.visit('/'); @@ -91,7 +91,7 @@ describe('Regression: Monitoring - Metrics Namespaced (Virtualization)', () => { alerts.getWatchdogAlert(); }); - runAllRegressionMetricsTestsNamespace({ + runAllRegressionMetricsTestsNamespace1({ name: 'Virtualization', }); diff --git a/web/cypress/e2e/virtualization/02.coo_ivt_metrics_2.cy.ts b/web/cypress/e2e/virtualization/02.coo_ivt_metrics_2.cy.ts new file mode 100644 index 000000000..eea255db2 --- /dev/null +++ b/web/cypress/e2e/virtualization/02.coo_ivt_metrics_2.cy.ts @@ -0,0 +1,98 @@ +import { alerts } from '../../fixtures/monitoring/alert'; +import { runAllRegressionMetricsTests2 } from '../../support/monitoring/02.reg_metrics_2.cy'; +import { runAllRegressionMetricsTestsNamespace2 } from '../../support/monitoring/05.reg_metrics_namespace_2.cy'; +import { commonPages } from '../../views/common'; +import { nav } from '../../views/nav'; +import { guidedTour } from '../../views/tour'; + +// 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', +}; + +const KBV = { + namespace: 'openshift-cnv', + packageName: 'kubevirt-hyperconverged', + config: { + kind: 'HyperConverged', + name: 'kubevirt-hyperconverged', + }, + crd: { + kubevirt: 'kubevirts.kubevirt.io', + hyperconverged: 'hyperconvergeds.hco.kubevirt.io', + } +}; + +describe('Installation: COO and setting up Monitoring Plugin', { tags: ['@virtualization', '@slow'] }, () => { + + before(() => { + cy.beforeBlockCOO(MCP, MP); + }); + + it('1. Installation: COO and setting up Monitoring Plugin', () => { + cy.log('Installation: COO and setting up Monitoring Plugin'); + }); +}); + +describe('IVT: Monitoring UIPlugin + Virtualization', { tags: ['@virtualization', '@slow'] }, () => { + + before(() => { + cy.beforeBlockVirtualization(KBV); + }); + + it('1. Virtualization perspective - Observe Menu', () => { + cy.log('Virtualization perspective - Observe Menu and verify all submenus'); + cy.switchPerspective('Virtualization'); + guidedTour.closeKubevirtTour(); + }); +}); + +describe('Regression: Monitoring - Metrics (Virtualization)', { tags: ['@virtualization', '@metrics'] }, () => { + + beforeEach(() => { + cy.visit('/'); + cy.validateLogin(); + cy.switchPerspective('Virtualization'); + guidedTour.closeKubevirtTour(); + alerts.getWatchdogAlert(); + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); + cy.changeNamespace("All Projects"); + alerts.getWatchdogAlert(); + }); + + runAllRegressionMetricsTests2({ + name: 'Virtualization', + }); + +}); + +describe('Regression: Monitoring - Metrics Namespaced (Virtualization)', { tags: ['@virtualization', '@metrics'] }, () => { + + beforeEach(() => { + cy.visit('/'); + cy.validateLogin(); + cy.switchPerspective('Virtualization'); + guidedTour.closeKubevirtTour(); + alerts.getWatchdogAlert(); + nav.sidenav.clickNavLink(['Observe', 'Metrics']); + commonPages.titleShouldHaveText('Metrics'); + cy.changeNamespace(MP.namespace); + alerts.getWatchdogAlert(); + }); + + runAllRegressionMetricsTestsNamespace2({ + name: 'Virtualization', + }); + +}); \ No newline at end of file diff --git a/web/cypress/e2e/virtualization/03.coo_ivt_legacy_dashboards.cy.ts b/web/cypress/e2e/virtualization/03.coo_ivt_legacy_dashboards.cy.ts index 0d6874782..611fce248 100644 --- a/web/cypress/e2e/virtualization/03.coo_ivt_legacy_dashboards.cy.ts +++ b/web/cypress/e2e/virtualization/03.coo_ivt_legacy_dashboards.cy.ts @@ -33,7 +33,7 @@ const KBV = { } }; -describe('Installation: COO and setting up Monitoring Plugin', () => { +describe('Installation: COO and setting up Monitoring Plugin', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); @@ -44,7 +44,7 @@ describe('Installation: COO and setting up Monitoring Plugin', () => { }); }); -describe('IVT: Monitoring UIPlugin + Virtualization', () => { +describe('IVT: Monitoring UIPlugin + Virtualization', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockVirtualization(KBV); @@ -57,22 +57,16 @@ describe('IVT: Monitoring UIPlugin + Virtualization', () => { }); }); -describe('Regression: Monitoring - Legacy Dashboards (Virtualization)', () => { +describe('Regression: Monitoring - Legacy Dashboards (Virtualization)', { tags: ['@virtualization', '@dashboards'] }, () => { beforeEach(() => { cy.visit('/'); cy.validateLogin(); cy.switchPerspective('Virtualization'); guidedTour.closeKubevirtTour(); - //TODO: Begin: To be removed when OU-949 get merged - nav.sidenav.clickNavLink(['Observe', 'Alerting']); - commonPages.titleShouldHaveText('Alerting'); - cy.changeNamespace("All Projects"); - //TODO: End: To be removed when OU-949 get merged nav.sidenav.clickNavLink(['Observe', 'Dashboards']); commonPages.titleShouldHaveText('Dashboards'); - //TODO: Uncomment when OU-949 get merged - // cy.changeNamespace("All Projects"); + cy.changeNamespace("All Projects"); }); runAllRegressionLegacyDashboardsTests({ @@ -81,20 +75,18 @@ describe('Regression: Monitoring - Legacy Dashboards (Virtualization)', () => { }); -/* TODO: Uncomment when OU-949 get merged -// describe('Regression: Monitoring - Legacy Dashboards Namespaced (Virtualization)', () => { -// beforeEach(() => { -// cy.visit('/'); -// cy.validateLogin(); -// cy.switchPerspective('Virtualization'); -// guidedTour.closeKubevirtTour(); -// nav.sidenav.clickNavLink(['Observe', 'Dashboards']); -// commonPages.titleShouldHaveText('Dashboards'); -// cy.changeNamespace(MP.namespace); -// }); +describe('Regression: Monitoring - Legacy Dashboards Namespaced (Virtualization)', { tags: ['@virtualization', '@dashboards'] }, () => { + beforeEach(() => { + cy.visit('/'); + cy.validateLogin(); + cy.switchPerspective('Virtualization'); + guidedTour.closeKubevirtTour(); + nav.sidenav.clickNavLink(['Observe', 'Dashboards']); + commonPages.titleShouldHaveText('Dashboards'); + cy.changeNamespace(MP.namespace); + }); -// runAllRegressionLegacyDashboardsTestsNamespace({ -// name: 'Virtualization', -// }); -// }); -*/ \ No newline at end of file + runAllRegressionLegacyDashboardsTestsNamespace({ + name: 'Virtualization', + }); +}); \ No newline at end of file diff --git a/web/cypress/e2e/virtualization/04.coo_ivt_perses.cy.ts b/web/cypress/e2e/virtualization/04.coo_ivt_perses.cy.ts index 34080939e..8905c81e2 100644 --- a/web/cypress/e2e/virtualization/04.coo_ivt_perses.cy.ts +++ b/web/cypress/e2e/virtualization/04.coo_ivt_perses.cy.ts @@ -1,5 +1,5 @@ import { nav } from '../../views/nav'; -import { runBVTCOOPersesTests } from '../../support/perses/00.coo_bvt_perses.cy'; +import { runBVTCOOPersesTests } from '../../support/perses/00.coo_bvt_perses_admin.cy'; import { guidedTour } from '../../views/tour'; import { commonPages } from '../../views/common'; @@ -32,7 +32,7 @@ const KBV = { } }; -describe('Installation: COO and setting up Monitoring Plugin', () => { +describe('Installation: COO and setting up Monitoring Plugin', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockCOO(MCP, MP); @@ -43,7 +43,7 @@ describe('Installation: COO and setting up Monitoring Plugin', () => { }); }); -describe('Installation: Virtualization', () => { +describe('Installation: Virtualization', { tags: ['@virtualization', '@slow'] }, () => { before(() => { cy.beforeBlockVirtualization(KBV); @@ -56,7 +56,7 @@ describe('Installation: Virtualization', () => { }); }); -describe('IVT: COO - Dashboards (Perses) - Virtualization perspective', () => { +describe('IVT: COO - Dashboards (Perses) - Virtualization perspective', { tags: ['@virtualization', '@perses'] }, () => { beforeEach(() => { cy.visit('/'); diff --git a/web/cypress/fixtures/coo/acm-install.sh b/web/cypress/fixtures/coo/acm-install.sh index 661bd0a1f..893e9ed0d 100755 --- a/web/cypress/fixtures/coo/acm-install.sh +++ b/web/cypress/fixtures/coo/acm-install.sh @@ -1,7 +1,23 @@ #!/bin/bash -set -eux -oc patch Scheduler cluster --type='json' -p '[{ "op": "replace", "path": "/spec/mastersSchedulable", "value": true }]' +#set -eux +set -x +# This script will install ACM, MCH, MCO, and other test resources. +# The script will skip installation when MCO CR existed. +echo "[INFO] Checking for existing MultiClusterObservability CR..." +MCO_NAMESPACE="open-cluster-management-observability" +MCO_NAME="observability" +# The 'oc get ...' command will have a non-zero exit code if the resource is not found. +if oc get multiclusterobservability ${MCO_NAME} -n ${MCO_NAMESPACE} >/dev/null 2>&1; then + echo "[INFO] MultiClusterObservability CR '${MCO_NAME}' already exists in '${MCO_NAMESPACE}'." + echo "[INFO] Skipping installation to avoid conflicts and assuming a previous step is managing it." + exit 0 +else + echo "[INFO] No existing MultiClusterObservability CR found. Proceeding with installation." +fi +# patch node +oc patch Scheduler cluster --type='json' -p '[{ "op": "replace", "path": "/spec/mastersSchedulable", "value": true }]' +# install acm oc apply -f - </dev/null 2>&1; then echo "[INFO] Creating namespace open-cluster-management-observability" oc create ns open-cluster-management-observability @@ -168,5 +185,64 @@ spec: EOF sleep 1m oc wait --for=condition=Ready pod -l alertmanager=observability,app=multicluster-observability-alertmanager -n open-cluster-management-observability --timeout=300s -oc -n open-cluster-management-observability get pod -oc -n open-cluster-management-observability get svc | grep -E 'alertmanager|rbac-query' +# enable UIPlugin +oc apply -f - < 0 + labels: + cluster: "{{ $labels.cluster }}" + prometheus: "{{ $labels.prometheus }}" + severity: critical +EOF 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/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/coo/coo141_perses/import/accelerators-dashboard-cr-v1alpha1.yaml b/web/cypress/fixtures/coo/coo141_perses/import/accelerators-dashboard-cr-v1alpha1.yaml new file mode 100644 index 000000000..7430a57e6 --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses/import/accelerators-dashboard-cr-v1alpha1.yaml @@ -0,0 +1,305 @@ +apiVersion: perses.dev/v1alpha1 +kind: PersesDashboard +metadata: + labels: + app.kubernetes.io/name: perses-dashboard + app.kubernetes.io/instance: accelerators-dashboard + app.kubernetes.io/part-of: perses-operator + name: accelerators-dashboard + namespace: openshift-cluster-observability-operator +spec: + display: + name: Accelerators common metrics + panels: + "0_0": + kind: Panel + spec: + display: + name: GPU Utilization + 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: accelerator_gpu_utilization + seriesNameFormat: "{{vendor_id}}" + "0_1": + kind: Panel + spec: + display: + name: Memory Used Bytes + 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: accelerator_memory_used_bytes + seriesNameFormat: "{{vendor_id}}" + "0_2": + kind: Panel + spec: + display: + name: Memory Total Bytes + 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: accelerator_memory_total_bytes + seriesNameFormat: "{{vendor_id}}" + "0_3": + kind: Panel + spec: + display: + name: Power Usage (Watts) + 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: accelerator_power_usage_watts + seriesNameFormat: "{{vendor_id}}" + "0_4": + kind: Panel + spec: + display: + name: Temperature (Celsius) + 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: accelerator_temperature_celsius + seriesNameFormat: "{{vendor_id}}" + "0_5": + kind: Panel + spec: + display: + name: SM Clock (Hertz) + 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: accelerator_sm_clock_hertz + seriesNameFormat: "{{vendor_id}}" + "0_6": + kind: Panel + spec: + display: + name: Memory Clock (Hertz) + 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: accelerator_memory_clock_hertz + seriesNameFormat: "{{vendor_id}}" + layouts: + - kind: Grid + spec: + display: + title: Accelerators + collapse: + open: true + items: + - x: 0 + "y": 0 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_0" + - x: 12 + "y": 0 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_1" + - x: 0 + "y": 7 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_2" + - x: 12 + "y": 7 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_3" + - x: 0 + "y": 14 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_4" + - x: 12 + "y": 14 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_5" + - x: 0 + "y": 21 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_6" + 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 + duration: 0s + refreshInterval: 0s + datasources: {} diff --git a/web/cypress/fixtures/coo/coo141_perses/import/accelerators-dashboard-cr-v1alpha2.yaml b/web/cypress/fixtures/coo/coo141_perses/import/accelerators-dashboard-cr-v1alpha2.yaml new file mode 100644 index 000000000..2290ad049 --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses/import/accelerators-dashboard-cr-v1alpha2.yaml @@ -0,0 +1,306 @@ +apiVersion: perses.dev/v1alpha2 +kind: PersesDashboard +metadata: + labels: + app.kubernetes.io/name: perses-dashboard + app.kubernetes.io/instance: accelerators-dashboard + app.kubernetes.io/part-of: perses-operator + name: accelerators-dashboard + namespace: openshift-cluster-observability-operator +spec: + config: + display: + name: Accelerators common metrics + panels: + "0_0": + kind: Panel + spec: + display: + name: GPU Utilization + 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: accelerator_gpu_utilization + seriesNameFormat: "{{vendor_id}}" + "0_1": + kind: Panel + spec: + display: + name: Memory Used Bytes + 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: accelerator_memory_used_bytes + seriesNameFormat: "{{vendor_id}}" + "0_2": + kind: Panel + spec: + display: + name: Memory Total Bytes + 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: accelerator_memory_total_bytes + seriesNameFormat: "{{vendor_id}}" + "0_3": + kind: Panel + spec: + display: + name: Power Usage (Watts) + 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: accelerator_power_usage_watts + seriesNameFormat: "{{vendor_id}}" + "0_4": + kind: Panel + spec: + display: + name: Temperature (Celsius) + 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: accelerator_temperature_celsius + seriesNameFormat: "{{vendor_id}}" + "0_5": + kind: Panel + spec: + display: + name: SM Clock (Hertz) + 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: accelerator_sm_clock_hertz + seriesNameFormat: "{{vendor_id}}" + "0_6": + kind: Panel + spec: + display: + name: Memory Clock (Hertz) + 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: accelerator_memory_clock_hertz + seriesNameFormat: "{{vendor_id}}" + layouts: + - kind: Grid + spec: + display: + title: Accelerators + collapse: + open: true + items: + - x: 0 + "y": 0 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_0" + - x: 12 + "y": 0 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_1" + - x: 0 + "y": 7 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_2" + - x: 12 + "y": 7 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_3" + - x: 0 + "y": 14 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_4" + - x: 12 + "y": 14 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_5" + - x: 0 + "y": 21 + width: 12 + height: 7 + content: + $ref: "#/spec/panels/0_6" + 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 + duration: 0s + refreshInterval: 0s + datasources: {} diff --git a/web/cypress/fixtures/coo/coo141_perses/import/acm-vm-status.json b/web/cypress/fixtures/coo/coo141_perses/import/acm-vm-status.json new file mode 100644 index 000000000..5e66d30ef --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses/import/acm-vm-status.json @@ -0,0 +1,729 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "This dashboard provides a quick overview of the health status of Virtual Machines (VMs) across clusters in the KubeVirt environment. It helps users identify VMs that are currently in unhealthy states and those that have been in such states for an extended period, potentially making them candidates for cleanup. Use the filters to customize the view based on cluster, namespace, VM name, and duration in an unhealthy state for efficient monitoring and management.", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 41, + "iteration": 1726219957608, + "links": [], + "panels": [ + { + "datasource": null, + "description": "The total CPUs of the VMs that are listed in the dashboard", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "text", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.5.20", + "targets": [ + { + "exemplar": true, + "expr": "sum (\n(\n sum by (cluster, namespace, name) (\n kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"cores\", source=~\"default|domain\"}\n * ignoring (unit)(kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"sockets\", source=~\"default|domain\"})\n * ignoring (unit)(kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"threads\", source=~\"default|domain\"})\n ) or\n sum by (cluster, namespace, name) (\n kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"cores\", source=~\"default|domain\"}\n * ignoring (unit)(kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"sockets\", source=~\"default|domain\"})\n )\n or\n sum by (cluster, namespace, name) (\n kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"cores\", source=~\"default|domain\"})\n)\n + on(cluster, name, namespace) group_left(status)\n 0*(\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n )\n +${status:raw}\n) ", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Total Allocated CPU", + "type": "stat" + }, + { + "datasource": null, + "description": "The total Memory of the VMs that are listed in the dashboard", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "text", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.5.20", + "targets": [ + { + "exemplar": true, + "expr": "sum(\nmax by (cluster, namespace, name, status)(\n (kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"memory\"})\n + on(cluster, name, namespace) group_left(status)\n 0*(\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n )\n +${status:raw}\n))", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Total Allocated Memory", + "type": "stat" + }, + { + "datasource": null, + "description": "The total disk size of the VMs that are listed in the dashboard", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "text", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.5.20", + "targets": [ + { + "exemplar": true, + "expr": "sum (\n (kubevirt_vm_disk_allocated_size_bytes{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"})\n + on(cluster, name, namespace) group_left(status)\n 0*(\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n )\n +${status:raw}\n) ", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Total Allocated Disk", + "type": "stat" + }, + { + "datasource": null, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Cluster" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Namespace" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time in Status" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + }, + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VM Name" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + }, + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "", + "url": "/d/RnxEyj6Sz/executive-dashboards-single-virtual-machine-view?orgId=1&var-cluster=${__data.fields.Cluster}&var-name=${__data.fields[\"VM Name\"]}&var-namespace=${__data.fields.Namespace}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Status" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Allocated Disk" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time Since Last Migration" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeFromNow" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Last Migration" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeAsIso" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Allocated Memory" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + }, + { + "id": "decimals", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 5, + "options": { + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "8.5.20", + "targets": [ + { + "exemplar": true, + "expr": "sum by (cluster, namespace, name, status)(\n (kubevirt_vm_disk_allocated_size_bytes{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"})\n + on(cluster, name, namespace) group_left(status)\n 0*(\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n )\n +${status:raw}\n)\n", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + }, + { + "exemplar": true, + "expr": " (\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\")) > $days_in_status_gt * 24 * 60 * 60\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\")) < $days_in_status_lt * 24 * 60 * 60\n )\n ) +${status:raw}\n or\n (\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\")) > $days_in_status_gt * 24 * 60 * 60\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\")) < $days_in_status_lt * 24 * 60 * 60\n )\n ) +${status:raw}\n or\n (\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\")) > $days_in_status_gt * 24 * 60 * 60\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\")) < $days_in_status_lt * 24 * 60 * 60\n )\n ) +${status:raw}\n or\n (\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\")) > $days_in_status_gt * 24 * 60 * 60\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\")) < $days_in_status_lt * 24 * 60 * 60\n )\n ) +${status:raw}\n or\n (\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\")) > $days_in_status_gt * 24 * 60 * 60\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\")) < $days_in_status_lt * 24 * 60 * 60\n )\n +${status:raw}\n)\n", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "B" + }, + { + "exemplar": true, + "expr": "sum by (cluster, namespace, name, status)(\n (kubevirt_vmi_migration_end_time_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"}*1000)\n + on(cluster, name, namespace) group_left(status)\n 0*(\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n )\n +${status:raw}\n)\n", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "C" + }, + { + "exemplar": true, + "expr": "sum by (cluster, namespace, name, status)(\n (kubevirt_vmi_migration_end_time_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"}*1000)\n + on(cluster, name, namespace) group_left(status)\n 0*(\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n )\n +${status:raw}\n)\n", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "D" + }, + { + "exemplar": true, + "expr": "max by (cluster, namespace, name, status)(\n (kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"memory\"})\n + on(cluster, name, namespace) group_left(status)\n 0*(\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n )\n +${status:raw}\n)\n", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "E" + }, + { + "exemplar": true, + "expr": "(\n sum by (cluster, namespace, name) (\n kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"cores\", source=~\"default|domain\"}\n * ignoring (unit)(kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"sockets\", source=~\"default|domain\"})\n * ignoring (unit)(kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"threads\", source=~\"default|domain\"})\n ) or\n sum by (cluster, namespace, name) (\n kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"cores\", source=~\"default|domain\"}\n * ignoring (unit)(kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"sockets\", source=~\"default|domain\"})\n )\n or\n sum by (cluster, namespace, name) (\n kubevirt_vm_resource_requests{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\", resource=\"cpu\", unit=\"cores\", source=~\"default|domain\"})\n)\n + on(cluster, name, namespace) group_left(status)\n 0*(\n (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_starting_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"starting\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"running\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_non_running_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"stopped\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_error_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"error\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n or\n (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n > ($days_in_status_gt * 24 * 60 * 60)\n ) and (\n (time() - label_replace(kubevirt_vm_migrating_status_last_transition_timestamp_seconds{cluster=~\"$cluster\", name=~\"$name\", namespace=~\"$namespace\"} > 0, \"status\", \"migrating\", \"\", \"\"))\n < ($days_in_status_lt * 24 * 60 * 60)\n )\n )\n +${status:raw}", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "F" + } + ], + "title": "Virtual Machines List by Time In Status", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value #B": false, + "Value #D": true, + "clusterID": true, + "clusterType": true, + "container": true, + "endpoint": true, + "instance": true, + "job": true, + "pod": true, + "receive": true, + "service": true, + "tenant_id": true + }, + "indexByName": { + "Time": 0, + "Value #A": 17, + "Value #B": 20, + "Value #C": 18, + "Value #D": 19, + "Value #E": 16, + "Value #F": 15, + "cluster": 1, + "clusterID": 2, + "clusterType": 14, + "container": 3, + "endpoint": 4, + "instance": 5, + "job": 6, + "name": 8, + "namespace": 7, + "pod": 9, + "receive": 10, + "service": 11, + "status": 12, + "tenant_id": 13 + }, + "renameByName": { + "Value": "Time in Status", + "Value #A": "Allocated Disk", + "Value #B": "Time in Status", + "Value #C": "Time Since Last Migration", + "Value #D": "Last Migration", + "Value #E": "Allocated Memory", + "Value #F": "Allocated CPU", + "cluster": "Cluster", + "clusterID": "", + "clusterType": "", + "name": "VM Name", + "namespace": "Namespace", + "status": "Status", + "tenant_id": "" + } + } + } + ], + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 30, + "style": "dark", + "tags": [ + "ACM", + "KubeVirt", + "OpenShift", + "Virtualization" + ], + "templating": { + "list": [ + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": null, + "definition": "label_values(kubevirt_vm_running_status_last_transition_timestamp_seconds, cluster)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": "Cluster", + "multi": true, + "name": "cluster", + "options": [], + "query": { + "query": "label_values(kubevirt_vm_running_status_last_transition_timestamp_seconds, cluster)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": null, + "definition": "label_values(kubevirt_vmi_info, namespace)", + "description": "Filter the Virtual Machine by its Namespace", + "error": null, + "hide": 0, + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(kubevirt_vmi_info, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": null, + "definition": "label_values(kubevirt_vmi_info, name)", + "description": "Filter the Virtual Machine by its name", + "error": null, + "hide": 0, + "includeAll": true, + "label": "VM Name", + "multi": true, + "name": "name", + "options": [], + "query": { + "query": "label_values(kubevirt_vmi_info, name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allowCustomValue": false, + "current": { + "text": [ + "on(cluster,name,namespace) group_left()(0*(sum by(cluster,namespace,name)(kubevirt_vm_info)))" + ], + "value": [ + "on(cluster,name,namespace) group_left()(0*(sum by(cluster,namespace,name)(kubevirt_vm_info)))" + ] + }, + "includeAll": false, + "label": "Status", + "name": "status", + "options": [ + { + "selected": true, + "text": "All", + "value": "on(cluster,name,namespace) group_left()(0*(sum by(cluster,namespace,name)(kubevirt_vm_info)))" + }, + { + "selected": false, + "text": "stopped", + "value": "on(cluster,name,namespace) group_left()(0*(sum by(cluster,namespace,name)(kubevirt_vm_info{status_group=\"non_running\"}>0)))" + }, + { + "selected": false, + "text": "starting", + "value": "on(cluster,name,namespace) group_left()(0*(sum by(cluster,namespace,name)(kubevirt_vm_info{status_group=\"starting\"} > 0)))" + }, + { + "selected": false, + "text": "migrating", + "value": "on(cluster, name, namespace) group_left() (0*(sum by (cluster, namespace, name)(kubevirt_vm_info{status_group=\"migrating\"}>0)))" + }, + { + "selected": false, + "text": "error", + "value": "on(cluster,name,namespace) group_left()(0*(sum by(cluster,namespace,name)(kubevirt_vm_info{status_group=\"error\"}>0)))" + }, + { + "selected": false, + "text": "running", + "value": "on(cluster,name,namespace) group_left()(0*(sum by(cluster,namespace,name)(kubevirt_vm_info{status_group=\"running\"}>0)))" + } + ], + "query": "All : on(cluster\\,name\\,namespace) group_left()(0*(sum by(cluster\\,namespace\\,name)(kubevirt_vm_info))), stopped : on(cluster\\,name\\,namespace) group_left()(0*(sum by(cluster\\,namespace\\,name)(kubevirt_vm_info{status_group=\"non_running\"}>0))), starting : on(cluster\\,name\\,namespace) group_left()(0*(sum by(cluster\\,namespace\\,name)(kubevirt_vm_info{status_group=\"starting\"} > 0))), migrating : on(cluster\\, name\\, namespace) group_left() (0*(sum by (cluster\\, namespace\\, name)(kubevirt_vm_info{status_group=\"migrating\"}>0))), error : on(cluster\\,name\\,namespace) group_left()(0*(sum by(cluster\\,namespace\\,name)(kubevirt_vm_info{status_group=\"error\"}>0))), running : on(cluster\\,name\\,namespace) group_left()(0*(sum by(cluster\\,namespace\\,name)(kubevirt_vm_info{status_group=\"running\"}>0)))", + "type": "custom" + }, + { + "current": { + "selected": false, + "text": "0", + "value": "0" + }, + "description": "Filter the Virtual Machines that are in the specific status for more then the selected number of days", + "error": null, + "hide": 0, + "label": "Days in Status >", + "name": "days_in_status_gt", + "options": [ + { + "selected": true, + "text": "0", + "value": "0" + } + ], + "query": "0", + "skipUrlSync": false, + "type": "textbox" + }, + { + "current": { + "selected": true, + "text": "1000", + "value": "1000" + }, + "description": "Filter the Virtual Machines that are in the specific status for less then the selected number of days", + "error": null, + "hide": 0, + "label": "Days in Status <", + "name": "days_in_status_lt", + "options": [ + { + "selected": true, + "text": "1000", + "value": "1000" + } + ], + "query": "1000", + "skipUrlSync": false, + "type": "textbox" + } + ] + }, + "time": { + "from": "now-30d", + "to": "now" + }, + "timepicker": { + "hidden": false + }, + "timezone": "", + "title": "Service Level dashboards / Virtual Machines by Time in Status", + "uid": "lMD6V93Sz", + "version": 1 + } \ No newline at end of file diff --git a/web/cypress/fixtures/coo/coo141_perses/import/grafana_to_check_errors.json b/web/cypress/fixtures/coo/coo141_perses/import/grafana_to_check_errors.json new file mode 100644 index 000000000..fdc3a003e --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses/import/grafana_to_check_errors.json @@ -0,0 +1,15766 @@ +{ + "__requires": [ + { + "type": "panel", + "id": "bargauge", + "name": "Bar gauge", + "version": "" + }, + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "11.6.1" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [ + { + "icon": "external link", + "tags": [], + "targetBlank": true, + "title": "GitHub", + "type": "link", + "url": "https://github.com/rfmoz/grafana-dashboards" + }, + { + "icon": "external link", + "tags": [], + "targetBlank": true, + "title": "Grafana", + "type": "link", + "url": "https://grafana.com/grafana/dashboards/1860" + } + ], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 261, + "panels": [], + "title": "Quick CPU / Mem / Disk", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Resource pressure via PSI", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "dark-yellow", + "value": 70 + }, + { + "color": "dark-red", + "value": 90 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 323, + "options": { + "displayMode": "basic", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "text": {}, + "valueMode": "color" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_pressure_cpu_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": true, + "legendFormat": "CPU", + "range": false, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_pressure_memory_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": true, + "legendFormat": "Mem", + "range": false, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_pressure_io_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": true, + "legendFormat": "I/O", + "range": false, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_pressure_irq_stalled_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": true, + "legendFormat": "Irq", + "range": false, + "refId": "D", + "step": 240 + } + ], + "title": "Pressure", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Overall CPU busy percentage (averaged across all cores)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)" + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 85 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 95 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 20, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "100 * (1 - avg(rate(node_cpu_seconds_total{mode=\"idle\", instance=\"$node\"}[$__rate_interval])))", + "instant": true, + "legendFormat": "", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Busy", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "System load over all CPU cores together", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)" + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 85 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 95 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 6, + "y": 1 + }, + "id": 155, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "scalar(node_load1{instance=\"$node\",job=\"$job\"}) * 100 / count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu))", + "format": "time_series", + "instant": true, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Sys Load", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Real RAM usage excluding cache and reclaimable memory", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)" + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 80 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 9, + "y": 1 + }, + "id": 16, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "clamp_min((1 - (node_memory_MemAvailable_bytes{instance=\"$node\", job=\"$job\"} / node_memory_MemTotal_bytes{instance=\"$node\", job=\"$job\"})) * 100, 0)", + "format": "time_series", + "instant": true, + "range": false, + "refId": "B", + "step": 240 + } + ], + "title": "RAM Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Percentage of swap space currently used by the system", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)" + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 10 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 25 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 12, + "y": 1 + }, + "id": 21, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "((node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"}) / (node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"})) * 100", + "instant": true, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "SWAP Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Used Root FS", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)" + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 80 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 15, + "y": 1 + }, + "id": 154, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "(\n (node_filesystem_size_bytes{instance=\"$node\", job=\"$job\", mountpoint=\"/\", fstype!=\"rootfs\"}\n - node_filesystem_avail_bytes{instance=\"$node\", job=\"$job\", mountpoint=\"/\", fstype!=\"rootfs\"})\n / node_filesystem_size_bytes{instance=\"$node\", job=\"$job\", mountpoint=\"/\", fstype!=\"rootfs\"}\n) * 100\n", + "format": "time_series", + "instant": true, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Root FS Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 18, + "y": 1 + }, + "id": 14, + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu))", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "CPU Cores", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 20, + "y": 1 + }, + "id": 75, + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}", + "instant": true, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "RAM Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 22, + "y": 1 + }, + "id": 18, + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"}", + "instant": true, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "SWAP Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)" + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 70 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 18, + "y": 3 + }, + "id": 23, + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"}", + "format": "time_series", + "instant": true, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "RootFS Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 20, + "y": 3 + }, + "id": 15, + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "node_time_seconds{instance=\"$node\",job=\"$job\"} - node_boot_time_seconds{instance=\"$node\",job=\"$job\"}", + "instant": true, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Uptime", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 263, + "panels": [], + "title": "Basic CPU / Mem / Net / Disk", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "CPU time spent busy vs idle, split by activity type", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Busy Iowait" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Idle" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy System" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy User" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy Other" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 77, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "width": 250 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"system\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "instant": false, + "legendFormat": "Busy System", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Busy User", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"iowait\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Busy Iowait", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=~\".*irq\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Busy IRQs", + "range": true, + "refId": "D", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode!='idle',mode!='user',mode!='system',mode!='iowait',mode!='irq',mode!='softirq'}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Busy Other", + "range": true, + "refId": "E", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"idle\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Idle", + "range": true, + "refId": "F", + "step": 240 + } + ], + "title": "CPU Basic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "RAM and swap usage overview, including caches", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Swap used" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache + Buffer" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 78, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Total", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"} - (node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} + node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} + node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "legendFormat": "Used", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} + node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} + node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Cache + Buffer", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Free", + "range": true, + "refId": "D", + "step": 240 + }, + { + "editorMode": "code", + "expr": "(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "legendFormat": "Swap used", + "range": true, + "refId": "E", + "step": 240 + } + ], + "title": "Memory Basic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Per-interface network traffic (receive and transmit) in bits per second", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Tx.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 74, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", + "format": "time_series", + "legendFormat": "Rx {{device}}", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", + "format": "time_series", + "legendFormat": "Tx {{device}} ", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Basic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Percentage of filesystem space used for each mounted device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 152, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "((node_filesystem_size_bytes{instance=\"$node\", job=\"$job\", device!~\"rootfs\"} - node_filesystem_avail_bytes{instance=\"$node\", job=\"$job\", device!~\"rootfs\"}) / node_filesystem_size_bytes{instance=\"$node\", job=\"$job\", device!~\"rootfs\"}) * 100", + "format": "time_series", + "legendFormat": "{{mountpoint}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Disk Space Used Basic", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 265, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "CPU time usage split by state, normalized across all CPU cores", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 70, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Idle - Waiting for something to happen" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Iowait - Waiting for I/O to complete" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Irq - Servicing interrupts" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Nice - Niced processes executing in user mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Softirq - Servicing softirqs" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Steal - Time spent in other operating systems when running in a virtualized environment" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCE2DE", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "System - Processes executing in kernel mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "User - Normal processes executing in user mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#5195CE", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Guest CPU usage" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "custom.stacking", + "value": { + "group": "A", + "mode": "none" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 21 + }, + "id": 3, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 250 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{mode=\"system\",instance=\"$node\",job=\"$job\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "interval": "", + "legendFormat": "System - Processes executing in kernel mode", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{mode=\"user\",instance=\"$node\",job=\"$job\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "User - Normal processes executing in user mode", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{mode=\"nice\",instance=\"$node\",job=\"$job\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Nice - Niced processes executing in user mode", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{mode=\"iowait\",instance=\"$node\",job=\"$job\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Iowait - Waiting for I/O to complete", + "range": true, + "refId": "D", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{mode=\"irq\",instance=\"$node\",job=\"$job\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Irq - Servicing interrupts", + "range": true, + "refId": "E", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{mode=\"softirq\",instance=\"$node\",job=\"$job\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Softirq - Servicing softirqs", + "range": true, + "refId": "F", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{mode=\"steal\",instance=\"$node\",job=\"$job\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Steal - Time spent in other operating systems when running in a virtualized environment", + "range": true, + "refId": "G", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{mode=\"idle\",instance=\"$node\",job=\"$job\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "legendFormat": "Idle - Waiting for something to happen", + "range": true, + "refId": "H", + "step": 240 + }, + { + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_guest_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]))) > 0", + "format": "time_series", + "legendFormat": "Guest CPU usage", + "range": true, + "refId": "I", + "step": 240 + } + ], + "title": "CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Breakdown of physical memory and swap usage. Hardware-detected memory errors are also displayed", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap - Swap memory usage" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused - Free memory unassigned" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Hardware Corrupted - *./" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 21 + }, + "id": 24, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Slab_bytes{instance=\"$node\",job=\"$job\"} - node_memory_PageTables_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapCached_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Apps - Memory used by user-space applications", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_PageTables_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "PageTables - Memory used to map between virtual and physical memory addresses", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_SwapCached_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "SwapCache - Memory that keeps track of pages that have been fetched from swap but not yet been modified", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Slab_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Slab - Memory used by the kernel to cache data structures for its own use (caches like inode, dentry, etc)", + "range": true, + "refId": "D", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Cache - Parked file data (file content) cache", + "range": true, + "refId": "E", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Buffers - Block device (e.g. harddisk) cache", + "range": true, + "refId": "F", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Unused - Free memory unassigned", + "range": true, + "refId": "G", + "step": 240 + }, + { + "editorMode": "code", + "expr": "(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "legendFormat": "Swap - Swap space used", + "range": true, + "refId": "H", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_HardwareCorrupted_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working", + "range": true, + "refId": "I", + "step": 240 + } + ], + "title": "Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Incoming and outgoing network traffic per interface", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 433 + }, + "id": 84, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", + "format": "time_series", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Network interface utilization as a percentage of its maximum capacity", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 433 + }, + "id": 338, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])\n / ignoring(speed) node_network_speed_bytes{instance=\"$node\",job=\"$job\", speed!=\"-1\"}", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "(rate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])\n / ignoring(speed) node_network_speed_bytes{instance=\"$node\",job=\"$job\", speed!=\"-1\"})", + "format": "time_series", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Saturation", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Disk I/O operations per second for each device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "read (-) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 445 + }, + "id": 229, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\",device=~\"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+\"}[$__rate_interval])", + "legendFormat": "{{device}} - Read", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\",device=~\"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+\"}[$__rate_interval])", + "legendFormat": "{{device}} - Write", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk IOps", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Disk I/O throughput per device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "read (-) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read*./" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 445 + }, + "id": 42, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_read_bytes_total{instance=\"$node\",job=\"$job\",device=~\"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Read", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_written_bytes_total{instance=\"$node\",job=\"$job\",device=~\"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Write", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk Throughput", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Amount of available disk space per mounted filesystem, excluding rootfs. Based on block availability to non-root users", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 457 + }, + "id": 43, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "legendFormat": "{{mountpoint}}", + "metric": "", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_filesystem_free_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "hide": true, + "legendFormat": "{{mountpoint}} - Free", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "hide": true, + "legendFormat": "{{mountpoint}} - Size", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Filesystem Space Available", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Disk usage (used = total - available) per mountpoint", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 457 + }, + "id": 156, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'} - node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "legendFormat": "{{mountpoint}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Filesystem Used", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Percentage of time the disk was actively processing I/O operations", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 469 + }, + "id": 127, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_io_time_seconds_total{instance=\"$node\",job=\"$job\",device=~\"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+\"} [$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "{{device}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Disk I/O Utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "How often tasks experience CPU, memory, or I/O delays. “Some” indicates partial slowdown; “Full” indicates all tasks are stalled. Based on Linux PSI metrics:\nhttps://docs.kernel.org/accounting/psi.html", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "some (-) / full (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Some.*/" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Some.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 469 + }, + "id": 322, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_pressure_cpu_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "CPU - Some", + "range": true, + "refId": "CPU some", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_pressure_memory_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Memory - Some", + "range": true, + "refId": "Memory some", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_pressure_memory_stalled_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Memory - Full", + "range": true, + "refId": "Memory full", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_pressure_io_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "I/O - Some", + "range": true, + "refId": "I/O some", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_pressure_io_stalled_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "I/O - Full", + "range": true, + "refId": "I/O full", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_pressure_irq_stalled_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "IRQ - Full", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Pressure Stall Information", + "type": "timeseries" + } + ], + "title": "CPU / Memory / Net / Disk", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 266, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Displays committed memory usage versus the system's commit limit. Exceeding the limit is allowed under Linux overcommit policies but may increase OOM risks under high load", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*CommitLimit - *./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 732 + }, + "id": 135, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_Committed_AS_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Committed_AS – Memory promised to processes (not necessarily used)", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_CommitLimit_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "CommitLimit - Max allowable committed memory", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Memory Committed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Memory currently dirty (modified but not yet written to disk), being actively written back, or held by writeback buffers. High dirty or writeback memory may indicate disk I/O pressure or delayed flushing", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 732 + }, + "id": 130, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_Writeback_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Writeback – Memory currently being flushed to disk", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_WritebackTmp_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "WritebackTmp – FUSE temporary writeback buffers", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Dirty_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Dirty – Memory marked dirty (pending write to disk)", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_NFS_Unstable_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "NFS Unstable – Pages sent to NFS server, awaiting storage commit", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Memory Writeback and Dirty", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Kernel slab memory usage, separated into reclaimable and non-reclaimable categories. Reclaimable memory can be freed under memory pressure (e.g., caches), while unreclaimable memory is locked by the kernel for core functions", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 932 + }, + "id": 131, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_SUnreclaim_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "SUnreclaim – Non-reclaimable slab memory (kernel objects)", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "SReclaimable – Potentially reclaimable slab memory (e.g., inode cache)", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Memory Slab", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Memory used for mapped files (such as libraries) and shared memory (shmem and tmpfs), including variants backed by huge pages", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 932 + }, + "id": 138, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_Mapped_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Mapped – Memory mapped from files (e.g., libraries, mmap)", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Shmem_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Shmem – Shared memory used by processes and tmpfs", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_ShmemHugePages_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "ShmemHugePages – Shared memory (shmem/tmpfs) allocated with HugePages", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_ShmemPmdMapped_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "PMD Mapped – Shmem/tmpfs backed by Transparent HugePages (PMD)", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Memory Shared and Mapped", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Proportion of memory pages in the kernel's active and inactive LRU lists relative to total RAM. Active pages have been recently used, while inactive pages are less recently accessed but still resident in memory", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Active.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Inactive.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 942 + }, + "id": 136, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "(node_memory_Inactive_bytes{instance=\"$node\",job=\"$job\"}) \n/ \n(node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "legendFormat": "Inactive – Less recently used memory, more likely to be reclaimed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "(node_memory_Active_bytes{instance=\"$node\",job=\"$job\"}) \n/ \n(node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"})\n", + "format": "time_series", + "legendFormat": "Active – Recently used memory, retained unless under pressure", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Memory LRU Active / Inactive (%)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Breakdown of memory pages in the kernel's active and inactive LRU lists, separated by anonymous (heap, tmpfs) and file-backed (caches, mmap) pages.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 942 + }, + "id": 191, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_Inactive_file_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Inactive_file - File-backed memory on inactive LRU list", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Inactive_anon_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Inactive_anon – Anonymous memory on inactive LRU (incl. tmpfs & swap cache)", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Active_file_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Active_file - File-backed memory on active LRU list", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Active_anon_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Active_anon – Anonymous memory on active LRU (incl. tmpfs & swap cache)", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Memory LRU Active / Inactive Detail", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Tracks kernel memory used for CPU-local structures, per-thread stacks, and bounce buffers used for I/O on DMA-limited devices. These areas are typically small but critical for low-level operations", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 952 + }, + "id": 160, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_KernelStack_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "KernelStack – Kernel stack memory (per-thread, non-reclaimable)", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Percpu_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "PerCPU – Dynamically allocated per-CPU memory (used by kernel modules)", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Bounce_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Bounce Memory – I/O buffer for DMA-limited devices", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Memory Kernel / CPU / IO", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Usage of the kernel's vmalloc area, which provides virtual memory allocations for kernel modules and drivers. Includes total, used, and largest free block sizes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Total.*/" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 952 + }, + "id": 70, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_VmallocChunk_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Vmalloc Free Chunk – Largest available block in vmalloc area", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_VmallocTotal_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Vmalloc Total – Total size of the vmalloc memory area", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_VmallocUsed_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Vmalloc Used – Portion of vmalloc area currently in use", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Memory Vmalloc", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Memory used by anonymous pages (not backed by files), including standard and huge page allocations. Includes heap, stack, and memory-mapped anonymous regions", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 962 + }, + "id": 129, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_AnonHugePages_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "AnonHugePages – Anonymous memory using HugePages", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_AnonPages_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "AnonPages – Anonymous memory (non-file-backed)", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Memory Anonymous", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Memory that is locked in RAM and cannot be swapped out. Includes both kernel-unevictable memory and user-level memory locked with mlock()", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 962 + }, + "id": 137, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_Unevictable_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Unevictable – Kernel-pinned memory (not swappable)", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_Mlocked_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Mlocked – Application-locked memory via mlock()", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Memory Unevictable and MLocked", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "How much memory is directly mapped in the kernel using different page sizes (4K, 2M, 1G). Helps monitor large page utilization in the direct map region", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 972 + }, + "id": 128, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_DirectMap1G_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "DirectMap 1G – Memory mapped with 1GB pages", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_DirectMap2M_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "DirectMap 2M – Memory mapped with 2MB pages", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_DirectMap4k_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "DirectMap 4K – Memory mapped with 4KB pages", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Memory DirectMap", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Displays HugePages memory usage in bytes, including allocated, free, reserved, and surplus memory. All values are calculated based on the number of huge pages multiplied by their configured size", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 972 + }, + "id": 140, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_memory_HugePages_Free{instance=\"$node\",job=\"$job\"} * node_memory_Hugepagesize_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "HugePages Used – Currently allocated", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_HugePages_Rsvd{instance=\"$node\",job=\"$job\"} * node_memory_Hugepagesize_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "HugePages Reserved – Promised but unused", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_HugePages_Surp{instance=\"$node\",job=\"$job\"} * node_memory_Hugepagesize_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "HugePages Surplus – Dynamic pool extension", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_memory_HugePages_Total{instance=\"$node\",job=\"$job\"} * node_memory_Hugepagesize_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "HugePages Total – Reserved memory", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Memory HugePages", + "type": "timeseries" + } + ], + "title": "Memory Meminfo", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 267, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of memory pages being read from or written to disk (page-in and page-out operations). High page-out may indicate memory pressure or swapping activity", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 733 + }, + "id": 176, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_vmstat_pgpgin{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Pagesin - Page in ops", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_vmstat_pgpgout{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Pagesout - Page out ops", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Memory Pages In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate at which memory pages are being swapped in from or out to disk. High swap-out activity may indicate memory pressure", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 733 + }, + "id": 22, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_vmstat_pswpin{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Pswpin - Pages swapped in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_vmstat_pswpout{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Pswpout - Pages swapped out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Memory Pages Swap In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of memory page faults, split into total, major (disk-backed), and derived minor (non-disk) faults. High major fault rates may indicate memory pressure or insufficient RAM", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Pgfault - Page major and minor fault ops" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "none" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 913 + }, + "id": 175, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_vmstat_pgfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Pgfault - Page major and minor fault ops", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_vmstat_pgmajfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Pgmajfault - Major page fault ops", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_vmstat_pgfault{instance=\"$node\",job=\"$job\"}[$__rate_interval]) - irate(node_vmstat_pgmajfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Pgminfault - Minor page fault ops", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Memory Page Faults", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of Out-of-Memory (OOM) kill events. A non-zero value indicates the kernel has terminated one or more processes due to memory exhaustion", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "OOM Kills" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 913 + }, + "id": 307, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_vmstat_oom_kill{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "OOM Kills", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "OOM Killer", + "type": "timeseries" + } + ], + "title": "Memory Vmstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 293, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Tracks the system clock's estimated and maximum error, as well as its offset from the reference clock (e.g., via NTP). Useful for detecting synchronization drift", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 734 + }, + "id": 260, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_timex_estimated_error_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Estimated error", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_timex_offset_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Offset local vs reference", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_timex_maxerror_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Maximum error", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Time Synchronized Drift", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "NTP phase-locked loop (PLL) time constant used by the kernel to control time adjustments. Lower values mean faster correction but less stability", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 734 + }, + "id": 291, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_timex_loop_time_constant{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "PLL Time Constant", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Time PLL Adjust", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Shows whether the system clock is synchronized to a reliable time source, and the current frequency correction ratio applied by the kernel to maintain synchronization", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 884 + }, + "id": 168, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_timex_sync_status{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Sync status (1 = ok)", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_timex_frequency_adjustment_ratio{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Frequency Adjustment", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_timex_tick_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "legendFormat": "Tick Interval", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_timex_tai_offset_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "legendFormat": "TAI Offset", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Time Synchronized Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Displays the PPS signal's frequency offset and stability (jitter) in hertz. Useful for monitoring high-precision time sources like GPS or atomic clocks", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "rothz" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 884 + }, + "id": 333, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_timex_pps_frequency_hertz{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "PPS Frequency Offset", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_timex_pps_stability_hertz{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "PPS Frequency Stability", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "PPS Frequency / Stability", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Tracks PPS signal timing jitter and shift compared to system clock", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 894 + }, + "id": 334, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_timex_pps_jitter_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "PPS Jitter", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_timex_pps_shift_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "PPS Shift", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "PPS Time Accuracy", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of PPS synchronization diagnostics including calibration events, jitter violations, errors, and frequency stability exceedances", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 894 + }, + "id": 335, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_timex_pps_calibration_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "PPS Calibrations/sec", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_timex_pps_error_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "PPS Errors/sec", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_timex_pps_stability_exceeded_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "PPS Stability Exceeded/sec", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_timex_pps_jitter_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "PPS Jitter Events/sec", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "PPS Sync Events", + "type": "timeseries" + } + ], + "title": "System Timesync", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 312, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Processes currently in runnable or blocked states. Helps identify CPU contention or I/O wait bottlenecks.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 735 + }, + "id": 62, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_procs_blocked{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Blocked (I/O Wait)", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_procs_running{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Runnable (Ready for CPU)", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Processes Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Current number of processes in each state (e.g., running, sleeping, zombie). Requires --collector.processes to be enabled in node_exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "D" + }, + "properties": [ + { + "id": "displayName", + "value": "Uninterruptible Sleeping" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "I" + }, + "properties": [ + { + "id": "displayName", + "value": "Idle Kernel Thread" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "R" + }, + "properties": [ + { + "id": "displayName", + "value": "Running" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "S" + }, + "properties": [ + { + "id": "displayName", + "value": "Interruptible Sleeping" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "T" + }, + "properties": [ + { + "id": "displayName", + "value": "Stopped" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "X" + }, + "properties": [ + { + "id": "displayName", + "value": "Dead" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Z" + }, + "properties": [ + { + "id": "displayName", + "value": "Zombie" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 735 + }, + "id": 315, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_processes_state{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{ state }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Processes Detailed States", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of new processes being created on the system (forks/sec).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 765 + }, + "id": 148, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_forks_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Process Forks per second", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Processes Forks", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Shows CPU saturation per core, calculated as the proportion of time spent waiting to run relative to total time demanded (running + waiting).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*waiting.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 765 + }, + "id": 305, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_schedstat_running_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": true, + "interval": "", + "legendFormat": "CPU {{ cpu }} - Running", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_schedstat_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": true, + "interval": "", + "legendFormat": "CPU {{cpu}} - Waiting Queue", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_schedstat_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])\n/\n(irate(node_schedstat_running_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) + irate(node_schedstat_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]))\n", + "format": "time_series", + "interval": "", + "legendFormat": "CPU {{cpu}}", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "CPU Saturation per Core", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of active PIDs on the system and the configured maximum allowed. Useful for detecting PID exhaustion risk. Requires --collector.processes in node_exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "PIDs limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 775 + }, + "id": 313, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_processes_pids{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Number of PIDs", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_processes_max_processes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "PIDs limit", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "PIDs Number and Limit", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of active threads on the system and the configured thread limit. Useful for monitoring thread pressure. Requires --collector.processes in node_exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Threads limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 775 + }, + "id": 314, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_processes_threads{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Allocated threads", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_processes_max_threads{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Threads limit", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Threads Number and Limit", + "type": "timeseries" + } + ], + "title": "System Processes", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 269, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Per-second rate of context switches and hardware interrupts. High values may indicate intense CPU or I/O activity", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 816 + }, + "id": 8, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_context_switches_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Context switches", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_intr_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "Interrupts", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Context Switches / Interrupts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "System load average over 1, 5, and 15 minutes. Reflects the number of active or waiting processes. Values above CPU core count may indicate overload", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "CPU Core Count" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 816 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_load1{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Load 1m", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_load5{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Load 5m", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_load15{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Load 15m", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu))", + "format": "time_series", + "legendFormat": "CPU Core Count", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "System Load", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Real-time CPU frequency scaling per core, including average minimum and maximum allowed scaling frequencies", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "hertz" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + }, + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": false, + "viz": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + }, + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": false, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 826 + }, + "id": 321, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_cpu_scaling_frequency_hertz{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "avg(node_cpu_scaling_frequency_max_hertz{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "legendFormat": "Max", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "avg(node_cpu_scaling_frequency_min_hertz{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "legendFormat": "Min", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "CPU Frequency Scaling", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of scheduling timeslices executed per CPU. Reflects how frequently the scheduler switches tasks on each core", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 826 + }, + "id": 306, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_schedstat_timeslices_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Schedule Timeslices", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Breaks down hardware interrupts by type and device. Useful for diagnosing IRQ load on network, disk, or CPU interfaces. Requires --collector.interrupts to be enabled in node_exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 836 + }, + "id": 259, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_interrupts_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "{{ type }} - {{ info }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "IRQ Detail", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of bits of entropy currently available to the system's random number generators (e.g., /dev/random). Low values may indicate that random number generation could block or degrade performance of cryptographic operations", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "decbits" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Entropy pool max" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 836 + }, + "id": 151, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_entropy_available_bits{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Entropy available", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_entropy_pool_size_bits{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Entropy pool max", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Entropy", + "type": "timeseries" + } + ], + "title": "System Misc", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 304, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Monitors hardware sensor temperatures and critical thresholds as exposed by Linux hwmon. Includes CPU, GPU, and motherboard sensors where available", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "celsius" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Critical*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 737 + }, + "id": 158, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_hwmon_temp_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{ chip_name }} {{ sensor }}", + "range": true, + "refId": "A", + "step": 240 + }, + { + "expr": "node_hwmon_temp_crit_alarm_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "legendFormat": "{{ chip_name }} {{ sensor }} Critical Alarm", + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_hwmon_temp_crit_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{ chip_name }} {{ sensor }} Critical", + "range": true, + "refId": "C", + "step": 240 + }, + { + "expr": "node_hwmon_temp_crit_hyst_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "legendFormat": "{{ chip_name }} {{ sensor }} Critical Historical", + "refId": "D", + "step": 240 + }, + { + "expr": "node_hwmon_temp_max_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "legendFormat": "{{ chip_name }} {{ sensor }} Max", + "refId": "E", + "step": 240 + } + ], + "title": "Hardware Temperature Monitor", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Shows how hard each cooling device (fan/throttle) is working relative to its maximum capacity", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 737 + }, + "id": 300, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "100 * node_cooling_device_cur_state{instance=\"$node\",job=\"$job\"} / node_cooling_device_max_state{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{ name }} - {{ type }} ", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Cooling Device Utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Shows the online status of power supplies (e.g., AC, battery). A value of 1-Yes indicates the power supply is active/online", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bool_yes_no" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 747 + }, + "id": 302, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_power_supply_online{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{ power_supply }} online", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Power Supply", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Displays the current fan speeds (RPM) from hardware sensors via the hwmon interface", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "rotrpm" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 747 + }, + "id": 325, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_hwmon_fan_rpm{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{ chip_name }} {{ sensor }}", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_hwmon_fan_min_rpm{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "legendFormat": "{{ chip_name }} {{ sensor }} rpm min", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Hardware Fan Speed", + "type": "timeseries" + } + ], + "title": "Hardware Misc", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 296, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Current number of systemd units in each operational state, such as active, failed, inactive, or transitioning", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Failed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#73BF69", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Activating" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C8F2C2", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Deactivating" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 4228 + }, + "id": 298, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"activating\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Activating", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"active\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Active", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"deactivating\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Deactivating", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"failed\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Failed", + "range": true, + "refId": "D", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"inactive\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Inactive", + "range": true, + "refId": "E", + "step": 240 + } + ], + "title": "Systemd Units State", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Current number of active connections per systemd socket, as reported by the Node Exporter systemd collector", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 4228 + }, + "id": 331, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_systemd_socket_current_connections{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{ name }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Systemd Sockets Current", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of accepted connections per second for each systemd socket", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "eps" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 4238 + }, + "id": 297, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_systemd_socket_accepted_connections_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "{{ name }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Systemd Sockets Accepted", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of systemd socket connection refusals per second, typically due to service unavailability or backlog overflow", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "eps" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 4238 + }, + "id": 332, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_systemd_socket_refused_connections_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "{{ name }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Systemd Sockets Refused", + "type": "timeseries" + } + ], + "title": "Systemd", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 270, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of I/O operations completed per second for the device (after merges), including both reads and writes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "read (–) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 29 + }, + "id": 9, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "legendFormat": "{{device}} - Read", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "legendFormat": "{{device}} - Write", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk Read/Write IOps", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of bytes read from or written to the device per second", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "read (–) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 29 + }, + "id": 33, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_read_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Read", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_disk_written_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "legendFormat": "{{device}} - Write", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk Read/Write Data", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Average time for requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "read (–) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 389 + }, + "id": 37, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_read_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) / irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}} - Read", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_write_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) / irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}} - Write", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk Average Wait Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Average queue length of the requests that were issued to the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/sda_*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 389 + }, + "id": 35, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_io_time_weighted_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Average Queue Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of read and write requests merged per second that were queued to the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "read (–) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 399 + }, + "id": 133, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_reads_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "legendFormat": "{{device}} - Read", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_writes_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "legendFormat": "{{device}} - Write", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Merged", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Percentage of time the disk spent actively processing I/O operations, including general I/O, discards (TRIM), and write cache flushes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 399 + }, + "id": 36, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_io_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}} - General IO", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_discard_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}} - Discard/TRIM", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_flush_requests_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}} - Flush (write cache)", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Time Spent Doing I/Os", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Per-second rate of discard (TRIM) and flush (write cache) operations. Useful for monitoring low-level disk activity on SSDs and advanced storage", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 409 + }, + "id": 301, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_discards_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}} - Discards completed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_discards_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}} - Discards merged", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_disk_flush_requests_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}} - Flush", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Disk Ops Discards / Flush", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Shows how many disk sectors are discarded (TRIMed) per second. Useful for monitoring SSD behavior and storage efficiency", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 409 + }, + "id": 326, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_disk_discarded_sectors_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{device}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Disk Sectors Discarded Successfully", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of in-progress I/O requests at the time of sampling (active requests in the disk queue)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 419 + }, + "id": 34, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_disk_io_now{instance=\"$node\",job=\"$job\"}", + "interval": "", + "legendFormat": "{{device}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Instantaneous Queue Size", + "type": "timeseries" + } + ], + "title": "Storage Disk", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 271, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of file descriptors currently allocated system-wide versus the system limit. Important for detecting descriptor exhaustion risks", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max.*/" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 28, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_filefd_maximum{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Max open files", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_filefd_allocated{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "Open files", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "File Descriptor", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of free file nodes (inodes) available per mounted filesystem. A low count may prevent file creation even if disk space is available", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 41, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_filesystem_files_free{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "legendFormat": "{{mountpoint}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "File Nodes Free", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Indicates filesystems mounted in read-only mode or reporting device-level I/O errors.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bool_yes_no" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 370 + }, + "id": 44, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_filesystem_readonly{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "legendFormat": "{{mountpoint}} - ReadOnly", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_filesystem_device_error{instance=\"$node\",job=\"$job\",device!~'rootfs',fstype!~'tmpfs'}", + "format": "time_series", + "interval": "", + "legendFormat": "{{mountpoint}} - Device error", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Filesystem in ReadOnly / Error", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of file nodes (inodes) available per mounted filesystem. Reflects maximum file capacity regardless of disk size", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 370 + }, + "id": 219, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_filesystem_files{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "legendFormat": "{{mountpoint}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "File Nodes Size", + "type": "timeseries" + } + ], + "title": "Storage Filesystem", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 272, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of network packets received and transmitted per second, by interface.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 60, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_packets_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_network_transmit_packets_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic by Packets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of packet-level errors for each network interface. Receive errors may indicate physical or driver issues; transmit errors may reflect collisions or hardware faults", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 142, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_errs_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_network_transmit_errs_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of dropped packets per network interface. Receive drops can indicate buffer overflow or driver issues; transmit drops may result from outbound congestion or queuing limits", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 251 + }, + "id": 143, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_drop_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_network_transmit_drop_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Drop", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of compressed network packets received and transmitted per interface. These are common in low-bandwidth or special interfaces like PPP or SLIP", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 251 + }, + "id": 141, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_compressed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_network_transmit_compressed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Compressed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of incoming multicast packets received per network interface. Multicast is used by protocols such as mDNS, SSDP, and some streaming or cluster services", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 261 + }, + "id": 146, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_multicast_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Multicast", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of received packets that could not be processed due to missing protocol or handler in the kernel. May indicate unsupported traffic or misconfiguration", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 261 + }, + "id": 327, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_nohandler_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic NoHandler", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of frame errors on received packets, typically caused by physical layer issues such as bad cables, duplex mismatches, or hardware problems", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 271 + }, + "id": 145, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_frame_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Frame", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Tracks FIFO buffer overrun errors on network interfaces. These occur when incoming or outgoing packets are dropped due to queue or buffer overflows, often indicating congestion or hardware limits", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 271 + }, + "id": 144, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_receive_fifo_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "rate(node_network_transmit_fifo_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Fifo", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of packet collisions detected during transmission. Mostly relevant on half-duplex or legacy Ethernet networks", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 281 + }, + "id": 232, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_transmit_colls_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Collision", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of carrier errors during transmission. These typically indicate physical layer issues like faulty cabling or duplex mismatches", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 281 + }, + "id": 231, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(node_network_transmit_carrier_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "{{device}} - Tx out", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Carrier Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of ARP entries per interface. Useful for detecting excessive ARP traffic or table growth due to scanning or misconfiguration", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 291 + }, + "id": 230, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_arp_entries{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "{{ device }} ARP Table", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "ARP Entries", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Current and maximum connection tracking entries used by Netfilter (nf_conntrack). High usage approaching the limit may cause packet drops or connection issues", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NF conntrack limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 291 + }, + "id": 61, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_nf_conntrack_entries{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "NF conntrack entries", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_nf_conntrack_entries_limit{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "NF conntrack limit", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "NF Conntrack", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Operational and physical link status of each network interface. Values are Yes for 'up' or link present, and No for 'down' or no carrier.\"", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bool_yes_no" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 301 + }, + "id": 309, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_network_up{operstate=\"up\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "legendFormat": "{{interface}} - Operational state UP", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_network_carrier{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{device}} - Physical link", + "refId": "B" + } + ], + "title": "Network Operational Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Maximum speed of each network interface as reported by the operating system. This is a static hardware capability, not current throughput", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "fieldMinMax": false, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 6, + "x": 12, + "y": 301 + }, + "id": 280, + "options": { + "displayMode": "basic", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 30, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "manual", + "valueMode": "color" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_network_speed_bytes{instance=\"$node\",job=\"$job\"} * 8", + "format": "time_series", + "legendFormat": "{{ device }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Speed", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "MTU (Maximum Transmission Unit) in bytes for each network interface. Affects packet size and transmission efficiency", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 6, + "x": 18, + "y": 301 + }, + "id": 288, + "options": { + "displayMode": "basic", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 30, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "manual", + "valueMode": "color" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_network_mtu_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "legendFormat": "{{ device }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "MTU", + "type": "bargauge" + } + ], + "title": "Network Traffic", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 273, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Tracks TCP socket usage and memory per node", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 63, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_sockstat_TCP_alloc{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Allocated Sockets", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_TCP_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "In-Use Sockets", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_TCP_orphan{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Orphaned Sockets", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_TCP_tw{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "TIME_WAIT Sockets", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Sockstat TCP", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of UDP and UDPLite sockets currently in use", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 124, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_sockstat_UDPLITE_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "UDPLite - In-Use Sockets", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_UDP_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "UDP - In-Use Sockets", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Sockstat UDP", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Total number of sockets currently in use across all protocols (TCP, UDP, UNIX, etc.), as reported by /proc/net/sockstat", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 42 + }, + "id": 126, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_sockstat_sockets_used{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Total sockets", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Sockstat Used", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of FRAG and RAW sockets currently in use. RAW sockets are used for custom protocols or tools like ping; FRAG sockets are used internally for IP packet defragmentation", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 42 + }, + "id": 125, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_sockstat_FRAG_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "FRAG - In-Use Sockets", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_RAW_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "RAW - In-Use Sockets", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Sockstat FRAG / RAW", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Kernel memory used by TCP, UDP, and IP fragmentation buffers", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 52 + }, + "id": 220, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_sockstat_TCP_mem_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "TCP", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_UDP_mem_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "UDP", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_FRAG_memory{instance=\"$node\",job=\"$job\"}", + "interval": "", + "legendFormat": "Fragmentation", + "range": true, + "refId": "C" + } + ], + "title": "Sockstat Memory Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Average memory used per socket (TCP/UDP). Helps tune net.ipv4.tcp_rmem / tcp_wmem", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 52 + }, + "id": 339, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_sockstat_TCP_mem_bytes{instance=\"$node\",job=\"$job\"} / node_sockstat_TCP_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "TCP", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_UDP_mem_bytes{instance=\"$node\",job=\"$job\"} / node_sockstat_UDP_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "UDP", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Sockstat Average Socket Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "TCP/UDP socket memory usage in kernel (in pages)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 62 + }, + "id": 336, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_sockstat_TCP_mem{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "TCP", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_sockstat_UDP_mem{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "UDP", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP/UDP Kernel Buffer Memory Pages", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Packets processed and dropped by the softnet network stack per CPU. Drops may indicate CPU saturation or network driver limitations", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "drop (-) / process (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Dropped.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 62 + }, + "id": 290, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_softnet_processed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "CPU {{cpu}} - Processed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_softnet_dropped_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "CPU {{cpu}} - Dropped", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Softnet Packets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "How often the kernel was unable to process all packets in the softnet queue before time ran out. Frequent squeezes may indicate CPU contention or driver inefficiency", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "eps" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 72 + }, + "id": 310, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_softnet_times_squeezed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "CPU {{cpu}} - Times Squeezed", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Softnet Out of Quota", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Tracks the number of packets processed or dropped by Receive Packet Steering (RPS), a mechanism to distribute packet processing across CPUs", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Dropped.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 72 + }, + "id": 330, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_softnet_received_rps_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "CPU {{cpu}} - Processed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_softnet_flow_limit_count_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "CPU {{cpu}} - Dropped", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Softnet RPS", + "type": "timeseries" + } + ], + "title": "Network Sockstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 274, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of octets sent and received at the IP layer, as reported by /proc/net/netstat", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 163 + }, + "id": 221, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_IpExt_InOctets{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "IP Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_IpExt_OutOctets{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "legendFormat": "IP Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Netstat IP In / Out Octets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of TCP segments sent and received per second, including data and control segments", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Snd.*/" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 163 + }, + "id": 299, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_InSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "TCP Rx in", + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_OutSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "TCP Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of UDP datagrams sent and received per second, based on /proc/net/netstat", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 193 + }, + "id": 55, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_Udp_InDatagrams{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "UDP Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Udp_OutDatagrams{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "UDP Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "UDP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of ICMP messages sent and received per second, including error and control messages", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 193 + }, + "id": 115, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_Icmp_InMsgs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "ICMP Rx in", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Icmp_OutMsgs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "ICMP Tx out", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "ICMP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Tracks various TCP error and congestion-related events, including retransmissions, timeouts, dropped connections, and buffer issues", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 203 + }, + "id": 104, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_ListenOverflows{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "Listen Overflows", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_ListenDrops{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "Listen Drops", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_TCPSynRetrans{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "SYN Retransmits", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_RetransSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "Segment Retransmits", + "range": true, + "refId": "D" + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_InErrs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "Receive Errors", + "range": true, + "refId": "E" + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_OutRsts{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "RST Sent", + "range": true, + "refId": "F" + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_TCPRcvQDrop{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "Receive Queue Drops", + "range": true, + "refId": "G" + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_TCPOFOQueue{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "Out-of-order Queued", + "range": true, + "refId": "H" + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_TCPTimeouts{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "TCP Timeouts", + "range": true, + "refId": "I" + } + ], + "title": "TCP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of UDP and UDPLite datagram delivery errors, including missing listeners, buffer overflows, and protocol-specific issues", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 203 + }, + "id": 109, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_Udp_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "UDP Rx in Errors", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Udp_NoPorts{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "UDP No Listener", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_UdpLite_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "UDPLite Rx in Errors", + "range": true, + "refId": "C" + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Udp_RcvbufErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "UDP Rx in Buffer Errors", + "range": true, + "refId": "D", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Udp_SndbufErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "UDP Tx out Buffer Errors", + "range": true, + "refId": "E", + "step": 240 + } + ], + "title": "UDP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of incoming ICMP messages that contained protocol-specific errors, such as bad checksums or invalid lengths", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 213 + }, + "id": 50, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_Icmp_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "ICMP Rx In", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "ICMP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of TCP SYN cookies sent, validated, and failed. These are used to protect against SYN flood attacks and manage TCP handshake resources under load", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "eps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Failed.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 213 + }, + "id": 91, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesFailed{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "SYN Cookies Failed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesRecv{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "SYN Cookies Validated", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesSent{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "SYN Cookies Sent", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "TCP SynCookie", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of currently established TCP connections and the system's max supported limit. On Linux, MaxConn may return -1 to indicate a dynamic/unlimited configuration", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 223 + }, + "id": 85, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_netstat_Tcp_CurrEstab{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Current Connections", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_netstat_Tcp_MaxConn{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Max Connections", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of UDP packets currently queued in the receive (RX) and transmit (TX) buffers. A growing queue may indicate a bottleneck", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 223 + }, + "id": 337, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_udp_queues{instance=\"$node\",job=\"$job\",ip=\"v4\",queue=\"rx\"}", + "format": "time_series", + "interval": "", + "legendFormat": "UDP Rx in Queue", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_udp_queues{instance=\"$node\",job=\"$job\",ip=\"v4\",queue=\"tx\"}", + "format": "time_series", + "interval": "", + "legendFormat": "UDP Tx out Queue", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "UDP Queue", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of TCP connection initiations per second. 'Active' opens are initiated by this host. 'Passive' opens are accepted from incoming connections", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "eps" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 233 + }, + "id": 82, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_ActiveOpens{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "Active Opens", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_PassiveOpens{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "Passive Opens", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP Direct Transition", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of TCP sockets in key connection states. Requires the --collector.tcpstat flag on node_exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 233 + }, + "id": 320, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"established\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Established", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"fin_wait2\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "FIN_WAIT2", + "range": true, + "refId": "B", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"listen\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "Listen", + "range": true, + "refId": "C", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"time_wait\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "TIME_WAIT", + "range": true, + "refId": "D", + "step": 240 + }, + { + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"close_wait\", instance=\"$node\", job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "CLOSE_WAIT", + "range": true, + "refId": "E", + "step": 240 + } + ], + "title": "TCP Stat", + "type": "timeseries" + } + ], + "title": "Network Netstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 33 + }, + "id": 279, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Duration of each individual collector executed during a Node Exporter scrape. Useful for identifying slow or failing collectors", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 164 + }, + "id": 40, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_scrape_collector_duration_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{collector}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Node Exporter Scrape Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Rate of CPU time used by the process exposing this metric (user + system mode)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 164 + }, + "id": 308, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "irate(process_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "legendFormat": "Process CPU Usage", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Exporter Process CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Tracks the memory usage of the process exposing this metric (e.g., node_exporter), including current virtual memory and maximum virtual memory limit", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Virtual Memory Limit" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "Virtual Memory" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 10, + "x": 0, + "y": 174 + }, + "id": 149, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "process_virtual_memory_bytes{instance=\"$node\",job=\"$job\"}", + "interval": "", + "legendFormat": "Virtual Memory", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "process_virtual_memory_max_bytes{instance=\"$node\",job=\"$job\"}", + "interval": "", + "legendFormat": "Virtual Memory Limit", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Exporter Processes Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Number of file descriptors used by the exporter process versus its configured limit", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "Open file descriptors" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 10, + "x": 10, + "y": 174 + }, + "id": 64, + "options": { + "legend": { + "calcs": [ + "min", + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "process_max_fds{instance=\"$node\",job=\"$job\"}", + "interval": "", + "legendFormat": "Maximum open file descriptors", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "process_open_fds{instance=\"$node\",job=\"$job\"}", + "interval": "", + "legendFormat": "Open file descriptors", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Exporter File Descriptor Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "description": "Shows whether each Node Exporter collector scraped successfully (1 = success, 0 = failure), and whether the textfile collector returned an error.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "dark-red", + "value": 0 + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "bool" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 4, + "x": 20, + "y": 174 + }, + "id": 157, + "options": { + "displayMode": "basic", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "node_scrape_collector_success{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{collector}}", + "range": true, + "refId": "A", + "step": 240 + }, + { + "editorMode": "code", + "expr": "1 - node_textfile_scrape_error{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "legendFormat": "textfile", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Node Exporter Scrape", + "type": "bargauge" + } + ], + "title": "Node Exporter", + "type": "row" + } + ], + "refresh": "1m", + "schemaVersion": 41, + "tags": [ + "linux" + ], + "templating": { + "list": [ + { + "current": {}, + "includeAll": false, + "label": "Datasource", + "name": "ds_prometheus", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "definition": "", + "includeAll": false, + "label": "Job", + "name": "job", + "options": [], + "query": { + "query": "label_values(node_uname_info, job)", + "refId": "Prometheus-job-Variable-Query" + }, + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "definition": "label_values(node_uname_info{job=\"$job\"}, nodename)", + "includeAll": false, + "label": "Nodename", + "name": "nodename", + "options": [], + "query": { + "query": "label_values(node_uname_info{job=\"$job\"}, nodename)", + "refId": "Prometheus-nodename-Variable-Query" + }, + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${ds_prometheus}" + }, + "definition": "label_values(node_uname_info{job=\"$job\", nodename=\"$nodename\"}, instance)", + "includeAll": false, + "label": "Instance", + "name": "node", + "options": [], + "query": { + "query": "label_values(node_uname_info{job=\"$job\", nodename=\"$nodename\"}, instance)", + "refId": "Prometheus-node-Variable-Query" + }, + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Node Exporter Full", + "uid": "rYdddlPWk", + "version": 98, + "weekStart": "", + "gnetId": 1860 +} \ No newline at end of file diff --git a/web/cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json b/web/cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json new file mode 100644 index 000000000..5a0099cb2 --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json @@ -0,0 +1,422 @@ +{ + "kind": "Dashboard", + "metadata": { + "createdAt": "2026-01-28T18:26:38.108319384Z", + "name": "testing-perses-dashboard-json", + "project": "openshift-cluster-observability-operator", + "updatedAt": "2026-01-28T18:26:38.152574974Z", + "version": 1 + }, + "spec": { + "display": { + "name": "Testing Perses dashboard - JSON" + }, + "panels": { + "2ae6af9385b74280b00978aa304ce3bb": { + "kind": "Panel", + "spec": { + "display": { + "name": "time series table" + }, + "plugin": { + "kind": "TimeSeriesTable", + "spec": {} + }, + "queries": [ + { + "kind": "TimeSeriesQuery", + "spec": { + "plugin": { + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "up" + } + } + } + } + ] + } + }, + "31b8374866184a26beddf46ec30df0b5": { + "kind": "Panel", + "spec": { + "display": { + "name": "table" + }, + "plugin": { + "kind": "Table", + "spec": { + "density": "standard", + "enableFiltering": true + } + }, + "queries": [ + { + "kind": "TimeSeriesQuery", + "spec": { + "plugin": { + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "up" + } + } + } + } + ] + } + }, + "57ce686052c9464fa9486ff49d3757cd": { + "kind": "Panel", + "spec": { + "display": { + "name": "scat" + }, + "plugin": { + "kind": "StatChart", + "spec": { + "calculation": "last-number", + "format": { + "unit": "decimal" + }, + "legendMode": "auto", + "sparkline": {} + } + }, + "queries": [ + { + "kind": "TimeSeriesQuery", + "spec": { + "plugin": { + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "up" + } + } + } + } + ] + } + }, + "626437cc3e1a46fd84a6a690923c3531": { + "kind": "Panel", + "spec": { + "display": { + "name": "gauge" + }, + "plugin": { + "kind": "GaugeChart", + "spec": { + "calculation": "last-number", + "format": { + "unit": "percent-decimal" + }, + "thresholds": { + "steps": [ + { + "value": 0.8 + }, + { + "value": 0.9 + } + ] + } + } + }, + "queries": [ + { + "kind": "TimeSeriesQuery", + "spec": { + "plugin": { + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "up" + } + } + } + } + ] + } + }, + "940582f41e824aa3bc10f2d0454af98a": { + "kind": "Panel", + "spec": { + "display": { + "name": "markdown" + }, + "plugin": { + "kind": "Markdown", + "spec": { + "text": " eve " + } + }, + "queries": [ + { + "kind": "TimeSeriesQuery", + "spec": { + "plugin": { + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "up" + } + } + } + } + ] + } + }, + "9877adfad20947e5b0af761cdad2b246": { + "kind": "Panel", + "spec": { + "display": { + "name": "pie chart" + }, + "plugin": { + "kind": "PieChart", + "spec": { + "calculation": "last", + "format": { + "shortValues": true, + "unit": "decimal" + }, + "mode": "value", + "radius": 50, + "showLabels": false, + "sort": "desc" + } + }, + "queries": [ + { + "kind": "TimeSeriesQuery", + "spec": { + "plugin": { + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "up" + } + } + } + } + ] + } + }, + "99778df0db9b4855ae0cda2cc7eb731f": { + "kind": "Panel", + "spec": { + "display": { + "name": "bar chart" + }, + "plugin": { + "kind": "BarChart", + "spec": { + "calculation": "last", + "format": { + "shortValues": true, + "unit": "decimal" + }, + "mode": "value", + "sort": "desc" + } + }, + "queries": [ + { + "kind": "TimeSeriesQuery", + "spec": { + "plugin": { + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "up" + } + } + } + } + ] + } + }, + "c03e17f849d341bcb1139c65e68dad1d": { + "kind": "Panel", + "spec": { + "display": { + "name": "status history" + }, + "plugin": { + "kind": "StatusHistoryChart", + "spec": {} + }, + "queries": [ + { + "kind": "TimeSeriesQuery", + "spec": { + "plugin": { + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "vector(1)" + } + } + } + } + ] + } + } + }, + "layouts": [ + { + "kind": "Grid", + "spec": { + "display": { + "title": "Row 3", + "collapse": { + "open": true + } + }, + "items": [ + { + "x": 0, + "y": 0, + "width": 6, + "height": 6, + "content": { + "$ref": "#/spec/panels/99778df0db9b4855ae0cda2cc7eb731f" + } + }, + { + "x": 6, + "y": 0, + "width": 5, + "height": 6, + "content": { + "$ref": "#/spec/panels/626437cc3e1a46fd84a6a690923c3531" + } + }, + { + "x": 11, + "y": 0, + "width": 5, + "height": 6, + "content": { + "$ref": "#/spec/panels/940582f41e824aa3bc10f2d0454af98a" + } + }, + { + "x": 0, + "y": 6, + "width": 24, + "height": 9, + "content": { + "$ref": "#/spec/panels/9877adfad20947e5b0af761cdad2b246" + } + }, + { + "x": 16, + "y": 0, + "width": 8, + "height": 6, + "content": { + "$ref": "#/spec/panels/57ce686052c9464fa9486ff49d3757cd" + } + }, + { + "x": 0, + "y": 15, + "width": 12, + "height": 6, + "content": { + "$ref": "#/spec/panels/c03e17f849d341bcb1139c65e68dad1d" + } + }, + { + "x": 12, + "y": 15, + "width": 12, + "height": 6, + "content": { + "$ref": "#/spec/panels/31b8374866184a26beddf46ec30df0b5" + } + }, + { + "x": 0, + "y": 21, + "width": 12, + "height": 6, + "content": { + "$ref": "#/spec/panels/2ae6af9385b74280b00978aa304ce3bb" + } + } + ] + } + } + ], + "variables": [ + { + "kind": "ListVariable", + "spec": { + "defaultValue": "node-exporter", + "allowAllValue": false, + "allowMultiple": false, + "sort": "none", + "plugin": { + "kind": "PrometheusLabelValuesVariable", + "spec": { + "labelName": "job" + } + }, + "name": "job" + } + }, + { + "kind": "ListVariable", + "spec": { + "defaultValue": "ip-10-0-11-36.us-east-2.compute.internal", + "allowAllValue": false, + "allowMultiple": false, + "sort": "none", + "plugin": { + "kind": "PrometheusLabelValuesVariable", + "spec": { + "labelName": "instance", + "matchers": [ + "up{job=~\"$job\"}" + ] + } + }, + "name": "instance" + } + }, + { + "kind": "ListVariable", + "spec": { + "defaultValue": "1m", + "allowAllValue": false, + "allowMultiple": false, + "sort": "none", + "plugin": { + "kind": "StaticListVariable", + "spec": { + "values": [ + { + "label": "1m", + "value": "1m" + }, + { + "label": "5m", + "value": "5m" + } + ] + } + }, + "name": "interval" + } + }, + { + "kind": "TextVariable", + "spec": { + "value": "test", + "constant": true, + "name": "text" + } + } + ], + "duration": "30m", + "refreshInterval": "0s", + "datasources": {} + } +} \ No newline at end of file diff --git a/web/cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml b/web/cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml new file mode 100644 index 000000000..ef5cd77e2 --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml @@ -0,0 +1,264 @@ +kind: Dashboard +metadata: + createdAt: "2026-01-26T13:16:38.627059456Z" + name: testing-perses-dashboard-yaml + project: openshift-cluster-observability-operator + updatedAt: "2026-01-26T15:55:50.145711006Z" + version: 14 +spec: + display: + name: Testing Perses dashboard - YAML + panels: + 2ae6af9385b74280b00978aa304ce3bb: + kind: Panel + spec: + display: + name: time series table + plugin: + kind: TimeSeriesTable + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: up + 31b8374866184a26beddf46ec30df0b5: + kind: Panel + spec: + display: + name: table + plugin: + kind: Table + spec: + density: standard + enableFiltering: true + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: up + 57ce686052c9464fa9486ff49d3757cd: + kind: Panel + spec: + display: + name: "scat " + plugin: + kind: StatChart + spec: + calculation: last-number + format: + unit: decimal + legendMode: auto + sparkline: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: up + 626437cc3e1a46fd84a6a690923c3531: + kind: Panel + spec: + display: + name: gauge + plugin: + kind: GaugeChart + spec: + calculation: last-number + format: + unit: percent-decimal + thresholds: + steps: + - value: 0.8 + - value: 0.9 + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: up + 940582f41e824aa3bc10f2d0454af98a: + kind: Panel + spec: + display: + name: markdown + plugin: + kind: Markdown + spec: + text: eve + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: up + 9877adfad20947e5b0af761cdad2b246: + kind: Panel + spec: + display: + name: pie chart + plugin: + kind: PieChart + spec: + calculation: last + format: + shortValues: true + unit: decimal + mode: value + radius: 50 + showLabels: false + sort: desc + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: up + 99778df0db9b4855ae0cda2cc7eb731f: + kind: Panel + spec: + display: + name: bar chart + plugin: + kind: BarChart + spec: + calculation: last + format: + shortValues: true + unit: decimal + mode: value + sort: desc + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: up + c03e17f849d341bcb1139c65e68dad1d: + kind: Panel + spec: + display: + name: status history + plugin: + kind: StatusHistoryChart + spec: {} + queries: + - kind: TimeSeriesQuery + spec: + plugin: + kind: PrometheusTimeSeriesQuery + spec: + query: vector(1) + layouts: + - kind: Grid + spec: + display: + title: Row 3 + collapse: + open: true + items: + - x: 0 + "y": 0 + width: 6 + height: 6 + content: + $ref: "#/spec/panels/99778df0db9b4855ae0cda2cc7eb731f" + - x: 6 + "y": 0 + width: 5 + height: 6 + content: + $ref: "#/spec/panels/626437cc3e1a46fd84a6a690923c3531" + - x: 11 + "y": 0 + width: 5 + height: 6 + content: + $ref: "#/spec/panels/940582f41e824aa3bc10f2d0454af98a" + - x: 0 + "y": 6 + width: 24 + height: 9 + content: + $ref: "#/spec/panels/9877adfad20947e5b0af761cdad2b246" + - x: 16 + "y": 0 + width: 8 + height: 6 + content: + $ref: "#/spec/panels/57ce686052c9464fa9486ff49d3757cd" + - x: 0 + "y": 15 + width: 12 + height: 6 + content: + $ref: "#/spec/panels/c03e17f849d341bcb1139c65e68dad1d" + - x: 12 + "y": 15 + width: 12 + height: 6 + content: + $ref: "#/spec/panels/31b8374866184a26beddf46ec30df0b5" + - x: 0 + "y": 21 + width: 12 + height: 6 + content: + $ref: "#/spec/panels/2ae6af9385b74280b00978aa304ce3bb" + variables: + - kind: ListVariable + spec: + defaultValue: node-exporter + allowAllValue: false + allowMultiple: false + sort: none + plugin: + kind: PrometheusLabelValuesVariable + spec: + labelName: job + name: job + - kind: ListVariable + spec: + defaultValue: ip-10-0-11-36.us-east-2.compute.internal + allowAllValue: false + allowMultiple: false + sort: none + plugin: + kind: PrometheusLabelValuesVariable + spec: + labelName: instance + matchers: + - up{job=~"$job"} + name: instance + - kind: ListVariable + spec: + defaultValue: 1m + allowAllValue: false + allowMultiple: false + sort: none + plugin: + kind: StaticListVariable + spec: + values: + - label: 1m + value: 1m + - label: 5m + value: 5m + name: interval + - kind: TextVariable + spec: + value: test + constant: true + name: text + duration: 30m + refreshInterval: 0s + datasources: {} diff --git a/web/cypress/fixtures/coo/coo141_perses/rbac/rbac_perses_e2e_ci_users.sh b/web/cypress/fixtures/coo/coo141_perses/rbac/rbac_perses_e2e_ci_users.sh new file mode 100755 index 000000000..ec05d51db --- /dev/null +++ b/web/cypress/fixtures/coo/coo141_perses/rbac/rbac_perses_e2e_ci_users.sh @@ -0,0 +1,1393 @@ +#!/bin/bash + +# User variables (passed as arguments) +USER1="${USER1}" +USER2="${USER2}" +USER3="${USER3}" +USER4="${USER4}" +USER5="${USER5}" +USER6="${USER6}" + +oc create namespace perses-dev 2>/dev/null || true +oc create namespace observ-test 2>/dev/null || true +oc create namespace empty-namespace3 2>/dev/null || true +oc create namespace empty-namespace4 2>/dev/null || true + +oc apply -f - < [kubeconfig-path]" + exit 1 +fi + +# Build kubeconfig flag if provided +KUBECONFIG_FLAG="" +if [ -n "$KUBECONFIG_PATH" ]; then + KUBECONFIG_FLAG="--kubeconfig $KUBECONFIG_PATH" +fi + +# Check if the namespace exists +if ! oc get namespace "$NAMESPACE" $KUBECONFIG_FLAG &> /dev/null; then + echo "Namespace '$NAMESPACE' not found or already deleted." + exit 0 +fi + +echo "Attempting to force-delete namespace '$NAMESPACE' by removing finalizers..." + +# Step 1: Remove finalizers from common problematic resources +echo "Checking for resources with finalizers in namespace '$NAMESPACE'..." +# Focus on common resources that have finalizers (much faster than checking everything) +RESOURCE_TYPES="perses,persesdashboard,persistentvolumeclaims,pods,services,deployments,statefulsets,daemonsets" + +for resource_type in $(echo $RESOURCE_TYPES | tr ',' ' '); do + oc get "$resource_type" -n "$NAMESPACE" $KUBECONFIG_FLAG -o name 2>/dev/null | \ + while read -r item; do + if [ -n "$item" ]; then + # Check if the resource has finalizers + HAS_FINALIZERS=$(oc get "$item" -n "$NAMESPACE" $KUBECONFIG_FLAG -o jsonpath='{.metadata.finalizers}' 2>/dev/null || echo "") + if [ -n "$HAS_FINALIZERS" ] && [ "$HAS_FINALIZERS" != "[]" ]; then + echo " Removing finalizers from $item..." + oc patch "$item" -n "$NAMESPACE" $KUBECONFIG_FLAG --type json -p '[{"op": "remove", "path": "/metadata/finalizers"}]' 2>/dev/null || true + fi + fi + done +done + +# Step 2: Remove finalizers from the namespace itself (if it still exists) +# Check if namespace still exists before trying to remove its finalizers +if oc get namespace "$NAMESPACE" $KUBECONFIG_FLAG &> /dev/null; then + # 1. Retrieve the namespace JSON. + # 2. Use 'tr' to remove all newlines, creating a single-line JSON string. + # 3. Use 'sed' to perform a substitution: + # - It finds the pattern "finalizers": [ ] + # - It replaces the entire pattern with "finalizers": [] (an empty array). + # 4. Pipe the modified JSON directly to the Kubernetes /finalize endpoint. + echo "Removing finalizers from namespace '$NAMESPACE' itself..." + oc get namespace "$NAMESPACE" $KUBECONFIG_FLAG -o json | \ + tr -d '\n' | \ + sed 's/"finalizers": \[[^]]*\]/"finalizers": []/' | \ + oc replace $KUBECONFIG_FLAG --raw "/api/v1/namespaces/$NAMESPACE/finalize" -f - + + EXIT_CODE=$? + + if [ $EXIT_CODE -eq 0 ]; then + echo "" + echo "✅ Success: Finalizers for namespace '$NAMESPACE' have been cleared." + echo "The Namespace should now be fully deleted. Confirm with: oc get ns" + else + echo "" + echo "❌ Error: The command failed with exit code $EXIT_CODE." + echo "Please check your oc connection and the namespace name." + fi +else + echo "" + echo "✅ Success: Namespace '$NAMESPACE' was already deleted after removing resource finalizers." + EXIT_CODE=0 +fi \ No newline at end of file diff --git a/web/cypress/fixtures/coo/troubleshooting-panel-ui-plugin.yaml b/web/cypress/fixtures/coo/troubleshooting-panel-ui-plugin.yaml new file mode 100644 index 000000000..c8f976745 --- /dev/null +++ b/web/cypress/fixtures/coo/troubleshooting-panel-ui-plugin.yaml @@ -0,0 +1,6 @@ +apiVersion: observability.openshift.io/v1alpha1 +kind: UIPlugin +metadata: + name: troubleshooting-panel +spec: + type: TroubleshootingPanel \ No newline at end of file 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 8767d308f..ee37cebe4 100755 --- a/web/cypress/fixtures/export.sh +++ b/web/cypress/fixtures/export.sh @@ -9,12 +9,16 @@ # kubeadmin user export CYPRESS_BASE_URL=https:// export CYPRESS_LOGIN_IDP=kube:admin -export CYPRESS_LOGIN_USERS=kubeadmin: +export CYPRESS_LOGIN_IDP_DEV_USER=my_htpasswd_provider +export CYPRESS_LOGIN_USERS=kubeadmin:password,user1:password,user2:password export CYPRESS_KUBECONFIG_PATH=~/Downloads/kubeconfig # Set the following var to use custom Monitoring Plugin image (that goes on Cluster Monitoring Operator). The image will be patched in CMO CSV. export CYPRESS_MP_IMAGE= +# Set the following var to specify the Cluster Observability Operator namespace (defaults to openshift-cluster-observability-operator if not set) +export CYPRESS_COO_NAMESPACE=openshift-cluster-observability-operator + # Set the var to skip Cluster Observability and all the required operators installation. export CYPRESS_SKIP_COO_INSTALL=false @@ -33,6 +37,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/fixtures/monitoring/constants.ts b/web/cypress/fixtures/monitoring/constants.ts index 772a2f795..9f05c1ad6 100644 --- a/web/cypress/fixtures/monitoring/constants.ts +++ b/web/cypress/fixtures/monitoring/constants.ts @@ -98,6 +98,7 @@ export enum MetricsPagePredefinedQueries { export enum MetricsPageQueryInput { EXPRESSION_PRESS_SHIFT_ENTER_FOR_NEWLINES = 'Expression (press Shift+Enter for newlines)', INSERT_EXAMPLE_QUERY = 'sort_desc(sum(sum_over_time(ALERTS{alertstate="firing"}[24h])) by (alertname))', + INSERT_EXAMPLE_QUERY_NAMESPACE = 'sort_desc(sum(sum_over_time(ALERTS{alertstate="firing", namespace="openshift-monitoring"}[24h])) by (alertname))', VECTOR_QUERY='vector(1)', CPU_USAGE = 'OpenShift_Metrics_QueryTable_sum(node_namespace_pod_container_container_cpu_usage_seconds_total_sum_irate) by (pod).csv', MEMORY_USAGE = 'OpenShift_Metrics_QueryTable_sum(container_memory_working_set_bytes{container!=__}) by (pod).csv', @@ -132,7 +133,7 @@ export enum MetricsPageQueryInputByNamespace { RATE_OF_TRANSMITTED_PACKETS = 'OpenShift_Metrics_QueryTable_sum(irate(container_network_transmit_packets_total{namespace=\'openshift-monitoring\'}[2h])) by (pod).csv', RATE_OF_RECEIVED_PACKETS_DROPPED = 'OpenShift_Metrics_QueryTable_sum(irate(container_network_receive_packets_dropped_total{namespace=\'openshift-monitoring\'}[2h])) by (pod).csv', RATE_OF_TRANSMITTED_PACKETS_DROPPED = 'OpenShift_Metrics_QueryTable_sum(irate(container_network_transmit_packets_dropped_total{namespace=\'openshift-monitoring\'}[2h])) by (pod).csv', - CPU_UTILISATION_FROM_REQUESTS = 'sum(node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate{cluster="", namespace="openshift-monitoring"}) / sum(kube_pod_container_resource_requests{job="kube-state-metrics", cluster="", namespace="openshift-monitoring", resource="cpu"})', + CPU_UTILISATION_FROM_REQUESTS = 'sum(node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate{namespace="openshift-monitoring"}) / sum(kube_pod_container_resource_requests{job="kube-state-metrics", namespace="openshift-monitoring", resource="cpu"})', } export enum MetricsPageActions { diff --git a/web/cypress/fixtures/perses/constants.ts b/web/cypress/fixtures/perses/constants.ts index 48f9e520b..610ab9a51 100644 --- a/web/cypress/fixtures/perses/constants.ts +++ b/web/cypress/fixtures/perses/constants.ts @@ -21,14 +21,15 @@ export enum persesDashboardsRefreshInterval { } export const persesDashboardsDashboardDropdownCOO = { - ACCELERATORS_COMMON_METRICS:['Accelerators common metrics', 'perses'], - K8S_COMPUTE_RESOURCES_CLUSTER: ['Kubernetes / Compute Resources / Cluster', 'perses'], + ACCELERATORS_COMMON_METRICS:['Accelerators common metrics', 'perses', 'accelerators-dashboard'], + APM_DASHBOARD: ['Application Performance Monitoring (APM)', 'perses', 'apm-dashboard'], + K8S_COMPUTE_RESOURCES_CLUSTER: ['Kubernetes / Compute Resources / Cluster', 'perses', 'openshift-cluster-sample-dashboard'], } export const persesDashboardsDashboardDropdownPersesDev = { - PERSES_DASHBOARD_SAMPLE: ['Perses Dashboard Sample', 'perses'], - PROMETHEUS_OVERVIEW: ['Prometheus / Overview', 'perses'], - THANOS_COMPACT_OVERVIEW: ['Thanos / Compact / Overview', 'perses'], + PERSES_DASHBOARD_SAMPLE: ['Perses Dashboard Sample', 'perses', 'perses-dashboard-sample'], + PROMETHEUS_OVERVIEW: ['Prometheus / Overview', 'perses', 'prometheus-overview'], + THANOS_COMPACT_OVERVIEW: ['Thanos / Compact / Overview', 'perses', 'thanos-compact-overview'], } export enum persesDashboardsAcceleratorsCommonMetricsPanels { @@ -39,4 +40,136 @@ export enum persesDashboardsAcceleratorsCommonMetricsPanels { TEMPERATURE_CELCIUS = 'Temperature (Celsius)', SM_CLOCK_HERTZ = 'SM Clock (Hertz)', MEMORY_CLOCK_HERTZ = 'Memory Clock (Hertz)', +} + +export const listPersesDashboardsPageSubtitle = 'View and manage dashboards.'; + +export const listPersesDashboardsEmptyState = { + TITLE: 'No results found', + BODY: 'No results match the filter criteria. Clear filters to show results.', + +} + +export const listPersesDashboardsNoDashboardsFoundState = { + TITLE: 'No dashboards found', + BODY: 'No Perses dashboards are currently available in this project.', +} + +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', + CREATE_DASHBOARD: 'Create Dashboard', + IMPORT_DASHBOARD: 'Import Dashboard', +} + +export enum persesDashboardsAddListVariableSource { + STATIC_LIST_VARIABLE= 'Static List Variable', + DATASOURCE_VARIABLE= 'Datasource Variable', + PROMETHEUS_LABEL_VARIABLE= 'Prometheus Label Values Variable', + PROMETHEUS_NAMES_VARIABLE= 'Prometheus Label 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', +} + +export const persesDashboardsAddPanelAddQueryType ={ + BAR_GAUGE_HEAT_HISTOGRAM_PIE_STAT_STATUS_TABLE_TIMESERIES : { + CLICKHOUSE_TIME_SERIES_QUERY: 'ClickHouse Time Series Query', + LOKI_TIME_SERIES_QUERY: 'Loki Time Series Query', + PROMETHEUS_TIME_SERIES_QUERY: 'Prometheus Time Series Query', + VICTORIALOGS_TIME_SERIES_QUERY: 'VictoriaLogs Time Series Query', + }, + FLAME_CHART : { + PYROSCOPE_PROFILE_QUERY: 'Pyroscope Profile Query', + }, + LOGS_TABLE : { + CLICKHOUSE_LOG_QUERY: 'ClickHouse Log Query', + LOKI_LOG_QUERY: 'Loki Log Query', + VICTORIALOGS_LOG_QUERY: 'Victorialogs Log Query', + }, + SCATTER_TRACE_TRACINGGANTT : { + TEMPO_TRACE_QUERY: 'Tempo Trace Query', + }, +} + +export const persesCreateDashboard = { + DIALOG_MAX_LENGTH_VALIDATION: 'Danger alert:bad request: code=400, message=cannot contain more than 75 characters, internal=cannot contain more than 75 characters', + DIALOG_DUPLICATED_NAME_PF_VALIDATION_PREFIX: 'Dashboard name ', + DIALOG_DUPLICATED_NAME_PF_VALIDATION_SUFFIX: ' already exists in this project: error status;', + DIALOG_DUPLICATED_NAME_BKD_VALIDATION: 'Danger alert:document already exists', +} + +export const persesDashboardsEmptyDashboard = { + TITLE: 'Empty Dashboard', + DESCRIPTION: 'To get started add something to your dashboard', +} + +export const persesDashboardSampleQueries = { + CPU_LINE_MULTI_SERIES: 'avg without (cpu)(rate(node_cpu_seconds_total{job=\'$job\',instance=\~\'$instance\',mode!=\"nice\",mode!=\"steal\",mode!=\"irq\"}[$interval]))', + CPU_LINE_MULTI_SERIES_LEGEND: '{{}{{}mode{}}{}} mode - {{}{{}job{}}{}} {{}{{}instance{}}{}}', + CPU_LINE_MULTI_SERIES_SERIES_SELECTOR: 'up{{}job=~"$job"{}}', +} + +export const persesDashboardsRenameDashboard = { + DIALOG_MAX_LENGTH_VALIDATION: 'Must be 75 or fewer characters long: error status;', +} + +export const persesDashboardsDuplicateDashboard = { + DIALOG_DUPLICATED_NAME_VALIDATION: "already exists", //use contains +} + +export const persesDashboardsImportDashboard = { + DIALOG_TITLE: '1. Provide a dashboard (JSON or YAML)', + DIALOG_UPLOAD_JSON_YAML_FILE: 'Upload a dashboard file or paste the dashboard definition directly in the editor below.', + DIALOG_UNABLE_TO_DETECT_DASHBOARD_FORMAT: 'Unable to detect dashboard format. Please provide a valid Perses or Grafana dashboard.', + DIALOG_GRAFANA_DASHBOARD_DETECTED: 'Grafana dashboard detected. It will be automatically migrated to Perses format. Note: migration may be partial as not all Grafana features are supported.', + DIALOG_PERSES_DASHBOARD_DETECTED: 'Perses dashboard detected.', + DIALOG_FAILED_TO_MIGRATE_GRAFANA_DASHBOARD: 'Danger alert:Failed to migrate dashboard: internal server error', + DIALOG_DUPLICATED_DASHBOARD_ERROR: 'Danger alert:document already exists', } \ No newline at end of file diff --git a/web/cypress/support/commands/auth-commands.ts b/web/cypress/support/commands/auth-commands.ts index af43f4b9f..3bc26eec8 100644 --- a/web/cypress/support/commands/auth-commands.ts +++ b/web/cypress/support/commands/auth-commands.ts @@ -3,11 +3,11 @@ import { guidedTour } from '../../views/tour'; -export {}; +export { }; declare global { namespace Cypress { interface Chainable { - switchPerspective(perspective: string); + switchPerspective(...perspectives: string[]); uiLogin(provider: string, username: string, password: string, oauthurl?: string); uiLogout(); cliLogin(username?, password?, hostapi?); @@ -17,198 +17,255 @@ declare global { adminCLI(command: string, options?); executeAndDelete(command: string); validateLogin(): Chainable; + relogin(provider: string, username: string, password: string): Chainable; } } } - // Core login function (used by both session and non-session versions) - function performLogin( - provider: string, - username: string, - password: string, - oauthurl: string - ): void { - cy.visit(Cypress.config('baseUrl')); - cy.log('Session - after visiting'); - cy.window().then( - ( - win: any, // eslint-disable-line @typescript-eslint/no-explicit-any - ) => { - // Check if auth is disabled (for a local development environment) - if (win.SERVER_FLAGS?.authDisabled) { - cy.task('log', ' skipping login, console is running with auth disabled'); - return; +// Core login function (used by both session and non-session versions) +function performLogin( + provider: string, + username: string, + password: string, + oauthurl: string +): void { + cy.visit(Cypress.config('baseUrl')); + cy.log('Session - after visiting'); + cy.window().then( + ( + win: any, // eslint-disable-line @typescript-eslint/no-explicit-any + ) => { + // Check if auth is disabled (for a local development environment) + if (win.SERVER_FLAGS?.authDisabled) { + cy.task('log', ' skipping login, console is running with auth disabled'); + return; + } + cy.exec( + `oc get node --selector=hypershift.openshift.io/managed --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + ).then((result) => { + cy.log(result.stdout); + cy.task('log', result.stdout); + if (result.stdout.includes('Ready')) { + cy.log(`Attempting login via cy.origin to: ${oauthurl}`); + cy.task('log', `Attempting login via cy.origin to: ${oauthurl}`); + cy.origin( + oauthurl, + { args: { username, password } }, + ({ username, password }) => { + cy.get('#inputUsername').type(username); + cy.get('#inputPassword').type(password); + cy.get('button[type=submit]').click(); + }, + ); + } else { + cy.task('log', ` Logging in as ${username} using fallback on ${oauthurl}`); + cy.origin( + oauthurl, + { args: { provider, username, password } }, + ({ provider, username, password }) => { + cy.get('[data-test-id="login"]').should('be.visible'); + cy.get('body').then(($body) => { + if ($body.text().includes(provider)) { + cy.contains(provider).should('be.visible').click(); + } + }); + cy.get('#inputUsername').type(username); + cy.get('#inputPassword').type(password); + cy.get('button[type=submit]').click(); + } + ); } - cy.exec( - `oc get node --selector=hypershift.openshift.io/managed --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, - ).then((result) => { - cy.log(result.stdout); - cy.task('log', result.stdout); - if (result.stdout.includes('Ready')) { - cy.log(`Attempting login via cy.origin to: ${oauthurl}`); - cy.task('log', `Attempting login via cy.origin to: ${oauthurl}`); - cy.origin( - oauthurl, - { args: { username, password } }, - ({ username, password }) => { - cy.get('#inputUsername').type(username); - cy.get('#inputPassword').type(password); - cy.get('button[type=submit]').click(); - }, - ); - } else { - cy.task('log', ` Logging in as ${username} using fallback on ${oauthurl}`); - cy.origin( - oauthurl, - { args: { provider, username, password } }, - ({ provider, username, password }) => { - cy.get('[data-test-id="login"]').should('be.visible'); - cy.get('body').then(($body) => { - if ($body.text().includes(provider)) { - cy.contains(provider).should('be.visible').click(); - } - }); - cy.get('#inputUsername').type(username); - cy.get('#inputPassword').type(password); - cy.get('button[type=submit]').click(); - } - ); - } - }); - }, - ); - } + }); + }, + ); +} - Cypress.Commands.add('validateLogin', () => { - cy.wait(2000); - cy.byTestID("username", {timeout: 120000}).should('be.visible'); - guidedTour.close(); - }); +Cypress.Commands.add('validateLogin', () => { + cy.log('validateLogin'); + cy.visit('/'); + cy.wait(2000); + cy.byTestID("username", { timeout: 120000 }).should('be.visible'); + cy.wait(10000); + guidedTour.close(); +}); - // Session-wrapped login - Cypress.Commands.add( - 'login', - ( - provider: string = Cypress.env('LOGIN_IDP'), - username: string = Cypress.env('LOGIN_USERNAME'), - password: string = Cypress.env('LOGIN_PASSWORD'), - oauthurl: string, - ) => { - cy.session( - [provider, username], - () => { - performLogin(provider, username, password, oauthurl); - }, - { - cacheAcrossSpecs: true, - validate() { - cy.validateLogin(); - }, - }, - ); +// Session-wrapped login +Cypress.Commands.add( + 'login', + ( + provider: string = Cypress.env('LOGIN_IDP'), + username: string = Cypress.env('LOGIN_USERNAME'), + password: string = Cypress.env('LOGIN_PASSWORD'), + oauthurl: string, + ) => { + cy.session( + [provider, username], + () => { + performLogin(provider, username, password, oauthurl); + }, + { + cacheAcrossSpecs: true, + validate() { + cy.validateLogin(); + }, }, ); + }, +); - // Non-session login (for use within sessions) - Cypress.Commands.add('loginNoSession', (provider: string, username: string, password: string, oauthurl: string) => { - performLogin(provider, username, password, oauthurl); - cy.validateLogin(); +// Non-session login (for use within sessions) +Cypress.Commands.add('loginNoSession', (provider: string, username: string, password: string, oauthurl: string) => { + performLogin(provider, username, password, oauthurl); + cy.validateLogin(); +}); + +Cypress.Commands.add('switchPerspective', (...perspectives: string[]) => { + /* If side bar is collapsed then expand it + before switching perspecting */ + cy.wait(2000); + cy.get('body').then((body) => { + if (body.find('.pf-m-collapsed').length > 0) { + cy.get('#nav-toggle').click(); + } }); + nav.sidenav.switcher.changePerspectiveTo(...perspectives); + cy.wait(3000); + guidedTour.close(); +}); - Cypress.Commands.add('switchPerspective', (perspective: string) => { - /* If side bar is collapsed then expand it - before switching perspecting */ - cy.wait(2000); - cy.get('body').then((body) => { - if (body.find('.pf-m-collapsed').length > 0) { - cy.get('#nav-toggle').click(); +// To avoid influence from upstream login change +Cypress.Commands.add('uiLogin', (provider: string, username: string, password: string) => { + cy.log('Commands uiLogin'); + cy.clearCookie('openshift-session-token'); + cy.visit('/'); + cy.window().then( + ( + win: any, // eslint-disable-line @typescript-eslint/no-explicit-any + ) => { + if (win.SERVER_FLAGS?.authDisabled) { + cy.task('log', 'Skipping login, console is running with auth disabled'); + return; } - }); - nav.sidenav.switcher.changePerspectiveTo(perspective); - }); + cy.get('h1').should('have.text', 'Login'); + cy.get('body').then(($body) => { + if ($body.text().includes(provider)) { + cy.contains(provider).should('be.visible').click(); + } else if ($body.find('li.idp').length > 0) { + //Using the last idp if doesn't provider idp name + cy.get('li.idp').last().click(); + } + }); + cy.get('#inputUsername').type(username); + cy.get('#inputPassword').type(password); + cy.get('button[type=submit]').click(); + cy.byTestID('username', { timeout: 120000 }).should('be.visible'); + }, + ); + cy.switchPerspective('Administrator'); +}); - // To avoid influence from upstream login change - Cypress.Commands.add('uiLogin', (provider: string, username: string, password: string) => { - cy.log('Commands uiLogin'); +// Relogin command for use after clearing sessions +// Fetches OAuth URL and uses cy.origin() for cross-origin login like the other login commands +Cypress.Commands.add('relogin', (provider: string, username: string, password: string) => { + cy.log('Commands relogin - fetching OAuth URL and performing fresh login'); + + cy.uiLogout(); + // Get the OAuth URL from the cluster (same as performLoginAndAuth does) + cy.exec( + `oc get oauthclient openshift-browser-client -o go-template --template="{{index .redirectURIs 0}}" --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + ).then((result) => { + if (result.stderr !== '') { + throw new Error(`Failed to get OAuth URL: ${result.stderr}`); + } + + const oauth = result.stdout; + const oauthurl = new URL(oauth); + const oauthorigin = oauthurl.origin; + cy.log(`OAuth origin: ${oauthorigin}`); + + // Now perform login using cy.origin() for cross-origin OAuth cy.clearCookie('openshift-session-token'); - cy.visit('/'); - cy.window().then( - ( - win: any, // eslint-disable-line @typescript-eslint/no-explicit-any - ) => { - if (win.SERVER_FLAGS?.authDisabled) { - cy.task('log', 'Skipping login, console is running with auth disabled'); - return; - } - cy.get('[data-test-id="login"]').should('be.visible'); + cy.visit(Cypress.config('baseUrl')); + + // Use cy.origin() for cross-origin login (OAuth is on a different domain) + cy.origin( + oauthorigin, + { args: { provider, username, password } }, + ({ provider, username, password }) => { + // Wait for login page to load + cy.get('[data-test-id="login"]', { timeout: 60000 }).should('be.visible'); + + // Select the IDP if available cy.get('body').then(($body) => { if ($body.text().includes(provider)) { cy.contains(provider).should('be.visible').click(); - } else if ($body.find('li.idp').length > 0) { - //Using the last idp if doesn't provider idp name - cy.get('li.idp').last().click(); } }); - cy.get('#inputUsername').type(username); + + // Fill in login form + cy.get('#inputUsername', { timeout: 30000 }).should('be.visible').type(username); cy.get('#inputPassword').type(password); cy.get('button[type=submit]').click(); - cy.byTestID('username', { timeout: 120000 }).should('be.visible'); - }, + } ); + + // Wait for successful login back on the main origin + cy.byTestID('username', { timeout: 120000 }).should('be.visible'); cy.switchPerspective('Administrator'); }); +}); - Cypress.Commands.add('uiLogout', () => { - cy.window().then( - ( - win: any, // eslint-disable-line @typescript-eslint/no-explicit-any - ) => { - if (win.SERVER_FLAGS?.authDisabled) { - cy.log('Skipping logout, console is running with auth disabled'); - return; - } - cy.log('Log out UI'); - cy.byTestID('username').click(); - cy.byTestID('log-out').should('be.visible'); - cy.byTestID('log-out').click({ force: true }); - }, - ); - }); +Cypress.Commands.add('uiLogout', () => { + cy.window().then( + ( + win: any, // eslint-disable-line @typescript-eslint/no-explicit-any + ) => { + if (win.SERVER_FLAGS?.authDisabled) { + cy.log('Skipping logout, console is running with auth disabled'); + return; + } + cy.log('Log out UI'); + cy.byTestID('username').click(); + cy.wait(3000); + cy.byTestID('log-out').click({ force: true }); + }, + ); +}); - Cypress.Commands.add('cliLogin', (username?, password?, hostapi?) => { - const loginUsername = username || Cypress.env('LOGIN_USERNAME'); - const loginPassword = password || Cypress.env('LOGIN_PASSWORD'); - const hostapiurl = hostapi || Cypress.env('HOST_API'); - cy.exec( - `oc login -u ${loginUsername} -p ${loginPassword} ${hostapiurl} --insecure-skip-tls-verify=true`, - { failOnNonZeroExit: false }, - ).then((result) => { - cy.log(result.stderr); - cy.log(result.stdout); - }); +Cypress.Commands.add('cliLogin', (username?, password?, hostapi?) => { + const loginUsername = username || Cypress.env('LOGIN_USERNAME'); + const loginPassword = password || Cypress.env('LOGIN_PASSWORD'); + const hostapiurl = hostapi || Cypress.env('HOST_API'); + cy.exec( + `oc login -u ${loginUsername} -p ${loginPassword} ${hostapiurl} --insecure-skip-tls-verify=true`, + { failOnNonZeroExit: false }, + ).then((result) => { + cy.log(result.stderr); + cy.log(result.stdout); }); +}); - Cypress.Commands.add('cliLogout', () => { - cy.exec(`oc logout`, { failOnNonZeroExit: false }).then((result) => { - cy.log(result.stderr); - cy.log(result.stdout); - }); +Cypress.Commands.add('cliLogout', () => { + cy.exec(`oc logout`, { failOnNonZeroExit: false }).then((result) => { + cy.log(result.stderr); + cy.log(result.stdout); }); +}); - Cypress.Commands.add('adminCLI', (command: string) => { - const kubeconfig = Cypress.env('KUBECONFIG_PATH'); - cy.log(`Run admin command: ${command}`); - cy.exec(`${command} --kubeconfig ${kubeconfig}`); - }); - - Cypress.Commands.add('executeAndDelete', (command: string) => { - cy.exec(command, { failOnNonZeroExit: false }) - .then(result => { - if (result.code !== 0) { - cy.task('logError', `Command "${command}" failed: ${result.stderr || result.stdout}`); - } else { - cy.task('log', `Command "${command}" executed successfully`); - } - }); - }); \ No newline at end of file +Cypress.Commands.add('adminCLI', (command: string) => { + const kubeconfig = Cypress.env('KUBECONFIG_PATH'); + cy.log(`Run admin command: ${command}`); + cy.exec(`${command} --kubeconfig ${kubeconfig}`); +}); + +Cypress.Commands.add('executeAndDelete', (command: string) => { + cy.exec(command, { failOnNonZeroExit: false }) + .then(result => { + if (result.code !== 0) { + cy.task('logError', `Command "${command}" failed: ${result.stderr || result.stdout}`); + } else { + cy.task('log', `Command "${command}" executed successfully`); + } + }); +}); \ No newline at end of file diff --git a/web/cypress/support/commands/incident-commands.ts b/web/cypress/support/commands/incident-commands.ts index aea8084c8..8a88f4b3c 100644 --- a/web/cypress/support/commands/incident-commands.ts +++ b/web/cypress/support/commands/incident-commands.ts @@ -3,47 +3,82 @@ export {}; declare global { namespace Cypress { interface Chainable { - createKubePodCrashLoopingAlert(): Chainable; + createKubePodCrashLoopingAlert(testName?: string): Chainable; cleanupIncidentPrometheusRules(): Chainable; } } } -// Apply incident fixture manifests to the cluster -Cypress.Commands.add('createKubePodCrashLoopingAlert', () => { +Cypress.Commands.add('createKubePodCrashLoopingAlert', (testName?: string) => { const kubeconfigPath = Cypress.env('KUBECONFIG_PATH'); - // Generate a random alert name for this test run - const randomAlertName = `CustomPodCrashLooping_${Math.random().toString(36).substring(2, 15)}`; + const alertName = testName + ? `CustomPodCrashLooping_${testName}` + : `CustomPodCrashLooping_${Math.random().toString(36).substring(2, 15)}`; - // Store the alert name globally so tests can access it - Cypress.env('CURRENT_ALERT_NAME', randomAlertName); + const shouldReuseResources = !!testName; - cy.log(`Generated random alert name: ${randomAlertName}`); + cy.log(`Using alert name: ${alertName}${shouldReuseResources ? ' (reuse mode)' : ' (create new)'}`); - // Read the template and replace the placeholder - cy.readFile('./cypress/fixtures/incidents/prometheus_rule_pod_crash_loop.yaml').then((template) => { - const yamlContent = template.replace(/\{\{ALERT_NAME\}\}/g, randomAlertName); - - // Write the modified YAML to a temporary file - cy.writeFile('./cypress/fixtures/incidents/temp_prometheus_rule.yaml', yamlContent).then(() => { - // Apply the modified YAML - cy.exec( - `oc apply -f ./cypress/fixtures/incidents/temp_prometheus_rule.yaml --kubeconfig ${kubeconfigPath}`, - ); + if (!testName) { + Cypress.env('CURRENT_ALERT_NAME', alertName); + } + + const createOrUpdatePrometheusRule = () => { + cy.readFile('./cypress/fixtures/incidents/prometheus_rule_pod_crash_loop.yaml').then((template) => { + const yamlContent = template.replace(/\{\{ALERT_NAME\}\}/g, alertName); - // Clean up temporary file - cy.exec('rm ./cypress/fixtures/incidents/temp_prometheus_rule.yaml'); + cy.writeFile('./cypress/fixtures/incidents/temp_prometheus_rule.yaml', yamlContent).then(() => { + cy.exec( + `oc apply -f ./cypress/fixtures/incidents/temp_prometheus_rule.yaml --kubeconfig ${kubeconfigPath}`, + ); + + cy.exec('rm ./cypress/fixtures/incidents/temp_prometheus_rule.yaml'); + }); }); - }); + }; + + const createPod = () => { + cy.exec( + `oc apply -f ./cypress/fixtures/incidents/pod_crash_loop.yaml --kubeconfig ${kubeconfigPath}`, + ); + }; - cy.exec( - `oc apply -f ./cypress/fixtures/incidents/pod_crash_loop.yaml --kubeconfig ${kubeconfigPath}`, - ); + if (shouldReuseResources) { + cy.exec( + `oc get prometheusrule kubernetes-monitoring-podcrash-rules -n openshift-monitoring -o yaml --kubeconfig ${kubeconfigPath}`, + { failOnNonZeroExit: false } + ).then((result) => { + if (result.code === 0 && result.stdout.includes(`alert: ${alertName}`)) { + cy.log(`PrometheusRule with alert '${alertName}' already exists, reusing it`); + } else { + if (result.code === 0) { + cy.log(`PrometheusRule exists but does not contain alert '${alertName}', updating it`); + } else { + cy.log('PrometheusRule does not exist, creating it'); + } + createOrUpdatePrometheusRule(); + } + }); + + cy.exec( + `oc get -f ./cypress/fixtures/incidents/pod_crash_loop.yaml --kubeconfig ${kubeconfigPath}`, + { failOnNonZeroExit: false } + ).then((result) => { + if (result.code === 0) { + cy.log('Crash looping pod already exists, reusing it'); + } else { + cy.log('Crash looping pod does not exist, creating it'); + createPod(); + } + }); + } else { + createOrUpdatePrometheusRule(); + createPod(); + } - // Return the alert name for the test to use - return cy.wrap(randomAlertName); + return cy.wrap(alertName); }); // Clean up incident fixture manifests from the cluster diff --git a/web/cypress/support/commands/operator-commands.ts b/web/cypress/support/commands/operator-commands.ts index ac0472744..bc88096e5 100644 --- a/web/cypress/support/commands/operator-commands.ts +++ b/web/cypress/support/commands/operator-commands.ts @@ -6,6 +6,7 @@ import Shadow = Cypress.Shadow; import 'cypress-wait-until'; import { operatorHubPage } from '../../views/operator-hub-page'; import { nav } from '../../views/nav'; +import { DataTestIDs, LegacyTestIDs } from '../../../src/components/data-test'; export { }; @@ -35,9 +36,41 @@ const useSession = Cypress.env('SESSION'); export const operatorAuthUtils = { // Core login and auth logic (shared between session and non-session versions) performLoginAndAuth(useSession: boolean): void { - cy.adminCLI( - `oc adm policy add-cluster-role-to-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, - ); + if (`${Cypress.env('LOGIN_USERNAME')}` === 'kubeadmin') { + cy.adminCLI( + `oc adm policy add-cluster-role-to-user cluster-admin ${Cypress.env('LOGIN_USERNAME')}`, + ); + } else { + cy.adminCLI( + `oc project openshift-monitoring`, + ); + cy.adminCLI( + `oc adm policy add-role-to-user monitoring-edit ${Cypress.env('LOGIN_USERNAME')} -n openshift-monitoring`, + ); + cy.adminCLI( + `oc adm policy add-role-to-user monitoring-alertmanager-edit --role-namespace openshift-monitoring ${Cypress.env('LOGIN_USERNAME')}`, + ); + + cy.adminCLI( + `oc adm policy add-role-to-user view ${Cypress.env('LOGIN_USERNAME')} -n openshift-monitoring`, + ); + + cy.adminCLI( + `oc project default`, + ); + + cy.adminCLI( + `oc adm policy add-role-to-user monitoring-edit ${Cypress.env('LOGIN_USERNAME')} -n default`, + ); + cy.adminCLI( + `oc adm policy add-role-to-user monitoring-alertmanager-edit --role-namespace default ${Cypress.env('LOGIN_USERNAME')}`, + ); + + cy.adminCLI( + `oc adm policy add-role-to-user view ${Cypress.env('LOGIN_USERNAME')} -n default`, + ); + + } cy.exec( `oc get oauthclient openshift-browser-client -o go-template --template="{{index .redirectURIs 0}}" --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ).then((result) => { @@ -102,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)]; @@ -173,7 +207,6 @@ const operatorUtils = { cy.log(`Monitoring plugin pod is now running in namespace: ${MP.namespace}`); cy.reload(true); }); - // }); } else { cy.log('MP_IMAGE is NOT set. Skipping patching the image in CMO operator CSV.'); @@ -192,7 +225,7 @@ const operatorUtils = { 'Operator installed successfully', ); cy.exec( - `oc label namespaces ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `oc label namespace ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); } else if (Cypress.env('KONFLUX_COO_BUNDLE_IMAGE')) { cy.log('KONFLUX_COO_BUNDLE_IMAGE is set. COO operator will be installed from Konflux bundle.'); @@ -204,10 +237,10 @@ const operatorUtils = { `oc create namespace ${MCP.namespace} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); cy.exec( - `oc label namespaces ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `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')) { @@ -220,10 +253,10 @@ const operatorUtils = { `oc create namespace ${MCP.namespace} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); cy.exec( - `oc label namespaces ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `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')) { @@ -250,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.) @@ -273,19 +308,36 @@ 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 }); }, - 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}` }, @@ -294,36 +346,77 @@ 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.'); cy.exec(`oc new-project perses-dev --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.log('Create openshift-cluster-sample-dashboard instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/openshift-cluster-sample-dashboard.yaml --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 perses-dashboard-sample instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/perses-dashboard-sample.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 prometheus-overview-variables instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/prometheus-overview-variables.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 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 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 Querier instance.'); - cy.exec(`oc apply -f ./cypress/fixtures/coo/thanos-querier-datasource.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 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 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 namespaces ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `oc label namespace ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); cy.log('Create Monitoring UI Plugin instance.'); @@ -340,9 +433,9 @@ const operatorUtils = { }); cy.exec( - `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=perses -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `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: readyTimeoutMilliseconds, + timeout: installTimeoutMilliseconds, failOnNonZeroExit: true } ).then((result) => { @@ -351,7 +444,7 @@ const operatorUtils = { }); cy.exec( - `sleep 15 && oc wait --for=jsonpath='{.metadata.name}'=health-analyzer --timeout=60s servicemonitor/health-analyzer --namespace=openshift-cluster-observability-operator -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `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 @@ -366,6 +459,42 @@ const operatorUtils = { 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')}`); + + 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')}`, + { + 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.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')}`, + { + 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(`Reloading the page`); + cy.reload(true); + cy.log(`Waiting for 10 seconds before clicking the application launcher`); + cy.wait(10000); + cy.log(`Clicking the application launcher`); + cy.byLegacyTestID(LegacyTestIDs.ApplicationLauncher).should('be.visible').click(); + cy.byTestID(DataTestIDs.MastHeadApplicationItem).contains('Signal Correlation').should('be.visible'); + }, + revertMonitoringPluginImage(MP: { namespace: string }): void { if (Cypress.env('MP_IMAGE')) { cy.log('MP_IMAGE is set. Lets revert CMO operator CSV'); @@ -388,11 +517,16 @@ const operatorUtils = { `sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/name=monitoring-plugin -n ${MP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, { timeout: readyTimeoutMilliseconds, - failOnNonZeroExit: true + failOnNonZeroExit: false } ).then((result) => { - expect(result.code).to.eq(0); - cy.log(`Monitoring plugin pod is now running in namespace: ${MP.namespace}`); + if (result.code === 0) { + cy.log(`Monitoring plugin pod is now running in namespace: ${MP.namespace}`); + } else if (result.stderr.includes('no matching resources found')) { + cy.log(`No monitoring-plugin pods found in namespace ${MP.namespace} - this is expected on fresh clusters`); + } else { + throw new Error(`Failed to wait for monitoring-plugin pods: ${result.stderr}`); + } }); cy.reload(true); @@ -416,27 +550,44 @@ const operatorUtils = { cy.log('Delete Monitoring UI Plugin instance.'); cy.executeAndDelete( - `oc delete ${config.kind} ${config.name} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `oc delete ${config.kind} ${config.name} --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); - - // Common cleanup steps - cy.log('Remove openshift-cluster-sample-dashboard instance.'); - cy.executeAndDelete(`oc delete -f ./cypress/fixtures/coo/openshift-cluster-sample-dashboard.yaml --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 --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 --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 --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/thanos-querier-datasource.yaml --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 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 --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + 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')) { @@ -444,7 +595,7 @@ const operatorUtils = { // First check if the namespace exists cy.exec( - `oc get namespace openshift-cluster-observability-operator --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `oc get namespace ${MCP.namespace} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, { timeout: readyTimeoutMilliseconds, failOnNonZeroExit: false @@ -453,32 +604,107 @@ const operatorUtils = { if (checkResult.code === 0) { // Namespace exists, proceed with deletion cy.log('Namespace exists, proceeding with deletion'); + + // Step 1: Delete CSV (ClusterServiceVersion) cy.exec( - `oc delete namespace openshift-cluster-observability-operator --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `oc delete csv --all -n ${MCP.namespace} --ignore-not-found --wait=false --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, { - timeout: readyTimeoutMilliseconds, + timeout: 30000, failOnNonZeroExit: false } ).then((result) => { if (result.code === 0) { - cy.log(`Cluster Observability Operator namespace is now deleted`); + cy.log(`CSV deletion initiated in ${MCP.namespace}`); } else { - cy.log(`Primary delete failed: ${result.stderr}`); - cy.log(`Attempting force delete...`); - - cy.exec( - `./cypress/fixtures/coo/force_delete_ns.sh openshift-cluster-observability-operator --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, - { - failOnNonZeroExit: false, - timeout: readyTimeoutMilliseconds - } - ).then((forceResult) => { - if (forceResult.code !== 0) { - cy.log(`Force delete also failed: ${forceResult.stderr}`); + cy.log(`CSV deletion failed or not found: ${result.stderr}`); + } + }); + + // Step 2: Delete Subscription + cy.exec( + `oc delete subscription --all -n ${MCP.namespace} --ignore-not-found --wait=false --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + { + timeout: 30000, + failOnNonZeroExit: false + } + ).then((result) => { + if (result.code === 0) { + cy.log(`Subscription deletion initiated in ${MCP.namespace}`); + } else { + cy.log(`Subscription deletion failed or not found: ${result.stderr}`); + } + }); + + // Step 3: Initiate namespace deletion without waiting (--wait=false prevents timeout) + cy.exec( + `oc delete namespace ${MCP.namespace} --ignore-not-found --wait=false --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + { + timeout: 30000, // Short timeout since we're not waiting + failOnNonZeroExit: false + } + ).then((result) => { + if (result.code === 0) { + cy.log(`Namespace deletion initiated for ${MCP.namespace}`); + } else { + cy.log(`Failed to initiate deletion: ${result.stderr}`); + } + }); + + + const checkIntervalMs = 15000; // Check every 15 seconds + const startTime = Date.now(); + const maxWaitTimeMs = 600000; //10min + + const checkStatus = () => { + const elapsed = Date.now() - startTime; + + if (elapsed > maxWaitTimeMs) { + cy.log(`${elapsed}ms - Timeout reached (${maxWaitTimeMs / 60000}m). Namespace ${MCP.namespace} still terminating. Attempting force-delete.`); + // Execute the shell script to remove finalizers + return cy.exec(`./cypress/fixtures/coo/force_delete_ns.sh ${MCP.namespace} ${Cypress.env('KUBECONFIG_PATH')}`, + { failOnNonZeroExit: false, timeout: installTimeoutMilliseconds }).then((result) => { + cy.log(`${elapsed}ms - Force delete output: ${result.stdout}`); + if (result.code !== 0) { + cy.log(`Force delete failed with exit code ${result.code}: ${result.stderr}`); } }); } + + // Command to check the namespace status + // Use 'oc get ns -o jsonpath' for minimal output and fastest check + cy.exec(`oc get ns ${MCP.namespace} --kubeconfig ${`${Cypress.env('KUBECONFIG_PATH')}`} -o jsonpath='{.status.phase}'`, { failOnNonZeroExit: false }) + .then((result) => { + const status = result.stdout.trim(); + + if (status === 'Terminating') { + cy.log(`${elapsed}ms - ${MCP.namespace} is still 'Terminating'. Retrying in ${checkIntervalMs / 1000}s. Elapsed: ${Math.round(elapsed / 1000)}s`); + cy.exec( + `./cypress/fixtures/coo/force_delete_ns.sh ${MCP.namespace} ${Cypress.env('KUBECONFIG_PATH')}`, + { failOnNonZeroExit: false, timeout: installTimeoutMilliseconds } + ).then((forceResult) => { + cy.log(`${elapsed}ms - Force delete output: ${forceResult.stdout}`); + if (forceResult.code !== 0) { + cy.log(`Force delete failed with exit code ${forceResult.code}: ${forceResult.stderr}`); + } + }); + // Wait and call recursively + cy.wait(checkIntervalMs).then(checkStatus); + } else if (status === 'NotFound') { + cy.log(`${elapsed}ms - ${MCP.namespace} is successfully deleted.`); + // Stop recursion + } else { + // Handles 'Active' or other unexpected states if the delete command failed silently earlier + cy.log(`${elapsed}ms - ${MCP.namespace} changed to unexpected state: ${status}. Stopping monitoring.`); + } + }); + }; + + checkStatus(); + + cy.then(() => { + operatorUtils.waitForPodsDeleted(MCP.namespace); }); + } else { cy.log('Namespace does not exist, skipping deletion'); } @@ -486,6 +712,52 @@ const operatorUtils = { } }, + waitForPodsDeleted(namespace: string, maxWaitMs: number = 120000): void { + const kubeconfigPath = Cypress.env('KUBECONFIG_PATH'); + const checkIntervalMs = 5000; + const startTime = Date.now(); + const podPatterns = 'monitoring|perses|perses-0|health-analyzer|troubleshooting-panel|korrel8r'; + + const checkPods = () => { + const elapsed = Date.now() - startTime; + + if (elapsed > maxWaitMs) { + throw new Error(`Timeout: Pods still exist after ${maxWaitMs / 1000}s`); + } + + cy.exec( + `oc get pods -n ${namespace} --kubeconfig ${kubeconfigPath} -o name 2>&1 | grep -E '${podPatterns}' | wc -l`, + { failOnNonZeroExit: false } + ).then((result) => { + const count = parseInt(result.stdout.trim(), 10); + + if (count === 0 || result.stderr.includes('not found')) { + cy.log(`✓ All target pods deleted after ${elapsed}ms`); + } else { + cy.log(`${elapsed}ms - ${count} pod(s) still exist, retrying...`); + cy.wait(checkIntervalMs).then(checkPods); + } + }); + }; + + checkPods(); + }, + + cleanupTroubleshootingPanel(MCP: { namespace: string, config1?: { kind: string, name: string } }): void { + const config1 = MCP.config1 || { kind: 'UIPlugin', name: 'troubleshooting-panel' }; + + if (Cypress.env('SKIP_ALL_INSTALL')) { + cy.log('SKIP_ALL_INSTALL is set. Skipping Troubleshooting Panel instance deletion.'); + return; + } + + cy.log('Delete Troubleshooting Panel instance.'); + cy.executeAndDelete( + `oc delete ${config1.kind} ${config1.name} --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + ); + + }, + RemoveClusterAdminRole(): void { cy.log('Remove cluster-admin role from user.'); cy.executeAndDelete( @@ -594,6 +866,7 @@ Cypress.Commands.add('beforeBlock', (MP: { namespace: string, operatorName: stri cy.log('SKIP_ALL_INSTALL is set. Skipping COO cleanup and operator verifications (preserves existing setup).'); return; } + operatorUtils.cleanupTroubleshootingPanel(MCP); operatorUtils.cleanup(MCP); operatorUtils.revertMonitoringPluginImage(MP); cy.log('Cleanup COO (no session) completed'); @@ -607,7 +880,9 @@ 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); operatorUtils.RemoveClusterAdminRole(); operatorUtils.collectDebugInfo(MP, MCP); @@ -621,15 +896,12 @@ Cypress.Commands.add('beforeBlock', (MP: { namespace: string, operatorName: stri Cypress.Commands.add('beforeBlockACM', (MCP, MP) => { cy.beforeBlockCOO(MCP, MP); - cy.log('=== [Setup] Installing ACM Operator & MCO ==='); + cy.log('=== [Setup] Installing ACM test resources ==='); cy.exec('bash ./cypress/fixtures/coo/acm-install.sh', { env: { KUBECONFIG: Cypress.env('KUBECONFIG_PATH'), }, failOnNonZeroExit: false, timeout: 1200000, // long time script }); - cy.exec(`oc apply -f ./cypress/fixtures/coo/acm-uiplugin.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - // add example alerts for test - cy.exec(`oc apply -f ./cypress/fixtures/coo/acm-alerrule-test.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); cy.log('ACM environment setup completed'); }); diff --git a/web/cypress/support/commands/perses-commands.ts b/web/cypress/support/commands/perses-commands.ts new file mode 100644 index 000000000..1a5228067 --- /dev/null +++ b/web/cypress/support/commands/perses-commands.ts @@ -0,0 +1,140 @@ +export { }; + +import { nav } from '../../views/nav'; +import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { listPersesDashboardsOUIAIDs } from '../../../src/components/data-test'; + +// Display name prefixes/exact matches for test-created PersesDashboards to delete before Perses tests. +// Examples: "Test Dashboard0.24...", "Dashboard to test duplication0.33...", "Testing Dashboard - UP 0.32..." +const PERSES_TEST_DASHBOARD_NAME_PREFIXES = [ + 'Testing Dashboard - UP ', + 'Renamed dashboard ', + 'Duplicate dashboard ', + 'Test Dashboard', + 'Dashboard to test rename', + 'Dashboard to test duplication', + 'DashboardToTestDuplication', +]; +const PERSES_TEST_DASHBOARD_NAME_EXACT = [ + 'Testing Perses dashboard - YAML', + 'Testing Perses dashboard - JSON', + 'Service Level dashboards / Virtual Machines by Time in Status', +]; + +declare global { + namespace Cypress { + interface Chainable { + setupPersesRBACandExtraDashboards(): Chainable; + cleanupExtraDashboards(): Chainable; + /** Delete test Perses dashboards via UI (list page). Call before Perses tests. */ + cleanupPersesTestDashboardsBeforeTests(): Chainable; + } + } + } + +Cypress.Commands.add('setupPersesRBACandExtraDashboards', () => { + + if (`${Cypress.env('LOGIN_USERNAME1')}` !== 'kubeadmin' && `${Cypress.env('LOGIN_USERNAME2')}` !== undefined) { + cy.exec( + './cypress/fixtures/coo/coo141_perses/rbac/rbac_perses_e2e_ci_users.sh', + { + env: { + USER1: `${Cypress.env('LOGIN_USERNAME1')}`, + USER2: `${Cypress.env('LOGIN_USERNAME2')}`, + USER3: `${Cypress.env('LOGIN_USERNAME3')}`, + USER4: `${Cypress.env('LOGIN_USERNAME4')}`, + USER5: `${Cypress.env('LOGIN_USERNAME5')}`, + USER6: `${Cypress.env('LOGIN_USERNAME6')}`, + }, + } + ); + + cy.log('Create openshift-cluster-sample-dashboard instance.'); + cy.exec(`sed 's/namespace: openshift-cluster-observability-operator/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/openshift-cluster-sample-dashboard.yaml | oc apply -f - --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create perses-dashboard-sample instance.'); + cy.exec(`sed 's/namespace: perses-dev/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/perses-dashboard-sample.yaml | oc apply -f - --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create prometheus-overview-variables instance.'); + cy.exec(`sed 's/namespace: perses-dev/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/prometheus-overview-variables.yaml | oc apply -f - --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create thanos-compact-overview-1var instance.'); + cy.exec(`sed 's/namespace: perses-dev/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/thanos-compact-overview-1var.yaml | oc apply -f - --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Create Thanos Querier instance.'); + cy.exec(`sed 's/namespace: perses-dev/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/thanos-querier-datasource.yaml | oc apply -f - --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + } +}); + +Cypress.Commands.add('cleanupExtraDashboards', () => { + + cy.log('Remove openshift-cluster-sample-dashboard instance.'); + cy.exec(`sed 's/namespace: openshift-cluster-observability-operator/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/openshift-cluster-sample-dashboard.yaml | oc delete -f - --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove perses-dashboard-sample instance.'); + cy.exec(`sed 's/namespace: perses-dev/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/perses-dashboard-sample.yaml | oc delete -f - --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove prometheus-overview-variables instance.'); + cy.exec(`sed 's/namespace: perses-dev/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/prometheus-overview-variables.yaml | oc delete -f - --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove thanos-compact-overview-1var instance.'); + cy.exec(`sed 's/namespace: perses-dev/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/thanos-compact-overview-1var.yaml | oc delete -f - --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove Thanos Querier instance.'); + cy.exec(`sed 's/namespace: perses-dev/namespace: observ-test/g' ./cypress/fixtures/coo/coo141_perses/dashboards/thanos-querier-datasource.yaml | oc delete -f - --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + + cy.log('Remove observ-test namespace'); + cy.exec(`oc delete namespace observ-test --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + +}); + +function isTestDashboardName(displayName: string | undefined): boolean { + if (!displayName) return false; + if (PERSES_TEST_DASHBOARD_NAME_EXACT.includes(displayName)) return true; + return PERSES_TEST_DASHBOARD_NAME_PREFIXES.some((p) => displayName.startsWith(p)); +} + +const MAX_UI_CLEANUP_ITERATIONS = 50; + +Cypress.Commands.add('cleanupPersesTestDashboardsBeforeTests', () => { + cy.log('Perses cleanup: remove test dashboards via UI.'); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(5000); + cy.changeNamespace('All Projects'); + cy.wait(5000); + cy.get(`[data-ouia-component-id^="${listPersesDashboardsOUIAIDs.PersesDashListDataViewTable}"]`, { timeout: 15000 }) + .should('be.visible') + .then(() => { + runDeleteOneMatching(0); + }); +}); + +function runDeleteOneMatching(iteration: number): void { + if (iteration >= MAX_UI_CLEANUP_ITERATIONS) return; + cy.get('body').then(($body) => { + const $table = $body.find(`[data-ouia-component-id^="${listPersesDashboardsOUIAIDs.PersesDashListDataViewTable}"]`); + if ($table.length === 0) return; + const $rows = $table.find('tbody tr'); + if ($rows.length === 0) return; + let deleteIndex = -1; + let deleteName = ''; + for (let i = 0; i < $rows.length; i++) { + const $row = $rows.eq(i); + const $link = $row.find('a').filter((_, el) => (Cypress.$(el).text().trim().length > 0)).first(); + const name = $link.length ? $link.text().trim() : ''; + if (name && isTestDashboardName(name)) { + deleteIndex = i; + deleteName = name; + break; + } + } + if (deleteIndex < 0) return; + cy.log(`Perses cleanup (UI): deleting "${deleteName}" (row ${deleteIndex})`); + listPersesDashboardsPage.clickKebabIcon(deleteIndex); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + cy.wait(4000); + cy.then(() => runDeleteOneMatching(iteration + 1)); + }); +} + diff --git a/web/cypress/support/commands/utility-commands.ts b/web/cypress/support/commands/utility-commands.ts index abea6032c..15d2fdd21 100644 --- a/web/cypress/support/commands/utility-commands.ts +++ b/web/cypress/support/commands/utility-commands.ts @@ -12,6 +12,7 @@ declare global { changeNamespace(namespace: string): Chainable; aboutModal(): Chainable; podImage(pod: string, namespace: string): Chainable; + assertNamespace(namespace: string, exists: boolean): Chainable; } } } @@ -65,8 +66,8 @@ Cypress.Commands.add('waitUntilWithCustomTimeout', ( cy.byLegacyTestID(LegacyTestIDs.NamespaceBarDropdown).find('button').scrollIntoView().should('be.visible'); cy.byLegacyTestID(LegacyTestIDs.NamespaceBarDropdown).find('button').scrollIntoView().should('be.visible').click({force: true}); } else { - cy.byClass(Classes.NamespaceDropdown).scrollIntoView().should('be.visible'); - cy.byClass(Classes.NamespaceDropdown).scrollIntoView().should('be.visible').click({force: true}); + cy.get(Classes.NamespaceDropdown).scrollIntoView().should('be.visible'); + cy.get(Classes.NamespaceDropdown).scrollIntoView().should('be.visible').click({force: true}); } }); cy.get('body').then(($body) => { @@ -88,13 +89,15 @@ Cypress.Commands.add('waitUntilWithCustomTimeout', ( Cypress.Commands.add('aboutModal', () => { cy.log('Getting OCP version'); - cy.byTestID(DataTestIDs.MastHeadHelpIcon).should('be.visible'); - cy.byTestID(DataTestIDs.MastHeadHelpIcon).should('be.visible').click({force: true}); - cy.byTestID(DataTestIDs.MastHeadApplicationItem).contains('About').should('be.visible').click(); - cy.byAriaLabel('About modal').find('div[class*="co-select-to-copy"]').eq(0).should('be.visible').then(($ocpversion) => { - cy.log('OCP version: ' + $ocpversion.text()); - }); - cy.byAriaLabel('Close Dialog').should('be.visible').click(); + if (Cypress.env('LOGIN_USERNAME') === 'kubeadmin') { + cy.byTestID(DataTestIDs.MastHeadHelpIcon).should('be.visible'); + cy.byTestID(DataTestIDs.MastHeadHelpIcon).should('be.visible').click({force: true}); + cy.byTestID(DataTestIDs.MastHeadApplicationItem).contains('About').should('be.visible').click(); + cy.byAriaLabel('About modal').find('div[class*="co-select-to-copy"]').eq(0).should('be.visible').then(($ocpversion) => { + cy.log('OCP version: ' + $ocpversion.text()); + }); + cy.byAriaLabel('Close Dialog').should('be.visible').click(); + } }); @@ -114,6 +117,8 @@ Cypress.Commands.add('waitUntilWithCustomTimeout', ( Cypress.Commands.add('podImage', (pod: string, namespace: string) => { cy.log('Get pod image'); + cy.switchPerspective('Core platform', 'Administrator'); + cy.wait(5000); cy.clickNavLink(['Workloads', 'Pods']); cy.changeNamespace(namespace); cy.byTestID('page-heading').contains('Pods').should('be.visible'); @@ -138,3 +143,48 @@ Cypress.Commands.add('waitUntilWithCustomTimeout', ( }); + Cypress.Commands.add('assertNamespace', (namespace: string, exists: boolean) => { + cy.log('Asserting Namespace: ' + namespace + ' exists: ' + exists); + cy.wait(2000); + cy.get('body').then(($body) => { + const hasNamespaceBarDropdown = $body.find('[data-test-id="'+LegacyTestIDs.NamespaceBarDropdown+'"]').length > 0; + if (hasNamespaceBarDropdown) { + cy.byLegacyTestID(LegacyTestIDs.NamespaceBarDropdown).find('button').scrollIntoView().should('be.visible'); + cy.byLegacyTestID(LegacyTestIDs.NamespaceBarDropdown).find('button').scrollIntoView().should('be.visible').click({force: true}); + } else { + cy.get(Classes.NamespaceDropdown).scrollIntoView().should('be.visible'); + cy.get(Classes.NamespaceDropdown).scrollIntoView().should('be.visible').click({force: true}); + } + }); + cy.get('body').then(($body) => { + const hasShowSystemSwitch = $body.find('[data-test="'+DataTestIDs.NamespaceDropdownShowSwitch+'"]').length > 0; + if (hasShowSystemSwitch) { + cy.get('[data-test="'+DataTestIDs.NamespaceDropdownShowSwitch+'"]').then(($element)=> { + if ($element.attr('data-checked-state') !== 'true') { + cy.byTestID(DataTestIDs.NamespaceDropdownShowSwitch).siblings('span').eq(0).should('be.visible'); + cy.byTestID(DataTestIDs.NamespaceDropdownShowSwitch).siblings('span').eq(0).should('be.visible').click({force: true}); + } + }); + } + }); + cy.byTestID(DataTestIDs.NamespaceDropdownTextFilter).type(namespace, {delay: 100}); + if (exists) { + cy.log('Namespace: ' + namespace + ' exists'); + cy.byTestID(DataTestIDs.NamespaceDropdownMenuLink).contains(namespace).should('be.visible'); + } else { + cy.log('Namespace: ' + namespace + ' does not exist'); + cy.byTestID(DataTestIDs.NamespaceDropdownMenuLink).should('not.exist'); + } + + cy.get('body').then(($body) => { + const hasNamespaceBarDropdown = $body.find('[data-test-id="'+LegacyTestIDs.NamespaceBarDropdown+'"]').length > 0; + if (hasNamespaceBarDropdown) { + cy.byLegacyTestID(LegacyTestIDs.NamespaceBarDropdown).find('button').scrollIntoView().should('be.visible'); + cy.byLegacyTestID(LegacyTestIDs.NamespaceBarDropdown).find('button').scrollIntoView().should('be.visible').click({force: true}); + } else { + cy.get(Classes.NamespaceDropdownExpanded).scrollIntoView().should('be.visible'); + cy.get(Classes.NamespaceDropdownExpanded).scrollIntoView().should('be.visible').click({force: true}); + } + }); + + }); \ No newline at end of file diff --git a/web/cypress/support/commands/virtualization-commands.ts b/web/cypress/support/commands/virtualization-commands.ts index 3406226bc..bcb609ba3 100644 --- a/web/cypress/support/commands/virtualization-commands.ts +++ b/web/cypress/support/commands/virtualization-commands.ts @@ -170,26 +170,26 @@ const virtualizationUtils = { cy.executeAndDelete(`oc patch kubevirt.kubevirt.io/kubevirt -n ${KBV.namespace} --type=merge -p '{"metadata":{"finalizers":[]}}' --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); - cy.executeAndDelete(`oc delete HyperConverged kubevirt-hyperconverged -n ${KBV.namespace} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.executeAndDelete(`oc delete HyperConverged kubevirt-hyperconverged -n ${KBV.namespace} --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); cy.log('Remove Openshift Virtualization subscription'); - cy.executeAndDelete(`oc delete subscription ${config.name} -n ${KBV.namespace} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.executeAndDelete(`oc delete subscription ${config.name} -n ${KBV.namespace} --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); cy.log('Remove Openshift Virtualization CSV'); - cy.executeAndDelete(`oc delete csv -n ${KBV.namespace} -l operators.coreos.com/kubevirt-hyperconverged.openshift-cnv --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + cy.executeAndDelete(`oc delete csv -n ${KBV.namespace} -l operators.coreos.com/kubevirt-hyperconverged.openshift-cnv --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); cy.log('Remove Openshift Virtualization namespace'); - cy.executeAndDelete(`oc delete namespace ${KBV.namespace} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); + cy.executeAndDelete(`oc delete namespace ${KBV.namespace} --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`); cy.log('Delete Hyperconverged CRD instance.'); cy.executeAndDelete( - `oc delete crd --dry-run=client -l operators.coreos.com/kubevirt-hyperconverged.openshift-cnv --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `oc delete crd --dry-run=client -l operators.coreos.com/kubevirt-hyperconverged.openshift-cnv --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); cy.log('Delete Kubevirt instance.'); cy.executeAndDelete( - `oc delete crd -l operators.coreos.com/kubevirt-hyperconverged.openshift-cnv --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, + `oc delete crd -l operators.coreos.com/kubevirt-hyperconverged.openshift-cnv --ignore-not-found --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`, ); } @@ -218,8 +218,7 @@ const virtualizationUtils = { validate() { cy.validateLogin(); // Additional validation for Virtualization setup - cy.visit('/k8s/all-namespaces/virtualization-overview'); - cy.url().should('include', '/k8s/all-namespaces/virtualization-overview'); + cy.switchPerspective('Virtualization'); guidedTour.closeKubevirtTour(); }, 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 2b67d383a..28a728c32 100644 --- a/web/cypress/support/incidents_prometheus_query_mocks/mock-generators.ts +++ b/web/cypress/support/incidents_prometheus_query_mocks/mock-generators.ts @@ -159,7 +159,7 @@ export function createIncidentMock( endpoint: 'metrics', instance: '10.128.0.134:8443', job: 'health-analyzer', - namespace: 'openshift-cluster-observability-operator', + namespace: Cypress.env('COO_NAMESPACE'), pod: 'health-analyzer-55fc4cbbb6-5gjcv', prometheus: 'openshift-monitoring/k8s', service: 'health-analyzer' diff --git a/web/cypress/support/index.ts b/web/cypress/support/index.ts index cd0800821..a12bc4a54 100644 --- a/web/cypress/support/index.ts +++ b/web/cypress/support/index.ts @@ -1,3 +1,5 @@ +import '@cypress/grep'; + import './selectors'; import './commands/selector-commands'; import './commands/auth-commands'; @@ -6,6 +8,7 @@ import './commands/incident-commands'; import './commands/utility-commands'; import './incidents_prometheus_query_mocks'; import './commands/virtualization-commands'; +import './commands/perses-commands'; export const checkErrors = () => cy.window().then((win) => { @@ -24,7 +27,8 @@ Cypress.on('uncaught:exception', (err) => { message.includes('Unauthorized') || message.includes('Bad Gateway') || message.includes(`Cannot read properties of null (reading 'default')`) || - message.includes(`(intermediate value) is not a function`) + message.includes(`(intermediate value) is not a function`) || + message.includes(`Cannot read properties of null (reading '0')`) ) { console.warn('Ignored frontend exception:', err.message); return false; diff --git a/web/cypress/support/monitoring/00.bvt_monitoring.cy.ts b/web/cypress/support/monitoring/00.bvt_monitoring.cy.ts index 3b19650a7..bee9e9c38 100644 --- a/web/cypress/support/monitoring/00.bvt_monitoring.cy.ts +++ b/web/cypress/support/monitoring/00.bvt_monitoring.cy.ts @@ -56,8 +56,8 @@ export function testBVTMonitoring(perspective: PerspectiveConfig) { cy.get(`[class="pf-v6-c-code-block__content"]`).invoke('text').then((expText) => { cy.log(`${expText}`); cy.wrap(expText).as('alertExpression'); - }); - + }); + cy.log('5.5. click on Alert Details Page'); detailsPage.clickAlertDesc(`${WatchdogAlert.ALERT_DESC}`); diff --git a/web/cypress/support/monitoring/00.bvt_monitoring_namespace.cy.ts b/web/cypress/support/monitoring/00.bvt_monitoring_namespace.cy.ts index 71a3afd4e..a4bf1f057 100644 --- a/web/cypress/support/monitoring/00.bvt_monitoring_namespace.cy.ts +++ b/web/cypress/support/monitoring/00.bvt_monitoring_namespace.cy.ts @@ -20,7 +20,7 @@ export function runBVTMonitoringTestsNamespace(perspective: PerspectiveConfig) { export function testBVTMonitoringTestsNamespace(perspective: PerspectiveConfig) { - it(`${perspective.name} perspective - Alerting > Alerting Details page > Alerting Rule > Metrics`, () => { + it(`${perspective.name} perspective - Alerting > Alerting Details page > Alerting Rule > Metrics`, () => { cy.log('4.1. use sidebar nav to go to Observe > Alerting'); commonPages.titleShouldHaveText('Alerting'); listPage.tabShouldHaveText('Alerts'); @@ -56,8 +56,8 @@ export function testBVTMonitoringTestsNamespace(perspective: PerspectiveConfig) cy.get(`[class="pf-v6-c-code-block__content"]`).invoke('text').then((expText) => { cy.log(`${expText}`); cy.wrap(expText).as('alertExpression'); - }); - + }); + cy.log('4.5. click on Alert Details Page'); detailsPage.clickAlertDesc(`${WatchdogAlert.ALERT_DESC}`); @@ -92,7 +92,7 @@ export function testBVTMonitoringTestsNamespace(perspective: PerspectiveConfig) cy.log('5.3 silence alert page'); commonPages.titleShouldHaveText('Silence alert'); - commonPages.projectDropdownShouldExist(); + commonPages.projectDropdownShouldNotExist(); // Launches create silence form silenceAlertPage.silenceAlertSectionDefault(); diff --git a/web/cypress/support/monitoring/01.reg_alerts.cy.ts b/web/cypress/support/monitoring/01.reg_alerts.cy.ts index 39cf5dad1..6ddf75abd 100644 --- a/web/cypress/support/monitoring/01.reg_alerts.cy.ts +++ b/web/cypress/support/monitoring/01.reg_alerts.cy.ts @@ -82,6 +82,8 @@ export function testAlertsRegression(perspective: PerspectiveConfig) { nav.sidenav.clickNavLink(['Observe', 'Alerting']); commonPages.titleShouldHaveText('Alerting'); listPage.filter.clearAllFilters(); + listPage.filter.byName(`${WatchdogAlert.ALERTNAME}`); + listPage.ARRows.countShouldBe(1); listPage.ARRows.expandRow(); listPage.ARRows.assertNoKebab(); listPage.ARRows.clickAlert(); @@ -109,7 +111,6 @@ export function testAlertsRegression(perspective: PerspectiveConfig) { commonPages.titleShouldHaveText(`${WatchdogAlert.ALERTNAME}`); nav.sidenav.clickNavLink(['Observe', 'Alerting']); nav.tabs.switchTab('Silences'); - cy.changeNamespace('openshift-monitoring'); cy.log('3.8 Assert Kebab on Silence List page for Expired alert'); silencesListPage.filter.byName(`${WatchdogAlert.ALERTNAME}`); @@ -133,6 +134,7 @@ export function testAlertsRegression(perspective: PerspectiveConfig) { silenceAlertPage.alertLabelsSectionDefault(); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('alertname', `${WatchdogAlert.ALERTNAME}`, false, false); // silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('severity', `${SEVERITY}`, false, false); + cy.log('https://issues.redhat.com/browse/OU-1110 - [Namespace-level] - Admin user - Create, Edit, Recreate silences is showing namespace dropdown'); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('namespace', `${WatchdogAlert.NAMESPACE}`, false, false); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('prometheus', 'openshift-monitoring/k8s', false, false); silenceAlertPage.clickSubmit(); @@ -152,6 +154,7 @@ export function testAlertsRegression(perspective: PerspectiveConfig) { silenceAlertPage.alertLabelsSectionDefault(); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('alertname', `${WatchdogAlert.ALERTNAME}`, false, false); // silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('severity', `${SEVERITY}`, false, false); + cy.log('https://issues.redhat.com/browse/OU-1110 - [Namespace-level] - Admin user - Create, Edit, Recreate silences is showing namespace dropdown'); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('namespace', `${WatchdogAlert.NAMESPACE}`, false, false); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('prometheus', 'openshift-monitoring/k8s', false, false); silenceAlertPage.clickSubmit(); diff --git a/web/cypress/support/monitoring/02.reg_metrics_1.cy.ts b/web/cypress/support/monitoring/02.reg_metrics_1.cy.ts new file mode 100644 index 000000000..1d8a834a1 --- /dev/null +++ b/web/cypress/support/monitoring/02.reg_metrics_1.cy.ts @@ -0,0 +1,202 @@ +import { metricsPage } from '../../views/metrics'; +import { Classes, DataTestIDs } from '../../../src/components/data-test'; +import { GraphTimespan, MetricsPagePredefinedQueries, MetricsPageQueryInput, MetricsPageUnits } from '../../fixtures/monitoring/constants'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runAllRegressionMetricsTests1(perspective: PerspectiveConfig) { + testMetricsRegression1(perspective); +} + +export function testMetricsRegression1(perspective: PerspectiveConfig) { + + it(`${perspective.name} perspective - Metrics`, () => { + cy.log('1.1 Metrics page loaded'); + metricsPage.shouldBeLoaded(); + + cy.log('1.2 Units dropdown'); + metricsPage.unitsDropdownAssertion(); + + cy.log('1.3 Refresh interval dropdown'); + metricsPage.refreshIntervalDropdownAssertion(); + + cy.log('1.4 Actions dropdown'); + metricsPage.actionsDropdownAssertion(); + + cy.log('1.5 Predefined queries'); + metricsPage.predefinedQueriesAssertion(); + + cy.log('1.6 Kebab dropdown'); + metricsPage.kebabDropdownAssertionWithoutQuery(); + + }); + + it(`${perspective.name} perspective - Metrics > Actions - No query added`, () => { + cy.log('2.1 Only one query loaded'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); + + cy.log('2.2 Actions >Add query'); + metricsPage.clickActionsAddQuery(); + + cy.log('2.3 Only one query added, resulting in 2 rows'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); + + cy.log('2.3.1 Assert 2 rows - Empty state'); + metricsPage.addQueryAssertion(); + metricsPage.expandCollapseAllQueryAssertion(true); + metricsPage.expandCollapseRowAssertion(true, 1, false, false); + + cy.log('2.4 Actions > Collapse all query tables'); + metricsPage.clickActionsExpandCollapseAllQuery(false); + + cy.log('2.5 All queries collapsed'); + metricsPage.expandCollapseAllQueryAssertion(false); + metricsPage.expandCollapseRowAssertion(false, 0, false, false); + metricsPage.expandCollapseRowAssertion(false, 1, false, false); + + cy.log('2.6 Actions > Expand all query tables'); + metricsPage.clickActionsExpandCollapseAllQuery(true); + + cy.log('2.7 All queries expanded'); + metricsPage.expandCollapseAllQueryAssertion(true); + metricsPage.shouldBeLoaded(); + + cy.log('2.8 Actions > Delete all queries'); + metricsPage.clickActionsDeleteAllQueries(); + + cy.log('2.9 Only one query deleted, resulting in 1 row'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); + + }); + + it(`${perspective.name} perspective - Metrics > Actions - One query added`, () => { + cy.log('3.1 Only one query loaded'); + metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.FILESYSTEM_USAGE); + metricsPage.shouldBeLoadedWithGraph(); + + cy.log('3.2 Kebab dropdown'); + metricsPage.kebabDropdownAssertionWithQuery(); + + cy.log('3.3 Actions >Add query'); + metricsPage.clickActionsAddQuery(); + + cy.log('3.4 Only one query added, resulting in 2 rows'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); + + cy.log('3.4.1 Assert 2 rows'); + metricsPage.expandCollapseAllQueryAssertion(true); + metricsPage.expandCollapseRowAssertion(true, 0, false, false); + metricsPage.expandCollapseRowAssertion(true, 1, true, false); + + cy.log('3.5 Actions > Collapse all query tables'); + metricsPage.clickActionsExpandCollapseAllQuery(false); + + cy.log('3.6 All queries collapsed'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'false'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'false'); + + cy.log('3.6.1 Assert 2 rows - Empty state'); + metricsPage.expandCollapseAllQueryAssertion(false); + metricsPage.expandCollapseRowAssertion(false, 0, false, false); + metricsPage.expandCollapseRowAssertion(false, 1, true, false); + + cy.log('3.7 Actions > Expand all query tables'); + metricsPage.clickActionsExpandCollapseAllQuery(true); + + cy.log('3.8 All queries expanded'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); + + cy.log('3.8.1 Assert 2 rows'); + metricsPage.expandCollapseAllQueryAssertion(true); + metricsPage.expandCollapseRowAssertion(true, 0, false, false); + metricsPage.expandCollapseRowAssertion(true, 1, true, false); + + cy.log('3.9 Actions > Delete all queries'); + metricsPage.clickActionsDeleteAllQueries(); + + cy.log('3.10 Only one query deleted, resulting in 1 row'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); + metricsPage.shouldBeLoaded(); + + }); + + it(`${perspective.name} perspective - Metrics > Insert Example Query`, () => { + cy.log('4.1 Insert Example Query'); + metricsPage.clickInsertExampleQuery(); + metricsPage.shouldBeLoadedWithGraph(); + cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); + metricsPage.graphAxisXAssertion(GraphTimespan.THIRTY_MINUTES); + + cy.log('4.2 Graph Timespan Dropdown'); + metricsPage.clickActionsDeleteAllQueries(); + metricsPage.enterQueryInput(0, MetricsPageQueryInput.VECTOR_QUERY); + metricsPage.clickRunQueriesButton(); + metricsPage.graphTimespanDropdownAssertion(); + + cy.log('4.3 Select and Assert each timespan'); + Object.values(GraphTimespan).forEach((timespan) => { + metricsPage.clickGraphTimespanDropdown(timespan); + metricsPage.graphAxisXAssertion(timespan); + }); + + cy.log('4.4 Enter Graph Timespan'); + metricsPage.clickActionsDeleteAllQueries(); + metricsPage.enterQueryInput(0, MetricsPageQueryInput.VECTOR_QUERY); + metricsPage.clickRunQueriesButton(); + Object.values(GraphTimespan).forEach((timespan) => { + metricsPage.enterGraphTimespan(timespan); + metricsPage.graphAxisXAssertion(timespan); + }); + + cy.log('4.5 Prepare to test Reset Zoom Button'); + metricsPage.clickActionsDeleteAllQueries(); + metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.CPU_USAGE); + metricsPage.clickGraphTimespanDropdown(GraphTimespan.ONE_WEEK); + + cy.log('4.6 Reset Zoom Button'); + metricsPage.clickResetZoomButton(); + cy.byTestID(DataTestIDs.MetricGraphTimespanInput).should('have.attr', 'value', GraphTimespan.THIRTY_MINUTES); + + cy.log('4.7 Hide Graph Button'); + metricsPage.clickHideGraphButton(); + cy.byTestID(DataTestIDs.MetricGraph).should('not.exist'); + + cy.log('4.8 Show Graph Button'); + metricsPage.clickShowGraphButton(); + cy.byTestID(DataTestIDs.MetricGraph).should('be.visible'); + + cy.log('4.9 Disconnected Checkbox'); + cy.byTestID(DataTestIDs.MetricDisconnectedCheckbox).should('be.visible'); + + cy.log('4.10 Prepare to test Stacked Checkbox'); + metricsPage.clickActionsDeleteAllQueries(); + metricsPage.clickInsertExampleQuery(); + + cy.log('4.11 Stacked Checkbox'); + metricsPage.clickStackedCheckboxAndAssert(); + + }); + + //https://issues.redhat.com/browse/OU-974 - [Metrics] - Units - undefined showing in Y axis and tooltip + it(`${perspective.name} perspective - Metrics > Units`, () => { + cy.log('5.1 Preparation to test Units dropdown'); + metricsPage.clickInsertExampleQuery(); + metricsPage.unitsDropdownAssertion(); + + cy.log('5.2 Units dropdown'); + Object.values(MetricsPageUnits).forEach((unit) => { + metricsPage.clickUnitsDropdown(unit); + metricsPage.unitsAxisYAssertion(unit); + }); + }); + +} + diff --git a/web/cypress/support/monitoring/02.reg_metrics.cy.ts b/web/cypress/support/monitoring/02.reg_metrics_2.cy.ts similarity index 64% rename from web/cypress/support/monitoring/02.reg_metrics.cy.ts rename to web/cypress/support/monitoring/02.reg_metrics_2.cy.ts index 8a2c7d718..33cf58461 100644 --- a/web/cypress/support/monitoring/02.reg_metrics.cy.ts +++ b/web/cypress/support/monitoring/02.reg_metrics_2.cy.ts @@ -1,217 +1,21 @@ import { metricsPage } from '../../views/metrics'; -import { Classes, DataTestIDs } from '../../../src/components/data-test'; -import { GraphTimespan, MetricGraphEmptyState, MetricsPagePredefinedQueries, MetricsPageQueryInput, MetricsPageQueryKebabDropdown } from '../../fixtures/monitoring/constants'; +import { Classes, DataTestIDs, LegacyTestIDs } from '../../../src/components/data-test'; +import { MetricGraphEmptyState, MetricsPagePredefinedQueries, MetricsPageQueryInput, MetricsPageQueryKebabDropdown } from '../../fixtures/monitoring/constants'; export interface PerspectiveConfig { name: string; beforeEach?: () => void; } -export function runAllRegressionMetricsTests(perspective: PerspectiveConfig) { - testMetricsRegression(perspective); - testMetricsRegression1(perspective); +export function runAllRegressionMetricsTests2(perspective: PerspectiveConfig) { + testMetricsRegression2(perspective); } -export function testMetricsRegression(perspective: PerspectiveConfig) { - - it(`${perspective.name} perspective - Metrics`, () => { - cy.log('1.1 Metrics page loaded'); - metricsPage.shouldBeLoaded(); - - cy.log('1.2 Units dropdown'); - metricsPage.unitsDropdownAssertion(); - - cy.log('1.3 Refresh interval dropdown'); - metricsPage.refreshIntervalDropdownAssertion(); - - cy.log('1.4 Actions dropdown'); - metricsPage.actionsDropdownAssertion(); - - cy.log('1.5 Predefined queries'); - metricsPage.predefinedQueriesAssertion(); - - cy.log('1.6 Kebab dropdown'); - metricsPage.kebabDropdownAssertionWithoutQuery(); - - }); - - it(`${perspective.name} perspective - Metrics > Actions - No query added`, () => { - cy.log('2.1 Only one query loaded'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); - - cy.log('2.2 Actions >Add query'); - metricsPage.clickActionsAddQuery(); - - cy.log('2.3 Only one query added, resulting in 2 rows'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); - - cy.log('2.3.1 Assert 2 rows - Empty state'); - metricsPage.addQueryAssertion(); - metricsPage.expandCollapseAllQueryAssertion(true); - metricsPage.expandCollapseRowAssertion(true, 1, false, false); - - cy.log('2.4 Actions > Collapse all query tables'); - metricsPage.clickActionsExpandCollapseAllQuery(false); - - cy.log('2.5 All queries collapsed'); - metricsPage.expandCollapseAllQueryAssertion(false); - metricsPage.expandCollapseRowAssertion(false, 0, false, false); - metricsPage.expandCollapseRowAssertion(false, 1, false, false); - - cy.log('2.6 Actions > Expand all query tables'); - metricsPage.clickActionsExpandCollapseAllQuery(true); - - cy.log('2.7 All queries expanded'); - metricsPage.expandCollapseAllQueryAssertion(true); - metricsPage.shouldBeLoaded(); - - cy.log('2.8 Actions > Delete all queries'); - metricsPage.clickActionsDeleteAllQueries(); - - cy.log('2.9 Only one query deleted, resulting in 1 row'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - - }); - - it(`${perspective.name} perspective - Metrics > Actions - One query added`, () => { - cy.log('3.1 Only one query loaded'); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.FILESYSTEM_USAGE); - metricsPage.shouldBeLoadedWithGraph(); - - cy.log('3.2 Kebab dropdown'); - metricsPage.kebabDropdownAssertionWithQuery(); - - cy.log('3.3 Actions >Add query'); - metricsPage.clickActionsAddQuery(); - - cy.log('3.4 Only one query added, resulting in 2 rows'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); - - cy.log('3.4.1 Assert 2 rows'); - metricsPage.expandCollapseAllQueryAssertion(true); - metricsPage.expandCollapseRowAssertion(true, 0, false, false); - metricsPage.expandCollapseRowAssertion(true, 1, true, false); - - cy.log('3.5 Actions > Collapse all query tables'); - metricsPage.clickActionsExpandCollapseAllQuery(false); - - cy.log('3.6 All queries collapsed'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'false'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'false'); - - cy.log('3.6.1 Assert 2 rows - Empty state'); - metricsPage.expandCollapseAllQueryAssertion(false); - metricsPage.expandCollapseRowAssertion(false, 0, false, false); - metricsPage.expandCollapseRowAssertion(false, 1, true, false); - - cy.log('3.7 Actions > Expand all query tables'); - metricsPage.clickActionsExpandCollapseAllQuery(true); - - cy.log('3.8 All queries expanded'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); - - cy.log('3.8.1 Assert 2 rows'); - metricsPage.expandCollapseAllQueryAssertion(true); - metricsPage.expandCollapseRowAssertion(true, 0, false, false); - metricsPage.expandCollapseRowAssertion(true, 1, true, false); - - cy.log('3.9 Actions > Delete all queries'); - metricsPage.clickActionsDeleteAllQueries(); - - cy.log('3.10 Only one query deleted, resulting in 1 row'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - metricsPage.shouldBeLoaded(); - - }); - - it(`${perspective.name} perspective - Metrics > Insert Example Query`, () => { - cy.log('4.1 Insert Example Query'); - metricsPage.clickInsertExampleQuery(); - metricsPage.shouldBeLoadedWithGraph(); - cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); - metricsPage.graphAxisXAssertion(GraphTimespan.THIRTY_MINUTES); - - cy.log('4.2 Graph Timespan Dropdown'); - metricsPage.clickActionsDeleteAllQueries(); - metricsPage.enterQueryInput(0, MetricsPageQueryInput.VECTOR_QUERY); - metricsPage.clickRunQueriesButton(); - metricsPage.graphTimespanDropdownAssertion(); - - cy.log('4.3 Select and Assert each timespan'); - Object.values(GraphTimespan).forEach((timespan) => { - metricsPage.clickGraphTimespanDropdown(timespan); - metricsPage.graphAxisXAssertion(timespan); - }); - - cy.log('4.4 Enter Graph Timespan'); - metricsPage.clickActionsDeleteAllQueries(); - metricsPage.enterQueryInput(0, MetricsPageQueryInput.VECTOR_QUERY); - metricsPage.clickRunQueriesButton(); - Object.values(GraphTimespan).forEach((timespan) => { - metricsPage.enterGraphTimespan(timespan); - metricsPage.graphAxisXAssertion(timespan); - }); - - cy.log('4.5 Prepare to test Reset Zoom Button'); - metricsPage.clickActionsDeleteAllQueries(); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.CPU_USAGE); - metricsPage.graphCardInlineInfoAssertion(true); - metricsPage.clickGraphTimespanDropdown(GraphTimespan.ONE_WEEK); - metricsPage.graphCardInlineInfoAssertion(false); - - cy.log('4.6 Reset Zoom Button'); - metricsPage.clickResetZoomButton(); - metricsPage.graphCardInlineInfoAssertion(true); - - cy.log('4.7 Hide Graph Button'); - metricsPage.clickHideGraphButton(); - cy.byTestID(DataTestIDs.MetricGraph).should('not.exist'); - - cy.log('4.8 Show Graph Button'); - metricsPage.clickShowGraphButton(); - cy.byTestID(DataTestIDs.MetricGraph).should('be.visible'); - - cy.log('4.9 Stacked Checkbox'); - cy.byTestID(DataTestIDs.MetricStackedCheckbox).should('not.exist'); - - cy.log('4.10 Disconnected Checkbox'); - cy.byTestID(DataTestIDs.MetricDisconnectedCheckbox).should('be.visible'); - - cy.log('4.11 Prepare to test Stacked Checkbox'); - metricsPage.clickActionsDeleteAllQueries(); - metricsPage.clickInsertExampleQuery(); - - cy.log('4.12 Stacked Checkbox'); - metricsPage.clickStackedCheckboxAndAssert(); - - }); - - /** - * TODO: uncomment when this bug gets fixed - * https://issues.redhat.com/browse/OU-974 - [Metrics] - Units - undefined showing in Y axis and tooltip - it(`${perspective.name} perspective - Metrics > Units`, () => { - cy.log('5.1 Preparation to test Units dropdown'); - cy.visit('/monitoring/query-browser'); - metricsPage.clickInsertExampleQuery(); - metricsPage.unitsDropdownAssertion(); - - cy.log('5.2 Units dropdown'); - Object.values(MetricsPageUnits).forEach((unit) => { - metricsPage.clickUnitsDropdown(unit); - metricsPage.unitsAxisYAssertion(unit); - }); - }); - */ -} -export function testMetricsRegression1(perspective: PerspectiveConfig) { +export function testMetricsRegression2(perspective: PerspectiveConfig) { it(`${perspective.name} perspective - Metrics > Add Query - Run Queries - Kebab icon`, () => { cy.log('6.1 Preparation to test Add Query button'); + metricsPage.clickActionsDeleteAllQueries(); cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); metricsPage.clickInsertExampleQuery(); cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); @@ -461,23 +265,8 @@ export function testMetricsRegression1(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('namespace-bar-dropdown').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'); @@ -491,7 +280,7 @@ export function testMetricsRegression1(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'); diff --git a/web/cypress/support/monitoring/03.reg_legacy_dashboards.cy.ts b/web/cypress/support/monitoring/03.reg_legacy_dashboards.cy.ts index cbf5cba4f..9ae86762a 100644 --- a/web/cypress/support/monitoring/03.reg_legacy_dashboards.cy.ts +++ b/web/cypress/support/monitoring/03.reg_legacy_dashboards.cy.ts @@ -1,6 +1,6 @@ import { nav } from '../../views/nav'; import { legacyDashboardsPage } from '../../views/legacy-dashboards'; -import { API_PERFORMANCE_DASHBOARD_PANELS, LegacyDashboardsDashboardDropdown, MetricsPageQueryInput, WatchdogAlert } from '../../fixtures/monitoring/constants'; +import { LegacyDashboardsDashboardDropdown, MetricsPageQueryInput, WatchdogAlert } from '../../fixtures/monitoring/constants'; import { Classes, LegacyDashboardPageTestIDs, DataTestIDs } from '../../../src/components/data-test'; import { metricsPage } from '../../views/metrics'; import { alertingRuleDetailsPage } from '../../views/alerting-rule-details-page'; @@ -32,10 +32,10 @@ export function testLegacyDashboardsRegression(perspective: PerspectiveConfig) { cy.log('1.4 Dashboard dropdown'); legacyDashboardsPage.dashboardDropdownAssertion(LegacyDashboardsDashboardDropdown); + legacyDashboardsPage.clickDashboardDropdown('API_PERFORMANCE'); + cy.log('1.5 Dashboard API Performance panels'); - for (const panel of Object.values(API_PERFORMANCE_DASHBOARD_PANELS)) { - legacyDashboardsPage.dashboardAPIPerformancePanelAssertion(panel); - } + legacyDashboardsPage.dashboardAPIPerformancePanelAssertion(); cy.log('1.6 Inspect - API Request Duration by Verb - 99th Percentile'); cy.byTestID(LegacyDashboardPageTestIDs.Inspect).eq(0).scrollIntoView().should('be.visible').click(); diff --git a/web/cypress/support/monitoring/04.reg_alerts_namespace.cy.ts b/web/cypress/support/monitoring/04.reg_alerts_namespace.cy.ts index 5a749d625..14dbc08b2 100644 --- a/web/cypress/support/monitoring/04.reg_alerts_namespace.cy.ts +++ b/web/cypress/support/monitoring/04.reg_alerts_namespace.cy.ts @@ -49,8 +49,8 @@ export function testAlertsRegressionNamespace(perspective: PerspectiveConfig) { cy.log('2.1 use sidebar nav to go to Observe > Alerting'); nav.tabs.switchTab('Silences'); silencesListPage.createSilence(); - commonPages.projectDropdownShouldExist(); - silenceAlertPage.assertNamespaceLabelNamespaceValueDisabled('namespace', `${WatchdogAlert.NAMESPACE}`, true); + cy.log('https://issues.redhat.com/browse/OU-1109 - [Namespace-level] - Dev user - Create a silence - namespace label does not have a value'); + silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('namespace', `${WatchdogAlert.NAMESPACE}`, false, false); silenceAlertPage.assertCommentNoError(); silenceAlertPage.clickSubmit(); silenceAlertPage.assertCommentWithError(); @@ -126,13 +126,12 @@ export function testAlertsRegressionNamespace(perspective: PerspectiveConfig) { cy.log('3.10 Recreate silence'); silenceDetailsPage.recreateSilence(false); commonPages.titleShouldHaveText('Recreate silence'); - commonPages.projectDropdownShouldExist(); + commonPages.projectDropdownShouldNotExist(); silenceAlertPage.silenceAlertSectionDefault(); silenceAlertPage.durationSectionDefault(); silenceAlertPage.alertLabelsSectionDefault(); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('alertname', `${WatchdogAlert.ALERTNAME}`, false, false); - silenceAlertPage.assertNamespaceLabelNamespaceValueDisabled('namespace', `${WatchdogAlert.NAMESPACE}`, true); - // silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('severity', `${SEVERITY}`, false, false); + cy.log('https://issues.redhat.com/browse/OU-1109 - [Namespace-level] - Dev user - Create a silence - namespace label does not have a value'); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('namespace', `${WatchdogAlert.NAMESPACE}`, false, false); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('prometheus', 'openshift-monitoring/k8s', false, false); silenceAlertPage.clickSubmit(); @@ -146,12 +145,13 @@ export function testAlertsRegressionNamespace(perspective: PerspectiveConfig) { silencesListPage.filter.byName( `${WatchdogAlert.ALERTNAME}`); silencesListPage.rows.editSilence(); commonPages.titleShouldHaveText('Edit silence'); + commonPages.projectDropdownShouldNotExist(); silenceAlertPage.silenceAlertSectionDefault(); silenceAlertPage.editAlertWarning(); silenceAlertPage.editDurationSectionDefault(); silenceAlertPage.alertLabelsSectionDefault(); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('alertname', `${WatchdogAlert.ALERTNAME}`, false, false); - // silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('severity', `${SEVERITY}`, false, false); + cy.log('https://issues.redhat.com/browse/OU-1109 - [Namespace-level] - Dev user - Create a silence - namespace label does not have a value'); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('namespace', `${WatchdogAlert.NAMESPACE}`, false, false); silenceAlertPage.assertLabelNameLabelValueRegExNegMatcher('prometheus', 'openshift-monitoring/k8s', false, false); silenceAlertPage.clickSubmit(); @@ -183,6 +183,7 @@ export function testAlertsRegressionNamespace(perspective: PerspectiveConfig) { listPage.filter.byName(`${WatchdogAlert.ALERTNAME}`); listPage.ARRows.countShouldBe(1); }); + it(`${perspective.name} perspective - Alerting > Alerting Rules`, () => { cy.log('4.1 use sidebar nav to go to Observe > Alerting'); @@ -190,7 +191,6 @@ export function testAlertsRegressionNamespace(perspective: PerspectiveConfig) { alertingRuleListPage.shouldBeLoaded(); cy.log('4.2 clear all filters, verify filters and tags'); - // listPage.filter.clearAllFilters('alerting-rules'); listPage.filter.selectFilterOption(true, AlertingRulesAlertState.FIRING, false); listPage.filter.selectFilterOption(false, AlertingRulesAlertState.PENDING, false); listPage.filter.selectFilterOption(false, AlertingRulesAlertState.SILENCED, false); diff --git a/web/cypress/support/monitoring/05.reg_metrics_namespace_1.cy.ts b/web/cypress/support/monitoring/05.reg_metrics_namespace_1.cy.ts new file mode 100644 index 000000000..b9401deb6 --- /dev/null +++ b/web/cypress/support/monitoring/05.reg_metrics_namespace_1.cy.ts @@ -0,0 +1,200 @@ +import { metricsPage } from '../../views/metrics'; +import { Classes, DataTestIDs } from '../../../src/components/data-test'; +import { MetricsPageUnits, GraphTimespan, MetricsPagePredefinedQueries, MetricsPageQueryInput, MetricsPageQueryKebabDropdown, MetricsPageQueryInputByNamespace } from '../../fixtures/monitoring/constants'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runAllRegressionMetricsTestsNamespace1(perspective: PerspectiveConfig) { + testMetricsRegressionNamespace1(perspective); + +} + +export function testMetricsRegressionNamespace1(perspective: PerspectiveConfig) { + it(`${perspective.name} perspective - Metrics`, () => { + cy.log('1.1 Metrics page loaded'); + metricsPage.shouldBeLoaded(); + + cy.log('1.2 Units dropdown'); + metricsPage.unitsDropdownAssertion(); + + cy.log('1.3 Refresh interval dropdown'); + metricsPage.refreshIntervalDropdownAssertion(); + + cy.log('1.4 Actions dropdown'); + metricsPage.actionsDropdownAssertion(); + + cy.log('1.5 Predefined queries'); + metricsPage.predefinedQueriesAssertion(); + + cy.log('1.6 Kebab dropdown'); + metricsPage.kebabDropdownAssertionWithoutQuery(); + + }); + + it(`${perspective.name} perspective - Metrics > Actions - No query added`, () => { + cy.log('2.1 Only one query loaded'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); + + cy.log('2.2 Actions >Add query'); + metricsPage.clickActionsAddQuery(); + + cy.log('2.3 Only one query added, resulting in 2 rows'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); + + cy.log('2.3.1 Assert 2 rows - Empty state'); + metricsPage.addQueryAssertion(); + metricsPage.expandCollapseAllQueryAssertion(true); + metricsPage.expandCollapseRowAssertion(true, 1, false, false); + + cy.log('2.4 Actions > Collapse all query tables'); + metricsPage.clickActionsExpandCollapseAllQuery(false); + + cy.log('2.5 All queries collapsed'); + metricsPage.expandCollapseAllQueryAssertion(false); + metricsPage.expandCollapseRowAssertion(false, 0, false, false); + metricsPage.expandCollapseRowAssertion(false, 1, false, false); + + cy.log('2.6 Actions > Expand all query tables'); + metricsPage.clickActionsExpandCollapseAllQuery(true); + + cy.log('2.7 All queries expanded'); + metricsPage.expandCollapseAllQueryAssertion(true); + metricsPage.shouldBeLoaded(); + + cy.log('2.8 Actions > Delete all queries'); + metricsPage.clickActionsDeleteAllQueries(); + + cy.log('2.9 Only one query deleted, resulting in 1 row'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); + + }); + + it(`${perspective.name} perspective - Metrics > Actions - One query added`, () => { + cy.log('3.1 Only one query loaded'); + metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.FILESYSTEM_USAGE); + metricsPage.shouldBeLoadedWithGraph(); + + cy.log('3.2 Kebab dropdown'); + metricsPage.kebabDropdownAssertionWithQuery(); + + cy.log('3.3 Actions >Add query'); + metricsPage.clickActionsAddQuery(); + + cy.log('3.4 Only one query added, resulting in 2 rows'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); + + cy.log('3.4.1 Assert 2 rows'); + metricsPage.expandCollapseAllQueryAssertion(true); + metricsPage.expandCollapseRowAssertion(true, 0, false, false); + metricsPage.expandCollapseRowAssertion(true, 1, true, false); + + cy.log('3.5 Actions > Collapse all query tables'); + metricsPage.clickActionsExpandCollapseAllQuery(false); + + cy.log('3.6 All queries collapsed'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'false'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'false'); + + cy.log('3.6.1 Assert 2 rows - Empty state'); + metricsPage.expandCollapseAllQueryAssertion(false); + metricsPage.expandCollapseRowAssertion(false, 0, false, false); + metricsPage.expandCollapseRowAssertion(false, 1, true, false); + + cy.log('3.7 Actions > Expand all query tables'); + metricsPage.clickActionsExpandCollapseAllQuery(true); + + cy.log('3.8 All queries expanded'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); + + cy.log('3.8.1 Assert 2 rows'); + metricsPage.expandCollapseAllQueryAssertion(true); + metricsPage.expandCollapseRowAssertion(true, 0, false, false); + metricsPage.expandCollapseRowAssertion(true, 1, true, false); + + cy.log('3.9 Actions > Delete all queries'); + metricsPage.clickActionsDeleteAllQueries(); + + cy.log('3.10 Only one query deleted, resulting in 1 row'); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); + cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); + metricsPage.shouldBeLoaded(); + + }); + + it(`${perspective.name} perspective - Metrics > Insert Example Query`, () => { + cy.log('4.1 Insert Example Query'); + metricsPage.clickInsertExampleQuery(); + metricsPage.shouldBeLoadedWithGraph(); + cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); + metricsPage.graphAxisXAssertion(GraphTimespan.THIRTY_MINUTES); + + cy.log('4.2 Graph Timespan Dropdown'); + metricsPage.clickActionsDeleteAllQueries(); + metricsPage.enterQueryInput(0, MetricsPageQueryInput.VECTOR_QUERY); + metricsPage.clickRunQueriesButton(); + metricsPage.graphTimespanDropdownAssertion(); + + cy.log('4.3 Select and Assert each timespan'); + Object.values(GraphTimespan).forEach((timespan) => { + metricsPage.clickGraphTimespanDropdown(timespan); + metricsPage.graphAxisXAssertion(timespan); + }); + + cy.log('4.4 Enter Graph Timespan'); + metricsPage.clickActionsDeleteAllQueries(); + metricsPage.enterQueryInput(0, MetricsPageQueryInput.VECTOR_QUERY); + metricsPage.clickRunQueriesButton(); + Object.values(GraphTimespan).forEach((timespan) => { + metricsPage.enterGraphTimespan(timespan); + metricsPage.graphAxisXAssertion(timespan); + }); + + cy.log('4.5 Prepare to test Reset Zoom Button'); + metricsPage.clickActionsDeleteAllQueries(); + metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.RATE_OF_TRANSMITTED_PACKETS_DROPPED); + metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.RATE_OF_RECEIVED_PACKETS_DROPPED); + metricsPage.clickGraphTimespanDropdown(GraphTimespan.ONE_WEEK); + + cy.log('4.6 Reset Zoom Button'); + metricsPage.clickResetZoomButton(); + cy.byTestID(DataTestIDs.MetricGraphTimespanInput).should('have.attr', 'value', GraphTimespan.THIRTY_MINUTES); + + cy.log('4.7 Hide Graph Button'); + metricsPage.clickHideGraphButton(); + cy.byTestID(DataTestIDs.MetricGraph).should('not.exist'); + + cy.log('4.8 Show Graph Button'); + metricsPage.clickShowGraphButton(); + cy.byTestID(DataTestIDs.MetricGraph).should('be.visible'); + + cy.log('4.9 Disconnected Checkbox'); + cy.byTestID(DataTestIDs.MetricDisconnectedCheckbox).should('be.visible'); + + cy.log('4.10 Prepare to test Stacked Checkbox'); + metricsPage.clickActionsDeleteAllQueries(); + metricsPage.clickInsertExampleQuery(); + + cy.log('4.11 Stacked Checkbox'); + metricsPage.clickStackedCheckboxAndAssert(); + }); + + //https://issues.redhat.com/browse/OU-974 - [Metrics] - Units - undefined showing in Y axis and tooltip + it(`${perspective.name} perspective - Metrics > Units`, () => { + cy.log('5.1 Preparation to test Units dropdown'); + metricsPage.clickInsertExampleQuery(); + metricsPage.unitsDropdownAssertion(); + + cy.log('5.2 Units dropdown'); + Object.values(MetricsPageUnits).forEach((unit) => { + metricsPage.clickUnitsDropdown(unit); + metricsPage.unitsAxisYAssertion(unit); + }); + }); +} diff --git a/web/cypress/support/monitoring/05.reg_metrics_namespace.cy.ts b/web/cypress/support/monitoring/05.reg_metrics_namespace_2.cy.ts similarity index 64% rename from web/cypress/support/monitoring/05.reg_metrics_namespace.cy.ts rename to web/cypress/support/monitoring/05.reg_metrics_namespace_2.cy.ts index efb58631c..c567a36be 100644 --- a/web/cypress/support/monitoring/05.reg_metrics_namespace.cy.ts +++ b/web/cypress/support/monitoring/05.reg_metrics_namespace_2.cy.ts @@ -1,221 +1,24 @@ -import { nav } from '../../views/nav'; import { metricsPage } from '../../views/metrics'; -import { Classes, DataTestIDs, IDs } from '../../../src/components/data-test'; -import { GraphTimespan, MetricGraphEmptyState, MetricsPagePredefinedQueries, MetricsPageQueryInput, MetricsPageQueryKebabDropdown, MetricsPageQueryInputByNamespace } from '../../fixtures/monitoring/constants'; +import { Classes, DataTestIDs } from '../../../src/components/data-test'; +import { MetricsPageUnits, GraphTimespan, MetricsPagePredefinedQueries, MetricsPageQueryInput, MetricsPageQueryKebabDropdown, MetricsPageQueryInputByNamespace } from '../../fixtures/monitoring/constants'; export interface PerspectiveConfig { name: string; beforeEach?: () => void; } -export function runAllRegressionMetricsTestsNamespace(perspective: PerspectiveConfig) { - testMetricsRegressionNamespace(perspective); - testMetricsRegressionNamespace1(perspective); +export function runAllRegressionMetricsTestsNamespace2(perspective: PerspectiveConfig) { + testMetricsRegressionNamespace2(perspective); } -export function testMetricsRegressionNamespace(perspective: PerspectiveConfig) { - it(`${perspective.name} perspective - Metrics`, () => { - cy.log('1.1 Metrics page loaded'); - metricsPage.shouldBeLoaded(); - - cy.log('1.2 Units dropdown'); - metricsPage.unitsDropdownAssertion(); - - cy.log('1.3 Refresh interval dropdown'); - metricsPage.refreshIntervalDropdownAssertion(); - - cy.log('1.4 Actions dropdown'); - metricsPage.actionsDropdownAssertion(); - - cy.log('1.5 Predefined queries'); - metricsPage.predefinedQueriesAssertion(); - - cy.log('1.6 Kebab dropdown'); - metricsPage.kebabDropdownAssertionWithoutQuery(); - - }); - - it(`${perspective.name} perspective - Metrics > Actions - No query added`, () => { - cy.log('2.1 Only one query loaded'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); - - cy.log('2.2 Actions >Add query'); - metricsPage.clickActionsAddQuery(); - - cy.log('2.3 Only one query added, resulting in 2 rows'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); - - cy.log('2.3.1 Assert 2 rows - Empty state'); - metricsPage.addQueryAssertion(); - metricsPage.expandCollapseAllQueryAssertion(true); - metricsPage.expandCollapseRowAssertion(true, 1, false, false); - - cy.log('2.4 Actions > Collapse all query tables'); - metricsPage.clickActionsExpandCollapseAllQuery(false); - - cy.log('2.5 All queries collapsed'); - metricsPage.expandCollapseAllQueryAssertion(false); - metricsPage.expandCollapseRowAssertion(false, 0, false, false); - metricsPage.expandCollapseRowAssertion(false, 1, false, false); - - cy.log('2.6 Actions > Expand all query tables'); - metricsPage.clickActionsExpandCollapseAllQuery(true); - - cy.log('2.7 All queries expanded'); - metricsPage.expandCollapseAllQueryAssertion(true); - metricsPage.shouldBeLoaded(); - - cy.log('2.8 Actions > Delete all queries'); - metricsPage.clickActionsDeleteAllQueries(); - - cy.log('2.9 Only one query deleted, resulting in 1 row'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - - }); - - it(`${perspective.name} perspective - Metrics > Actions - One query added`, () => { - cy.log('3.1 Only one query loaded'); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.FILESYSTEM_USAGE); - metricsPage.shouldBeLoadedWithGraph(); - - cy.log('3.2 Kebab dropdown'); - metricsPage.kebabDropdownAssertionWithQuery(); - - cy.log('3.3 Actions >Add query'); - metricsPage.clickActionsAddQuery(); - - cy.log('3.4 Only one query added, resulting in 2 rows'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); - - cy.log('3.4.1 Assert 2 rows'); - metricsPage.expandCollapseAllQueryAssertion(true); - metricsPage.expandCollapseRowAssertion(true, 0, false, false); - metricsPage.expandCollapseRowAssertion(true, 1, true, false); - - cy.log('3.5 Actions > Collapse all query tables'); - metricsPage.clickActionsExpandCollapseAllQuery(false); - - cy.log('3.6 All queries collapsed'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'false'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'false'); - - cy.log('3.6.1 Assert 2 rows - Empty state'); - metricsPage.expandCollapseAllQueryAssertion(false); - metricsPage.expandCollapseRowAssertion(false, 0, false, false); - metricsPage.expandCollapseRowAssertion(false, 1, true, false); - - cy.log('3.7 Actions > Expand all query tables'); - metricsPage.clickActionsExpandCollapseAllQuery(true); - - cy.log('3.8 All queries expanded'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); - - cy.log('3.8.1 Assert 2 rows'); - metricsPage.expandCollapseAllQueryAssertion(true); - metricsPage.expandCollapseRowAssertion(true, 0, false, false); - metricsPage.expandCollapseRowAssertion(true, 1, true, false); - - cy.log('3.9 Actions > Delete all queries'); - metricsPage.clickActionsDeleteAllQueries(); - - cy.log('3.10 Only one query deleted, resulting in 1 row'); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); - cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - metricsPage.shouldBeLoaded(); - - }); - - it(`${perspective.name} perspective - Metrics > Insert Example Query`, () => { - cy.log('4.1 Insert Example Query'); - metricsPage.clickInsertExampleQuery(); - metricsPage.shouldBeLoadedWithGraph(); - cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); - metricsPage.graphAxisXAssertion(GraphTimespan.THIRTY_MINUTES); - - cy.log('4.2 Graph Timespan Dropdown'); - metricsPage.clickActionsDeleteAllQueries(); - metricsPage.enterQueryInput(0, MetricsPageQueryInput.VECTOR_QUERY); - metricsPage.clickRunQueriesButton(); - metricsPage.graphTimespanDropdownAssertion(); - - cy.log('4.3 Select and Assert each timespan'); - Object.values(GraphTimespan).forEach((timespan) => { - metricsPage.clickGraphTimespanDropdown(timespan); - metricsPage.graphAxisXAssertion(timespan); - }); - - cy.log('4.4 Enter Graph Timespan'); - metricsPage.clickActionsDeleteAllQueries(); - metricsPage.enterQueryInput(0, MetricsPageQueryInput.VECTOR_QUERY); - metricsPage.clickRunQueriesButton(); - Object.values(GraphTimespan).forEach((timespan) => { - metricsPage.enterGraphTimespan(timespan); - metricsPage.graphAxisXAssertion(timespan); - }); - - cy.log('4.5 Prepare to test Reset Zoom Button'); - metricsPage.clickActionsDeleteAllQueries(); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.RATE_OF_TRANSMITTED_PACKETS_DROPPED); - metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.RATE_OF_RECEIVED_PACKETS_DROPPED); - metricsPage.graphCardInlineInfoAssertion(true); - metricsPage.clickGraphTimespanDropdown(GraphTimespan.ONE_WEEK); - metricsPage.graphCardInlineInfoAssertion(false); - - cy.log('4.6 Reset Zoom Button'); - metricsPage.clickResetZoomButton(); - metricsPage.graphCardInlineInfoAssertion(true); - - cy.log('4.7 Hide Graph Button'); - metricsPage.clickHideGraphButton(); - cy.byTestID(DataTestIDs.MetricGraph).should('not.exist'); - - cy.log('4.8 Show Graph Button'); - metricsPage.clickShowGraphButton(); - cy.byTestID(DataTestIDs.MetricGraph).should('be.visible'); - - cy.log('4.9 Stacked Checkbox'); - cy.byTestID(DataTestIDs.MetricStackedCheckbox).should('be.visible'); - - cy.log('4.10 Disconnected Checkbox'); - cy.byTestID(DataTestIDs.MetricDisconnectedCheckbox).should('be.visible'); - - cy.log('4.11 Prepare to test Stacked Checkbox'); - metricsPage.clickActionsDeleteAllQueries(); - metricsPage.clickInsertExampleQuery(); - - cy.log('4.12 Stacked Checkbox'); - metricsPage.clickStackedCheckboxAndAssert(); - }); - - /** - * TODO: uncomment when this bug gets fixed - * https://issues.redhat.com/browse/OU-974 - [Metrics] - Units - undefined showing in Y axis and tooltip - it(`${perspective.name} perspective - Metrics > Units`, () => { - cy.log('5.1 Preparation to test Units dropdown'); - cy.visit('/monitoring/query-browser'); - metricsPage.clickInsertExampleQuery(); - metricsPage.unitsDropdownAssertion(); - - cy.log('5.2 Units dropdown'); - Object.values(MetricsPageUnits).forEach((unit) => { - metricsPage.clickUnitsDropdown(unit); - metricsPage.unitsAxisYAssertion(unit); - }); - }); - */ -} -export function testMetricsRegressionNamespace1(perspective: PerspectiveConfig) { +export function testMetricsRegressionNamespace2(perspective: PerspectiveConfig) { it(`${perspective.name} perspective - Metrics > Add Query - Run Queries - Kebab icon`, () => { cy.log('6.1 Preparation to test Add Query button'); metricsPage.shouldBeLoaded(); cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); metricsPage.clickInsertExampleQuery(); - cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); + cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); cy.log('6.2 Only one query added, resulting in 2 rows'); metricsPage.clickActionsAddQuery(); @@ -223,7 +26,7 @@ export function testMetricsRegressionNamespace1(perspective: PerspectiveConfig) cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(1).should('have.attr', 'aria-expanded', 'true'); cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.EXPRESSION_PRESS_SHIFT_ENTER_FOR_NEWLINES); - cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); + cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); cy.log('6.3 Preparation to test Run Queries button'); cy.get(Classes.MetricsPageQueryInput).eq(0).should('be.visible').clear(); @@ -248,8 +51,8 @@ export function testMetricsRegressionNamespace1(perspective: PerspectiveConfig) metricsPage.expandCollapseRowAssertion(false, 0, true, true); metricsPage.expandCollapseRowAssertion(true, 1, true, true); cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.VECTOR_QUERY); - cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); - cy.byTestID(DataTestIDs.MetricGraph).should('be.visible'); + cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); + cy.byTestID(DataTestIDs.MetricGraph).scrollIntoView().should('be.visible'); metricsPage.clickKebabDropdown(0); cy.get(Classes.MenuItemDisabled).contains(MetricsPageQueryKebabDropdown.HIDE_ALL_SERIES).should('be.visible'); cy.byTestID(DataTestIDs.MetricsPageExportCsvDropdownItem).should('not.exist'); @@ -261,8 +64,8 @@ export function testMetricsRegressionNamespace1(perspective: PerspectiveConfig) metricsPage.expandCollapseRowAssertion(true, 0, true, true); metricsPage.expandCollapseRowAssertion(true, 1, true, true); cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.VECTOR_QUERY); - cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); - cy.byTestID(DataTestIDs.MetricGraph).should('be.visible'); + cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); + cy.byTestID(DataTestIDs.MetricGraph).scrollIntoView().should('be.visible'); metricsPage.clickKebabDropdown(0); cy.byTestID(DataTestIDs.MetricsPageHideShowAllSeriesDropdownItem).contains(MetricsPageQueryKebabDropdown.HIDE_ALL_SERIES).should('be.visible'); cy.byTestID(DataTestIDs.MetricsPageExportCsvDropdownItem).contains(MetricsPageQueryKebabDropdown.EXPORT_AS_CSV).should('be.visible'); @@ -287,7 +90,7 @@ export function testMetricsRegressionNamespace1(perspective: PerspectiveConfig) cy.byTestID(DataTestIDs.MetricsPageExportCsvDropdownItem).should('not.exist'); metricsPage.clickKebabDropdown(1); cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.VECTOR_QUERY); - cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); + cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); cy.byTestID(DataTestIDs.MetricGraph).should('not.exist'); cy.byTestID(DataTestIDs.MetricsPageNoQueryEnteredTitle).should('be.visible'); cy.byTestID(DataTestIDs.MetricsPageNoQueryEntered).should('be.visible'); @@ -312,7 +115,7 @@ export function testMetricsRegressionNamespace1(perspective: PerspectiveConfig) cy.byTestID(DataTestIDs.MetricsPageExportCsvDropdownItem).contains(MetricsPageQueryKebabDropdown.EXPORT_AS_CSV).should('be.visible'); metricsPage.clickKebabDropdown(1); cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.VECTOR_QUERY); - cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); + cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); cy.byTestID(DataTestIDs.MetricGraph).scrollIntoView().should('be.visible'); cy.log('6.10 Kebab icon - Hide all series'); @@ -385,14 +188,14 @@ export function testMetricsRegressionNamespace1(perspective: PerspectiveConfig) cy.byTestID(DataTestIDs.MetricsPageDeleteQueryDropdownItem).contains(MetricsPageQueryKebabDropdown.DELETE_QUERY).should('be.visible').click(); cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 1); cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); - cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); + cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); cy.byTestID(DataTestIDs.MetricsPageSelectAllUnselectAllButton).should('have.length', 1); cy.log('6.17 Kebab icon - Duplicate query'); metricsPage.clickKebabDropdown(0); cy.byTestID(DataTestIDs.MetricsPageDuplicateQueryDropdownItem).contains(MetricsPageQueryKebabDropdown.DUPLICATE_QUERY).should('be.visible').click(); - cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); - cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY); + cy.get(Classes.MetricsPageQueryInput).eq(0).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); + cy.get(Classes.MetricsPageQueryInput).eq(1).should('contain', MetricsPageQueryInput.INSERT_EXAMPLE_QUERY_NAMESPACE); cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).should('have.length', 2); metricsPage.expandCollapseRowAssertion(true, 1, true, true); cy.byTestID(DataTestIDs.MetricsPageExpandCollapseRowButton).find('button').eq(0).should('have.attr', 'aria-expanded', 'true'); diff --git a/web/cypress/support/monitoring/06.reg_legacy_dashboards_namespace.cy.ts b/web/cypress/support/monitoring/06.reg_legacy_dashboards_namespace.cy.ts index 24c2828d1..4f57241fd 100644 --- a/web/cypress/support/monitoring/06.reg_legacy_dashboards_namespace.cy.ts +++ b/web/cypress/support/monitoring/06.reg_legacy_dashboards_namespace.cy.ts @@ -1,12 +1,13 @@ import { nav } from '../../views/nav'; import { legacyDashboardsPage } from '../../views/legacy-dashboards'; -import { KUBERNETES_COMPUTE_RESOURCES_NAMESPACE_PODS_PANELS, LegacyDashboardsDashboardDropdownNamespace, MetricsPageQueryInput, MetricsPageQueryInputByNamespace, WatchdogAlert } from '../../fixtures/monitoring/constants'; +import { LegacyDashboardsDashboardDropdownNamespace, MetricsPageQueryInputByNamespace, WatchdogAlert } from '../../fixtures/monitoring/constants'; import { Classes, LegacyDashboardPageTestIDs, DataTestIDs } from '../../../src/components/data-test'; import { metricsPage } from '../../views/metrics'; import { alertingRuleDetailsPage } from '../../views/alerting-rule-details-page'; import { alerts } from '../../fixtures/monitoring/alert'; import { listPage } from '../../views/list-page'; import { commonPages } from '../../views/common'; +import { guidedTour } from '../../views/tour'; export interface PerspectiveConfig { name: string; @@ -33,9 +34,7 @@ export function testLegacyDashboardsRegressionNamespace(perspective: Perspective legacyDashboardsPage.dashboardDropdownAssertion(LegacyDashboardsDashboardDropdownNamespace); cy.log('1.5 Dashboard Kubernetes Compute Resources Namespace Pods panels'); - for (const panel of Object.values(KUBERNETES_COMPUTE_RESOURCES_NAMESPACE_PODS_PANELS)) { - legacyDashboardsPage.dashboardKubernetesComputeResourcesNamespacePodsPanelAssertion(panel); - } + legacyDashboardsPage.dashboardKubernetesComputeResourcesNamespacePodsPanelAssertion(); cy.log('1.6 Inspect - CPU Utilisation (from requests)'); cy.byTestID(LegacyDashboardPageTestIDs.Inspect).eq(0).scrollIntoView().should('be.visible').click(); @@ -53,6 +52,7 @@ export function testLegacyDashboardsRegressionNamespace(perspective: Perspective cy.log('2.2 Empty state'); cy.changeNamespace('default'); + legacyDashboardsPage.shouldBeLoaded(); cy.byTestID(DataTestIDs.MetricGraphNoDatapointsFound).eq(0).scrollIntoView().should('be.visible'); legacyDashboardsPage.clickKebabDropdown(0); cy.byTestID(LegacyDashboardPageTestIDs.ExportAsCsv).should('be.visible'); @@ -62,6 +62,13 @@ export function testLegacyDashboardsRegressionNamespace(perspective: Perspective it(`${perspective.name} perspective - Dashboards (legacy) - No kebab dropdown`, () => { cy.log('3.1 Single Stat - No kebab dropdown'); + cy.visit('/'); + guidedTour.close(); + cy.validateLogin(); + nav.sidenav.clickNavLink(['Observe', 'Dashboards']); + commonPages.titleShouldHaveText('Dashboards'); + cy.changeNamespace('openshift-monitoring'); + legacyDashboardsPage.shouldBeLoaded(); cy.byLegacyTestID('chart-1').find('[data-test="'+DataTestIDs.KebabDropdownButton+'"]').should('not.exist'); cy.log('3.2 Table - No kebab dropdown'); diff --git a/web/cypress/support/perses/00.coo_bvt_perses.cy.ts b/web/cypress/support/perses/00.coo_bvt_perses_admin.cy.ts similarity index 74% rename from web/cypress/support/perses/00.coo_bvt_perses.cy.ts rename to web/cypress/support/perses/00.coo_bvt_perses_admin.cy.ts index 28c06d3ef..cee213e00 100644 --- a/web/cypress/support/perses/00.coo_bvt_perses.cy.ts +++ b/web/cypress/support/perses/00.coo_bvt_perses_admin.cy.ts @@ -1,7 +1,7 @@ import { persesDashboardsAcceleratorsCommonMetricsPanels, persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev } from '../../fixtures/perses/constants'; import { commonPages } from '../../views/common'; import { persesDashboardsPage } from '../../views/perses-dashboards'; -import { persesDataTestIDs } from '../../../src/components/data-test'; +import { persesMUIDataTestIDs } from '../../../src/components/data-test'; export interface PerspectiveConfig { name: string; @@ -22,7 +22,7 @@ export function testBVTCOOPerses(perspective: PerspectiveConfig) { persesDashboardsPage.timeRangeDropdownAssertion(); persesDashboardsPage.refreshIntervalDropdownAssertion(); persesDashboardsPage.dashboardDropdownAssertion(persesDashboardsDashboardDropdownCOO); - cy.wait(1000); + cy.wait(2000); cy.changeNamespace('perses-dev'); persesDashboardsPage.dashboardDropdownAssertion(persesDashboardsDashboardDropdownPersesDev); }); @@ -31,8 +31,8 @@ export function testBVTCOOPerses(perspective: PerspectiveConfig) { cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses) > Accelerators common metrics dashboard`); cy.changeNamespace('openshift-cluster-observability-operator'); persesDashboardsPage.clickDashboardDropdown(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0] as keyof typeof persesDashboardsDashboardDropdownCOO); - cy.byDataTestID(persesDataTestIDs.variableDropdown+'-cluster').should('be.visible'); - persesDashboardsPage.panelGroupHeaderAssertion('Accelerators'); + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-cluster').should('be.visible'); + persesDashboardsPage.panelGroupHeaderAssertion('Accelerators', 'Open'); persesDashboardsPage.panelHeadersAcceleratorsCommonMetricsAssertion(); persesDashboardsPage.expandPanel(persesDashboardsAcceleratorsCommonMetricsPanels.GPU_UTILIZATION); persesDashboardsPage.collapsePanel(persesDashboardsAcceleratorsCommonMetricsPanels.GPU_UTILIZATION); @@ -42,16 +42,16 @@ export function testBVTCOOPerses(perspective: PerspectiveConfig) { cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses) > Perses Dashboard Sample dashboard`); cy.changeNamespace('perses-dev'); persesDashboardsPage.clickDashboardDropdown(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0] as keyof typeof persesDashboardsDashboardDropdownPersesDev); - cy.byDataTestID(persesDataTestIDs.variableDropdown+'-job').should('be.visible'); - cy.byDataTestID(persesDataTestIDs.variableDropdown+'-instance').should('be.visible'); - cy.byDataTestID(persesDataTestIDs.variableDropdown+'-interval').should('be.visible'); - cy.byDataTestID(persesDataTestIDs.variableDropdown+'-text').should('be.visible'); - persesDashboardsPage.panelGroupHeaderAssertion('Row 1'); - persesDashboardsPage.expandPanel('RAM Used'); - persesDashboardsPage.collapsePanel('RAM Used'); - persesDashboardsPage.statChartValueAssertion('RAM Used', true); + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-job').should('be.visible'); + 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', '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 new file mode 100644 index 000000000..b9efd17ac --- /dev/null +++ b/web/cypress/support/perses/00.coo_bvt_perses_admin_1.cy.ts @@ -0,0 +1,240 @@ +import { persesDashboardsAcceleratorsCommonMetricsPanels, persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev, persesDashboardsEmptyDashboard } from '../../fixtures/perses/constants'; +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { persesMUIDataTestIDs } from '../../../src/components/data-test'; +import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { persesDashboardsPanelGroup } from '../../views/perses-dashboards-panelgroup'; +import { persesDashboardsPanel } from '../../views/perses-dashboards-panel'; +import { persesDashboardsEditVariables } from '../../views/perses-dashboards-edit-variables'; +import { persesCreateDashboardsPage } from '../../views/perses-dashboards-create-dashboard'; +import { persesDashboardsAddListVariableSource } from '../../fixtures/perses/constants'; +import { persesDashboardSampleQueries } from '../../fixtures/perses/constants'; +import { persesDashboardsAddListPanelType } from '../../fixtures/perses/constants'; +import { commonPages } from '../../views/common'; +import { nav } from '../../views/nav'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runBVTCOOPersesTests1(perspective: PerspectiveConfig) { + testBVTCOOPerses1(perspective); +} + +export function testBVTCOOPerses1(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - Dashboards (Perses) page`, () => { + cy.log(`1.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + persesDashboardsPage.shouldBeLoaded1(); + }); + + it(`2.${perspective.name} perspective - Accelerators common metrics dashboard `, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses) > Accelerators common metrics dashboard`); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + 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', 'Open'); + persesDashboardsPage.panelHeadersAcceleratorsCommonMetricsAssertion(); + persesDashboardsPage.expandPanel(persesDashboardsAcceleratorsCommonMetricsPanels.GPU_UTILIZATION); + persesDashboardsPage.collapsePanel(persesDashboardsAcceleratorsCommonMetricsPanels.GPU_UTILIZATION); + }); + + it(`3.${perspective.name} perspective - Perses Dashboard Sample dashboard`, () => { + cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses) > Perses Dashboard Sample dashboard`); + cy.changeNamespace('perses-dev'); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + cy.wait(2000); + persesDashboardsPage.clickDashboardDropdown(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0] as keyof typeof persesDashboardsDashboardDropdownPersesDev); + cy.byDataTestID(persesMUIDataTestIDs.variableDropdown+'-job').should('be.visible'); + 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', 'Open'); + persesDashboardsPage.expandPanel('RAM Total'); + persesDashboardsPage.collapsePanel('RAM Total'); + persesDashboardsPage.statChartValueAssertion('RAM Total', true); + persesDashboardsPage.searchAndSelectVariable('job', 'node-exporter'); + 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[0]); + 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 v1alpha1)'); + persesDashboardsPage.downloadDashboard(true, persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2], 'YAML (CR v1alpha2)'); + persesDashboardsPage.viewJSON(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2], 'openshift-cluster-observability-operator'); + + }); + + it(`5.${perspective.name} perspective - Duplicate from a project to another, Rename and Delete`, () => { + cy.log(`5.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`5.2. Change namespace to perses-dev`); + cy.changeNamespace('perses-dev'); + listPersesDashboardsPage.countDashboards('3'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`5.3. Click on the Kebab icon - Duplicate to another project`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDuplicateOption(); + listPersesDashboardsPage.duplicateDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown('openshift-cluster-observability-operator'); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + persesDashboardsPage.shouldBeLoadedAfterDuplicate(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`5.4. Click on the Kebab icon - Rename`); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0] + ' - Renamed'); + listPersesDashboardsPage.renameDashboardRenameButton(); + + cy.log(`5.5. Click on the Kebab icon - Delete`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']);s + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0] + ' - Renamed'); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`5.6. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`5.7. Search for the renamed dashboard`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0] + ' - Renamed'); + listPersesDashboardsPage.countDashboards('0'); + listPersesDashboardsPage.clearAllFilters(); + + }); + + it(`6.${perspective.name} perspective - Create Dashboard with panel groups, panels and variables`, () => { + let dashboardName = 'Testing Dashboard - UP '; + let randomSuffix = Math.random().toString(5); + dashboardName += randomSuffix; + cy.log(`6.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`6.2. Click on Create button`); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + + cy.log(`6.3. Create Dashboard`); + persesCreateDashboardsPage.selectProject('perses-dev'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + cy.log(`6.4. Add Variable`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('interval', false, false, '', '', '', undefined, undefined); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('1m'); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('5m'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('job', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('job'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('instance', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('instance'); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_addSeriesSelector(persesDashboardSampleQueries.CPU_LINE_MULTI_SERIES_SERIES_SELECTOR); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`6.5. Add Panel Group`); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('Panel Group Up', 'Open', ''); + + cy.log(`6.6. Add Panel`); + persesDashboardsPage.clickEditActionButton('AddPanel'); + persesDashboardsPanel.addPanelShouldBeLoaded(); + persesDashboardsPanel.addPanel('Up', 'Panel Group Up', persesDashboardsAddListPanelType.TIME_SERIES_CHART, 'This is a line chart test', 'up'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`6.7. Back and check panel`); + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.panelGroupHeaderAssertion('Panel Group Up', 'Open'); + persesDashboardsPage.assertPanel('Up', 'Panel Group Up', 'Open'); + persesDashboardsPage.assertVariableBeVisible('interval'); + persesDashboardsPage.assertVariableBeVisible('job'); + persesDashboardsPage.assertVariableBeVisible('instance'); + + cy.log(`6.8. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`6.9. Click on Edit Variables button and Delete all variables`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`6.10. Assert variables not exist`); + persesDashboardsPage.assertVariableNotExist('interval'); + persesDashboardsPage.assertVariableNotExist('job'); + persesDashboardsPage.assertVariableNotExist('instance'); + + cy.log(`6.11. Delete Panel`); + persesDashboardsPanel.deletePanel('Up'); + persesDashboardsPanel.clickDeletePanelButton(); + + cy.log(`6.12. Delete Panel Group`); + persesDashboardsPanelGroup.clickPanelGroupAction('Panel Group Up', 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.get('h2').contains(persesDashboardsEmptyDashboard.TITLE).scrollIntoView().should('be.visible'); + cy.get('p').contains(persesDashboardsEmptyDashboard.DESCRIPTION).scrollIntoView().should('be.visible'); + + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`6.13. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`6.14. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`6.15. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('0'); + listPersesDashboardsPage.clearAllFilters(); + + }); + +} diff --git a/web/cypress/support/perses/01.coo_list_perses_admin.cy.ts b/web/cypress/support/perses/01.coo_list_perses_admin.cy.ts new file mode 100644 index 000000000..ee5797cce --- /dev/null +++ b/web/cypress/support/perses/01.coo_list_perses_admin.cy.ts @@ -0,0 +1,353 @@ +import { persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev } from '../../fixtures/perses/constants'; +import { commonPages } from '../../views/common'; +import { listPersesDashboardsPage } from "../../views/perses-dashboards-list-dashboards"; +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { nav } from '../../views/nav'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOOListPersesTests(perspective: PerspectiveConfig) { + testCOOListPerses(perspective); +} + +export function runCOOListPersesDuplicateDashboardTests(perspective: PerspectiveConfig) { + testCOOListPersesDuplicateDashboard(perspective); +} + +export function testCOOListPerses(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - List Dashboards (Perses) 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.ACCELERATORS_COMMON_METRICS[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`1.3. Clear all filters`); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`1.4. Filter by Project and Name`); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.countDashboards('3'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`1.5. Clear all filters`); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`1.6. Filter by Project`); + listPersesDashboardsPage.filter.byProject('perses-dev'); + + cy.log(`1.7. Clear all filters`); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`1.8. Sort by Dashboard - Ascending`); + listPersesDashboardsPage.sortBy('Dashboard'); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0], 0); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.APM_DASHBOARD[0], 1); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0], 2); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0], 3); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0], 4); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0], 5); + + cy.log(`1.9. Sort by Dashboard - Descending`); + listPersesDashboardsPage.sortBy('Dashboard'); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0], 0); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0], 1); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0], 2); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0], 3); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.APM_DASHBOARD[0], 4); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0], 5); + + cy.log(`1.10. Filter by Name - Empty state`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`1.11. Clear all filters`); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`1.12. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + }); + + + it(`2.${perspective.name} perspective - Kebab icon - Options available - Rename dashboard - Max length validation`, () => { + cy.log(`2.1. Filter by Name and click on the Kebab icon`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.assertKebabIconOptions(); + listPersesDashboardsPage.clickKebabIcon(); + + cy.log(`2.2. Click on the Kebab icon - Rename dashboard`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName('1234567890123456789012345678901234567890123456789012345678901234567890123456'); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.assertRenameDashboardMaxLength(); + listPersesDashboardsPage.renameDashboardCancelButton(); + + cy.log(`2.3. Clear all filters and filter by Name`); + listPersesDashboardsPage.clearAllFilters(); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clearAllFilters(); + + }); + + it(`3.${perspective.name} perspective - Kebab icon - Options available - Rename dashboard`, () => { + let dashboardName = 'Dashboard to test rename'; + let randomSuffix = Math.random().toString(5); + dashboardName += randomSuffix; + + cy.log(`3.1. Filter by Name and click on the Kebab icon`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + + cy.log(`3.2. Click on the Kebab icon - Rename dashboard - Cancel`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(dashboardName); + listPersesDashboardsPage.renameDashboardCancelButton(); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0], 0); + + cy.log(`3.3. Click on the Kebab icon - Rename dashboard - Rename`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(dashboardName); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`3.4. Clear all filters and filter by Name`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`3.5. Click on dashboard and verify the name`); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.shouldBeLoadedAfterRename(dashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`3.6. Rename back to the original name`); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`3.7. Click on the Kebab icon - Rename dashboard - Rename`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`3.8. Clear all filters and filter by Name`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`3.9. Click on dashboard and verify the name`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.shouldBeLoadedAfterRename(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + persesDashboardsPage.backToListPersesDashboardsPage(); + + }); + + //TODO: Add test for Rename to an existing dashboard name to be addressed by https://issues.redhat.com/browse/OU-1220 + it(`4.${perspective.name} perspective - Kebab icon - Options available - Rename dashboard to an existing dashboard name`, () => { + cy.log(`4.1. Filter by Name and click on the Kebab icon`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + + + cy.log(`4.2. Click on the Kebab icon - Rename dashboard - Rename`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0]); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`4.3. Clear all filters and filter by Name`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0]); + listPersesDashboardsPage.countDashboards('2'); + + cy.log(`4.4. Sort by Last Modified - Descending`); + listPersesDashboardsPage.sortBy('Last Modified'); + listPersesDashboardsPage.sortBy('Last Modified'); + listPersesDashboardsPage.clickKebabIcon(0); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + listPersesDashboardsPage.renameDashboardRenameButton(); + cy.wait(2000); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`4.5. Clear all filters and filter by Name`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + listPersesDashboardsPage.filter.byProject('perses-dev'); + cy.wait(2000); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0]); + cy.wait(2000); + listPersesDashboardsPage.countDashboards('1'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); +} + + export function testCOOListPersesDuplicateDashboard(perspective: PerspectiveConfig) { + + it(`5.${perspective.name} perspective - Duplicate - existing dashboard ID in the same project`, () => { + 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 and click on the Kebab icon`); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`5.3. Click on the Kebab icon - Duplicate`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDuplicateOption(); + listPersesDashboardsPage.assertDuplicateProjectDropdown('openshift-cluster-observability-operator'); + listPersesDashboardsPage.assertDuplicateProjectDropdown('perses-dev'); + listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown('perses-dev'); + listPersesDashboardsPage.duplicateDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + listPersesDashboardsPage.assertDuplicateDashboardAlreadyExists(); + listPersesDashboardsPage.duplicateDashboardCancelButton(); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + }); + + it(`6.${perspective.name} perspective - Duplicate - existing dashboard name in the same project`, () => { + 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 and click on the Kebab icon`); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`6.3. Click on the Kebab icon - Duplicate`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDuplicateOption(); + listPersesDashboardsPage.duplicateDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + persesDashboardsPage.shouldBeLoadedAfterDuplicate(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + + cy.log(`6.4. Back to the list and duplicate to another project`); + persesDashboardsPage.backToListPersesDashboardsPage(); + + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('2'); + + cy.log(`6.5. Sort by Last Modified - Descending`); + listPersesDashboardsPage.sortBy('Last Modified'); + + cy.log(`6.6. Click on the Kebab icon - Duplicate with the same Dashboard name`); + listPersesDashboardsPage.clickKebabIcon(0); + listPersesDashboardsPage.clickDuplicateOption(); + listPersesDashboardsPage.duplicateDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + listPersesDashboardsPage.assertDuplicateDashboardAlreadyExists(); + listPersesDashboardsPage.duplicateDashboardCancelButton(); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + }); + + it(`7.${perspective.name} perspective - Duplicate - existing dashboard ID in another project`, () => { + 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 and click on the Kebab icon`); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('2'); + + cy.log(`7.3. Click on the Kebab icon - Duplicate`); + listPersesDashboardsPage.clickKebabIcon(0); + listPersesDashboardsPage.clickDuplicateOption(); + listPersesDashboardsPage.duplicateDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown('openshift-cluster-observability-operator'); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + persesDashboardsPage.shouldBeLoadedAfterDuplicate(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + persesDashboardsPage.backToListPersesDashboardsPage(); + + }); + + it(`8.${perspective.name} perspective - Delete and Cancel and then Delete`, () => { + 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 and click on the Kebab icon`); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('2'); + listPersesDashboardsPage.sortBy('Last Modified'); + listPersesDashboardsPage.sortBy('Last Modified'); + + cy.log(`8.4. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(0); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardCancelButton(); + listPersesDashboardsPage.countDashboards('2'); + + cy.log(`8.5. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(0); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.countDashboards('1'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`8.6. Filter by Name and click on the Kebab icon`); + listPersesDashboardsPage.filter.byProject('openshift-cluster-observability-operator'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`8.7. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + }); + } \ No newline at end of file diff --git a/web/cypress/support/perses/01.coo_list_perses_admin_namespace.cy.ts b/web/cypress/support/perses/01.coo_list_perses_admin_namespace.cy.ts new file mode 100644 index 000000000..e61785806 --- /dev/null +++ b/web/cypress/support/perses/01.coo_list_perses_admin_namespace.cy.ts @@ -0,0 +1,133 @@ +import { persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev } from '../../fixtures/perses/constants'; +import { commonPages } from '../../views/common'; +import { listPersesDashboardsPage } from "../../views/perses-dashboards-list-dashboards"; +import { persesDashboardsPage } from '../../views/perses-dashboards'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOOListPersesTestsNamespace(perspective: PerspectiveConfig) { + testCOOListPersesNamespace(perspective); +} + +export function testCOOListPersesNamespace(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - List Dashboards (Perses) page`, () => { + cy.log(`1.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`1.2. Change namespace to perses-dev`); + cy.changeNamespace('perses-dev'); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.countDashboards('3'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`1.3. Clear all filters`); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`1.4. Sort by Dashboard - Ascending`); + listPersesDashboardsPage.sortBy('Dashboard'); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0], 0); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0], 1); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0], 2); + + cy.log(`1.5. Sort by Dashboard - Descending`); + listPersesDashboardsPage.sortBy('Dashboard'); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.THANOS_COMPACT_OVERVIEW[0], 0); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0], 1); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0], 2); + + + cy.log(`1.6. Change namespace to openshift-cluster-observability-operator`); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.countDashboards('3'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`1.7. Clear all filters`); + listPersesDashboardsPage.clearAllFilters(); + + + cy.log(`1.8. Sort by Dashboard - Ascending`); + listPersesDashboardsPage.sortBy('Dashboard'); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0], 0); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.APM_DASHBOARD[0], 1); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0], 2); + + cy.log(`1.9. Sort by Dashboard - Descending`); + listPersesDashboardsPage.sortBy('Dashboard'); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0], 0); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.APM_DASHBOARD[0], 1); + listPersesDashboardsPage.assertDashboardName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0], 2); + + cy.log(`1.10. Filter by Name - Empty state`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PROMETHEUS_OVERVIEW[0]); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`1.11. Clear all filters`); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`1.12. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.APM_DASHBOARD[0]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + persesDashboardsPage.shouldBeLoaded1(); + }); + + it(`2.${perspective.name} perspective - Duplicate from a project to another, Rename and Delete`, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + commonPages.titleShouldHaveText('Dashboards'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`2.2. Change namespace to perses-dev`); + cy.changeNamespace('perses-dev'); + listPersesDashboardsPage.countDashboards('3'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`2.3. Click on the Kebab icon - Duplicate to another project`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDuplicateOption(); + listPersesDashboardsPage.duplicateDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown('openshift-cluster-observability-operator'); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + persesDashboardsPage.shouldBeLoadedAfterDuplicate(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`2.4. Click on the Kebab icon - Rename`); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0] + ' - Renamed'); + listPersesDashboardsPage.renameDashboardRenameButton(); + + cy.log(`2.5. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clearAllFilters(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0] + ' - Renamed'); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`2.6. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`2.7. Search for the renamed dashboard`); + listPersesDashboardsPage.clearAllFilters(); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0] + ' - Renamed'); + listPersesDashboardsPage.countDashboards('0'); + listPersesDashboardsPage.clearAllFilters(); + + }); + + +} 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..57289d3e4 --- /dev/null +++ b/web/cypress/support/perses/02.coo_edit_perses_admin.cy.ts @@ -0,0 +1,518 @@ +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/perses-dashboards-list-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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`1.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + //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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`2.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + //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[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + //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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`3.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + //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[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + + 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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`4.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + //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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`5.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + //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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`6.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + //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[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + 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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`7.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + //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'); + //TODO: https://issues.redhat.com/browse/OU-1223 - Upstream ref: [Edit Dashboard] - Edit Panel Group from closed/opened vice-versa is not shown right away + persesDashboardsPanelGroup.editPanelGroup('PanelGroup2', 'Closed', ''); + persesDashboardsPage.clickEditActionButton('Save'); + //persesDashboardsPage.panelGroupHeaderAssertion('PanelGroup2', 'Closed'); //TODO: uncomment when https://issues.redhat.com/browse/OU-1223 is fixed + + 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[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + 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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`8.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + //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[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + 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[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + 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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`9.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + //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[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + 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..7bd2ff9ea --- /dev/null +++ b/web/cypress/support/perses/02.coo_edit_perses_admin_1.cy.ts @@ -0,0 +1,281 @@ +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/perses-dashboards-list-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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`10.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + + 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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`11.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + + 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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`12.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + + 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[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`13.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + + 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 - Perform changes and Cancel`, () => { + 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(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`14.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + // persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`14.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(`14.5. Add variable`); + persesDashboardsEditVariables.clickButton('Add'); + + cy.log(`14.6. Apply changes`); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`14.7. Assert Variable before cancelling`); + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + + cy.log(`14.8. Click on Add Panel Group button`); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('PanelGroup Perform Changes and Cancel', 'Open', ''); + + cy.log(`14.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(`14.10. Click on Cancel button`); + persesDashboardsPage.clickEditActionButton('Cancel'); + + cy.log(`14.11. Assert variable not exist`); + persesDashboardsPage.assertVariableNotExist('ListVariable'); + + cy.log(`14.12. Assert panel group not exist`); + persesDashboardsPage.assertPanelGroupNotExist('PanelGroup Perform Changes and Cancel'); + + cy.log(`14.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(`15.${perspective.name} perspective - Try to editAccelerators and APM dashboards`, () => { + 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.ACCELERATORS_COMMON_METRICS[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`15.3. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + //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 saving`); + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + + cy.log(`15.8. Click on Add Panel Group button`); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('PanelGroup Perform Changes and Save', 'Open', ''); + + cy.log(`15.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(`15.10. Click on Save button`); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`15.11. Back and check panel group`); + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + + cy.log(`15.12. Assert Variable before deleting`); + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + + cy.log(`15.13. Assert panel group exists`); + persesDashboardsPage.panelGroupHeaderAssertion('PanelGroup Perform Changes and Save', 'Open'); + + cy.log(`15.14. Assert panel exists`); + persesDashboardsPage.assertPanel('Panel Perform Changes and Save', 'PanelGroup Perform Changes and Save', 'Open'); + + cy.log (`15.15. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`15.16. Delete variable`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickDeleteVariableButton(1); + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.assertVariableNotExist('ListVariable'); + + cy.log(`15.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/support/perses/03.coo_create_perses_admin.cy.ts b/web/cypress/support/perses/03.coo_create_perses_admin.cy.ts new file mode 100644 index 000000000..15d844faf --- /dev/null +++ b/web/cypress/support/perses/03.coo_create_perses_admin.cy.ts @@ -0,0 +1,209 @@ +import { listPersesDashboardsPage } from "../../views/perses-dashboards-list-dashboards"; +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { persesDashboardsAddListPanelType, persesDashboardSampleQueries, persesDashboardsEmptyDashboard } from '../../fixtures/perses/constants'; +import { persesCreateDashboardsPage } from '../../views/perses-dashboards-create-dashboard'; +import { persesDashboardsPanelGroup } from "../../views/perses-dashboards-panelgroup"; +import { persesDashboardsPanel } from "../../views/perses-dashboards-panel"; +import { persesDashboardsEditVariables } from "../../views/perses-dashboards-edit-variables"; +import { persesDashboardsAddListVariableSource } from "../../fixtures/perses/constants"; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOOCreatePersesTests(perspective: PerspectiveConfig) { + testCOOCreatePerses(perspective); +} + +export function testCOOCreatePerses(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - Create Dashboard validation with max length`, () => { + let dashboardName = 'Test Dashboard'; + let randomSuffix = Math.random().toString(5); + dashboardName += randomSuffix; + cy.log(`1.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`1.2. Click on Create button`); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + + cy.log(`1.3. Verify Project dropdown`); + persesCreateDashboardsPage.assertProjectDropdown('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.assertProjectDropdown('observ-test'); + persesCreateDashboardsPage.assertProjectDropdown('perses-dev'); + + cy.log(`1.4. Verify Max Length Validation`); + persesCreateDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.enterDashboardName('1234567890123456789012345678901234567890123456789012345678901234567890123456'); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesCreateDashboardsPage.assertMaxLengthValidation(); + + cy.log(`1.5. Verify Name input`); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + }); + + it(`2.${perspective.name} perspective - Create Dashboard with duplicated name in the same project`, () => { + //dashboard name with spaces + let dashboardName = 'Dashboard to test duplication'; + let randomSuffix = Math.random().toString(5); + dashboardName += randomSuffix; + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`2.2. Click on Create button`); + listPersesDashboardsPage.clickCreateButton(); + + cy.log(`2.3. Verify Project dropdown`); + persesCreateDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + cy.log(`2.4. Create another dashboard with the same name`); + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesCreateDashboardsPage.assertDuplicatedNameValidation(dashboardName); + + //dashboard name without spaces + cy.log(`2.5. Create another dashboard with the same name without spaces`); + dashboardName = 'DashboardToTestDuplication'; + dashboardName += randomSuffix; + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + cy.log(`2.6. Create another dashboard with the same name without spaces`); + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesCreateDashboardsPage.assertDuplicatedNameValidation(dashboardName); + + cy.log(`2.7. Create another dashboard with the same name in other project`); + persesCreateDashboardsPage.selectProject('perses-dev'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + }); + + it(`3.${perspective.name} perspective - Create Dashboard with panel groups, panels and variables`, () => { + let dashboardName = 'Testing Dashboard - UP '; + let randomSuffix = Math.random().toString(5); + dashboardName += randomSuffix; + cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`3.2. Click on Create button`); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + + cy.log(`3.3. Create Dashboard`); + persesCreateDashboardsPage.selectProject('perses-dev'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + cy.log(`3.4. Add Variable`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('interval', false, false, '', '', '', undefined, undefined); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('1m'); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('5m'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('job', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('job'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('instance', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('instance'); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_addSeriesSelector(persesDashboardSampleQueries.CPU_LINE_MULTI_SERIES_SERIES_SELECTOR); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`3.5. Add Panel Group`); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('Panel Group Up', 'Open', ''); + + cy.log(`3.6. Add Panel`); + persesDashboardsPage.clickEditActionButton('AddPanel'); + persesDashboardsPanel.addPanelShouldBeLoaded(); + persesDashboardsPanel.addPanel('Up', 'Panel Group Up', persesDashboardsAddListPanelType.TIME_SERIES_CHART, 'This is a line chart test', 'up'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`3.7. Back and check panel`); + persesDashboardsPage.backToListPersesDashboardsPage(); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.panelGroupHeaderAssertion('Panel Group Up', 'Open'); + persesDashboardsPage.assertPanel('Up', 'Panel Group Up', 'Open'); + persesDashboardsPage.assertVariableBeVisible('interval'); + persesDashboardsPage.assertVariableBeVisible('job'); + persesDashboardsPage.assertVariableBeVisible('instance'); + + cy.log(`3.8. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`3.9. Click on Edit Variables button and Delete all variables`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`3.10. Assert variables not exist`); + persesDashboardsPage.assertVariableNotExist('interval'); + persesDashboardsPage.assertVariableNotExist('job'); + persesDashboardsPage.assertVariableNotExist('instance'); + + cy.log(`3.11. Delete Panel`); + persesDashboardsPanel.deletePanel('Up'); + persesDashboardsPanel.clickDeletePanelButton(); + + cy.log(`3.12. Delete Panel Group`); + persesDashboardsPanelGroup.clickPanelGroupAction('Panel Group Up', 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.get('h2').contains(persesDashboardsEmptyDashboard.TITLE).scrollIntoView().should('be.visible'); + cy.get('p').contains(persesDashboardsEmptyDashboard.DESCRIPTION).scrollIntoView().should('be.visible'); + + }); + + //TODO: Verify Create project dropdown not only showing perses projects, but all namespaces you have access to, independently of having perses object (that creates a perses project) + // it(`4.${perspective.name} perspective - Verify Create project dropdown not only showing perses projects, but all namespaces you have access to, independently of having perses object (that creates a perses project)`, () => { + // cy.log(`4.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + // listPersesDashboardsPage.shouldBeLoaded(); + + // cy.log(`4.2. Click on Create button`); + // listPersesDashboardsPage.clickCreateButton(); + // persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + + // cy.log(`4.3. Verify Project dropdown`); + // persesCreateDashboardsPage.assertProjectDropdown('openshift-cluster-observability-operator'); + // openshift-monitoringas an example of a namespace that you have access to and does not have any perses object created yet, but you are able to create a dashboard + // persesCreateDashboardsPage.assertProjectDropdown('openshift-monitoring); + + // }); + +} diff --git a/web/cypress/support/perses/04.coo_import_perses_admin.cy.ts b/web/cypress/support/perses/04.coo_import_perses_admin.cy.ts new file mode 100644 index 000000000..2e25c6139 --- /dev/null +++ b/web/cypress/support/perses/04.coo_import_perses_admin.cy.ts @@ -0,0 +1,218 @@ +import { listPersesDashboardsPage } from "../../views/perses-dashboards-list-dashboards"; +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { persesImportDashboardsPage } from "../../views/perses-dashboards-import-dashboard"; +import { nav } from "../../views/nav"; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOOImportPersesTests(perspective: PerspectiveConfig) { + testCOOImportPerses(perspective); +} + +export function testCOOImportPerses(perspective: PerspectiveConfig) { + + it(`1. ${perspective.name} perspective - Import Dashboard - wrong format`, () => { + cy.log(`1.1 use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`1.2 Click on Import button`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + + cy.log(`1.3 Upload wrong format file`); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/accelerators-dashboard-cr-v1alpha1.yaml'); + persesImportDashboardsPage.assertUnableToDetectDashboardFormat(); + + cy.log(`1.4 Clear file`); + persesImportDashboardsPage.clickClearFileButton(); + + cy.log(`1.5 Upload another wrong format file`); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/accelerators-dashboard-cr-v1alpha2.yaml'); + persesImportDashboardsPage.assertUnableToDetectDashboardFormat(); + + cy.log(`1.6 Clear file`); + persesImportDashboardsPage.clickClearFileButton(); + + cy.log(`1.7 Upload Grafana dashboard file`); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/grafana_to_check_errors.json'); + persesImportDashboardsPage.assertGrafanaDashboardDetected(); + + cy.log(`1.8 Select a project`); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + + cy.log(`1.9 Import dashboard`); + persesImportDashboardsPage.clickImportFileButton(); + + cy.log(`1.10 Assert failed to migrate Grafana dashboard`); + persesImportDashboardsPage.assertFailedToMigrateGrafanaDashboard(); + + cy.log(`1.11 Cancel import`); + persesImportDashboardsPage.clickCancelButton(); + + }); + + it(`2. ${perspective.name} perspective - Import Dashboard - ACM Grafana dashboard`, () => { + cy.log(`2.1 use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`2.2 Click on Import button`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + + cy.log(`2.3 Upload Grafana dashboard file`); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/acm-vm-status.json'); + persesImportDashboardsPage.assertGrafanaDashboardDetected(); + + cy.log(`2.4 Select a project`); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + + cy.log(`2.5 Import dashboard`); + persesImportDashboardsPage.clickImportFileButton(); + persesDashboardsPage.closeSuccessAlert(); + + cy.log(`2.6 Assert dashboard is imported`); + persesDashboardsPage.shouldBeLoadedEditionMode('Service Level dashboards / Virtual Machines by Time in Status'); + + cy.log(`2.7 Back to list of dashboards`); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`2.8 Filter by Name`); + listPersesDashboardsPage.filter.byName('Service Level dashboards / Virtual Machines by Time in Status'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clearAllFilters(); + cy.wait(2000); + + cy.log(`2.9 Import the same dashboard - Duplicated error`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/acm-vm-status.json'); + persesImportDashboardsPage.assertGrafanaDashboardDetected(); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesImportDashboardsPage.clickImportFileButton(); + persesImportDashboardsPage.assertDuplicatedDashboardError(); + persesImportDashboardsPage.clickCancelButton(); + + }); + + it(`3. ${perspective.name} perspective - Import Dashboard - Perses dashboard - JSON file`, () => { + cy.log(`3.1 use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`3.2 Click on Import button`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + + cy.log(`3.3 Upload Perses dashboard JSON file`); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + + cy.log(`3.4 Select a project`); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + + cy.log(`3.5 Import dashboard`); + persesImportDashboardsPage.clickImportFileButton(); + persesDashboardsPage.closeSuccessAlert(); + + cy.log(`3.6 Assert dashboard is imported`); + persesDashboardsPage.shouldBeLoadedEditionMode('Testing Perses dashboard - JSON'); + + cy.log(`3.7 Back to list of dashboards`); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`3.8 Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - JSON'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clearAllFilters(); + cy.wait(2000); + + cy.log(`3.9 Import the same dashboard - Duplicated error`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesImportDashboardsPage.clickImportFileButton(); + persesImportDashboardsPage.assertDuplicatedDashboardError(); + persesImportDashboardsPage.clickCancelButton(); + + }); + + it(`4. ${perspective.name} perspective - Import Dashboard - Perses dashboard - YAML file`, () => { + cy.log(`4.1 use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`4.2 Click on Import button`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + + cy.log(`4.3 Upload Perses dashboard YAML file`); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + + cy.log(`4.4 Select a project`); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + + cy.log(`4.5 Import dashboard`); + persesImportDashboardsPage.clickImportFileButton(); + persesDashboardsPage.closeSuccessAlert(); + + cy.log(`4.6 Assert dashboard is imported`); + persesDashboardsPage.shouldBeLoadedEditionMode('Testing Perses dashboard - YAML'); + + cy.log(`4.7 Back to list of dashboards`); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`4.8 Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - YAML'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clearAllFilters(); + cy.wait(2000); + + cy.log(`4.9 Import the same dashboard - Duplicated error`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesImportDashboardsPage.clickImportFileButton(); + persesImportDashboardsPage.assertDuplicatedDashboardError(); + persesImportDashboardsPage.clickCancelButton(); + + }); + + it(`5. ${perspective.name} perspective - Delete imported dashboard`, () => { + const dashboardsToDelete = [ + 'Testing Perses dashboard - JSON', + 'Testing Perses dashboard - YAML', + 'Service Level dashboards / Virtual Machines by Time in Status' + ]; + + cy.log(`5.1 Navigate to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + dashboardsToDelete.forEach((dashboardName, index) => { + cy.log(`5.${index + 2}.1 Filter by Name: ${dashboardName}`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + cy.wait(2000); + + cy.log(`5.${index + 2}.2 Delete dashboard via Kebab menu`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`5.${index + 2}.3 Verify dashboard is deleted`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + }); +} diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user1.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user1.cy.ts new file mode 100644 index 000000000..46bac9512 --- /dev/null +++ b/web/cypress/support/perses/99.coo_rbac_perses_user1.cy.ts @@ -0,0 +1,598 @@ +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { persesCreateDashboardsPage } from '../../views/perses-dashboards-create-dashboard'; +import { persesDashboardsAddListVariableSource, persesDashboardSampleQueries, persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev, persesDashboardsEmptyDashboard } from '../../fixtures/perses/constants'; +import { persesDashboardsEditVariables } from '../../views/perses-dashboards-edit-variables'; +import { persesDashboardsPanelGroup } from '../../views/perses-dashboards-panelgroup'; +import { persesAriaLabels } from '../../../src/components/data-test'; +import { persesDashboardsPanel } from '../../views/perses-dashboards-panel'; +import { persesDashboardsAddListPanelType } from '../../fixtures/perses/constants'; +import { persesImportDashboardsPage } from '../../views/perses-dashboards-import-dashboard'; +import { nav } from '../../views/nav'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOORBACPersesTestsDevUser1(perspective: PerspectiveConfig) { + testCOORBACPersesTestsDevUser1(perspective); +} + +/** + * User1 has access to: + * - openshift-cluster-observability-operator namespace as persesdashboard-editor-role and persesdatasource-editor-role + * - observ-test namespace as persesdashboard-viewer-role and persesdatasource-viewer-role + * - no access to perses-dev, empty-namespace3, empty-namespace4 namespaces + * - openshift-monitoring namespace as view role + */ +export function testCOORBACPersesTestsDevUser1(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - List Dashboards - Namespace validation and Dashboard search`, () => { + cy.log(`1.1. Namespace validation`); + listPersesDashboardsPage.shouldBeLoaded(); + cy.assertNamespace('All Projects', true); + cy.assertNamespace('openshift-cluster-observability-operator', true); + cy.assertNamespace('observ-test', true); + cy.assertNamespace('openshift-monitoring', true); + cy.assertNamespace('perses-dev', false); + cy.assertNamespace('empty-namespace3', false); + cy.assertNamespace('empty-namespace4', false); + + cy.log(`1.2. All Projects validation - Dashboard search - ${persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]} dashboard`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.removeTag(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + + cy.log(`1.3. All Projects validation - Dashboard search - ${persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]} dashboard`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.countDashboards('2'); + listPersesDashboardsPage.removeTag(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + + cy.log(`1.4. All Projects validation - Dashboard search - ${persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]} dashboard`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.removeTag(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.removeTag('perses-dev'); + + cy.log(`1.5. All Projects validation - Dashboard search - empty state`); + listPersesDashboardsPage.filter.byProject('empty-namespace3'); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.removeTag('empty-namespace3'); + + cy.log(`1.6. All Projects validation - Dashboard search - empty state`); + listPersesDashboardsPage.filter.byProject('openshift-monitoring'); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.removeTag('openshift-monitoring'); + + }); + + it(`2.${perspective.name} perspective - Edit button validation - Editable dashboard`, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`2.2 change namespace to openshift-cluster-observability-operator`); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`2.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`2.4. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`2.5. Click on Edit button`); + 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(`2.6. Click on Edit Variables button - Add components`); + 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.7. Add variable`); + persesDashboardsEditVariables.clickButton('Add'); + + cy.log(`2.8. Apply changes`); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`2.9. Assert Variable before saving`); + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + + cy.log(`2.10. Click on Add Panel Group button`); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('PanelGroup Perform Changes and Save', 'Open', ''); + + cy.log(`2.11. 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', persesDashboardsAddListPanelType.TIME_SERIES_CHART, undefined, 'up'); + cy.wait(2000); + + cy.log(`2.13. Click on Save button`); + persesDashboardsPage.clickEditActionButton('Save'); + cy.wait(2000); + + cy.log(`2.14. Assert Panel with Data - Export Time Series Data As CSV button is visible and clickable `); + cy.wait(2000); + cy.byAriaLabel(persesAriaLabels.PanelExportTimeSeriesDataAsCSV).eq(0).click({ force: true }); + cy.wait(1000); + persesDashboardsPage.assertFilename('panelPerformChangesAndSave_data.csv'); + + cy.wait(2000); + + cy.log(`2.15. Back and check changes`); + persesDashboardsPage.backToListPersesDashboardsPage(); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.searchAndSelectVariable('ListVariable', 'All'); + persesDashboardsPage.panelGroupHeaderAssertion('PanelGroup Perform Changes and Save', 'Open'); + persesDashboardsPage.assertPanel('Panel Perform Changes and Save', 'PanelGroup Perform Changes and Save', 'Open'); + + cy.log(`2.16. Click on Edit Variables button - Delete components`); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickDeleteVariableButton(1); + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.assertVariableNotExist('ListVariable'); + + cy.log(`2.17. Click on Delete Panel button`); + persesDashboardsPanel.deletePanel('Panel Perform Changes and Save'); + persesDashboardsPanel.clickDeletePanelButton(); + + cy.log(`2.18. Click on Delete Panel Group button`); + persesDashboardsPanelGroup.clickPanelGroupAction('PanelGroup Perform Changes and Save', 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + persesDashboardsPage.assertPanelGroupNotExist('PanelGroup Perform Changes and Save'); + persesDashboardsPage.assertPanelNotExist('Panel Perform Changes and Save'); + persesDashboardsPage.assertVariableNotExist('ListVariable'); + + }); + + it(`3.${perspective.name} perspective - Edit button validation - Not editable dashboard`, () => { + cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`3.2 change namespace to observ-test`); + cy.changeNamespace('observ-test'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`3.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`3.4. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + persesDashboardsPage.shouldBeLoaded1(); + + cy.log(`3.5. Verify Edit button is not editable`); + persesDashboardsPage.assertEditButtonIsDisabled(); + + }); + + /** + * When we have admin permission or editor permission to at least one namespace, + * the Create button is always enabled and Select project dropdown is filtering out namespaces that we do not have access to + */ + it(`4.${perspective.name} perspective - Create button validation - Disabled / Enabled`, () => { + cy.log(`4.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`4.2 change namespace to observ-test`); + cy.changeNamespace('observ-test'); + + cy.log(`4.3. Verify Create button is still enabled`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertProjectDropdown('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace3'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); + + cy.log(`4.4 change namespace to openshift-cluster-observability-operator`); + cy.changeNamespace('openshift-cluster-observability-operator'); + + cy.log(`4.5. Verify Create button is enabled`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + + cy.log(`4.6 change namespace to All Projects`); + cy.changeNamespace('All Projects'); + + cy.log(`4.7. Verify Create button is enabled`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + + }); + + it(`5.${perspective.name} perspective - Create Dashboard with panel groups, panels and variables`, () => { + let dashboardName = 'Testing Dashboard - UP '; + let randomSuffix = Math.random().toString(5); + dashboardName += randomSuffix; + cy.log(`5.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.changeNamespace('openshift-cluster-observability-operator'); + + cy.log(`5.2. Click on Create button`); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + + cy.log(`5.3. Create Dashboard`); + persesCreateDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + cy.log(`5.4. Add Variable`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('interval', false, false, '', '', '', undefined, undefined); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('1m'); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('5m'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('job', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('job'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('instance', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('instance'); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_addSeriesSelector(persesDashboardSampleQueries.CPU_LINE_MULTI_SERIES_SERIES_SELECTOR); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`5.5. Add Panel Group`); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('Panel Group Up', 'Open', ''); + + cy.log(`5.6. Add Panel`); + persesDashboardsPage.clickEditActionButton('AddPanel'); + persesDashboardsPanel.addPanelShouldBeLoaded(); + persesDashboardsPanel.addPanel('Up', 'Panel Group Up', persesDashboardsAddListPanelType.TIME_SERIES_CHART, 'This is a line chart test', 'up'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`5.7. Back and check panel`); + persesDashboardsPage.backToListPersesDashboardsPage(); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.panelGroupHeaderAssertion('Panel Group Up', 'Open'); + persesDashboardsPage.assertPanel('Up', 'Panel Group Up', 'Open'); + persesDashboardsPage.assertVariableBeVisible('interval'); + persesDashboardsPage.assertVariableBeVisible('job'); + persesDashboardsPage.assertVariableBeVisible('instance'); + + cy.log(`5.8. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`5.9. Click on Edit Variables button and Delete all variables`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`5.10. Assert variables not exist`); + persesDashboardsPage.assertVariableNotExist('interval'); + persesDashboardsPage.assertVariableNotExist('job'); + persesDashboardsPage.assertVariableNotExist('instance'); + + cy.log(`5.11. Delete Panel`); + persesDashboardsPanel.deletePanel('Up'); + persesDashboardsPanel.clickDeletePanelButton(); + + cy.log(`5.12. Delete Panel Group`); + persesDashboardsPanelGroup.clickPanelGroupAction('Panel Group Up', 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + persesDashboardsPage.closeSuccessAlert(); + + cy.get('h2').contains(persesDashboardsEmptyDashboard.TITLE).scrollIntoView().should('be.visible'); + cy.get('p').contains(persesDashboardsEmptyDashboard.DESCRIPTION).scrollIntoView().should('be.visible'); + + }); + + it(`6.${perspective.name} perspective - Kebab icon - Enabled / Disabled`, () => { + cy.log(`6.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`6.2. Change namespace to observ-test`); + cy.changeNamespace('observ-test'); + + cy.log(`6.3. Assert Kebab icon is disabled`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.assertKebabIconDisabled(); + + cy.log(`6.4. Change namespace to openshift-cluster-observability-operator`); + cy.changeNamespace('openshift-cluster-observability-operator'); + + cy.log(`6.5. Assert Kebab icon is enabled`); + listPersesDashboardsPage.clearAllFilters(); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.assertKebabIconOptions(); + listPersesDashboardsPage.clickKebabIcon(); + + cy.log(`6.2. Change namespace to All Projects`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`6.3. Filter by Project and Name`); + listPersesDashboardsPage.filter.byProject('observ-test'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.assertKebabIconDisabled(); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`6.4. Filter by Project and Name`); + listPersesDashboardsPage.filter.byProject('openshift-cluster-observability-operator'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.assertKebabIconOptions(); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clearAllFilters(); + + }); + + + it(`7.${perspective.name} perspective - Rename to a new dashboard name`, () => { + let dashboardName = 'Renamed dashboard '; + let randomSuffix = Math.random().toString(5); + dashboardName += randomSuffix; + + cy.log(`7.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`7.2. Change namespace to openshift-cluster-observability-operator`); + cy.changeNamespace('openshift-cluster-observability-operator'); + + cy.log(`7.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`7.4. Click on the Kebab icon - Rename`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(dashboardName); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`7.5. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.shouldBeLoadedAfterRename(dashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`7.6. Rename back to the original name`); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`7.7. Filter by Name`); + cy.changeNamespace('openshift-cluster-observability-operator'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.shouldBeLoadedAfterRename(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + persesDashboardsPage.backToListPersesDashboardsPage(); + + }); + + it(`8.${perspective.name} perspective - Duplicate and verify project dropdown and Delete`, () => { + let dashboardName = 'Duplicate dashboard '; + let randomSuffix = Math.random().toString(5); + dashboardName += randomSuffix; + + cy.log(`8.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`8.2. Change namespace to openshift-cluster-observability-operator`); + cy.changeNamespace('openshift-cluster-observability-operator'); + + cy.log(`8.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`8.4. Click on the Kebab icon - Duplicate`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDuplicateOption(); + + cy.log(`8.5. Assert project dropdown options`); + listPersesDashboardsPage.assertDuplicateProjectDropdownExists('openshift-cluster-observability-operator'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('observ-test'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('perses-dev'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('empty-namespace3'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('empty-namespace4'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('openshift-monitoring'); + + cy.log(`8.6. Enter new dashboard name`); + listPersesDashboardsPage.duplicateDashboardEnterName(dashboardName); + listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown('openshift-cluster-observability-operator'); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedAfterDuplicate(dashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`8.7. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`8.8. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + persesDashboardsPage.closeSuccessAlert(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`8.9. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + }); + + it(`9.${perspective.name} perspective - Delete dashboard`, () => { + cy.log(`9.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`9.3. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Dashboard - UP'); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`9.4. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + persesDashboardsPage.closeSuccessAlert(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`9.5. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Dashboard - UP'); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + }); + + it(`10.${perspective.name} perspective - Import button validation - Enabled / Disabled`, () => { + cy.log(`10.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`10.2 change namespace to observ-test`); + cy.changeNamespace('observ-test'); + + cy.log(`10.3. Verify Import button is still enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + + cy.log(`10.4. Verify project dropdown options`); + persesImportDashboardsPage.assertProjectDropdown('openshift-cluster-observability-operator'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace3'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesImportDashboardsPage.clickCancelButton(); + + cy.log(`10.5 change namespace to openshift-cluster-observability-operator`); + cy.changeNamespace('openshift-cluster-observability-operator'); + + cy.log(`10.6. Verify Import button is enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + + cy.log(`10.7 change namespace to All Projects`); + cy.changeNamespace('All Projects'); + + cy.log(`10.8. Verify Import button is enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + }); + + it(`11.${perspective.name} perspective - Import button validation - YAML`, () => { + cy.log(`11.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`11.2 change namespace to observ-test`); + cy.changeNamespace('observ-test'); + + cy.log(`11.3. Verify Import button is still enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + + cy.log(`11.4. Select a project`); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + + cy.log(`11.5. Import dashboard`); + persesImportDashboardsPage.clickImportFileButton(); + persesDashboardsPage.closeSuccessAlert(); + + cy.log(`11.6. Assert dashboard is imported`); + persesDashboardsPage.shouldBeLoadedEditionMode('Testing Perses dashboard - YAML'); + + cy.log(`11.7. Back to list of dashboards`); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`11.8. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - YAML'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`11.9. Import the same dashboard - Duplicated error`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + persesImportDashboardsPage.selectProject('openshift-cluster-observability-operator'); + persesImportDashboardsPage.clickImportFileButton(); + persesImportDashboardsPage.assertDuplicatedDashboardError(); + persesImportDashboardsPage.clickCancelButton(); + + cy.log(`11.10. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - YAML'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`11.11. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - YAML'); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + +} diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user2.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user2.cy.ts new file mode 100644 index 000000000..6c19bf1bf --- /dev/null +++ b/web/cypress/support/perses/99.coo_rbac_perses_user2.cy.ts @@ -0,0 +1,152 @@ +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev } from '../../fixtures/perses/constants'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOORBACPersesTestsDevUser2(perspective: PerspectiveConfig) { + testCOORBACPersesTestsDevUser2(perspective); +} + +/** + * User2 has access to: + * - perses-dev namespace as persesdashboard-viewer-role and persesdatasource-viewer-role + * - no access to openshift-cluster-observability-operator and observ-test namespaces + * - openshift-monitoring as view role + */ +export function testCOORBACPersesTestsDevUser2(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - List Dashboards - Namespace validation and Dashboard search`, () => { + cy.log(`1.1. Namespace validation`); + listPersesDashboardsPage.shouldBeLoaded(); + cy.assertNamespace('All Projects', true); + cy.assertNamespace('openshift-cluster-observability-operator', false); + cy.assertNamespace('observ-test', false); + cy.assertNamespace('empty-namespace3', false); + cy.assertNamespace('empty-namespace4', false); + cy.assertNamespace('openshift-monitoring', true); + cy.assertNamespace('perses-dev', true); + + cy.log(`1.2. All Projects validation - Dashboard search - ${persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[2]} dashboard`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.removeTag(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.removeTag('perses-dev'); + + cy.changeNamespace('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.removeTag(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + + cy.log(`1.3. All Projects validation - Dashboard search - ${persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[2]} dashboard`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.removeTag(persesDashboardsDashboardDropdownCOO.ACCELERATORS_COMMON_METRICS[0]); + + cy.log(`1.4. All Projects validation - Dashboard search - ${persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[2]} dashboard`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.removeTag(persesDashboardsDashboardDropdownCOO.K8S_COMPUTE_RESOURCES_CLUSTER[0]); + + cy.log(`1.5. All Projects validation - Dashboard search - empty state`); + listPersesDashboardsPage.filter.byProject('empty-namespace4'); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.removeTag('empty-namespace4'); + + cy.log(`1.6. All Projects validation - Dashboard search - empty state`); + listPersesDashboardsPage.filter.byProject('openshift-monitoring'); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.removeTag('openshift-monitoring'); + + }); + + it(`2.${perspective.name} perspective - Edit button validation - Not Editable dashboard`, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`2.2 change namespace to perses-dev`); + cy.changeNamespace('perses-dev'); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`2.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`2.4. Click on a dashboard`); + listPersesDashboardsPage.clickDashboard(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.assertEditButtonIsDisabled(); + + }); + + it(`3.${perspective.name} perspective - Create button validation - Disabled`, () => { + cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`3.2. Verify Create button is disabled`); + listPersesDashboardsPage.assertCreateButtonIsDisabled(); + + cy.log(`3.3 change namespace to perses-dev`); + cy.changeNamespace('perses-dev'); + + cy.log(`3.4. Verify Create button is disabled`); + listPersesDashboardsPage.assertCreateButtonIsDisabled(); + + cy.log(`3.5. Change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + listPersesDashboardsPage.assertCreateButtonIsDisabled(); + + }); + + it(`4.${perspective.name} perspective - Kebab icon - Disabled`, () => { + cy.log(`4.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`4.2. Change namespace to perses-dev`); + cy.changeNamespace('perses-dev'); + + cy.log(`4.3. Assert Kebab icon is disabled`); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.assertKebabIconDisabled(); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`4.4. Change namespace to All Projects`); + cy.changeNamespace('All Projects'); + + cy.log(`4.5. Assert Kebab icon is disabled`); + listPersesDashboardsPage.filter.byProject('perses-dev'); + listPersesDashboardsPage.filter.byName(persesDashboardsDashboardDropdownPersesDev.PERSES_DASHBOARD_SAMPLE[0]); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.assertKebabIconDisabled(); + listPersesDashboardsPage.clearAllFilters(); + + }); + + it(`5.${perspective.name} perspective - Import button validation - Disabled`, () => { + cy.log(`5.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`5.2. Change namespace to perses-dev`); + cy.changeNamespace('perses-dev'); + + cy.log(`5.3. Verify Import button is disabled`); + listPersesDashboardsPage.assertImportButtonIsDisabled(); + + cy.log(`5.5. Change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + listPersesDashboardsPage.assertImportButtonIsDisabled(); + + cy.log(`5.6. Change namespace to All Projects`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.assertImportButtonIsDisabled(); + + }); + +} diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user3.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user3.cy.ts new file mode 100644 index 000000000..9d930a0d3 --- /dev/null +++ b/web/cypress/support/perses/99.coo_rbac_perses_user3.cy.ts @@ -0,0 +1,480 @@ +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { persesCreateDashboardsPage } from '../../views/perses-dashboards-create-dashboard'; +import { persesDashboardsAddListVariableSource, persesDashboardSampleQueries, persesDashboardsEmptyDashboard, persesDashboardsTimeRange } from '../../fixtures/perses/constants'; +import { persesDashboardsEditVariables } from '../../views/perses-dashboards-edit-variables'; +import { persesDashboardsPanelGroup } from '../../views/perses-dashboards-panelgroup'; +import { persesDashboardsPanel } from '../../views/perses-dashboards-panel'; +import { persesDashboardsAddListPanelType } from '../../fixtures/perses/constants'; +import { persesImportDashboardsPage } from '../../views/perses-dashboards-import-dashboard'; +import { nav } from '../../views/nav'; +import { persesAriaLabels } from '../../../src/components/data-test'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOORBACPersesTestsDevUser3(perspective: PerspectiveConfig) { + testCOORBACPersesTestsDevUser3(perspective); +} + +let dashboardName = 'Testing Dashboard - UP '; +let randomSuffix = Math.random().toString(5); +dashboardName += randomSuffix; + +/** + * User3 has access to: + * - empty-namespace3 namespace as persesdashboard-editor-role and persesdatasource-editor-role + * - no access to openshift-cluster-observability-operator, observ-test, perses-dev namespaces, empty-namespace4 namespaces + * - openshift-monitoring namespace as view role + */ +export function testCOORBACPersesTestsDevUser3(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - List Dashboards - Namespace validation and Dashboard search`, () => { + cy.log(`1.1. Namespace validation`); + listPersesDashboardsPage.noDashboardsFoundState(); + cy.assertNamespace('All Projects', true); + cy.assertNamespace('openshift-cluster-observability-operator', false); + cy.assertNamespace('observ-test', false); + cy.assertNamespace('perses-dev', false); + cy.assertNamespace('empty-namespace3', true); + cy.assertNamespace('empty-namespace4', false); + cy.assertNamespace('openshift-monitoring', true); + + cy.log(`1.2. All Projects validation - Dashboard search - empty state`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertProjectDropdown('empty-namespace3'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); + + cy.log(`1.3. empty-namespace3 validation - Dashboard search - empty state`); + cy.changeNamespace('empty-namespace3'); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertProjectDropdown('empty-namespace3'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); + + cy.log(`1.4. openshift-monitoring validation - Dashboard search - empty state`); + cy.changeNamespace('openshift-monitoring'); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertProjectDropdown('empty-namespace3'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); + + }); + + /** + * When we have admin permission or editor permission to at least one namespace, + * the Create button is always enabled and Select project dropdown is filtering out namespaces that we do not have access to + */ + it(`2.${perspective.name} perspective - Create button validation - Disabled / Enabled`, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.log(`2.2 change namespace to empty-namespace3`); + cy.changeNamespace('empty-namespace3'); + + cy.log(`2.3. Verify Create button is still enabled`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertProjectDropdown('empty-namespace3'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); + + cy.log(`2.4 change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + + cy.log(`2.5. Verify Create button is enabled`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + + cy.log(`2.6 change namespace to All Projects`); + cy.changeNamespace('All Projects'); + + cy.log(`2.7. Verify Create button is enabled`); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + + }); + + it(`3.${perspective.name} perspective - Create Dashboard with panel groups, panels and variables`, () => { + cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.changeNamespace('openshift-monitoring'); + + cy.log(`3.2. Click on Create button`); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + + cy.log(`3.3. Create Dashboard`); + persesCreateDashboardsPage.selectProject('empty-namespace3'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + cy.log(`3.4. Add Variable`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('interval', false, false, '', '', '', undefined, undefined); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('1m'); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('5m'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('job', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('job'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('instance', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('instance'); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_addSeriesSelector(persesDashboardSampleQueries.CPU_LINE_MULTI_SERIES_SERIES_SELECTOR); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`3.5. Add Panel Group`); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('Panel Group Up', 'Open', ''); + + cy.log(`3.6. Add Panel`); + persesDashboardsPage.clickEditActionButton('AddPanel'); + persesDashboardsPanel.addPanelShouldBeLoaded(); + persesDashboardsPanel.addPanel('Up', 'Panel Group Up', persesDashboardsAddListPanelType.TIME_SERIES_CHART, 'This is a line chart test', 'up'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`3.7. Back and check panel`); + persesDashboardsPage.backToListPersesDashboardsPage(); + cy.changeNamespace('empty-namespace3'); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.panelGroupHeaderAssertion('Panel Group Up', 'Open'); + persesDashboardsPage.assertPanel('Up', 'Panel Group Up', 'Open'); + persesDashboardsPage.assertVariableBeVisible('interval'); + persesDashboardsPage.assertVariableBeVisible('job'); + persesDashboardsPage.assertVariableBeVisible('instance'); + + cy.log(`3.8. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`3.9. Click on Edit Variables button and Delete all variables`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`3.10. Assert variables not exist`); + persesDashboardsPage.assertVariableNotExist('interval'); + persesDashboardsPage.assertVariableNotExist('job'); + persesDashboardsPage.assertVariableNotExist('instance'); + + cy.log(`3.11. Delete Panel`); + persesDashboardsPanel.deletePanel('Up'); + persesDashboardsPanel.clickDeletePanelButton(); + + cy.log(`3.12. Delete Panel Group`); + persesDashboardsPanelGroup.clickPanelGroupAction('Panel Group Up', 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.get('h2').contains(persesDashboardsEmptyDashboard.TITLE).scrollIntoView().should('be.visible'); + cy.get('p').contains(persesDashboardsEmptyDashboard.DESCRIPTION).scrollIntoView().should('be.visible'); + + }); + + it(`4.${perspective.name} perspective - Kebab icon - Enabled / Disabled`, () => { + cy.log(`4.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`4.2. Change namespace to empty-namespace3`); + cy.changeNamespace('empty-namespace3'); + + cy.log(`4.3. Assert Kebab icon is enabled `); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.assertKebabIconOptions(); + listPersesDashboardsPage.clickKebabIcon(); + + cy.log(`4.4. Change namespace to All Projects`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`4.5. Filter by Project and Name`); + listPersesDashboardsPage.filter.byProject('empty-namespace3'); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.assertKebabIconOptions(); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clearAllFilters(); + + }); + + + it(`5.${perspective.name} perspective - Rename to a new dashboard name`, () => { + let renamedDashboardName = 'Renamed dashboard '; + let randomSuffix = Math.random().toString(5); + renamedDashboardName += randomSuffix; + + cy.log(`5.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`5.2. Change namespace to empty-namespace3`); + cy.changeNamespace('empty-namespace3'); + + cy.log(`5.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`5.4. Click on the Kebab icon - Rename`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(renamedDashboardName); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`5.5. Filter by Name`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + listPersesDashboardsPage.filter.byName(renamedDashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickDashboard(renamedDashboardName); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.shouldBeLoadedAfterRename(renamedDashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`5.6. Rename back to the original name`); + cy.changeNamespace('empty-namespace3'); + listPersesDashboardsPage.filter.byName(renamedDashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(dashboardName); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`5.7. Filter by Name`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.shouldBeLoadedAfterRename(dashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + }); + + it(`6.${perspective.name} perspective - Duplicate and verify project dropdown and Delete`, () => { + let duplicatedDashboardName = 'Duplicate dashboard '; + let randomSuffix = Math.random().toString(5); + duplicatedDashboardName += randomSuffix; + + cy.log(`6.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`6.2. Change namespace to empty-namespace3`); + cy.changeNamespace('empty-namespace3'); + + cy.log(`6.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`6.4. Click on the Kebab icon - Duplicate`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDuplicateOption(); + + cy.log(`6.5. Assert project dropdown options`); + listPersesDashboardsPage.assertDuplicateProjectDropdownExists('empty-namespace3'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('openshift-cluster-observability-operator'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('openshift-monitoring'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('observ-test'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('perses-dev'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('empty-namespace4'); + + cy.log(`6.6. Enter new dashboard name`); + listPersesDashboardsPage.duplicateDashboardEnterName(duplicatedDashboardName); + listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown('empty-namespace3'); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(duplicatedDashboardName); + persesDashboardsPage.shouldBeLoadedAfterDuplicate(duplicatedDashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`6.7. Filter by Name`); + listPersesDashboardsPage.filter.byName(duplicatedDashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`6.8. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + persesDashboardsPage.closeSuccessAlert(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`6.9. Filter by Name`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + listPersesDashboardsPage.filter.byName(duplicatedDashboardName); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + }); + + it(`7.${perspective.name} perspective - Delete dashboard`, () => { + cy.log(`7.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`7.2. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`7.3. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + + cy.log(`7.4. Filter by Name`); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + + it(`8.${perspective.name} perspective - Import button validation - Enabled / Disabled`, () => { + cy.log(`8.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.log(`8.2 change namespace to empty-namespace3`); + cy.changeNamespace('empty-namespace3'); + + cy.log(`8.3. Verify Import button is still enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + + cy.log(`8.4. Verify project dropdown options`); + persesImportDashboardsPage.assertProjectDropdown('empty-namespace3'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('openshift-cluster-observability-operator'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('openshift-monitoring'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesImportDashboardsPage.clickCancelButton(); + + cy.log(`8.5 change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + + cy.log(`8.6. Verify Import button is enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + + cy.log(`8.7 change namespace to All Projects`); + cy.changeNamespace('All Projects'); + + cy.log(`8.8. Verify Import button is enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + }); + + it(`9.${perspective.name} perspective - Import button validation - YAML`, () => { + cy.log(`9.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.log(`9.2 change namespace to empty-namespace3`); + cy.changeNamespace('empty-namespace3'); + + cy.log(`9.3. Verify Import button is still enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + + cy.log(`9.4. Select a project`); + persesImportDashboardsPage.selectProject('empty-namespace3'); + + cy.log(`9.5. Import dashboard`); + persesImportDashboardsPage.clickImportFileButton(); + persesDashboardsPage.closeSuccessAlert(); + + cy.log(`9.6. Assert dashboard is imported`); + persesDashboardsPage.shouldBeLoadedEditionMode('Testing Perses dashboard - YAML'); + cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).contains(persesDashboardsTimeRange.LAST_30_MINUTES).scrollIntoView().should('be.visible'); + + cy.log(`9.7. Back to list of dashboards`); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`9.8. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - YAML'); + listPersesDashboardsPage.countDashboards('1'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`9.9. Import the same dashboard - Duplicated error`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.yaml'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + persesImportDashboardsPage.selectProject('empty-namespace3'); + persesImportDashboardsPage.clickImportFileButton(); + persesImportDashboardsPage.assertDuplicatedDashboardError(); + persesImportDashboardsPage.clickCancelButton(); + + cy.log(`9.10. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - YAML'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`9.11. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - YAML'); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + }); + + +} diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user4.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user4.cy.ts new file mode 100644 index 000000000..db76cafc9 --- /dev/null +++ b/web/cypress/support/perses/99.coo_rbac_perses_user4.cy.ts @@ -0,0 +1,72 @@ +import { persesImportDashboardsPage } from '../../views/perses-dashboards-import-dashboard'; +import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOORBACPersesTestsDevUser4(perspective: PerspectiveConfig) { + testCOORBACPersesTestsDevUser4(perspective); +} + +/** + * User4 has access to: + * - empty-namespace4 namespace as persesdashboard-viewer-role and persesdatasource-viewer-role + * - no access to openshift-cluster-observability-operator, observ-test, perses-dev namespaces, empty-namespace3 namespaces + * - openshift-monitoring namespace as view role + */ +export function testCOORBACPersesTestsDevUser4(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - List Dashboards - Namespace validation and Dashboard search`, () => { + cy.log(`1.1. Namespace validation`); + listPersesDashboardsPage.noDashboardsFoundState(); + cy.assertNamespace('All Projects', true); + cy.assertNamespace('openshift-cluster-observability-operator', false); + cy.assertNamespace('observ-test', false); + cy.assertNamespace('perses-dev', false); + cy.assertNamespace('empty-namespace3', false); + cy.assertNamespace('empty-namespace4', true); + cy.assertNamespace('openshift-monitoring', true); + + cy.log(`1.2. All Projects validation - Dashboard search - empty state`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.assertCreateButtonIsDisabled(); + + cy.log(`1.3. empty-namespace4 validation - Dashboard search - empty state`); + cy.changeNamespace('empty-namespace4'); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.assertCreateButtonIsDisabled(); + + cy.log(`1.4. openshift-monitoring validation - Dashboard search - empty state`); + cy.changeNamespace('openshift-monitoring'); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.assertCreateButtonIsDisabled(); + + }); + + it(`2.${perspective.name} perspective - Import button validation - Disabled`, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.log(`2.2 change namespace to empty-namespace4`); + cy.changeNamespace('empty-namespace4'); + + cy.log(`2.3. Verify Import button is disabled`); + listPersesDashboardsPage.assertImportButtonIsDisabled(); + + cy.log(`2.4. Change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + cy.log(`2.5. Verify Import button is disabled`); + listPersesDashboardsPage.assertImportButtonIsDisabled(); + + cy.log(`2.6. Change namespace to All Projects`); + cy.changeNamespace('All Projects'); + cy.log(`2.7. Verify Import button is disabled`); + listPersesDashboardsPage.assertImportButtonIsDisabled(); + + }); + + +} \ No newline at end of file diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user5.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user5.cy.ts new file mode 100644 index 000000000..3ef66916a --- /dev/null +++ b/web/cypress/support/perses/99.coo_rbac_perses_user5.cy.ts @@ -0,0 +1,424 @@ +import { persesDashboardsPage } from '../../views/perses-dashboards'; +import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; +import { persesCreateDashboardsPage } from '../../views/perses-dashboards-create-dashboard'; +import { persesDashboardsAddListVariableSource, persesDashboardSampleQueries, persesDashboardsEmptyDashboard } from '../../fixtures/perses/constants'; +import { persesDashboardsEditVariables } from '../../views/perses-dashboards-edit-variables'; +import { persesDashboardsPanelGroup } from '../../views/perses-dashboards-panelgroup'; +import { persesDashboardsPanel } from '../../views/perses-dashboards-panel'; +import { persesDashboardsAddListPanelType } from '../../fixtures/perses/constants'; +import { persesImportDashboardsPage } from '../../views/perses-dashboards-import-dashboard'; +import { nav } from '../../views/nav'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOORBACPersesTestsDevUser5(perspective: PerspectiveConfig) { + testCOORBACPersesTestsDevUser5(perspective); +} + +let dashboardName = 'Testing Dashboard - UP '; +let randomSuffix = Math.random().toString(5); +dashboardName += randomSuffix; + +/** + * User5 has access to: + * - openshift-monitoring namespace as admin + * - no access to openshift-cluster-observability-operator, observ-test, perses-dev namespaces, empty-namespace3 namespaces, empty-namespace4 namespaces + */ +export function testCOORBACPersesTestsDevUser5(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - List Dashboards - Namespace validation and Dashboard search`, () => { + cy.log(`1.1. Namespace validation`); + listPersesDashboardsPage.noDashboardsFoundState(); + cy.assertNamespace('All Projects', true); + cy.assertNamespace('openshift-monitoring', true); + cy.assertNamespace('openshift-cluster-observability-operator', false); + cy.assertNamespace('observ-test', false); + cy.assertNamespace('perses-dev', false); + cy.assertNamespace('empty-namespace3', false); + cy.assertNamespace('empty-namespace4', false); + + cy.log(`1.2. All Projects validation - Dashboard search - empty state`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertProjectDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace3'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); + + cy.log(`1.3. openshift-monitoring validation - Dashboard search - empty state`); + cy.changeNamespace('openshift-monitoring'); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.assertCreateButtonIsEnabled(); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + persesCreateDashboardsPage.assertProjectDropdown('openshift-monitoring'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('openshift-cluster-observability-operator'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace3'); + persesCreateDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesCreateDashboardsPage.createDashboardDialogCancelButton(); + + }); + + it(`2.${perspective.name} perspective - Create Dashboard with panel groups, panels and variables`, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.changeNamespace('openshift-monitoring'); + + cy.log(`2.2. Click on Create button`); + listPersesDashboardsPage.clickCreateButton(); + persesCreateDashboardsPage.createDashboardShouldBeLoaded(); + + cy.log(`2.3. Create Dashboard`); + persesCreateDashboardsPage.selectProject('openshift-monitoring'); + persesCreateDashboardsPage.enterDashboardName(dashboardName); + persesCreateDashboardsPage.createDashboardDialogCreateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(dashboardName); + persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard(); + + cy.log(`2.4. Add Variable`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('interval', false, false, '', '', '', undefined, undefined); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('1m'); + persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue('5m'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('job', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('job'); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Add Variable'); + persesDashboardsEditVariables.addListVariable('instance', false, false, '', '', '', persesDashboardsAddListVariableSource.PROMETHEUS_LABEL_VARIABLE, undefined); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName('instance'); + persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_addSeriesSelector(persesDashboardSampleQueries.CPU_LINE_MULTI_SERIES_SERIES_SELECTOR); + persesDashboardsEditVariables.clickButton('Add'); + + persesDashboardsEditVariables.clickButton('Apply'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`2.5. Add Panel Group`); + persesDashboardsPage.clickEditButton(); + persesDashboardsPage.clickEditActionButton('AddGroup'); + persesDashboardsPanelGroup.addPanelGroup('Panel Group Up', 'Open', ''); + + cy.log(`2.6. Add Panel`); + persesDashboardsPage.clickEditActionButton('AddPanel'); + persesDashboardsPanel.addPanelShouldBeLoaded(); + persesDashboardsPanel.addPanel('Up', 'Panel Group Up', persesDashboardsAddListPanelType.TIME_SERIES_CHART, 'This is a line chart test', 'up'); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.log(`2.7. Back and check panel`); + persesDashboardsPage.backToListPersesDashboardsPage(); + cy.changeNamespace('openshift-monitoring'); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.panelGroupHeaderAssertion('Panel Group Up', 'Open'); + persesDashboardsPage.assertPanel('Up', 'Panel Group Up', 'Open'); + persesDashboardsPage.assertVariableBeVisible('interval'); + persesDashboardsPage.assertVariableBeVisible('job'); + persesDashboardsPage.assertVariableBeVisible('instance'); + + cy.log(`2.8. Click on Edit button`); + persesDashboardsPage.clickEditButton(); + + cy.log(`2.9. Click on Edit Variables button and Delete all variables`); + persesDashboardsPage.clickEditActionButton('EditVariables'); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickDeleteVariableButton(0); + persesDashboardsEditVariables.clickButton('Apply'); + + cy.log(`2.10. Assert variables not exist`); + persesDashboardsPage.assertVariableNotExist('interval'); + persesDashboardsPage.assertVariableNotExist('job'); + persesDashboardsPage.assertVariableNotExist('instance'); + + cy.log(`2.11. Delete Panel`); + persesDashboardsPanel.deletePanel('Up'); + persesDashboardsPanel.clickDeletePanelButton(); + + cy.log(`2.12. Delete Panel Group`); + persesDashboardsPanelGroup.clickPanelGroupAction('Panel Group Up', 'delete'); + persesDashboardsPanelGroup.clickDeletePanelGroupButton(); + persesDashboardsPage.clickEditActionButton('Save'); + + cy.get('h2').contains(persesDashboardsEmptyDashboard.TITLE).scrollIntoView().should('be.visible'); + cy.get('p').contains(persesDashboardsEmptyDashboard.DESCRIPTION).scrollIntoView().should('be.visible'); + + }); + + it(`3.${perspective.name} perspective - Kebab icon - Enabled / Disabled`, () => { + cy.log(`3.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`3.2. Change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + + cy.log(`3.3. Assert Kebab icon is enabled `); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.assertKebabIconOptions(); + listPersesDashboardsPage.clickKebabIcon(); + + cy.log(`3.4. Change namespace to All Projects`); + cy.changeNamespace('All Projects'); + listPersesDashboardsPage.clearAllFilters(); + + cy.log(`3.5. Filter by Project and Name`); + listPersesDashboardsPage.filter.byProject('openshift-monitoring'); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.assertKebabIconOptions(); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clearAllFilters(); + + }); + + + it(`4.${perspective.name} perspective - Rename to a new dashboard name`, () => { + let renamedDashboardName = 'Renamed dashboard '; + let randomSuffix = Math.random().toString(5); + renamedDashboardName += randomSuffix; + + cy.log(`4.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`4.2. Change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + + cy.log(`4.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`4.4. Click on the Kebab icon - Rename`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(renamedDashboardName); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(5000); + + cy.log(`4.5. Filter by Name`); + listPersesDashboardsPage.filter.byName(renamedDashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickDashboard(renamedDashboardName); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.shouldBeLoadedAfterRename(renamedDashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`4.6. Rename back to the original name`); + cy.changeNamespace('openshift-monitoring'); + listPersesDashboardsPage.filter.byName(renamedDashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickRenameDashboardOption(); + listPersesDashboardsPage.renameDashboardEnterName(dashboardName); + listPersesDashboardsPage.renameDashboardRenameButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(5000); + + cy.log(`4.7. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickDashboard(dashboardName); + persesDashboardsPage.shouldBeLoaded1(); + persesDashboardsPage.shouldBeLoadedAfterRename(dashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + }); + + it(`5.${perspective.name} perspective - Duplicate and verify project dropdown and Delete`, () => { + let duplicatedDashboardName = 'Duplicate dashboard '; + let randomSuffix = Math.random().toString(5); + duplicatedDashboardName += randomSuffix; + + cy.log(`5.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`5.2. Change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + + cy.log(`5.3. Filter by Name`); + listPersesDashboardsPage.filter.byName(dashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`5.4. Click on the Kebab icon - Duplicate`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDuplicateOption(); + + cy.log(`5.5. Assert project dropdown options`); + listPersesDashboardsPage.assertDuplicateProjectDropdownExists('openshift-monitoring'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('openshift-cluster-observability-operator'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('empty-namespace3'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('observ-test'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('perses-dev'); + listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists('empty-namespace4'); + + cy.log(`5.6. Enter new dashboard name`); + listPersesDashboardsPage.duplicateDashboardEnterName(duplicatedDashboardName); + listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown('openshift-monitoring'); + listPersesDashboardsPage.duplicateDashboardDuplicateButton(); + persesDashboardsPage.shouldBeLoadedEditionMode(duplicatedDashboardName); + persesDashboardsPage.shouldBeLoadedAfterDuplicate(duplicatedDashboardName); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`5.7. Filter by Name`); + listPersesDashboardsPage.filter.byName(duplicatedDashboardName); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`5.8. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`5.9. Filter by Name`); + listPersesDashboardsPage.filter.byName(duplicatedDashboardName); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + }); + + it(`6.${perspective.name} perspective - Delete dashboard`, () => { + cy.log(`6.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.shouldBeLoaded(); + + cy.log(`6.2. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Dashboard - UP'); + listPersesDashboardsPage.countDashboards('1'); + + cy.log(`6.3. Click on the Kebab icon - Delete`); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + + cy.log(`6.4. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Dashboard - UP'); + listPersesDashboardsPage.countDashboards('0'); + listPersesDashboardsPage.clearAllFilters(); + + }); + + it(`7.${perspective.name} perspective - Import button validation - Enabled / Disabled`, () => { + cy.log(`7.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.log(`7.2 change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + + cy.log(`7.3. Verify Import button is still enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + + cy.log(`7.4. Verify project dropdown options`); + persesImportDashboardsPage.assertProjectDropdown('openshift-monitoring'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('openshift-cluster-observability-operator'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace3'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('observ-test'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('perses-dev'); + persesImportDashboardsPage.assertProjectNotExistsInDropdown('empty-namespace4'); + persesImportDashboardsPage.clickCancelButton(); + + cy.log(`7.5. Change namespace to All Projects`); + cy.changeNamespace('All Projects'); + + cy.log(`7.6. Verify Import button is enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + }); + + it(`8.${perspective.name} perspective - Import button validation - JSON`, () => { + cy.log(`8.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.log(`8.2 change namespace to openshift-monitoring`); + cy.changeNamespace('openshift-monitoring'); + + cy.log(`8.3. Verify Import button is enabled`); + listPersesDashboardsPage.assertImportButtonIsEnabled(); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + + cy.log(`8.4. Select a project`); + persesImportDashboardsPage.selectProject('openshift-monitoring'); + + cy.log(`8.5. Import dashboard`); + persesImportDashboardsPage.clickImportFileButton(); + persesDashboardsPage.closeSuccessAlert(); + + cy.log(`8.6. Assert dashboard is imported`); + persesDashboardsPage.shouldBeLoadedEditionMode('Testing Perses dashboard - JSON'); + + cy.log(`8.7. Back to list of dashboards`); + persesDashboardsPage.backToListPersesDashboardsPage(); + + cy.log(`8.8. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - JSON'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clearAllFilters(); + cy.wait(2000); + + cy.log(`8.9. Import the same dashboard - Duplicated error`); + listPersesDashboardsPage.clickImportButton(); + persesImportDashboardsPage.importDashboardShouldBeLoaded(); + persesImportDashboardsPage.uploadFile('./cypress/fixtures/coo/coo141_perses/import/testing-perses-dashboard.json'); + persesImportDashboardsPage.assertPersesDashboardDetected(); + persesImportDashboardsPage.selectProject('openshift-monitoring'); + persesImportDashboardsPage.clickImportFileButton(); + persesImportDashboardsPage.assertDuplicatedDashboardError(); + persesImportDashboardsPage.clickCancelButton(); + + cy.log(`8.10. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - JSON'); + listPersesDashboardsPage.countDashboards('1'); + listPersesDashboardsPage.clickKebabIcon(); + listPersesDashboardsPage.clickDeleteOption(); + listPersesDashboardsPage.deleteDashboardDeleteButton(); + listPersesDashboardsPage.emptyState(); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(2000); + + cy.log(`8.11. Filter by Name`); + listPersesDashboardsPage.filter.byName('Testing Perses dashboard - JSON'); + listPersesDashboardsPage.countDashboards('0'); + nav.sidenav.clickNavLink(['Observe', 'Alerting']); + nav.sidenav.clickNavLink(['Observe', 'Dashboards (Perses)']); + cy.wait(2000); + }); + + +} diff --git a/web/cypress/support/perses/99.coo_rbac_perses_user6.cy.ts b/web/cypress/support/perses/99.coo_rbac_perses_user6.cy.ts new file mode 100644 index 000000000..b3f5eabc6 --- /dev/null +++ b/web/cypress/support/perses/99.coo_rbac_perses_user6.cy.ts @@ -0,0 +1,36 @@ +import { listPersesDashboardsPage } from '../../views/perses-dashboards-list-dashboards'; + +export interface PerspectiveConfig { + name: string; + beforeEach?: () => void; +} + +export function runCOORBACPersesTestsDevUser6(perspective: PerspectiveConfig) { + testCOORBACPersesTestsDevUser6(perspective); +} + +/** + * User6 has access to: + * - no access to any namespaces + */ +export function testCOORBACPersesTestsDevUser6(perspective: PerspectiveConfig) { + + it(`1.${perspective.name} perspective - List Dashboards - Namespace validation and Dashboard search`, () => { + cy.log(`1.1. Namespace validation`); + listPersesDashboardsPage.noDashboardsFoundState(); + listPersesDashboardsPage.projectDropdownNotExists(); + + cy.log(`1.2. Create button validation`); + listPersesDashboardsPage.assertCreateButtonIsDisabled(); + }); + + it(`2.${perspective.name} perspective - Import button validation - Disabled`, () => { + cy.log(`2.1. use sidebar nav to go to Observe > Dashboards (Perses)`); + listPersesDashboardsPage.noDashboardsFoundState(); + + cy.log(`2.2. Verify Import button is disabled`); + listPersesDashboardsPage.assertImportButtonIsDisabled(); + }); + + +} \ No newline at end of file diff --git a/web/cypress/support/test-tags.d.ts b/web/cypress/support/test-tags.d.ts new file mode 100644 index 000000000..4b10efaa5 --- /dev/null +++ b/web/cypress/support/test-tags.d.ts @@ -0,0 +1,22 @@ +type BasicTag = '@smoke' | '@demo' | '@flaky' | '@xfail' | '@slow'; + +type HighLevelComponentTag = '@monitoring' | '@incidents' | '@coo' | '@virtualization' | '@alerts' | '@metrics' | '@dashboards'; + +type SpecificFeatureTag = `@${string}-${string}`; + +type JiraTag = `@JIRA-${string}`; + +type AllowedTag = BasicTag | HighLevelComponentTag | SpecificFeatureTag | JiraTag; +type TestTags = AllowedTag | AllowedTag[]; + +declare namespace Cypress { + interface SuiteConfigOverrides { + tags?: TestTags; + } + interface TestConfigOverrides { + tags?: TestTags; + } +} + +export {}; + diff --git a/web/cypress/views/alerting-rule-list-page.ts b/web/cypress/views/alerting-rule-list-page.ts index 1106edb85..807956c41 100644 --- a/web/cypress/views/alerting-rule-list-page.ts +++ b/web/cypress/views/alerting-rule-list-page.ts @@ -55,7 +55,7 @@ export const alertingRuleListPage = { }, emptyState: () => { cy.log('alertingRuleListPage.emptyState'); - cy.byTestID(DataTestIDs.EmptyBoxBody).contains('No Alerting rules found').should('be.visible'); + cy.byTestID(DataTestIDs.EmptyBoxBody).contains('No alerting rules found').should('be.visible'); cy.bySemanticElement('button', 'Clear all filters').should('not.exist'); cy.byOUIAID(DataTestIDs.Table).should('not.exist'); }, diff --git a/web/cypress/views/common.ts b/web/cypress/views/common.ts index 49b2dfa1b..80e21a4aa 100644 --- a/web/cypress/views/common.ts +++ b/web/cypress/views/common.ts @@ -6,8 +6,14 @@ 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).should('be.visible'); + 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) => { diff --git a/web/cypress/views/details-page.ts b/web/cypress/views/details-page.ts index 1b268b1be..6fc1e5cfe 100644 --- a/web/cypress/views/details-page.ts +++ b/web/cypress/views/details-page.ts @@ -79,6 +79,9 @@ export const detailsPage = { cy.log('detailsPage.clickOnSilenceByKebab'); try { cy.byLegacyTestID(DataTestIDs.SilenceResourceLink).scrollIntoView(); + cy.get('table').should('be.visible'); + cy.wait(2000); + cy.get('table').find(Classes.SilenceKebabDropdown).should('be.visible'); cy.get('table').find(Classes.SilenceKebabDropdown).should('be.visible').click({force: true}); } catch (error) { cy.log(`${error.message}`); diff --git a/web/cypress/views/legacy-dashboards.ts b/web/cypress/views/legacy-dashboards.ts index c5125efb0..83691d9fa 100644 --- a/web/cypress/views/legacy-dashboards.ts +++ b/web/cypress/views/legacy-dashboards.ts @@ -10,14 +10,14 @@ export const legacyDashboardsPage = { commonPages.titleShouldHaveText(MonitoringPageTitles.DASHBOARDS); cy.byTestID(LegacyDashboardPageTestIDs.TimeRangeDropdown).contains(LegacyDashboardsTimeRange.LAST_30_MINUTES).should('be.visible'); cy.byTestID(LegacyDashboardPageTestIDs.PollIntervalDropdown).contains(MonitoringRefreshInterval.THIRTY_SECONDS).should('be.visible'); - //TODO: Uncomment when OU-949 gets merged - // cy.byLegacyTestID('namespace-bar-dropdown').find('span').invoke('text').then((text) => { - // if (text === 'Project: All Projects') { - // cy.byTestID(LegacyDashboardPageTestIDs.DashboardDropdown).find('input').should('have.value', LegacyDashboardsDashboardDropdown.API_PERFORMANCE[0]).and('be.visible'); - // } else { - // cy.byTestID(LegacyDashboardPageTestIDs.DashboardDropdown).find('input').should('have.value', LegacyDashboardsDashboardDropdownNamespace.K8S_COMPUTE_RESOURCES_NAMESPACE_PODS[0]).and('be.visible'); - // } - // }); + + cy.byLegacyTestID('namespace-bar-dropdown').find('span').invoke('text').then((text) => { + if (text === 'Project: All Projects') { + cy.byTestID(LegacyDashboardPageTestIDs.DashboardDropdown).find('input').should('have.value', LegacyDashboardsDashboardDropdown.API_PERFORMANCE[0]).and('be.visible'); + } else { + cy.byTestID(LegacyDashboardPageTestIDs.DashboardDropdown).find('input').should('have.value', LegacyDashboardsDashboardDropdownNamespace.K8S_COMPUTE_RESOURCES_NAMESPACE_PODS[0]).and('be.visible'); + } + }); }, clickTimeRangeDropdown: (timeRange: LegacyDashboardsTimeRange) => { @@ -59,6 +59,7 @@ export const legacyDashboardsPage = { clickDashboardDropdown: (dashboard: keyof typeof LegacyDashboardsDashboardDropdown) => { cy.log('legacyDashboardsPage.clickDashboardDropdown'); cy.byTestID(LegacyDashboardPageTestIDs.DashboardDropdown).find('button').scrollIntoView().should('be.visible').click(); + cy.wait(2000); cy.get(Classes.MenuItem).contains(LegacyDashboardsDashboardDropdown[dashboard][0]).should('be.visible').click(); }, @@ -76,25 +77,25 @@ export const legacyDashboardsPage = { cy.byTestID(LegacyDashboardPageTestIDs.DashboardDropdown).find('button').should('be.visible').click(); }, - dashboardAPIPerformancePanelAssertion: (panel: API_PERFORMANCE_DASHBOARD_PANELS) => { + dashboardAPIPerformancePanelAssertion: () => { cy.log('legacyDashboardsPage.dashboardAPIPerformancePanelAssertion'); function formatDataTestID(panel: API_PERFORMANCE_DASHBOARD_PANELS): string { return panel.toLowerCase().replace(/\s+/g, '-').concat('-chart'); } - const dataTestID = Object.values(API_PERFORMANCE_DASHBOARD_PANELS).map(formatDataTestID); - dataTestID.forEach((dataTestID) => { + const dataTestIDs = Object.values(API_PERFORMANCE_DASHBOARD_PANELS).map(formatDataTestID); + dataTestIDs.forEach((dataTestID) => { cy.log('Data test ID: ' + dataTestID); cy.byTestID(dataTestID).scrollIntoView().should('be.visible'); }); }, - dashboardKubernetesComputeResourcesNamespacePodsPanelAssertion: (panel: KUBERNETES_COMPUTE_RESOURCES_NAMESPACE_PODS_PANELS) => { + dashboardKubernetesComputeResourcesNamespacePodsPanelAssertion: () => { cy.log('legacyDashboardsPage.dashboardKubernetesComputeResourcesNamespacePodsPanelAssertion'); function formatDataTestID(panel: KUBERNETES_COMPUTE_RESOURCES_NAMESPACE_PODS_PANELS): string { return panel.toLowerCase().replace(/\s+/g, '-').concat('-chart'); } - const dataTestID = Object.values(KUBERNETES_COMPUTE_RESOURCES_NAMESPACE_PODS_PANELS).map(formatDataTestID); - dataTestID.forEach((dataTestID) => { + const dataTestIDs = Object.values(KUBERNETES_COMPUTE_RESOURCES_NAMESPACE_PODS_PANELS).map(formatDataTestID); + dataTestIDs.forEach((dataTestID) => { cy.log('Data test ID: ' + dataTestID); cy.byTestID(dataTestID).scrollIntoView().should('be.visible'); }); diff --git a/web/cypress/views/list-page.ts b/web/cypress/views/list-page.ts index 876c8ce22..f4b910f17 100644 --- a/web/cypress/views/list-page.ts +++ b/web/cypress/views/list-page.ts @@ -272,7 +272,7 @@ export const listPage = { }, emptyState: () => { cy.log('listPage.emptyState'); - cy.byTestID(DataTestIDs.EmptyBoxBody).contains('No Alerts found').should('be.visible'); + cy.byTestID(DataTestIDs.EmptyBoxBody).contains('No alerts found').should('be.visible'); cy.bySemanticElement('button', 'Clear all filters').should('not.exist'); cy.byTestID(DataTestIDs.DownloadCSVButton).should('not.exist'); cy.byOUIAID(DataTestIDs.Table).should('not.exist'); diff --git a/web/cypress/views/metrics.ts b/web/cypress/views/metrics.ts index 5acff5407..d2cf4bac6 100644 --- a/web/cypress/views/metrics.ts +++ b/web/cypress/views/metrics.ts @@ -142,7 +142,18 @@ export const metricsPage = { cy.get(Classes.MenuItem).contains(interval).should('be.visible'); }); + cy.get(Classes.MenuItem).contains(MonitoringRefreshInterval.FIFTEEN_SECONDS).click(); + cy.byTestID(DataTestIDs.MetricDropdownPollInterval).should( + 'contain', + MonitoringRefreshInterval.FIFTEEN_SECONDS, + ); + cy.byTestID(DataTestIDs.MetricDropdownPollInterval).should('be.visible').click(); + cy.get(Classes.MenuItem).contains(MonitoringRefreshInterval.REFRESH_OFF).click(); + cy.byTestID(DataTestIDs.MetricDropdownPollInterval).should( + 'contain', + MonitoringRefreshInterval.REFRESH_OFF, + ); }, clickAddQueryButton: () => { @@ -278,23 +289,19 @@ export const metricsPage = { clickGraphTimespanDropdown: (timespan: GraphTimespan) => { cy.log('metricsPage.clickGraphTimespanDropdown'); - cy.byTestID(DataTestIDs.MetricGraphTimespanDropdown).should('be.visible').click(); + cy.byTestID(DataTestIDs.MetricGraphTimespanDropdown).scrollIntoView().should('be.visible').click(); 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: () => { @@ -310,7 +317,7 @@ export const metricsPage = { clickResetZoomButton: () => { cy.log('metricsPage.clickResetZoomButton'); - cy.byTestID(DataTestIDs.MetricResetZoomButton).should('be.visible').click(); + cy.byTestID(DataTestIDs.MetricResetZoomButton).scrollIntoView().should('be.visible').click(); }, clickHideGraphButton: () => { @@ -327,18 +334,18 @@ export const metricsPage = { clickDisconnectedCheckbox: () => { cy.log('metricsPage.clickDisconnectedCheckbox'); - cy.byTestID(DataTestIDs.MetricDisconnectedCheckbox).should('be.visible').click(); + cy.byTestID(DataTestIDs.MetricDisconnectedCheckbox).scrollIntoView().should('be.visible').click(); }, clickStackedCheckbox: () => { cy.log('metricsPage.clickStackedCheckbox'); - cy.byTestID(DataTestIDs.MetricStackedCheckbox).should('be.visible').click(); + cy.byTestID(DataTestIDs.MetricStackedCheckbox).scrollIntoView().should('be.visible').click(); }, clickStackedCheckboxAndAssert: () => { cy.log('metricsPage.clickStackedCheckboxAndAssert'); cy.get('[id^="' + IDs.ChartAxis1ChartLabel + '"]').invoke('text').as('yAxisLabel'); - cy.byTestID(DataTestIDs.MetricStackedCheckbox).should('be.visible').click(); + cy.byTestID(DataTestIDs.MetricStackedCheckbox).scrollIntoView().should('be.visible').click(); cy.get('[id^="' + IDs.ChartAxis1ChartLabel + '"]').then(() => { cy.get('@yAxisLabel').then((value) => { cy.get('[id^="' + IDs.ChartAxis1ChartLabel + '"]').should('not.contain', value); @@ -350,7 +357,7 @@ export const metricsPage = { graphCardInlineInfoAssertion: (visible: boolean) => { cy.log('metricsPage.graphCardInlineInfoAssertion'); if (visible) { - cy.get(Classes.GraphCardInlineInfo).should('be.visible'); + cy.get(Classes.GraphCardInlineInfo).scrollIntoView().should('be.visible'); } else { cy.get(Classes.GraphCardInlineInfo).should('not.exist'); } @@ -358,7 +365,7 @@ export const metricsPage = { predefinedQueriesAssertion: () => { cy.log('metricsPage.predefinedQueriesAssertion'); - cy.byTestID(DataTestIDs.TypeaheadSelectInput).should('be.visible').click(); + cy.byTestID(DataTestIDs.TypeaheadSelectInput).scrollIntoView().should('be.visible').click(); const queries = Object.values(MetricsPagePredefinedQueries); queries.forEach((query) => { @@ -369,7 +376,7 @@ export const metricsPage = { clickPredefinedQuery: (query: MetricsPagePredefinedQueries) => { cy.log('metricsPage.clickPredefinedQuery'); - cy.byTestID(DataTestIDs.TypeaheadSelectInput).should('be.visible').click(); + cy.byTestID(DataTestIDs.TypeaheadSelectInput).scrollIntoView().should('be.visible').click(); cy.get(Classes.MetricsPagePredefinedQueriesMenuItem).contains(query).should('be.visible').click(); }, @@ -430,46 +437,50 @@ export const metricsPage = { */ graphAxisXAssertion: (graphTimespan: GraphTimespan) => { cy.log('metricsPage.graphAxisAssertion'); - - switch (graphTimespan) { - case GraphTimespan.FIVE_MINUTES: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 20); - break; - case GraphTimespan.FIFTEEN_MINUTES: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 15); - break; - case GraphTimespan.THIRTY_MINUTES: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length.lte', 30); - break; - case GraphTimespan.ONE_HOUR: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 12); - break; - case GraphTimespan.TWO_HOURS: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 24); - break; - case GraphTimespan.SIX_HOURS: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 12); - break; - case GraphTimespan.TWELVE_HOURS: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 12); - break; - case GraphTimespan.ONE_DAY: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 24); - break; - case GraphTimespan.TWO_DAYS: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 16); - break; - case GraphTimespan.ONE_WEEK: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 14); - break; - case GraphTimespan.TWO_WEEKS: - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 14); - break; - default: //30m is default - cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 15); - break; - } - + cy.get('body').then($body => { + if ($body.find('[id^="' + IDs.ChartAxis0ChartLabel + '"]').length > 0) { + switch (graphTimespan) { + case GraphTimespan.FIVE_MINUTES: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 20); + break; + case GraphTimespan.FIFTEEN_MINUTES: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 15); + break; + case GraphTimespan.THIRTY_MINUTES: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length.lte', 30); + break; + case GraphTimespan.ONE_HOUR: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 12); + break; + case GraphTimespan.TWO_HOURS: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 24); + break; + case GraphTimespan.SIX_HOURS: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 12); + break; + case GraphTimespan.TWELVE_HOURS: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 12); + break; + case GraphTimespan.ONE_DAY: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 24); + break; + case GraphTimespan.TWO_DAYS: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 16); + break; + case GraphTimespan.ONE_WEEK: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 14); + break; + case GraphTimespan.TWO_WEEKS: + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 14); + break; + default: //30m is default + cy.get('[id^="' + IDs.ChartAxis0ChartLabel + '"]').should('have.length', 15); + break; + } + } else { + cy.byTestID(DataTestIDs.MetricGraphNoDatapointsFound).scrollIntoView().contains(MetricGraphEmptyState.NO_DATAPOINTS_FOUND).should('be.visible'); + } + }); }, enterQueryInput: (index: number, query: string) => { diff --git a/web/cypress/views/nav.ts b/web/cypress/views/nav.ts index ae7b580ca..b4543083e 100644 --- a/web/cypress/views/nav.ts +++ b/web/cypress/views/nav.ts @@ -4,14 +4,29 @@ export const nav = { clickNavLink: (path: string[]) => { cy.log('Click navLink - ' + `${path}`); cy.clickNavLink(path); + cy.wait(2000); }, switcher: { - changePerspectiveTo: (perspective: string) => { - cy.log('Switch perspective - ' + `${perspective}`); - cy.byLegacyTestID('perspective-switcher-toggle').scrollIntoView().should('be.visible'); - cy.byLegacyTestID('perspective-switcher-toggle').scrollIntoView().should('be.visible').click({force: true}); - cy.byLegacyTestID('perspective-switcher-menu-option').contains(perspective).should('be.visible'); - cy.byLegacyTestID('perspective-switcher-menu-option').contains(perspective).should('be.visible').click({force: true}); + changePerspectiveTo: (...perspectives: string[]) => { + cy.get('body').then((body) => { + if (body.find('button[data-test-id="perspective-switcher-toggle"]:visible').length > 0) { + cy.byLegacyTestID('perspective-switcher-toggle').scrollIntoView().click({ force: true }); + + cy.get('[data-test-id="perspective-switcher-menu-option"]').then(($options) => { + const foundPerspective = perspectives.find(p => $options.text().includes(p)); + if (foundPerspective) { + cy.byLegacyTestID('perspective-switcher-menu-option') + .contains(foundPerspective) + .click({ force: true }); + } else { + cy.log('No matching perspective found'); + cy.get('body').type('{esc}'); + } + }); + + } + }); + cy.wait(2000); }, shouldHaveText: (perspective: string) => { cy.log('Should have text - ' + `${perspective}`); @@ -26,6 +41,7 @@ export const nav = { */ switchTab: (tabname: string) => { cy.get(Classes.HorizontalNav).contains(tabname).should('be.visible').click(); + cy.wait(2000); } } }; diff --git a/web/cypress/views/perses-dashboards-create-dashboard.ts b/web/cypress/views/perses-dashboards-create-dashboard.ts new file mode 100644 index 000000000..55a43389e --- /dev/null +++ b/web/cypress/views/perses-dashboards-create-dashboard.ts @@ -0,0 +1,75 @@ +import { Classes, IDs, persesAriaLabels } from "../../src/components/data-test"; +import { persesCreateDashboard, persesDashboardsModalTitles } from "../fixtures/perses/constants"; + +export const persesCreateDashboardsPage = { + + createDashboardShouldBeLoaded: () => { + cy.log('persesCreateDashboardsPage.createDashboardShouldBeLoaded'); + cy.byPFRole('dialog').find('h1').should('have.text', persesDashboardsModalTitles.CREATE_DASHBOARD); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible'); + cy.get('#' + IDs.persesDashboardCreateDashboardName).should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Create').should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Cancel').should('be.visible'); + }, + + selectProject: (project: string) => { + cy.log('persesCreateDashboardsPage.selectProject'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible').click({ force: true }); + }, + + assertProjectDropdown: (project: string) => { + cy.log('persesCreateDashboardsPage.assertProjectDropdown'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + }, + + assertProjectNotExistsInDropdown: (project: string) => { + cy.log('persesCreateDashboardsPage.assertProjectNotExistsInDropdown'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + cy.byPFRole('listbox').find('li').then((items) => { + items.each((index, item) => { + cy.log('Project: ' + item.innerText); + if (item.innerText === project) { + expect(item).to.not.exist; + } + }); + }); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + }, + + enterDashboardName: (name: string) => { + cy.log('persesCreateDashboardsPage.enterDashboardName'); + cy.get('#' + IDs.persesDashboardCreateDashboardName).should('be.visible').clear().type(name); + }, + + createDashboardDialogCreateButton: () => { + cy.log('persesCreateDashboardsPage.clickCreateButton'); + cy.byPFRole('dialog').find('button').contains('Create').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertMaxLengthValidation: () => { + cy.log('persesCreateDashboardsPage.assertMaxLengthValidation'); + cy.byPFRole('dialog').find('h4').should('have.text', persesCreateDashboard.DIALOG_MAX_LENGTH_VALIDATION).should('be.visible'); + }, + + assertDuplicatedNameValidation: (dashboardName: string) => { + cy.log('persesCreateDashboardsPage.assertDuplicatedNameValidation'); + if (dashboardName.includes(' ')) { + cy.byPFRole('dialog').find('h4').should('have.text', persesCreateDashboard.DIALOG_DUPLICATED_NAME_BKD_VALIDATION).should('be.visible'); + } else { + cy.byPFRole('dialog').find(Classes.PersesCreateDashboardDashboardNameError).should('have.text', `${persesCreateDashboard.DIALOG_DUPLICATED_NAME_PF_VALIDATION_PREFIX}"${dashboardName}"${persesCreateDashboard.DIALOG_DUPLICATED_NAME_PF_VALIDATION_SUFFIX}`).should('be.visible'); + } + }, + + createDashboardDialogCancelButton: () => { + cy.log('persesCreateDashboardsPage.clickCancelButton'); + cy.byPFRole('dialog').find('button').contains('Cancel').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + +} 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..5aaeb4c0e --- /dev/null +++ b/web/cypress/views/perses-dashboards-edit-variables.ts @@ -0,0 +1,168 @@ +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); + } + } + }, + + addListVariable_staticListVariable_enterValue: (value: string) => { + cy.log('persesDashboardsEditVariables.addListVariable_staticListVariable_enterValue'); + cy.wait(2000); + cy.get('h6').contains('List Options').next('div').eq(0).find('input[role="combobox"]').click().type(value+'{enter}'); + cy.wait(2000); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardAddVariableRunQueryButton).click(); + cy.wait(2000); + cy.get('h4').contains('Preview Values').parent('div').siblings('div').contains(value).should('be.visible'); + }, + + addListVariable_promLabelValuesVariable_enterLabelName: (labelName: string) => { + cy.log('persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_enterLabelName'); + cy.wait(2000); + cy.get('label').contains('Label Name').next('div').find('input').click().type(labelName); + cy.wait(2000); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardAddVariableRunQueryButton).click(); + cy.wait(2000); + }, + + addListVariable_promLabelValuesVariable_addSeriesSelector: (seriesSelector: string) => { + cy.log('persesDashboardsEditVariables.addListVariable_promLabelValuesVariable_addSeriesSelector'); + cy.wait(2000); + cy.bySemanticElement('button', 'Add Series Selector').click(); + cy.get('label').contains('Series Selector').next('div').find('input').click().type(seriesSelector); + cy.wait(2000); + cy.byDataTestID(persesMUIDataTestIDs.editDashboardAddVariableRunQueryButton).click(); + cy.wait(2000); + }, + + /** + * + * @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-import-dashboard.ts b/web/cypress/views/perses-dashboards-import-dashboard.ts new file mode 100644 index 000000000..9f4c83acf --- /dev/null +++ b/web/cypress/views/perses-dashboards-import-dashboard.ts @@ -0,0 +1,117 @@ +import { Classes, IDs, persesAriaLabels } from "../../src/components/data-test"; +import { persesCreateDashboard, persesDashboardsImportDashboard, persesDashboardsModalTitles } from "../fixtures/perses/constants"; + +export const persesImportDashboardsPage = { + + importDashboardShouldBeLoaded: () => { + cy.log('persesImportDashboardsPage.importDashboardShouldBeLoaded'); + cy.wait(2000); + cy.byPFRole('dialog').find('h1').should('have.text', persesDashboardsModalTitles.IMPORT_DASHBOARD); + cy.bySemanticElement('label').contains(persesDashboardsImportDashboard.DIALOG_TITLE).should('be.visible'); + cy.bySemanticElement('span').contains(persesDashboardsImportDashboard.DIALOG_UPLOAD_JSON_YAML_FILE).should('be.visible'); + cy.get('#' + IDs.persesDashboardImportDashboardUploadFileInput).should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Upload').should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Clear').should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Import').should('be.visible'); + cy.byPFRole('dialog').find('button').contains('Cancel').should('be.visible'); + }, + + uploadFile: (file: string) => { + cy.log('persesImportDashboardsPage.uploadFile'); + // Normalize path separators for cross-platform compatibility (Mac/Linux/Windows) + const normalizedPath = file.replace(/\\/g, '/'); + + cy.readFile(normalizedPath).then((content) => { + const textContent = typeof content === 'object' ? JSON.stringify(content, null, 2) : content; + + // Monaco editor requires special handling - click to focus, then set value via Monaco API + cy.get(Classes.ImportDashboardTextArea).should('be.visible').click({ force: true }); + + cy.window().then((win) => { + const models = (win as any).monaco?.editor?.getModels?.(); + if (Array.isArray(models) && models.length > 0) { + models[0].setValue(textContent); + } else { + cy.get(Classes.ImportDashboardTextArea).clear().type(textContent); + } + }); + }); + cy.wait(2000); + }, + + clickClearFileButton: () => { + cy.log('persesImportDashboardsPage.clearFile'); + cy.byPFRole('dialog').find('button').contains('Clear').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + clickImportFileButton: () => { + cy.log('persesImportDashboardsPage.clickImportFileButton'); + cy.byPFRole('dialog').find('button').contains('Import').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + clickCancelButton: () => { + cy.log('persesImportDashboardsPage.clickCancelButton'); + cy.byPFRole('dialog').find('button').contains('Cancel').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertUnableToDetectDashboardFormat: () => { + cy.log('persesImportDashboardsPage.assertUnableToDetectDashboardFormat'); + cy.byPFRole('dialog').find('span').contains(persesDashboardsImportDashboard.DIALOG_UNABLE_TO_DETECT_DASHBOARD_FORMAT).should('be.visible'); + }, + + assertGrafanaDashboardDetected: () => { + cy.log('persesImportDashboardsPage.assertGrafanaDashboardDetected'); + cy.byPFRole('dialog').find('span').contains(persesDashboardsImportDashboard.DIALOG_GRAFANA_DASHBOARD_DETECTED).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).should('be.visible'); + }, + + assertPersesDashboardDetected: () => { + cy.log('persesImportDashboardsPage.assertPersesDashboardDetected'); + cy.byPFRole('dialog').find('span').contains(persesDashboardsImportDashboard.DIALOG_PERSES_DASHBOARD_DETECTED).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).should('be.visible'); + }, + + selectProject: (project: string) => { + cy.log('persesImportDashboardsPage.selectProject'); + cy.byAriaLabel(persesAriaLabels.importDashboardProjectInputButton).should('be.visible').click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible').click({ force: true }); + }, + + assertProjectDropdown: (project: string) => { + cy.log('persesImportDashboardsPage.assertProjectDropdown'); + cy.byAriaLabel(persesAriaLabels.importDashboardProjectInputButton).should('be.visible').click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.importDashboardProjectInputButton).should('be.visible').click({ force: true }); + }, + + assertProjectNotExistsInDropdown: (project: string) => { + cy.log('persesImportDashboardsPage.assertProjectNotExistsInDropdown'); + cy.byAriaLabel(persesAriaLabels.importDashboardProjectInputButton).should('be.visible').click({ force: true }); + cy.byPFRole('listbox').find('li').each(($item) => { + expect($item.text().trim()).to.not.equal(project); + }); + cy.byAriaLabel(persesAriaLabels.importDashboardProjectInputButton).should('be.visible').click({ force: true }); + }, + + assertFailedToMigrateGrafanaDashboard: () => { + cy.log('persesImportDashboardsPage.assertFailedToMigrateGrafanaDashboard'); + cy.byPFRole('dialog').find('h4').contains(persesDashboardsImportDashboard.DIALOG_FAILED_TO_MIGRATE_GRAFANA_DASHBOARD).should('be.visible'); + }, + + assertDuplicatedDashboardError: () => { + cy.log('persesImportDashboardsPage.assertDuplicatedDashboardError'); + cy.byPFRole('dialog').find('h4').contains(persesDashboardsImportDashboard.DIALOG_DUPLICATED_DASHBOARD_ERROR).should('be.visible'); + }, + + dismissDuplicatedDashboardError: () => { + cy.log('persesImportDashboardsPage.dismissDuplicatedDashboardError'); + cy.byAriaLabel(persesAriaLabels.importDashboardDuplicatedDashboardError).scrollIntoView().should('be.visible').click({ force: true }); + cy.wait(2000); + }, + +} diff --git a/web/cypress/views/perses-dashboards-list-dashboards.ts b/web/cypress/views/perses-dashboards-list-dashboards.ts new file mode 100644 index 000000000..4d5799ae0 --- /dev/null +++ b/web/cypress/views/perses-dashboards-list-dashboards.ts @@ -0,0 +1,291 @@ +import { commonPages } from "./common"; +import { DataTestIDs, Classes, listPersesDashboardsOUIAIDs, listPersesDashboardsDataTestIDs, IDs, persesAriaLabels, LegacyTestIDs } from "../../src/components/data-test"; +import { listPersesDashboardsEmptyState, listPersesDashboardsNoDashboardsFoundState, listPersesDashboardsPageSubtitle, persesDashboardsDuplicateDashboard, persesDashboardsRenameDashboard } from "../fixtures/perses/constants"; +import { MonitoringPageTitles } from "../fixtures/monitoring/constants"; + +export const listPersesDashboardsPage = { + + emptyState: () => { + cy.log('listPersesDashboardsPage.emptyState'); + cy.byTestID(listPersesDashboardsDataTestIDs.EmptyStateTitle).should('be.visible').contains(listPersesDashboardsEmptyState.TITLE); + cy.byTestID(listPersesDashboardsDataTestIDs.EmptyStateBody).should('be.visible').contains(listPersesDashboardsEmptyState.BODY); + cy.byTestID(listPersesDashboardsDataTestIDs.ClearAllFiltersButton).should('be.visible'); + }, + + noDashboardsFoundState: () => { + cy.log('listPersesDashboardsPage.noDashboardsFoundState'); + cy.byTestID(listPersesDashboardsDataTestIDs.EmptyStateTitle).should('be.visible').contains(listPersesDashboardsNoDashboardsFoundState.TITLE); + cy.byTestID(listPersesDashboardsDataTestIDs.EmptyStateBody).should('be.visible').contains(listPersesDashboardsNoDashboardsFoundState.BODY); + cy.byTestID(listPersesDashboardsDataTestIDs.ClearAllFiltersButton).should('not.exist'); + }, + + shouldBeLoaded: () => { + cy.log('listPersesDashboardsPage.shouldBeLoaded'); + cy.byOUIAID(listPersesDashboardsOUIAIDs.PersesBreadcrumb).should('not.exist'); + commonPages.titleShouldHaveText(MonitoringPageTitles.DASHBOARDS); + cy.byOUIAID(listPersesDashboardsOUIAIDs.PageHeaderSubtitle).should('contain', listPersesDashboardsPageSubtitle).should('be.visible'); + cy.byTestID(DataTestIDs.PersesCreateDashboardButton).scrollIntoView().should('be.visible'); + cy.byTestID(DataTestIDs.FavoriteStarButton).should('be.visible'); + cy.byOUIAID(listPersesDashboardsOUIAIDs.PersesDashListDataViewTable).should('be.visible'); + + }, + + filter: { + byName: (name: string) => { + cy.log('listPersesDashboardsPage.filter.byName'); + cy.wait(1000); + cy.byOUIAID(listPersesDashboardsOUIAIDs.persesListDataViewFilters).contains('button',/Name|Project/).click( { force: true }); + cy.wait(1000); + cy.get(Classes.FilterDropdownOption).should('be.visible').contains('Name').click( { force: true }); + cy.wait(1000); + cy.byTestID(listPersesDashboardsDataTestIDs.NameFilter).should('be.visible').type(name); + cy.wait(1000); + cy.byTestID(listPersesDashboardsDataTestIDs.NameFilter).find('input').should('have.attr', 'value', name); + cy.wait(2000); + }, + byProject: (project: string) => { + cy.log('listPersesDashboardsPage.filter.byProject'); + cy.byOUIAID(listPersesDashboardsOUIAIDs.persesListDataViewFilters).contains('button',/Name|Project/).click( { force: true }); + cy.wait(1000); + cy.get(Classes.FilterDropdownOption).should('be.visible').contains('Project').click( { force: true }); + cy.wait(1000); + cy.byTestID(listPersesDashboardsDataTestIDs.ProjectFilter).should('be.visible').type(project); + cy.wait(1000); + cy.byTestID(listPersesDashboardsDataTestIDs.ProjectFilter).find('input').should('have.attr', 'value', project); + cy.wait(2000); + }, + }, + + countDashboards: (count: string) => { + cy.log('listPersesDashboardsPage.countDashboards'); + cy.wait(2000); + cy.get('#'+ IDs.persesDashboardCount,).find(Classes.PersesListDashboardCount).invoke('text').should((text) => { + const total = text.split('of')[1].trim(); + expect(total).to.equal(count); + }); + }, + + clearAllFilters: () => { + cy.log('listPersesDashboardsPage.clearAllFilters'); + cy.byOUIAID(listPersesDashboardsOUIAIDs.persesListDataViewHeaderClearAllFiltersButton).click(); + cy.wait(5000); + }, + + sortBy: (column: string) => { + cy.log('listPersesDashboardsPage.sortBy'); + cy.byOUIAID(listPersesDashboardsOUIAIDs.persesListDataViewHeaderSortButton).contains(column).scrollIntoView().click(); + }, + + /** + * If index is not provided, it asserts the existence of the dashboard by appending the name to the prefix to build data-test id, expecting to be unique + * If index is provided, it asserts the existence of the dashboard by the index. + * @param name - The name of the dashboard to assert + * @param index - The index of the dashboard to assert (optional) + */ + assertDashboardName: (name: string, index?: number) => { + cy.log('listPersesDashboardsPage.assertDashboardName'); + const idx = index !== undefined ? index : 0; + if (index === undefined) { + cy.byTestID(listPersesDashboardsDataTestIDs.DashboardLinkPrefix+name).should('be.visible').contains(name); + } else { + cy.byOUIAID(listPersesDashboardsOUIAIDs.persesListDataViewTableDashboardNameTD+idx.toString()).should('be.visible').contains(name); + } + }, + + clickDashboard: (name: string, index?: number) => { + const idx = index !== undefined ? index : 0; + cy.log('listPersesDashboardsPage.clickDashboard'); + // cy.byTestID(listPersesDashboardsDataTestIDs.DashboardLinkPrefix+name).eq(idx).should('be.visible').click(); + cy.get('a').contains(name).eq(idx).should('be.visible').click(); + cy.wait(15000); + }, + + removeTag: (value: string) => { + cy.log('listPersesDashboardsPage.removeTag'); + cy.byAriaLabel('Close '+ value).click(); + }, + + clickCreateButton: () => { + cy.log('persesDashboardsPage.clickCreateButton'); + cy.byTestID(DataTestIDs.PersesCreateDashboardButton).scrollIntoView().should('be.visible').and('not.have.attr', 'disabled'); + cy.byTestID(DataTestIDs.PersesCreateDashboardButton).click({ force: true }); + cy.wait(2000); + }, + + assertCreateButtonIsEnabled: () => { + cy.log('persesDashboardsPage.assertCreateButtonIsEnabled'); + cy.byTestID(DataTestIDs.PersesCreateDashboardButton).scrollIntoView().should('be.visible').should('not.have.attr', 'disabled'); + }, + + assertCreateButtonIsDisabled: () => { + cy.log('persesDashboardsPage.assertCreateButtonIsDisabled'); + cy.byTestID(DataTestIDs.PersesCreateDashboardButton).scrollIntoView().should('be.visible').should('have.attr', 'disabled'); + }, + + clickKebabIcon: (index?: number) => { + const idx = index !== undefined ? index : 0; + cy.log('persesDashboardsPage.clickKebabIcon'); + cy.byAriaLabel(persesAriaLabels.persesDashboardKebabIcon).eq(idx).scrollIntoView().should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertKebabIconOptions: () => { + cy.log('persesDashboardsPage.assertKebabIconOptions'); + cy.byPFRole('menuitem').contains('Rename dashboard').should('be.visible'); + cy.byPFRole('menuitem').contains('Duplicate dashboard').should('be.visible'); + cy.byPFRole('menuitem').contains('Delete dashboard').should('be.visible'); + }, + + assertKebabIconDisabled: () => { + cy.log('persesDashboardsPage.assertKebabIconDisabled'); + cy.byAriaLabel(persesAriaLabels.persesDashboardKebabIcon).scrollIntoView().should('be.visible').should('have.attr', 'disabled'); + }, + + clickRenameDashboardOption: () => { + cy.log('listPersesDashboardsPage.clickRenameDashboardOption'); + cy.wait(1000); + cy.byPFRole('menuitem').contains('Rename dashboard').should('be.visible').click({ force: true }); + cy.wait(1000); + }, + + renameDashboardEnterName: (name: string) => { + cy.log('listPersesDashboardsPage.renameDashboardEnterName'); + cy.get('#'+IDs.persesDashboardRenameDashboardName).should('be.visible').clear().type(name); + cy.wait(1000); + }, + + renameDashboardCancelButton: () => { + cy.log('listPersesDashboardsPage.renameDashboardCancel'); + cy.byPFRole('dialog').find('button').contains('Cancel').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + renameDashboardRenameButton: () => { + cy.log('listPersesDashboardsPage.renameDashboardRename'); + cy.byPFRole('dialog').find('button').contains('Rename').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertRenameDashboardMaxLength: () => { + cy.log('listPersesDashboardsPage.assertRenameDashboardMaxLength'); + cy.byPFRole('dialog').find(Classes.PersesCreateDashboardDashboardNameError).should('have.text', persesDashboardsRenameDashboard.DIALOG_MAX_LENGTH_VALIDATION).should('be.visible'); + }, + + clickDuplicateOption: () => { + cy.log('listPersesDashboardsPage.clickDuplicateOption'); + cy.byPFRole('menuitem').contains('Duplicate dashboard').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertDuplicateProjectDropdown: (project: string) => { + cy.log('listPersesDashboardsPage.assertDuplicateProjectDropdown'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).should('be.visible').clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + }, + + duplicateDashboardEnterName: (name: string) => { + cy.log('listPersesDashboardsPage.duplicateDashboardEnterName'); + cy.get('#' + IDs.persesDashboardDuplicateDashboardName).should('be.visible').clear().type(name); + cy.wait(1000); + }, + + duplicateDashboardCancelButton: () => { + cy.log('listPersesDashboardsPage.duplicateDashboardCancel'); + cy.byPFRole('dialog').find('button').contains('Cancel').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + duplicateDashboardDuplicateButton: () => { + cy.log('listPersesDashboardsPage.duplicateDashboardDuplicate'); + cy.byPFRole('dialog').find('button').contains('Duplicate').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertDuplicateDashboardAlreadyExists: () => { + cy.log('listPersesDashboardsPage.assertDuplicateDashboardAlreadyExists'); + cy.byPFRole('dialog').find(Classes.PersesCreateDashboardDashboardNameError) + .contains(persesDashboardsDuplicateDashboard.DIALOG_DUPLICATED_NAME_VALIDATION) + .should('be.visible'); + }, + + duplicateDashboardSelectProjectDropdown: (project: string) => { + cy.log('listPersesDashboardsPage.duplicateDashboardSelectProjectDropdown'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertDuplicateProjectDropdownExists: (project: string) => { + cy.log('listPersesDashboardsPage.assertDuplicateProjectDropdownExists'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + cy.byAriaLabel(persesAriaLabels.dialogProjectInput).clear().type(project); + cy.byPFRole('option').contains(project).should('be.visible'); + cy.log('Project: ' + project + ' is available in the dropdown'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + }, + + assertDuplicateProjectDropdownNotExists: (project: string) => { + cy.log('listPersesDashboardsPage.assertDuplicateProjectDropdownNotExists'); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + cy.byPFRole('listbox').find('li').then((items) => { + items.each((index, item) => { + cy.log('Project: ' + item.innerText); + if (item.innerText === project) { + expect(item).to.not.exist; + } + }); + }); + cy.get(Classes.PersesCreateDashboardProjectDropdown).should('be.visible').click({ force: true }); + }, + + clickDeleteOption: () => { + cy.log('listPersesDashboardsPage.clickDeleteOption'); + cy.byPFRole('menuitem').contains('Delete dashboard').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + deleteDashboardCancelButton: () => { + cy.log('listPersesDashboardsPage.deleteDashboardCancel'); + cy.byPFRole('dialog').find('button').contains('Cancel').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + deleteDashboardDeleteButton: () => { + cy.log('listPersesDashboardsPage.deleteDashboardDelete'); + cy.byPFRole('dialog').find('button').contains('Delete').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + projectDropdownNotExists: () => { + cy.log('listPersesDashboardsPage.projectDropdownNotExists'); + cy.byLegacyTestID(LegacyTestIDs.NamespaceBarDropdown).should('not.exist'); + cy.get(Classes.NamespaceDropdown).should('not.exist'); + }, + + clickImportButton: () => { + cy.log('listPersesDashboardsPage.clickImportButton'); + cy.byAriaLabel(persesAriaLabels.dashboardActionsMenu).scrollIntoView().should('be.visible').click({ force: true }); + cy.wait(2000); + cy.byPFRole('menuitem').contains('Import').should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + dismissDuplicatedDashboardError: () => { + cy.log('listPersesDashboardsPage.dismissDuplicatedDashboardError'); + cy.byAriaLabel(persesAriaLabels.importDashboardDuplicatedDashboardError).scrollIntoView().should('be.visible').click({ force: true }); + cy.wait(2000); + }, + + assertImportButtonIsEnabled: () => { + cy.log('listPersesDashboardsPage.assertImportButtonIsEnabled'); + cy.byAriaLabel(persesAriaLabels.dashboardActionsMenu).scrollIntoView().should('be.visible').should('not.have.attr', 'disabled'); + }, + + assertImportButtonIsDisabled: () => { + cy.log('listPersesDashboardsPage.assertImportButtonIsDisabled'); + cy.byAriaLabel(persesAriaLabels.dashboardActionsMenu).scrollIntoView().should('be.visible').should('have.attr', 'disabled'); + }, +} diff --git a/web/cypress/views/perses-dashboards-panel.ts b/web/cypress/views/perses-dashboards-panel.ts new file mode 100644 index 000000000..7ee5a096d --- /dev/null +++ b/web/cypress/views/perses-dashboards-panel.ts @@ -0,0 +1,169 @@ +import { commonPages } from "./common"; +import { persesAriaLabels, IDs, editPersesDashboardsAddPanel, Classes } from "../../src/components/data-test"; +import { persesDashboardsModalTitles, persesDashboardsAddPanelAddQueryType, persesDashboardsAddListPanelType } from "../fixtures/perses/constants"; + +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(new RegExp(`^${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, query?: string | 'up' | 'haproxy_up', legend?: 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); + + cy.wait(1000); + persesDashboardsPanel.clickDropdownAndSelectOption('Type', type); + + switch (type) { + //BAR_GAUGE_HEAT_HISTOGRAM_PIE_STAT_STATUS_TABLE_TIMESERIES + case persesDashboardsAddListPanelType.BAR_CHART: + // case persesDashboardsAddListPanelType.HEATMAP_CHART: + // case persesDashboardsAddListPanelType.HISTOGRAM_CHART: + // case persesDashboardsAddListPanelType.PIE_CHART: + // case persesDashboardsAddListPanelType.STAT_CHART: + case persesDashboardsAddListPanelType.STATUS_HISTORY_CHART: + case persesDashboardsAddListPanelType.TABLE: + case persesDashboardsAddListPanelType.TIME_SERIES_CHART: + case persesDashboardsAddListPanelType.TIME_SERIES_TABLE: + + cy.wait(2000); + persesDashboardsPanel.clickDropdownAndSelectOption('Query Type', persesDashboardsAddPanelAddQueryType.BAR_GAUGE_HEAT_HISTOGRAM_PIE_STAT_STATUS_TABLE_TIMESERIES.PROMETHEUS_TIME_SERIES_QUERY); + cy.wait(2000); + if (query !== undefined && query !== '') { + persesDashboardsPanel.enterPromQLQuery(query); + } + else { + persesDashboardsPanel.enterPromQLQuery('up'); + } + if (legend !== undefined && legend !== '') { + cy.get('label').contains('Legend').next('div').find('input').click().type(legend); + } + break; + } + cy.get('#'+IDs.persesDashboardAddPanelForm).parent('div').find('h2').siblings('div').find('button').contains('Add').should('be.visible').click(); + }, + + enterPromQLQuery: (query: string | 'up' | 'haproxy_up') => { + cy.log('persesDashboardsPanel.enterPromQLQuery'); + cy.get('[data-testid="promql_expression_editor"] .cm-line') + .then(($el) => { + $el[0].textContent = `${query}`; + $el[0].dispatchEvent(new Event('input', { bubbles: true })); + $el[0].dispatchEvent(new Event('blur', { bubbles: true })); + }); + cy.wait(3000); + cy.get('body').then(($body) => { + const $el = $body.find('[data-testid="promql_expression_editor"] .cm-tooltip-autocomplete.cm-tooltip.cm-tooltip-below li[aria-selected="true"]'); + if ($el.length > 0) { + // Use native DOM events since $el[0] is a raw DOM element + $el[0].dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); + $el[0].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + } + }); + + cy.wait(3000); + cy.get('[data-testid="promql_expression_editor"] .cm-line').click(); + cy.wait(3000); + cy.bySemanticElement('button', 'Run Query').scrollIntoView().should('be.visible').click({ force: true }); + cy.wait(3000); + }, + + 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..02cfdb7ae --- /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.byPFRole('dialog').find('button').contains('Add').should('be.visible').click({ force: true }); + }, + + 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 9448cd1af..59b21c8a1 100644 --- a/web/cypress/views/perses-dashboards.ts +++ b/web/cypress/views/perses-dashboards.ts @@ -1,6 +1,7 @@ import { commonPages } from "./common"; -import { DataTestIDs, Classes, LegacyTestIDs, persesAriaLabels, persesDataTestIDs } 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, persesDashboardsEmptyDashboard, persesDashboardsModalTitles } from "../fixtures/perses/constants"; import { persesDashboardsTimeRange, persesDashboardsRefreshInterval, persesDashboardsDashboardDropdownCOO, persesDashboardsDashboardDropdownPersesDev, persesDashboardsAcceleratorsCommonMetricsPanels } from "../fixtures/perses/constants"; export const persesDashboardsPage = { @@ -8,83 +9,169 @@ 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.get('#' + IDs.persesDashboardRefreshIntervalDropdown).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'); }, + //TODO: change back to shouldBeLoaded when customizable-dashboards gets merged + shouldBeLoaded1: () => { + cy.log('persesDashboardsPage.shouldBeLoaded'); + commonPages.titleShouldHaveText(MonitoringPageTitles.DASHBOARDS); + cy.byOUIAID(listPersesDashboardsOUIAIDs.PageHeaderSubtitle).scrollIntoView().should('contain', listPersesDashboardsPageSubtitle).should('be.visible'); + + cy.byTestID(persesDashboardDataTestIDs.editDashboardButtonToolbar).scrollIntoView().should('be.visible'); + + cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).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.get('#' + IDs.persesDashboardRefreshIntervalDropdown).scrollIntoView().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').scrollIntoView().should('be.visible'); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').scrollIntoView().should('be.visible'); + }, + + shouldBeLoadedEditionMode: (dashboardName: string) => { + cy.log('persesDashboardsPage.shouldBeLoadedEditionMode'); + cy.wait(10000); + commonPages.titleShouldHaveText(MonitoringPageTitles.DASHBOARDS); + cy.byOUIAID(listPersesDashboardsOUIAIDs.PageHeaderSubtitle).scrollIntoView().should('contain', listPersesDashboardsPageSubtitle).should('be.visible'); + cy.byTestID(listPersesDashboardsDataTestIDs.PersesBreadcrumbDashboardNameItem).scrollIntoView().should('contain', dashboardName).should('be.visible'); + persesDashboardsPage.assertEditModeButtons(); + + cy.byAriaLabel(persesAriaLabels.TimeRangeDropdown).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.get('#' + IDs.persesDashboardRefreshIntervalDropdown).scrollIntoView().should('be.visible'); + + cy.get('#' + IDs.persesDashboardDownloadButton).scrollIntoView().should('be.visible'); + cy.byAriaLabel(persesAriaLabels.EditJSONButton).scrollIntoView().should('be.visible'); + + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('input').scrollIntoView().should('be.visible'); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('input').should('have.value', dashboardName); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('button').scrollIntoView().should('be.visible'); + + }, + + shouldBeLoadedEditionModeFromCreateDashboard: () => { + cy.log('persesDashboardsPage.shouldBeLoadedEditionModeFromCreateDashboard'); + cy.wait(10000); + cy.get('h2').contains(persesDashboardsEmptyDashboard.TITLE).scrollIntoView().should('be.visible'); + cy.get('p').contains(persesDashboardsEmptyDashboard.DESCRIPTION).scrollIntoView().should('be.visible'); + + cy.get('h2').siblings('div').find('[aria-label="Add panel"]').scrollIntoView().should('be.visible'); + cy.get('h2').siblings('div').find('[aria-label="Edit variables"]').scrollIntoView().should('be.visible'); + }, + + shouldBeLoadedAfterRename: (dashboardName: string) => { + cy.log('persesDashboardsPage.shouldBeLoadedAfterRename'); + persesDashboardsPage.shouldBeLoadedAfter(dashboardName); + }, + + shouldBeLoadedAfterDuplicate: (dashboardName: string) => { + cy.log('persesDashboardsPage.shouldBeLoadedAfterDuplicate'); + persesDashboardsPage.shouldBeLoadedAfter(dashboardName); + }, + + shouldBeLoadedAfter: (dashboardName: string) => { + cy.log('persesDashboardsPage.shouldBeLoadedAfter'); + cy.byTestID(listPersesDashboardsDataTestIDs.PersesBreadcrumbDashboardNameItem).scrollIntoView().should('contain', dashboardName).should('be.visible'); + cy.byTestID(DataTestIDs.PersesDashboardDropdown).find('input').should('have.value', dashboardName); + }, + 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.get('#' + IDs.persesDashboardRefreshIntervalDropdown).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.get('#' + IDs.persesDashboardRefreshIntervalDropdown).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).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(persesDataTestIDs.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: () => { @@ -93,34 +180,289 @@ export const persesDashboardsPage = { const panels = Object.values(persesDashboardsAcceleratorsCommonMetricsPanels); panels.forEach((panel) => { cy.log('Panel: ' + panel); - cy.byDataTestID(persesDataTestIDs.panelHeader).find('h6').contains(panel).scrollIntoView().should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(panel).scrollIntoView().should('be.visible'); }); }, - expandPanel: (panel: keyof typeof persesDashboardsAcceleratorsCommonMetricsPanels | string) => { + expandPanel: (panel: string) => { cy.log('persesDashboardsPage.expandPanel'); - cy.byDataTestID(persesDataTestIDs.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(persesDataTestIDs.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) => { cy.log('persesDashboardsPage.statChartValueAssertion'); cy.wait(2000); if (noData) { - cy.byDataTestID(persesDataTestIDs.panelHeader).find('h6').contains(panel).scrollIntoView().parents('header').siblings('figure').find('p').should('contain', 'No data').should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(panel).scrollIntoView().parents('header').siblings('figure').find('p').should('contain', 'No data').should('be.visible'); } else { - cy.byDataTestID(persesDataTestIDs.panelHeader).find('h6').contains(panel).scrollIntoView().parents('header').siblings('figure').find('h3').should('not.contain', 'No data').should('be.visible'); + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(panel).scrollIntoView().parents('header').siblings('figure').find('h3').should('not.contain', 'No data').should('be.visible'); } }, searchAndSelectVariable: (variable: string, value: string) => { cy.log('persesDashboardsPage.searchAndSelectVariable'); - cy.byDataTestID(persesDataTestIDs.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); + }, + + assertEditButtonIsDisabled: () => { + cy.log('persesDashboardsPage.assertEditButtonIsDisabled'); + cy.byTestID(persesDashboardDataTestIDs.editDashboardButtonToolbar).scrollIntoView().should('be.visible').should('have.attr', 'disabled'); + }, + + 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).eq(0).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).eq(0).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(); + persesDashboardsPage.closeSuccessAlert(); + 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'); + }); + + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(panel).siblings('div').eq(2).then((element1) => { + if (element1.find('[data-testid="ArrowCollapseIcon"]').length > 0 && element1.find('[data-testid="ArrowCollapseIcon"]').is(':visible')) { + cy.byAriaLabel(persesAriaLabels.EditPanelExpandCollapseButtonPrefix + panel + persesAriaLabels.EditPanelExpandCollapseButtonSuffix).find('[data-testid="ArrowCollapseIcon"]').eq(0).click({ force: true }); + } + }); + }, + + clickSaveDashboardButton: () => { + cy.log('persesDashboardsPage.clickSaveDashboardButton'); + cy.wait(2000); + 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 }); + } + }); + cy.wait(2000); + }, + + backToListPersesDashboardsPage: () => { + cy.log('persesDashboardsPage.backToListPersesDashboardsPage'); + cy.byTestID(listPersesDashboardsDataTestIDs.PersesBreadcrumbDashboardItem).scrollIntoView().should('be.visible').click({ force: true }); + cy.wait(5000); + }, + + 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).should('be.visible'); + } else { + cy.byAriaLabel(persesAriaLabels.OpenGroupButtonPrefix + group).scrollIntoView().should('be.visible').click({ force: true }); + cy.byDataTestID(persesMUIDataTestIDs.panelHeader).find('h6').contains(name).should('be.visible'); + cy.byAriaLabel(persesAriaLabels.CollapseGroupButtonPrefix + group).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 v1alpha1)' | 'YAML (CR v1alpha2)') => { + 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 v1alpha1)') { + filename = dashboardName + '-cr-v1alpha1' + '.yaml'; + } else if (format === 'YAML (CR v1alpha2)') { + filename = dashboardName + '-cr-v1alpha2' + '.yaml'; + } else { + filename = dashboardName + '.' + format.toLowerCase(); + } + persesDashboardsPage.assertFilename(filename); + }, + + assertFilename: (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); + }, + + closeSuccessAlert: () => { + cy.log('persesDashboardsPage.closeSuccessAlert'); + cy.wait(2000); + cy.get('body').then((body) => { + if (body.find('h4').length > 0 && body.find('h4').is(':visible')) { + cy.get('h4').siblings('div').find('button').scrollIntoView().should('be.visible').click({ force: true }); + } + }); + }, } diff --git a/web/cypress/views/silences-list-page.ts b/web/cypress/views/silences-list-page.ts index b1d3855ba..68fb0bcfd 100644 --- a/web/cypress/views/silences-list-page.ts +++ b/web/cypress/views/silences-list-page.ts @@ -15,7 +15,7 @@ export const silencesListPage = { }, firstTimeEmptyState: () => { cy.log('silencesListPage.firstTimeEmptyState'); - cy.byTestID(DataTestIDs.EmptyBoxBody).contains('No Silences found').should('be.visible'); + cy.byTestID(DataTestIDs.EmptyBoxBody).contains('No silences found').should('be.visible'); cy.bySemanticElement('button', 'Clear all filters').should('not.exist'); }, @@ -88,6 +88,7 @@ export const silencesListPage = { clickAlertKebab: () => { cy.log('silencesListPage.rows.clickAlertKebab'); + cy.wait(2000); cy.byTestID(DataTestIDs.KebabDropdownButton).should('be.visible').click(); }, diff --git a/web/cypress/views/tour.ts b/web/cypress/views/tour.ts index 9884cede1..b57b6b081 100644 --- a/web/cypress/views/tour.ts +++ b/web/cypress/views/tour.ts @@ -1,17 +1,43 @@ export const guidedTour = { close: () => { + const modalSelector = 'button[data-ouia-component-id="clustersOnboardingModal-ModalBoxCloseButton"]' + cy.log('close guided tour'); cy.get('body').then(($body) => { + //Core platform modal if ($body.find(`[data-test="guided-tour-modal"]`).length > 0) { + cy.log('Core platform modal detected, attempting to close...'); cy.byTestID('tour-step-footer-secondary').contains('Skip tour').click(); + } + //Kubevirt modal + else if ($body.find(`[aria-label="Welcome modal"]`).length > 0) { + cy.log('Kubevirt modal detected, attempting to close...'); + cy.get('[aria-label="Close"]').should('be.visible').click(); + } + //ACM Onboarding modal + else if ($body.find(modalSelector).length > 0) { + cy.log('Onboarding modal detected, attempting to close...'); + cy.get(modalSelector, { timeout: 20000 }) + .should('be.visible') + .should('not.be.disabled') + .click({ force: true }); + + cy.get(modalSelector, { timeout: 10000 }) + .should('not.exist') + .then(() => cy.log('Modal successfully closed')); } + // Prevents navigating away from the page before the tour is closed + cy.wait(2000); }); }, closeKubevirtTour: () => { + cy.log('close Kubevirt tour'); cy.get('body').then(($body) => { if ($body.find(`[aria-label="Welcome modal"]`).length > 0) { cy.get('[aria-label="Close"]').should('be.visible').click(); } + // Prevents navigating away from the page before the tour is closed + cy.wait(2000); }); }, }; \ No newline at end of file diff --git a/web/cypress/views/troubleshooting-panel.ts b/web/cypress/views/troubleshooting-panel.ts new file mode 100644 index 000000000..0238a7656 --- /dev/null +++ b/web/cypress/views/troubleshooting-panel.ts @@ -0,0 +1,32 @@ +import { DataTestIDs, Classes, LegacyTestIDs, IDs } from "../../src/components/data-test"; + +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: () => { + cy.log('troubleshootingPanelPage.signalCorrelationShouldNotBeVisible'); + cy.byLegacyTestID(LegacyTestIDs.ApplicationLauncher).should('be.visible').click(); + cy.byTestID(DataTestIDs.MastHeadApplicationItem).contains('Signal Correlation').should('not.exist'); + cy.byLegacyTestID(LegacyTestIDs.ApplicationLauncher).should('be.visible').click(); + }, + + troubleshootingPanelPageShouldBeLoadedEnabled: () => { + cy.log('troubleshootingPanelPage.troubleshootingPanelPageShouldBeLoadedEnabled'); + cy.get('h1').contains('Troubleshooting').should('be.visible'); + cy.byAriaLabel('Close').should('be.visible'); + cy.byButtonText('Focus').should('be.visible'); + cy.get('#query-toggle').should('be.visible'); + //svg path for refresh button + cy.get('.tp-plugin__panel-query-container').find('path').eq(1).should('have.attr', 'd', 'M440.65 12.57l4 82.77A247.16 247.16 0 0 0 255.83 8C134.73 8 33.91 94.92 12.29 209.82A12 12 0 0 0 24.09 224h49.05a12 12 0 0 0 11.67-9.26 175.91 175.91 0 0 1 317-56.94l-101.46-4.86a12 12 0 0 0-12.57 12v47.41a12 12 0 0 0 12 12H500a12 12 0 0 0 12-12V12a12 12 0 0 0-12-12h-47.37a12 12 0 0 0-11.98 12.57zM255.83 432a175.61 175.61 0 0 1-146-77.8l101.8 4.87a12 12 0 0 0 12.57-12v-47.4a12 12 0 0 0-12-12H12a12 12 0 0 0-12 12V500a12 12 0 0 0 12 12h47.35a12 12 0 0 0 12-12.6l-4.15-82.57A247.17 247.17 0 0 0 255.83 504c121.11 0 221.93-86.92 243.55-201.82a12 12 0 0 0-11.8-14.18h-49.05a12 12 0 0 0-11.67 9.26A175.86 175.86 0 0 1 255.83 432z'); + cy.byDataID('korrel8r_graph').should('be.visible'); + }, + + +}; diff --git a/web/i18n-scripts/README.md b/web/i18n-scripts/README.md index 56de0b179..88a6e72df 100644 --- a/web/i18n-scripts/README.md +++ b/web/i18n-scripts/README.md @@ -19,4 +19,4 @@ Once your login info is configured, you should be able to log in by running `sou 1. **Note the Memsource project link**: After the upload is complete, the script will output the Memsource project link. Make sure to copy it for the next step. 1. **Translate in Memsource**: Send an email to the globalization team `localization-requests@redhat.com` with CC to `team-observability-ui@redhat.com` with the Memsource project link and request translation for the required languages. 1. **Wait for translation completion**: Accessing memsource you can monitor the progress of the translations. -1. **Download translations**: once the translations are completed, run `./i18n-scripts/memsource-download.sh -p ` to download the translated `.po` files from Memsource and convert them back to the i18n JSON format used by the application. +1. **Download translations**: once the translations are completed, run `./i18n-scripts/memsource-download.sh -p ` to download the translated `.po` files from Memsource, convert them back to the i18n JSON format, and create a commit. Check the translations for encoding errors and adjust the commit. diff --git a/web/locales/en/plugin__monitoring-plugin.json b/web/locales/en/plugin__monitoring-plugin.json index 831879570..24f9c8c39 100644 --- a/web/locales/en/plugin__monitoring-plugin.json +++ b/web/locales/en/plugin__monitoring-plugin.json @@ -11,7 +11,6 @@ "Creator": "Creator", "Alerts": "Alerts", "Silences": "Silences", - "Alerting Rules": "Alerting Rules", "Alerting rules": "Alerting rules", "Alerting": "Alerting", "Severity": "Severity", @@ -42,6 +41,7 @@ "Silenced": "Silenced", "Not Firing": "Not Firing", "Alert state": "Alert state", + "No alerting rules found": "No alerting rules found", "Alert details": "Alert details", "Alerting rule": "Alerting rule", "Silenced by": "Silenced by", @@ -51,6 +51,7 @@ "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.": "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.", "Silenced: ": "Silenced: ", "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.", + "No alerts found": "No alerts found", "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.", "Critical": "Critical", "Info": "Info", @@ -82,6 +83,10 @@ "1d": "1d", "2d": "2d", "1w": "1w", + "Comment is required.": "Comment is required.", + "Alertmanager URL not set": "Alertmanager URL not set", + "Error saving Silence": "Error saving Silence", + "Forbidden: Missing permissions for silences": "Forbidden: Missing permissions for silences", "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.", "Duration": "Duration", "Silence alert from...": "Silence alert from...", @@ -105,6 +110,7 @@ "Silence": "Silence", "Cancel": "Cancel", "Silence details": "Silence details", + "Actions": "Actions", "Matchers": "Matchers", "No matchers": "No matchers", "Last updated at": "Last updated at", @@ -117,6 +123,7 @@ "Active": "Active", "Error loading silences from Alertmanager. Alertmanager may be unavailable.": "Error loading silences from Alertmanager. Alertmanager may be unavailable.", "Error": "Error", + "No silences found": "No silences found", "Expire {{count}} silence_one": "Expire {{count}} silence", "Expire {{count}} silence_other": "Expire {{count}} silences", "Expire Silence": "Expire Silence", @@ -132,9 +139,6 @@ "404: Not Found": "404: Not Found", "{{labels}} content is not available in the catalog at this time due to loading failures.": "{{labels}} content is not available in the catalog at this time due to loading failures.", "No datapoints found.": "No datapoints found.", - "Namespaces": "Namespaces", - "Project": "Project", - "Projects": "Projects", "Create new option \"{{option}}\"": "Create new option \"{{option}}\"", "Filter options": "Filter options", "Clear input value": "Clear input value", @@ -162,19 +166,94 @@ "Time range": "Time range", "Refresh interval": "Refresh interval", "Could not parse JSON data for dashboard \"{{dashboard}}\"": "Could not parse JSON data for dashboard \"{{dashboard}}\"", - "Dashboard Variables": "Dashboard Variables", + "Rename Dashboard": "Rename Dashboard", + "Dashboard name": "Dashboard name", + "Renaming...": "Renaming...", + "Rename": "Rename", + "Project \"{{project}}\" created successfully": "Project \"{{project}}\" created successfully", + "Failed to create project \"{{project}}\". Please try again.": "Failed to create project \"{{project}}\". Please try again.", + "Error creating project: {{error}}": "Error creating project: {{error}}", + "Duplicate Dashboard": "Duplicate Dashboard", + "Loading...": "Loading...", + "Failed to load project permissions. Please refresh the page and try again.": "Failed to load project permissions. Please refresh the page and try again.", + "Select namespace": "Select namespace", + "No namespace found for \"{{filter}}\"": "No namespace found for \"{{filter}}\"", + "Duplicate": "Duplicate", + "this dashboard": "this dashboard", + "Permanently delete dashboard?": "Permanently delete dashboard?", + "Are you sure you want to delete ": "Are you sure you want to delete ", + "? This action can not be undone.": "? This action can not be undone.", + "Deleting...": "Deleting...", + "Delete": "Delete", + "Must be 75 or fewer characters long": "Must be 75 or fewer characters long", + "Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!": "Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!", + "Checking permissions...": "Checking permissions...", + "Create": "Create", + "Dashboard actions": "Dashboard actions", + "Import": "Import", + "To create dashboards, contact your cluster administrator for permission.": "To create dashboards, contact your cluster administrator for permission.", + "Project is required": "Project is required", + "Dashboard name is required": "Dashboard name is required", + "Failed to create dashboard. Please try again.": "Failed to create dashboard. Please try again.", + "Create Dashboard": "Create Dashboard", + "Select project": "Select project", + "Select a project": "Select a project", + "No project found for \"{{filter}}\"": "No project found for \"{{filter}}\"", + "my-new-dashboard": "my-new-dashboard", + "Creating...": "Creating...", + "View and manage dashboards.": "View and manage dashboards.", + "Unable to detect dashboard format. Please provide a valid Perses or Grafana dashboard.": "Unable to detect dashboard format. Please provide a valid Perses or Grafana dashboard.", + "Invalid {{format}}: {{error}}": "Invalid {{format}}: {{error}}", + "Invalid file type. Please upload a JSON or YAML file (.json, .yaml, .yml)": "Invalid file type. Please upload a JSON or YAML file (.json, .yaml, .yml)", + "File size exceeds maximum allowed size of 5MB": "File size exceeds maximum allowed size of 5MB", + "Dashboard \"{{name}}\" imported successfully": "Dashboard \"{{name}}\" imported successfully", + "A valid dashboard is required": "A valid dashboard is required", + "Failed to import dashboard. Please try again.": "Failed to import dashboard. Please try again.", + "Error importing dashboard: {{error}}": "Error importing dashboard: {{error}}", + "Migration failed. Please try again.": "Migration failed. Please try again.", + "Import Dashboard": "Import Dashboard", + "1. Provide a dashboard (JSON or YAML)": "1. Provide a dashboard (JSON or YAML)", + "Upload a dashboard file or paste the dashboard definition directly in the editor below.": "Upload a dashboard file or paste the dashboard definition directly in the editor below.", + "Drag and drop a file or upload one": "Drag and drop a file or upload one", + "Upload": "Upload", + "Clear": "Clear", + "Grafana dashboard detected. It will be automatically migrated to Perses format. Note: migration may be partial as not all Grafana features are supported.": "Grafana dashboard detected. It will be automatically migrated to Perses format. Note: migration may be partial as not all Grafana features are supported.", + "Perses dashboard detected.": "Perses dashboard detected.", + "2. Select project": "2. Select project", + "Importing...": "Importing...", + "Rename dashboard": "Rename dashboard", + "Duplicate dashboard": "Duplicate dashboard", + "Delete dashboard": "Delete dashboard", + "You don't have permissions for dashboard actions": "You don't have permissions for dashboard actions", + "Dashboard": "Dashboard", + "Project": "Project", + "Created on": "Created on", + "Last Modified": "Last Modified", + "Filter by name": "Filter by name", + "Filter by project": "Filter by project", + "No dashboards found": "No dashboards found", + "No results match the filter criteria. Clear filters to show results.": "No results match the filter criteria. Clear filters to show results.", + "No Perses dashboards are currently available in this project.": "No Perses dashboards are currently available in this project.", + "Clear all filters": "Clear all filters", + "Dashboard not found": "Dashboard not found", + "The dashboard \"{{name}}\" was not found in project \"{{project}}\".": "The dashboard \"{{name}}\" was not found in project \"{{project}}\".", + "Empty Dashboard": "Empty Dashboard", + "To get started add something to your dashboard": "To get started add something to your dashboard", + "Edit": "Edit", + "You don't have permission to edit this dashboard": "You don't have permission to edit this dashboard", "No matching datasource found": "No matching datasource found", "No Dashboard Available in Selected Project": "No Dashboard Available in Selected Project", "To explore data, create a dashboard for this project": "To explore data, create a dashboard for this project", "No Perses Project Available": "No Perses Project Available", "To explore data, create a Perses Project": "To explore data, create a Perses Project", - "Empty Dashboard": "Empty Dashboard", - "To get started add something to your dashboard": "To get started add something to your dashboard", + "Project is required for fetching project dashboards": "Project is required for fetching project dashboards", "No projects found": "No projects found", "No results match the filter criteria.": "No results match the filter criteria.", "Clear filters": "Clear filters", "Select project...": "Select project...", - "Dashboard": "Dashboard", + "Projects": "Projects", + "All Projects": "All Projects", + "useToast must be used within ToastProvider": "useToast must be used within ToastProvider", "Refresh off": "Refresh off", "{{count}} second_one": "{{count}} second", "{{count}} second_other": "{{count}} seconds", @@ -197,6 +276,7 @@ "Component(s)": "Component(s)", "Alert": "Alert", "Incidents": "Incidents", + "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.": "Incident data is updated every few minutes. What you see may be up to 5 minutes old. Refresh the page to view updated information.", "Filter type selection": "Filter type selection", "Incident ID": "Incident ID", "Severity filters": "Severity filters", @@ -293,6 +373,7 @@ "Last Scrape": "Last Scrape", "Scrape Duration": "Scrape Duration", "Metrics targets": "Metrics targets", + "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" diff --git a/web/locales/es/plugin__monitoring-plugin.json b/web/locales/es/plugin__monitoring-plugin.json new file mode 100644 index 000000000..f800ffb83 --- /dev/null +++ b/web/locales/es/plugin__monitoring-plugin.json @@ -0,0 +1,305 @@ +{ + "Recreate silence": "Recrear silencio", + "Edit silence": "Editar silencio", + "Expire silence": "Caducar el silencio", + "Starts": "Comienza", + "Ends": "Termina", + "Expired": "Vencido", + "Name": "Nombre", + "Firing alerts": "Alertas de ejecución", + "State": "Estado", + "Creator": "Creador", + "Alerts": "Alertas", + "Silences": "Silencios", + "Alerting Rules": "Reglas de alerta", + "Alerting rules": "Reglas de alerta", + "Alerting": "Alertando", + "Severity": "Gravedad", + "Namespace": "Espacio de nombres", + "Source": "Fuente", + "Cluster": "Clúster", + "Silence alert": "Alerta de silencio", + "User": "Usuario", + "Platform": "Plataforma", + "Export as CSV": "Exportar como CSV", + "Total": "Total", + "Description": "Descripción", + "Active since": "Activo desde", + "Value": "Valor", + "{{name}} details": "Detalles de {{name}}", + "Alerting rule details": "Detalles de la regla de alerta", + "Summary": "Resumen", + "Message": "Mensaje", + "Runbook": "Libro de ejecución", + "For": "Para", + "Expression": "Expresión", + "Labels": "Etiquetas", + "Active alerts": "Alertas activas", + "None found": "No se encontró nada", + "Alert State": "Estado de alerta", + "Firing": "Ejecutado", + "Pending": "Pendiente", + "Silenced": "Silenciado", + "Not Firing": "Sin ejecución", + "Alert state": "Estado de alerta", + "No alerting rules found": "No se encontraron reglas de alerta", + "Alert details": "Detalles de alerta", + "Alerting rule": "Regla de alerta", + "Silenced by": "Silenciado por", + "Pending: ": "Pendiente: ", + "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "La alerta está activa pero espera la duración especificada en la regla de alerta antes de activarse.", + "Firing: ": "En ejecución: ", + "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.": "La alerta se activa porque la condición de alerta es verdadera y pasó la duración opcional \"for\". La alerta continuará ejecutándose mientras la condición siga siendo cierta.", + "Silenced: ": "Silenciado: ", + "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "La alerta ahora se silencia durante un período de tiempo definido. Silencia temporalmente las alertas según un conjunto de selectores de etiquetas que usted defina. No se enviarán notificaciones para alertas que coincidan con todos los valores o expresiones regulares enumerados.", + "No alerts found": "No se encontraron alertas", + "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Error al cargar silencios desde Alertmanager. Es posible que algunas de las alertas siguientes estén silenciadas.", + "Critical": "Crítico", + "Info": "Información", + "Warning": "Advertencia", + "None": "Ninguno", + "Since": "Desde", + "Inspect": "Inspeccionar", + "The condition that triggered the alert could have a critical impact. The alert requires immediate attention when fired and is typically paged to an individual or to a critical response team.": "La condición que desencadenó la alerta podría tener un impacto crítico. La alerta requiere atención inmediata cuando se activa y normalmente se envía a un individuo o a un equipo de respuesta crítico.", + "The alert provides a warning notification about something that might require attention in order to prevent a problem from occurring. Warnings are typically routed to a ticketing system for non-immediate review.": "La alerta proporciona una notificación de advertencia sobre algo que podría requerir atención para evitar que ocurra un problema. Por lo general, las advertencias se envían a un sistema de emisión de tickets para su revisión no inmediata.", + "The alert is provided for informational purposes only.": "La alerta se proporciona únicamente con fines informativos.", + "The alert has no defined severity.": "La alerta no tiene gravedad definida.", + "You can also create custom severity definitions for user workload alerts.": "También puede crear definiciones de gravedad personalizadas para alertas de carga de trabajo de usuarios.", + "Platform: ": "Plataforma: ", + "Platform-level alerts relate only to OpenShift namespaces. OpenShift namespaces provide core OpenShift functionality.": "Las alertas a nivel de plataforma se relacionan únicamente con los espacios de nombres de OpenShift. Los espacios de nombres de OpenShift proporcionan la funcionalidad principal de OpenShift.", + "User: ": "Usuario: ", + "User workload alerts relate to user-defined namespaces. These alerts are user-created and are customizable. User workload monitoring can be enabled post-installation to provide observability into your own services.": "Las alertas de carga de trabajo del usuario se relacionan con espacios de nombres definidos por el usuario. Estas alertas son creadas por el usuario y son personalizables. La supervisión de la carga de trabajo del usuario se puede habilitar después de la instalación para proporcionar capacidad de observación en sus propios servicios.", + "Create silence": "Crear silencio", + "Overwriting current silence": "Sobrescribir el silencio actual", + "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "Cuando se guarden los cambios, el silencio existente expirará y un nuevo silencio con la nueva configuración ocupará su lugar.", + "Invalid date / time": "Fecha/hora no válida", + "Datetime": "Fecha y hora", + "Select the negative matcher option to update the label value to a not equals matcher.": "Seleccione la opción de comparación negativa para actualizar el valor de la etiqueta a una comparación no igual.", + "If both the RegEx and negative matcher options are selected, the label value must not match the regular expression.": "Si se seleccionan las opciones RegEx y de comparación negativa, el valor de la etiqueta no debe coincidir con la expresión regular.", + "30m": "30 m", + "1h": "1 h", + "2h": "2 h", + "6h": "6 h", + "12h": "12 h", + "1d": "1 d", + "2d": "2 d", + "1w": "1 s", + "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "Silencia temporalmente las alertas según un conjunto de selectores de etiquetas que usted defina. No se enviarán notificaciones para alertas que coincidan con todos los valores o expresiones regulares enumerados.", + "Duration": "Duración", + "Silence alert from...": "Alerta de silencio de...", + "Now": "Ahora", + "For...": "Para...", + "Until...": "Hasta...", + "{{duration}} from now": "{{duration}} desde ahora", + "Start immediately": "Empieza inmediatamente", + "Alert labels": "Etiquetas de alerta", + "Alerts with labels that match these selectors will be silenced instead of firing. Label values can be matched exactly or with a <2>": "Las alertas con etiquetas que coincidan con estos selectores se silenciarán en lugar de activarse. Los valores de las etiquetas se pueden hacer coincidir exactamente o con un <2>", + "regular expression": "expresión regular", + "Label name": "Nombre de etiqueta", + "Label value": "Valor de etiqueta", + "Select all that apply:": "Seleccionar todas las que correspondan:", + "RegEx": "RegEx", + "Negative matcher": "Igualador negativo", + "Remove": "Eliminar", + "Add label": "Agregar etiqueta", + "Required": "Requerido", + "Comment": "Comentario", + "Silence": "Silencio", + "Cancel": "Cancelar", + "Silence details": "Detalles del silencio", + "Actions": "Acciones", + "Matchers": "Factores de coincidencia", + "No matchers": "No hay coincidencias", + "Last updated at": "Última actualización a las", + "Starts at": "Empieza a las", + "Ends at": "Termina a las", + "Created by": "Creado por", + "No Alerts found": "No se encontraron alertas", + "View alerting rule": "Ver regla de alerta", + "Silence State": "Estado de silencio", + "Active": "Activo", + "Error loading silences from Alertmanager. Alertmanager may be unavailable.": "Error al cargar silencios desde Alertmanager. Es posible que alertmanager no esté disponible.", + "No silences found": "No se encontraron silencios", + "Error": "Error", + "Expire {{count}} silence_one": "Caducar {{count}} silencio", + "Expire {{count}} silence_other": "Caducar {{count}} silencios", + "Expire Silence": "Caducar silencio", + "Are you sure you want to expire this silence?": "¿Está seguro de que quiere que este silencio caduque?", + "An error occurred": "Ocurrió un error", + "Restricted access": "Acceso restringido", + "You don't have access to this section due to cluster policy": "No tiene acceso a esta sección debido a la política del clúster", + "Error details": "Error de detalles", + "No {{label}} found": "No se encontraron {{label}}", + "Not found": "No encontrado", + "Try again": "Intentar de nuevo", + "Error loading {{label}}": "Error al cargar {{label}}", + "404: Not Found": "404: No encontrado", + "{{labels}} content is not available in the catalog at this time due to loading failures.": "El contenido {{labels}} no está disponible en el catálogo en este momento debido a fallas en la carga.", + "No datapoints found.": "No se encontraron puntos de datos.", + "Namespaces": "Espacios de nombres", + "Project": "Proyecto", + "Projects": "Proyectos", + "Create new option \"{{option}}\"": "Crear nueva opción “{{option}}”", + "Filter options": "Opciones de filtro", + "Clear input value": "Borrar valor de entrada", + "No results found": "No se encontraron resultados", + "Custom time range": "Rango de tiempo personalizado", + "From": "De", + "To": "Para", + "Save": "Guardar", + "Dashboards": "Paneles de control", + "Metrics dashboards": "Paneles de métricas", + "Error Loading Dashboards": "Error al cargar paneles", + "Error loading card": "Error al cargar la tarjeta", + "Error loading options": "Error al cargar opciones", + "Select a dashboard from the dropdown": "Seleccionar un panel del menú desplegable", + "panel.styles attribute not found": "Atributo panel.styles no encontrado", + "query results table": "tabla de resultados de la consulta", + "Last {{count}} minute_one": "Último {{count}} minuto", + "Last {{count}} minute_other": "Últimos {{count}} minutos", + "Last {{count}} hour_one": "Última {{count}} hora", + "Last {{count}} hour_other": "Últimas {{count}} horas", + "Last {{count}} day_one": "Último {{count}} día", + "Last {{count}} day_other": "Últimos {{count}} días", + "Last {{count}} week_one": "Última {{count}} semana", + "Last {{count}} week_other": "Últimas {{count}} semanas", + "Time range": "Intervalo de tiempo", + "Refresh interval": "Intervalo de actualización", + "Could not parse JSON data for dashboard \"{{dashboard}}\"": "No se pudieron analizar los datos JSON para el panel \"{{dashboard}}\"", + "Dashboard Variables": "Variables del panel", + "No matching datasource found": "No se encontró una fuente de datos que coincida", + "No Dashboard Available in Selected Project": "No hay ningún panel disponible en el proyecto seleccionado", + "To explore data, create a dashboard for this project": "A fin de explorar datos, cree un panel para este proyecto", + "No Perses Project Available": "No hay ningún proyecto Perses disponible", + "To explore data, create a Perses Project": "Para explorar los datos, cree un proyecto Perses", + "Empty Dashboard": "Panel vacío", + "To get started add something to your dashboard": "Para empezar, agregue algo a su panel", + "No projects found": "No se encontraron proyectos", + "No results match the filter criteria.": "Ningún resultado coincide con los criterios del filtro.", + "Clear filters": "Borrar filtros", + "Select project...": "Seleccionar proyecto...", + "Dashboard": "Panel", + "Refresh off": "Actualizar", + "{{count}} second_one": "{{count}} segundo", + "{{count}} second_other": "{{count}} segundos", + "{{count}} minute_one": "{{count}} minuto", + "{{count}} minute_other": "{{count}} minutos", + "{{count}} hour_one": "{{count}} hora", + "{{count}} hour_other": "{{count}} horas", + "{{count}} day_one": "{{count}} día", + "{{count}} day_other": "{{count}} días", + "Alerts Timeline": "Cronología de alertas", + "To view alerts, select an incident from the chart above or from the filters.": "Para ver las alertas, seleccione un incidente en el gráfico anterior o en los filtros.", + "Alert Name": "Nombre de la alerta", + "Component": "Componente", + "Start": "Iniciar", + "End": "Finalizar", + "Resolved": "Resuelto", + "Unknown": "Desconocido", + "Incidents Timeline": "Cronología de incidentes", + "ID": "ID", + "Component(s)": "Componente(s)", + "Alert": "Alerta", + "Incidents": "Incidentes", + "Clear all filters": "Borrar todos los filtros", + "Filter type selection": "Selección de tipo de filtro", + "Incident ID": "ID del incidente", + "Severity filters": "Filtros de gravedad", + "State filters": "Filtros de estado", + "Incident ID filters": "Filtros de ID de incidentes", + "Last 1 day": "Último 1 día", + "Last 3 days": "Últimos 3 días", + "Last 7 days": "Últimos 7 días", + "Last 15 days": "Últimos 15 días", + "Show graph": "Mostrar gráfico", + "Hide graph": "Ocultar gráfico", + "No incident selected.": "No se ha seleccionado ningún incidente.", + "The incident is critical.": "El incidente es crítico.", + "The incident might lead to critical.": "El incidente podría volverse crítico.", + "Informative": "Informativo", + "The incident is not critical.": "El incidente no es crítico.", + "The incident is currently firing.": "El incidente está activo en este momento.", + "The incident is not currently firing.": "El incidente no está activo en este momento.", + "component": "componente", + "components": "componentes", + "No labels": "Sin etiquetas", + "Expression (press Shift+Enter for newlines)": "Expresión (presione Shift+Enter para nuevas líneas)", + "Access restricted.": "Acceso restringido.", + "Failed to load metrics list.": "No se pudo cargar la lista de métricas.", + "Clear query": "Borrar consulta", + "Queries": "Solicitudes", + "Select query": "Seleccionar consulta", + "Add query": "Agregar solicitud", + "Collapse all query tables": "Contraer todas las tablas de solicitud", + "Expand all query tables": "Expandir todas las tablas de solicitud", + "Delete all queries": "Eliminar todas las solicitudes", + "Show series": "Mostrar serie", + "Hide series": "Ocultar serie", + "Disable query": "Deshabilitar solicitud", + "Enable query": "Habilitar solicitud", + "Hide all series": "Ocultar todas las series", + "Show all series": "Mostrar todas las series", + "Query must be enabled": "La solicitud debe estar habilitada", + "Delete query": "Eliminar solicitud", + "Duplicate query": "Duplicar solicitud", + "Error loading values": "Error al cargar valores", + "Unselect all": "Cancelar la selección de todo", + "Select all": "Seleccionar todo", + "Error loading custom data source": "Error al cargar la fuente de datos personalizada", + "An error occurred while loading the custom data source.": "Se produjo un error al cargar la fuente de datos personalizada.", + "No query entered": "No se ingresó ninguna solicitud", + "Enter a query in the box below to explore metrics for this cluster.": "Ingrese una solicitud en el siguiente cuadro para explorar las métricas de este clúster.", + "Insert example query": "Insertar solicitud de ejemplo", + "Run queries": "Ejecutar solicitudes", + "Bytes Binary (KiB, MiB)": "Bytes binarios (KiB, MiB)", + "Bytes Decimal (kb, MB)": "Bytes decimales (kB, MB)", + "Bytes Binary Per Second (KiB/s, MiB/s)": "Bytes binarios por segundo (KiB/s, MiB/s)", + "Bytes Decimal Per Second (kB/s, MB/s)": "Bytes decimales por segundo (kB/s, MB/s)", + "Packets Per Second": "Paquetes por segundo", + "Miliseconds": "Milisegundos", + "Seconds": "Segundos", + "Percentage": "Porcentaje", + "No Units": "Sin unidades", + "Metrics": "Métricas", + "This dropdown only formats results.": "Este menú desplegable solo cambia la forma en que se muestran los resultados.", + "graph timespan": "Intervalo de tiempo del gráfico", + "Reset zoom": "Restablecer zoom", + "Displaying with reduced resolution due to large dataset.": "Visualización con resolución reducida debido al gran conjunto de datos.", + "query browser chart": "consultar gráfico del navegador", + "Ungraphable results": "Resultados no graficables", + "Query results include range vectors, which cannot be graphed. Try adding a function to transform the data.": "Los resultados de la consulta incluyen vectores de rango, que no se pueden representar gráficamente. Intente agregar una función para transformar los datos.", + "Query result is a string, which cannot be graphed.": "El resultado de la consulta es una cadena que no se puede representar gráficamente.", + "The resulting dataset is too large to graph.": "El conjunto de datos resultante es demasiado grande para representarlo gráficamente.", + "Stacked": "Apilados", + "Check to show gaps for missing data": "Marcar para mostrar los intervalos sin datos", + "No gaps found in the data": "No se encontraron intervalos en los datos", + "Disconnected": "Desconectado", + "<0>{{firstIndex}} - {{lastIndex}} of <3>{{itemCount}} {{itemsTitle}}": "<0>{{firstIndex}} -{{lastIndex}} de <3>{{itemCount}} {{itemsTitle}}", + "Items per page": "Elementos por página", + "per page": "por página", + "Go to first page": "Ir a la primera página", + "Go to previous page": "Ir a la página anterior", + "Go to last page": "Ir a la última página", + "Go to next page": "Ir a la página siguiente", + "Current page": "Página actual", + "Pagination": "Paginación", + "of": "de", + "Up": "Arriba", + "Down": "Abajo", + "Target details": "Detalles del objetivo", + "Targets": "Objetivos", + "Error loading service monitor data": "Error al cargar los datos del monitor de servicio", + "Error loading pod monitor data": "Error al cargar los datos del monitor del pod", + "Endpoint": "Endpoint", + "Last scrape": "Último raspado", + "Scrape failed": "Raspado fallido", + "Status": "Estado", + "Monitor": "Monitor", + "Last Scrape": "Último raspado", + "Scrape Duration": "Duración del raspado", + "No metrics targets found": "No se encontraron objetivos de métricas", + "Metrics targets": "Objetivos de métricas", + "Error loading latest targets data": "Error al cargar los datos de objetivos más recientes", + "Search by endpoint or namespace...": "Buscar por endpoint o espacio de nombres...", + "Text": "Texto" +} \ No newline at end of file diff --git a/web/locales/fr/plugin__monitoring-plugin.json b/web/locales/fr/plugin__monitoring-plugin.json new file mode 100644 index 000000000..f9e1b1515 --- /dev/null +++ b/web/locales/fr/plugin__monitoring-plugin.json @@ -0,0 +1,305 @@ +{ + "Recreate silence": "Recréer le silence", + "Edit silence": "Modifier le silence", + "Expire silence": "Mettre fin au silence", + "Starts": "Démarre", + "Ends": "Prend fin", + "Expired": "Expiré", + "Name": "Nom", + "Firing alerts": "Alertes qui se déclenchent", + "State": "État", + "Creator": "Créateur", + "Alerts": "Alertes", + "Silences": "Silences", + "Alerting Rules": "Règles d’alerte", + "Alerting rules": "Règles d’alerte", + "Alerting": "Alerte", + "Severity": "Gravité", + "Namespace": "Espace de noms", + "Source": "Source", + "Cluster": "Cluster", + "Silence alert": "Mettre l’alerte en sourdine", + "User": "Utilisateur", + "Platform": "Plateforme", + "Export as CSV": "Exporter au format CSV", + "Total": "Total", + "Description": "Description", + "Active since": "Actif depuis", + "Value": "Valeur", + "{{name}} details": "Détails de {{name}}", + "Alerting rule details": "Détails de la règle d’alerte", + "Summary": "Résumé", + "Message": "Message", + "Runbook": "Runbook", + "For": "Pendant", + "Expression": "Expression", + "Labels": "Étiquettes", + "Active alerts": "Alertes actives", + "None found": "Alerte introuvable", + "Alert State": "État de l’alerte", + "Firing": "Se déclenche", + "Pending": "En attente", + "Silenced": "Mise en sourdine", + "Not Firing": "Ne se déclenche pas", + "No alerting rules found": "Aucune règle d’alerte trouvée", + "Alert state": "État d’alerte", + "Alert details": "Détails de l’alerte", + "Alerting rule": "Règle d’alerte", + "Silenced by": "Mise en sourdine par", + "Pending: ": "En attente : ", + "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "L’alerte est active mais attend la durée spécifiée dans la règle d’alerte avant de se déclencher.", + "Firing: ": "Se déclenche : ", + "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.": "L’alerte se déclenche, car la condition d’alerte est vraie et la durée facultative « pendant » est passée. L’alerte continuera à se déclencher tant que la condition reste vraie.", + "Silenced: ": "Mise en sourdine : ", + "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "L’alerte est désormais mise en sourdine pendant une période définie. Les silences désactivent temporairement les alertes en fonction d’un ensemble de sélecteurs d’étiquettes que vous définissez. Aucune notification ne sera envoyée pour les alertes correspondant à toutes les valeurs ou expressions régulières répertoriées.", + "No alerts found": "Aucune alerte trouvée", + "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Erreur lors du chargement des silences depuis le Gestionnaire d’alertes. Certaines des alertes ci-dessous ont peut-être été mises en sourdine.", + "Critical": "Critique", + "Info": "Info", + "Warning": "Avertissement", + "None": "Aucun", + "Since": "Depuis", + "Inspect": "Inspecter", + "The condition that triggered the alert could have a critical impact. The alert requires immediate attention when fired and is typically paged to an individual or to a critical response team.": "La condition qui a déclenché l’alerte pourrait avoir un impact critique. L’alerte nécessite une attention immédiate lorsqu’elle est déclenchée et est généralement envoyée à une personne ou à une équipe d’intervention critique.", + "The alert provides a warning notification about something that might require attention in order to prevent a problem from occurring. Warnings are typically routed to a ticketing system for non-immediate review.": "L’alerte fournit une notification d’avertissement concernant un élément qui pourrait nécessiter une attention particulière afin d’éviter qu’un problème ne se produise. Les avertissements sont généralement acheminés vers un système de ticket pour un examen non immédiat.", + "The alert is provided for informational purposes only.": "L’alerte est fournie à titre informatif uniquement.", + "The alert has no defined severity.": "L’alerte n’a pas de gravité définie.", + "You can also create custom severity definitions for user workload alerts.": "Vous pouvez également créer des définitions de gravité personnalisées pour les alertes de charge de travail des utilisateurs.", + "Platform: ": "Plateforme : ", + "Platform-level alerts relate only to OpenShift namespaces. OpenShift namespaces provide core OpenShift functionality.": "Les alertes au niveau de la plateforme concernent uniquement les espaces de noms OpenShift. Les espaces de noms OpenShift fournissent les fonctionnalités de base d’OpenShift.", + "User: ": "Utilisateur : ", + "User workload alerts relate to user-defined namespaces. These alerts are user-created and are customizable. User workload monitoring can be enabled post-installation to provide observability into your own services.": "Les alertes de charge de travail utilisateur concernent les espaces de noms définis par l’utilisateur. Ces alertes sont créées par l’utilisateur et sont personnalisables. La surveillance de la charge de travail des utilisateurs peut être activée après l’installation pour fournir une observabilité dans vos propres services.", + "Create silence": "Créer un silence", + "Overwriting current silence": "Remplacer le silence actuel", + "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "Lorsque les modifications sont enregistrées, le silence existant expire et un nouveau silence avec la nouvelle configuration prend sa place.", + "Invalid date / time": "Date/heure non valide", + "Datetime": "Date/heure", + "Select the negative matcher option to update the label value to a not equals matcher.": "Sélectionnez l’option de correspondance négative pour convertir la valeur de l’étiquette en correspondance « différent de ».", + "If both the RegEx and negative matcher options are selected, the label value must not match the regular expression.": "Si les options RegEx et de correspondance négative sont sélectionnées, la valeur de l’étiquette ne doit pas correspondre à l’expression régulière.", + "30m": "30 m", + "1h": "1 h", + "2h": "2 h", + "6h": "6 h", + "12h": "12 h", + "1d": "1 j", + "2d": "2 j", + "1w": "1 semaine", + "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "Les silences désactivent temporairement les alertes en fonction d’un ensemble de sélecteurs d’étiquettes que vous définissez. Aucune notification ne sera envoyée pour les alertes correspondant à toutes les valeurs ou expressions régulières répertoriées.", + "Duration": "Durée", + "Silence alert from...": "Mettre en sourdine l’alerte de...", + "Now": "Maintenant", + "For...": "Pendant...", + "Until...": "Jusqu’à...", + "{{duration}} from now": "{{duration}} à partir de maintenant", + "Start immediately": "Commencer immédiatement", + "Alert labels": "Étiquettes d’alerte", + "Alerts with labels that match these selectors will be silenced instead of firing. Label values can be matched exactly or with a <2>": "Les alertes dont les étiquettes correspondent à ces sélecteurs seront mises en sourdine au lieu de se déclencher. Les valeurs des étiquettes peuvent correspondre parfaitement ou avec une <2>.", + "regular expression": "expression régulière", + "Label name": "Nom de l’étiquette", + "Label value": "Valeur de l’étiquette", + "Select all that apply:": "Sélectionner tout ce qui est applicable", + "RegEx": "Expression régulière", + "Negative matcher": "Correspondance négative", + "Remove": "Supprimer", + "Add label": "Ajouter une étiquette", + "Required": "Requis", + "Comment": "Commentaire", + "Silence": "Mettre en sourdine", + "Cancel": "Annuler", + "Silence details": "Détails du silence", + "Actions": "Actions", + "Matchers": "Correspondances", + "No matchers": "Aucune correspondance", + "Last updated at": "Heure de la dernière mise à jour", + "Starts at": "Démarre à", + "Ends at": "Se termine à", + "Created by": "Auteur", + "No Alerts found": "Aucune alerte trouvée", + "View alerting rule": "Afficher la règle d’alerte", + "Silence State": "État du silence", + "Active": "Actif", + "Error loading silences from Alertmanager. Alertmanager may be unavailable.": "Erreur lors du chargement des silences depuis le Gestionnaire d’alertes. Ce dernier n’est peut-être pas disponible.", + "No silences found": "Aucun silence trouvé", + "Error": "Erreur", + "Expire {{count}} silence_one": "Mettre fin à {{count}} silence", + "Expire {{count}} silence_other": "Mettre fin à {{count}} silences", + "Expire Silence": "Mettre fin à Silence", + "Are you sure you want to expire this silence?": "Voulez-vous vraiment mettre fin à ce silence ?", + "An error occurred": "Une erreur s’est produite", + "Restricted access": "Accès limité", + "You don't have access to this section due to cluster policy": "Vous n’avez pas accès à cette section en raison des règles d’accès au cluster.", + "Error details": "Détails de l’erreur", + "No {{label}} found": "{{label}} introuvable", + "Not found": "Introuvable", + "Try again": "Réessayer", + "Error loading {{label}}": "Erreur lors du chargement {{label}}", + "404: Not Found": "404 : Introuvable", + "{{labels}} content is not available in the catalog at this time due to loading failures.": "Le contenu {{labels}} n’est pas disponible dans le catalogue pour le moment en raison d’échecs de chargement.", + "No datapoints found.": "Aucun point de données trouvé.", + "Namespaces": "Espaces de noms", + "Project": "Projet", + "Projects": "Projets", + "Create new option \"{{option}}\"": "Créer une nouvelle option \"{{option}} \"", + "Filter options": "Options de filtrage", + "Clear input value": "Effacer la valeur d'entrée", + "No results found": "Aucun résultat trouvé", + "Custom time range": "Plage de temps personnalisée", + "From": "Depuis", + "To": "À", + "Save": "Enregistrer", + "Dashboards": "Tableaux de bord", + "Metrics dashboards": "Tableaux de bord de métriques", + "Error Loading Dashboards": "Erreur de chargement des Tableaux de bord", + "Error loading card": "Erreur lors du chargement de la carte", + "Error loading options": "Erreur lors du chargement des options", + "Select a dashboard from the dropdown": "Sélectionner un tableau de bord dans la liste déroulante", + "panel.styles attribute not found": "Attribut panel.styles introuvable", + "query results table": "tableau des résultats de la requête", + "Last {{count}} minute_one": "{{count}} dernière minute", + "Last {{count}} minute_other": "{{count}} dernières minutes", + "Last {{count}} hour_one": "{{count}} dernière heure", + "Last {{count}} hour_other": "{{count}} dernières heures", + "Last {{count}} day_one": "{{count}} dernier jour", + "Last {{count}} day_other": "{{count}} derniers jours", + "Last {{count}} week_one": "{{count}} dernière semaine", + "Last {{count}} week_other": "{{count}} dernières semaines", + "Time range": "Intervalle de temps", + "Refresh interval": "Intervalle d’actualisation", + "Could not parse JSON data for dashboard \"{{dashboard}}\"": "Impossible d’analyser les données JSON pour le tableau de bord « {{dashboard}} »", + "Dashboard Variables": "Variables de tableau de bord", + "No matching datasource found": "Aucune source de données trouvée", + "No Dashboard Available in Selected Project": "Aucun tableau de bord disponible pour ce projet", + "To explore data, create a dashboard for this project": "Pour explorer les données, créer un tableau de bord pour ce projet", + "No Perses Project Available": "Aucun Projet Perses disponible", + "To explore data, create a Perses Project": "Pour explorer les données, créer un Projet Perses", + "Empty Dashboard": "Vider le Tableau de bord", + "To get started add something to your dashboard": "Pour commencer, ajouter quelquechose à votre tableau de bord", + "No projects found": "Aucun projet trouvé", + "No results match the filter criteria.": "Aucun résultat ne correspond aux critères de filtre.", + "Clear filters": "Effacer les filtres", + "Select project...": "Sélectionner un projet...", + "Dashboard": "Tableau de bord", + "Refresh off": "Actualiser", + "{{count}} second_one": "{{count}} seconde", + "{{count}} second_other": "{{count}} secondes", + "{{count}} minute_one": "{{count}} minute", + "{{count}} minute_other": "{{count}} minutes", + "{{count}} hour_one": "{{count}} heure", + "{{count}} hour_other": "{{count}} heures", + "{{count}} day_one": "{{count}} jour", + "{{count}} day_other": "{{count}} jours", + "Alerts Timeline": "Chronologie des alertes", + "To view alerts, select an incident from the chart above or from the filters.": "Pour afficher les alertes, sélectionner un incident de la charte ci-dessus ou à partir des filtres.", + "Alert Name": "Nom de L’alerte", + "Component": "Composant", + "Start": "Démarrer", + "End": "Fin", + "Resolved": "Résolu", + "Unknown": "Inconnu", + "Incidents Timeline": "Chronologie des incidents", + "ID": "ID", + "Component(s)": "Composant(s)", + "Alert": "Alerte", + "Incidents": "Incidents", + "Clear all filters": "Effacer tous les filtres", + "Filter type selection": "Sélection de type de filtre", + "Incident ID": "ID Incident", + "Severity filters": "Flitres de sévérité", + "State filters": "Indiquer les filtres", + "Incident ID filters": "Filtres d’ID d’incident", + "Last 1 day": "1 jour", + "Last 3 days": "Les 3 derniers jours", + "Last 7 days": "Les 7 derniers jours", + "Last 15 days": "Les 15 derniers jours", + "Show graph": "Afficher le graphique", + "Hide graph": "Masquer le graphique", + "No incident selected.": "Aucun incident sélectionné", + "The incident is critical.": "L’incident est critique.", + "The incident might lead to critical.": "L’incident peut évoluer vers état critique.", + "Informative": "Informatif", + "The incident is not critical.": "L’incident n’est pas critique.", + "The incident is currently firing.": "L’incident est en cours.", + "The incident is not currently firing.": "L’incident n’est pas en cours.", + "component": "composant", + "components": "composants", + "No labels": "Aucune étiquette", + "Expression (press Shift+Enter for newlines)": "Expression (appuyez sur Maj+Entrée pour insérer de nouvelles lignes)", + "Access restricted.": "Accès limité.", + "Failed to load metrics list.": "Échec du chargement de la liste des métriques.", + "Clear query": "Effacer la requête", + "Queries": "Demandes", + "Select query": "Sélectionner une requête", + "Add query": "Ajouter une demande", + "Collapse all query tables": "Réduire toutes les tables de recherche", + "Expand all query tables": "Développer toutes les tables de recherche", + "Delete all queries": "Supprimer toutes les demandes de recherche", + "Show series": "Afficher la série", + "Hide series": "Masquer la série", + "Disable query": "Désactiver la recherche", + "Enable query": "Activer la recherche", + "Hide all series": "Masquer toutes les séries", + "Show all series": "Afficher toutes les séries", + "Query must be enabled": "La demande doit être activée", + "Delete query": "Supprimer la recherche", + "Duplicate query": "Dupliquer la recherche", + "Error loading values": "Erreur lors du chargement des valeurs", + "Unselect all": "Tout désélectionner", + "Select all": "Tout sélectionner", + "Error loading custom data source": "Erreur de chargement de la source de donnés personnalisée", + "An error occurred while loading the custom data source.": "Une erreur s’est produite lors du chargement de la source de données personnalisée.", + "No query entered": "Aucune demande n’a été saisie", + "Enter a query in the box below to explore metrics for this cluster.": "Saisir une demande dans la case ci-dessous pour explorer les métriques pour ce cluster.", + "Insert example query": "Insérer un exemple requête", + "Run queries": "Exécuter les demandes", + "Bytes Binary (KiB, MiB)": "Bytes Binary (KiB, MiB)", + "Bytes Decimal (kb, MB)": "Bytes Decimal (kb, MB)", + "Bytes Binary Per Second (KiB/s, MiB/s)": "Bytes Binary Per Second (KiB/s, MiB/s)", + "Bytes Decimal Per Second (kB/s, MB/s)": "Bytes Decimal Per Second (kB/s, MB/s)", + "Packets Per Second": "Packets Per Second", + "Miliseconds": "Millisecondes", + "Seconds": "Secondes", + "Percentage": "Pourcentage", + "No Units": "Unités", + "Metrics": "Métriques", + "This dropdown only formats results.": "Ce menu déroulant ne formate que les résultats.", + "graph timespan": "Durée du graphique", + "Reset zoom": "Réinitialiser le zoom", + "Displaying with reduced resolution due to large dataset.": "Affichage avec une résolution réduite en raison d’un grand jeu de données.", + "query browser chart": "graphique du navigateur de requêtes", + "Ungraphable results": "Résultats non convertibles en graphiques", + "Query results include range vectors, which cannot be graphed. Try adding a function to transform the data.": "Les résultats de la requête incluent des vecteurs de plage qui ne peuvent pas être représentés graphiquement. Essayez d’ajouter une fonction pour transformer les données.", + "Query result is a string, which cannot be graphed.": "Le résultat de la requête est une chaîne qui ne peut pas être représentée graphiquement.", + "The resulting dataset is too large to graph.": "Le jeu de données résultant est trop volumineux pour être représenté graphiquement.", + "Stacked": "Empilé", + "Check to show gaps for missing data": "Vérifier les données manquantes", + "No gaps found in the data": "Aucun manquement de données", + "Disconnected": "Déconnecté", + "<0>{{firstIndex}} - {{lastIndex}} of <3>{{itemCount}} {{itemsTitle}}": "<0>{{firstIndex}} -{{lastIndex}} sur <3>{{itemCount}} {{itemsTitle}}", + "Items per page": "Éléments par page", + "per page": "par page", + "Go to first page": "Atteindre la première page", + "Go to previous page": "Atteindre la page précédente", + "Go to last page": "Atteindre la dernière page", + "Go to next page": "Atteindre la page suivante", + "Current page": "Page actuelle", + "Pagination": "Pagination", + "of": "sur", + "Up": "Vers le haut", + "Down": "Vers le bas", + "Target details": "Détails de la cible", + "Targets": "Cibles", + "Error loading service monitor data": "Erreur lors du chargement des données du moniteur de service", + "Error loading pod monitor data": "Erreur lors du chargement des données du moniteur de pod", + "Endpoint": "Point de terminaison", + "Last scrape": "Dernière extraction", + "Scrape failed": "Échec de l’extraction", + "Status": "Statut", + "Monitor": "Moniteur", + "Last Scrape": "Dernière extraction", + "Scrape Duration": "Durée de l’extraction", + "No metrics targets found": "Aucune cible de métriques trouvée", + "Metrics targets": "Cibles de métriques", + "Error loading latest targets data": "Erreur lors du chargement des dernières données de cibles", + "Search by endpoint or namespace...": "Recherche par point de terminaison ou espace de noms...", + "Text": "Texte" +} \ No newline at end of file diff --git a/web/locales/ja/plugin__monitoring-plugin.json b/web/locales/ja/plugin__monitoring-plugin.json index e63ffdd1a..3cc4cd351 100644 --- a/web/locales/ja/plugin__monitoring-plugin.json +++ b/web/locales/ja/plugin__monitoring-plugin.json @@ -1,100 +1,158 @@ { + "Recreate silence": "サイレンスの再作成", + "Edit silence": "サイレンスの編集", + "Expire silence": "サイレンスを期限切れにする", + "Starts": "開始", + "Ends": "終了", + "Expired": "期限切れ", + "Name": "名前", + "Firing alerts": "アラートの実行", + "State": "状態", + "Creator": "作成者", + "Alerts": "アラート", + "Silences": "サイレンス", + "Alerting Rules": "アラートルール", + "Alerting rules": "アラートルール", + "Alerting": "アラート", + "Severity": "重大度", + "Namespace": "Namespace", + "Source": "ソース", + "Cluster": "クラスター", + "Silence alert": "アラートをサイレンスにする", + "User": "User", + "Platform": "プラットフォーム", + "Export as CSV": "CSV としてエクスポート", + "Total": "合計", + "Description": "説明", + "Active since": "アクティブにされた時刻:", + "Value": "値", + "{{name}} details": "{{name}} の詳細", + "Alerting rule details": "アラートルールの詳細", + "Summary": "概要", + "Message": "メッセージ", + "Runbook": "runbook", + "For": "期間:", + "Expression": "式", + "Labels": "ラベル", + "Active alerts": "アクティブなアラート", + "None found": "何も見つかりません", + "Alert State": "アラート状態", "Firing": "実行中", "Pending": "保留中", "Silenced": "サイレンス", "Not Firing": "実行中ではない", - "Active": "アクティブ", - "Expired": "期限切れ", - "Ends": "終了", - "Since": "開始:", - "Critical": "重大", - "Info": "情報", - "Warning": "警告", - "None": "なし", + "No alerting rules found": "アラートルールはありません", + "Alert state": "アラート状態", + "Alert details": "アラートの詳細", + "Alerting rule": "アラートルール", + "Silenced by": "サイレンス:", "Pending: ": "保留中: ", - "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "アラートはアクティブですが、アラート実行前のアラートルールに指定される期間待機します。", + "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "アラートはアクティブですが、アラートルールで指定された期間が経過してからアラートが実行されます。", "Firing: ": "実行中: ", "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.": "アラート条件が true で、オプションの「for」期間が渡されているため、アラートが実行されます。条件が true である限りアラートが実行されます。", "Silenced: ": "サイレンス: ", "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "アラートは、定義された期間、サイレンス設定されます。サイレンスは、定義するラベルセレクターのセットに基づいてアラートを一時的にミュートします。表示されているすべての値または正規表現に一致するアラートの通知は送信されません。", - "Critical: ": "重大 (critical): ", + "No alerts found": "アラートはありません", + "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Alertmanager からのサイレンスの読み込み中にエラーが発生しました。以下の一部のアラートは実際にサイレンスにされている可能性があります。", + "Critical": "重大", + "Info": "情報", + "Warning": "警告", + "None": "なし", + "Since": "開始:", + "Inspect": "検査", "The condition that triggered the alert could have a critical impact. The alert requires immediate attention when fired and is typically paged to an individual or to a critical response team.": "アラートをトリガーした状態は重大な影響を与える可能性があります。このアラートには、実行時に早急な対応が必要となり、通常は個人または緊急対策チーム (Critical Response Team) に送信先が設定されます。", - "Warning: ": "警告: ", "The alert provides a warning notification about something that might require attention in order to prevent a problem from occurring. Warnings are typically routed to a ticketing system for non-immediate review.": "アラートは、問題の発生を防ぐために注意が必要になる可能性のある問題についての警告通知を提供します。通常、警告は即時のレビューが不要な場合にはチケットシステムにルーティングされます。", - "Info: ": "情報: ", "The alert is provided for informational purposes only.": "アラートは情報提供のみを目的として提供されます。", - "None: ": "なし: ", - "The alert has no defined severity.": "アラートには重大度が定義されていません。", + "The alert has no defined severity.": "アラートに重大度が定義されていません。", "You can also create custom severity definitions for user workload alerts.": "ユーザーワークロードアラートについてのカスタムの重大度定義を作成することもできます。", "Platform: ": "プラットフォーム: ", "Platform-level alerts relate only to OpenShift namespaces. OpenShift namespaces provide core OpenShift functionality.": "プラットフォームレベルのアラートは OpenShift namespace にのみ関連します。OpenShift namespace は OpenShift のコア機能を提供します。", "User: ": "ユーザー: ", "User workload alerts relate to user-defined namespaces. These alerts are user-created and are customizable. User workload monitoring can be enabled post-installation to provide observability into your own services.": "ユーザーワークロードアラートはユーザー定義の namespace に関連します。これらのアラートはユーザーによって作成され、カスタマイズ可能です。ユーザーワークロードのモニタリングはインストール後に有効にし、独自のサービスに対する可観測性を実現できます。", - "Starts": "開始", - "Platform": "プラットフォーム", - "User": "User", - "Name": "名前", - "Firing alerts": "アラートの実行", - "State": "状態", - "Creator": "作成者", - "Silenced by": "サイレンス:", - "Alerts": "アラート", - "Alert details": "アラートの詳細", - "Silence alert": "アラートをサイレンスにする", - "Severity": "重大度", - "Description": "説明", - "Summary": "概要", - "Message": "メッセージ", - "Runbook": "runbook", - "Source": "ソース", - "Labels": "ラベル", - "Alerting rule": "アラートルール", - "Active since": "アクティブにされた時刻:", - "Value": "値", - "{{name}} details": "{{name}} の詳細", - "Alerting rules": "アラートルール", - "Alerting rule details": "アラートルールの詳細", - "For": "期間:", - "Expression": "式", - "Active alerts": "アクティブなアラート", - "None found": "見つかりません", - "Expire silence": "サイレンスを期限切れにする", - "Are you sure you want to expire this silence?": "このサイレンスを期限切れにしてもよろしいですか?", - "An error occurred": "エラーが発生しました", + "Create silence": "サイレンスの作成", + "Overwriting current silence": "現在のサイレンスの上書き", + "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "変更が保存されると、現時点で既存のサイレンスが期限切れになり、新規の設定を使用した新規サイレンスがこの代わりとして実行されます。", + "Invalid date / time": "無効な日付/時間", + "Datetime": "日時", + "Select the negative matcher option to update the label value to a not equals matcher.": "ネガティブマッチャーオプションを選択し、ラベル値を not equals マッチャーに更新します。", + "If both the RegEx and negative matcher options are selected, the label value must not match the regular expression.": "RegEx オプションとネガティブマッチャーオプションの両方が選択されている場合、ラベル値は正規表現と一致しません。", + "30m": "30 分間", + "1h": "1 時間", + "2h": "2 時間", + "6h": "6 時間", + "12h": "12 時間", + "1d": "1 日", + "2d": "2 日", + "1w": "1 週間", + "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "サイレンスは、定義するラベルセレクターのセットに基づいてアラートを一時的にミュートします。表示されているすべての値または正規表現に一致するアラートの通知は送信されません。", + "Duration": "期間", + "Silence alert from...": "アラートをサイレンス設定するタイミング...", + "Now": "現在", + "For...": "期間...", + "Until...": "期限...", + "{{duration}} from now": "今から {{duration}}", + "Start immediately": "すぐに開始", + "Alert labels": "アラートのラベル", + "Alerts with labels that match these selectors will be silenced instead of firing. Label values can be matched exactly or with a <2>": "以下のセレクターに一致するラベルの付いたアラートは、発行されずにサイレンス設定されます。ラベル値は、完全一致検索とすることも、<2> で一致検索することもできます", + "regular expression": "正規表現", + "Label name": "ラベル名", + "Label value": "ラベルの値", + "Select all that apply:": "該当するものをすべて選択してください:", + "RegEx": "RegEx", + "Negative matcher": "ネガティブマッチャー", + "Remove": "削除", + "Add label": "ラベルの追加", + "Required": "必須", + "Comment": "コメント", + "Silence": "サイレンス", "Cancel": "キャンセル", - "Recreate silence": "サイレンスの再作成", - "Edit silence": "サイレンスの編集", - "View alerting rule": "アラートルールの表示", - "Silences": "サイレンス", "Silence details": "サイレンスの詳細", + "Actions": "アクション", "Matchers": "マッチャー", "No matchers": "マッチャーなし", "Last updated at": "最終更新:", "Starts at": "開始時刻:", "Ends at": "終了時刻:", "Created by": "作成者:", - "Comment": "コメント", - "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Alertmanager からのサイレンスの読み込み中にエラーが発生しました。以下の一部のアラートは実際にサイレンスにされている可能性があります。", - "Alert State": "アラート状態", - "Alert state": "アラート状態", - "Create silence": "サイレンスの作成", + "No Alerts found": "アラートが見つかりません", + "View alerting rule": "アラートルールの表示", "Silence State": "サイレンス状態", + "Active": "アクティブ", "Error loading silences from Alertmanager. Alertmanager may be unavailable.": "Alertmanager からのサイレンスの読み込みエラー。Alertmanager は利用不可になる場合があります。", + "No silences found": "サイレンスはありません", "Error": "エラー", - "Alerting": "Alerting", + "Expire {{count}} silence_one": "{{count}} つのサイレンスを期限切れにする", + "Expire {{count}} silence_other": "{{count}} つのサイレンスを期限切れにする", + "Expire Silence": "サイレンスを期限切れにする", + "Are you sure you want to expire this silence?": "このサイレンスを期限切れにしてもよろしいですか?", + "An error occurred": "エラーが発生しました", + "Restricted access": "制限されたアクセス", + "You don't have access to this section due to cluster policy": "クラスターポリシーにより、このセクションにアクセスできません", + "Error details": "エラーの詳細", + "No {{label}} found": "{{label}} が見つかりません", + "Not found": "見つかりません", + "Try again": "再試行", + "Error loading {{label}}": "読み込みエラー {{label}}", + "404: Not Found": "404: Not Found", + "{{labels}} content is not available in the catalog at this time due to loading failures.": "{{labels}} コンテンツは現時点では読み込みの失敗によりカタログでは利用できません。", + "No datapoints found.": "データポイントが見つかりません。", + "Namespaces": "namespaces", + "Project": "プロジェクト", + "Projects": "プロジェクト", + "Create new option \"{{option}}\"": "新しいオプション \"{{option}}\" の作成", + "Filter options": "フィルターオプション", + "Clear input value": "入力値のクリア", + "No results found": "結果が見つかりません", "Custom time range": "カスタム時間範囲", "From": "ソース", "To": "宛先", "Save": "保存", - "Filter options": "フィルターオプション", - "Select a dashboard from the dropdown": "ドロップダウンからダッシュボードを選択します", - "Error loading options": "オプションの読み込みエラー", - "Dashboard": "ダッシュボード", - "Refresh interval": "更新間隔", - "Dashboards": "Dashboard", - "Inspect": "検査", - "Error loading card": "カードの読み込みエラー", + "Dashboards": "ダッシュボード", "Metrics dashboards": "メトリクスダッシュボード", + "Error Loading Dashboards": "ダッシュボードの読み込みエラー", + "Error loading card": "カードの読み込みエラー", + "Error loading options": "オプションの読み込みエラー", + "Select a dashboard from the dropdown": "ドロップダウンからダッシュボードを選択します", "panel.styles attribute not found": "panel.styles 属性が見つかりません", "query results table": "クエリー結果テーブル", "Last {{count}} minute_one": "最後の {{count}} 分", @@ -106,47 +164,104 @@ "Last {{count}} week_one": "最後の {{count}} 週間", "Last {{count}} week_other": "最後の {{count}} 週間", "Time range": "時間の範囲", + "Refresh interval": "更新間隔", "Could not parse JSON data for dashboard \"{{dashboard}}\"": "ダッシュボード \"{{dashboard}}\" の JSON データを解析できませんでした", - "No labels": "ラベルなし", + "Dashboard Variables": "ダッシュボード変数", + "No matching datasource found": "一致するデータソースが見つかりません", + "No Dashboard Available in Selected Project": "選択したプロジェクトではダッシュボードが利用できません", + "To explore data, create a dashboard for this project": "データを探索するには、このプロジェクトのダッシュボードを作成してください", + "No Perses Project Available": "利用可能な Perses プロジェクトがありません", + "To explore data, create a Perses Project": "データを探索するには、Perses プロジェクトを作成してください", + "Empty Dashboard": "空のダッシュボード", + "To get started add something to your dashboard": "まずダッシュボードに何か追加してください", + "No projects found": "プロジェクトが見つかりません", + "No results match the filter criteria.": "フィルター条件にマッチする結果はありません。", + "Clear filters": "フィルターをクリア", + "Select project...": "プロジェクトの選択...", + "Dashboard": "ダッシュボード", + "Refresh off": "更新オフ", + "{{count}} second_one": "{{count}} 秒", + "{{count}} second_other": "{{count}} 秒", + "{{count}} minute_one": "{{count}} 分", + "{{count}} minute_other": "{{count}} 分", + "{{count}} hour_one": "{{count}} 時間", + "{{count}} hour_other": "{{count}} 時間", + "{{count}} day_one": "{{count}} 日", + "{{count}} day_other": "{{count}} 日", + "Alerts Timeline": "アラートタイムライン", + "To view alerts, select an incident from the chart above or from the filters.": "アラートを表示するには、上のグラフまたはフィルターからインシデントを選択してください。", + "Alert Name": "アラート名", + "Component": "コンポーネント", + "Start": "開始", + "End": "終了", + "Resolved": "解決済み", + "Unknown": "不明", + "Incidents Timeline": "インシデントタイムライン", + "ID": "ID", + "Component(s)": "コンポーネント", + "Alert": "アラート", + "Incidents": "インシデント", + "Clear all filters": "すべてのフィルターをクリア", + "Filter type selection": "フィルタータイプの選択", + "Incident ID": "インシデント ID", + "Severity filters": "重大度フィルター", + "State filters": "状態フィルター", + "Incident ID filters": "インシデント ID フィルター", + "Last 1 day": "過去 1 日間", + "Last 3 days": "過去 3 日間", + "Last 7 days": "過去 7 日間", + "Last 15 days": "過去 15 日間", + "Show graph": "グラフの表示", + "Hide graph": "グラフの非表示", + "No incident selected.": "インシデントが選択されていません。", + "The incident is critical.": "このインシデントは重大です。", + "The incident might lead to critical.": "このインシデントは重大な問題に発展するおそれがあります。", + "Informative": "情報提供", + "The incident is not critical.": "このインシデントは重大ではありません。", + "The incident is currently firing.": "このインシデントは現在も発生中です。", + "The incident is not currently firing.": "このインシデントは現在発生していません。", + "component": "コンポーネント", + "components": "コンポーネント", + "No labels": "ラベルがありません", + "Expression (press Shift+Enter for newlines)": "式 (Shift+Enter で改行)", + "Access restricted.": "アクセスが制限されています。", + "Failed to load metrics list.": "メトリクス一覧の読み込みに失敗しました。", + "Clear query": "クエリーをクリアする", + "Queries": "クエリー", + "Select query": "クエリーの選択", "Add query": "クエリーの追加", "Collapse all query tables": "すべてのクエリーテーブルを折りたたむ", "Expand all query tables": "すべてのクエリーテーブルを展開する", "Delete all queries": "すべてのクエリーを削除する", - "Show graph": "グラフの表示", - "Hide graph": "グラフの非表示", - "Hide table": "テーブルを非表示にする", - "Show table": "テーブルを表示する", "Show series": "シリーズを表示する", "Hide series": "シリーズを非表示にする", "Disable query": "クエリーの無効化", "Enable query": "クエリーの有効化", - "Query must be enabled": "クエリーを有効にする必要があります", "Hide all series": "すべてのシリーズを非表示", "Show all series": "すべてのシリーズを表示", + "Query must be enabled": "クエリーを有効にする必要があります", "Delete query": "クエリーの削除", "Duplicate query": "クエリーの複製", "Error loading values": "値の読み込みエラー", - "No datapoints found.": "データポイントが見つかりません。", "Unselect all": "すべて選択解除", "Select all": "すべて選択", + "Error loading custom data source": "カスタムデータソースの読み込みエラー", + "An error occurred while loading the custom data source.": "カスタムデータソースの読み込み中にエラーが発生しました。", "No query entered": "クエリーが入力されていません", "Enter a query in the box below to explore metrics for this cluster.": "以下のボックスにクエリーを入力し、このクラスターのメトリクスを参照します。", "Insert example query": "サンプルクエリーの挿入", "Run queries": "クエリーの実行", - "Metrics": "Metrics", - "Refresh off": "更新オフ", - "{{count}} second_one": "{{count}} 秒", - "{{count}} second_other": "{{count}} 秒", - "{{count}} minute_one": "{{count}} 分", - "{{count}} minute_other": "{{count}} 分", - "{{count}} hour_one": "{{count}} 時間", - "{{count}} hour_other": "{{count}} 時間", - "{{count}} day_one": "{{count}} 日", - "{{count}} day_other": "{{count}} 日", - "Expression (press Shift+Enter for newlines)": "式 (Shift+Enter で改行)", - "Access restricted.": "アクセスが制限されています。", - "Failed to load metrics list.": "メトリクス一覧の読み込みに失敗しました。", - "Clear query": "クエリーをクリアする", + "Bytes Binary (KiB, MiB)": "バイト数 (2 進) (KiB、MiB)", + "Bytes Decimal (kb, MB)": "バイト数 (10 進) (kb、MB)", + "Bytes Binary Per Second (KiB/s, MiB/s)": "1 秒あたりのバイト数 (2 進) (KiB/s、MiB/s)", + "Bytes Decimal Per Second (kB/s, MB/s)": "1 秒あたりのバイト数 (10 進) (kB/s、MB/s)", + "Packets Per Second": "1 秒あたりのパケット数", + "Miliseconds": "ミリ秒", + "Seconds": "秒", + "Percentage": "パーセンテージ", + "No Units": "単位なし", + "Metrics": "メトリクス", + "This dropdown only formats results.": "このドロップダウンでは結果のフォーマット設定のみが行われます。", "graph timespan": "グラフのタイムスパン", "Reset zoom": "ズームのリセット", "Displaying with reduced resolution due to large dataset.": "データセットが大きいため、解像度を下げて表示されます。", @@ -156,38 +271,9 @@ "Query result is a string, which cannot be graphed.": "クエリー結果は文字列で、グラフ化できません。", "The resulting dataset is too large to graph.": "結果として生成されるデータセットは大き過ぎるためにグラフ化できません。", "Stacked": "スタック", - "Invalid date / time": "無効な日付/時間", - "Datetime": "日時", - "Select the negative matcher option to update the label value to a not equals matcher.": "ネガティブマッチャーオプションを選択し、ラベル値を not equals マッチャーに更新します。", - "If both the RegEx and negative matcher options are selected, the label value must not match the regular expression.": "RegEx オプションとネガティブマッチャーオプションの両方が選択されている場合、ラベル値は正規表現と一致しません。", - "30m": "30 分間", - "1h": "1 時間", - "2h": "2 時間", - "6h": "6 時間", - "12h": "12 時間", - "1d": "1 日", - "2d": "2 日", - "1w": "1 週間", - "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "サイレンスは、定義するラベルセレクターのセットに基づいてアラートを一時的にミュートします。表示されているすべての値または正規表現に一致するアラートの通知は送信されません。", - "Duration": "期間", - "Silence alert from...": "アラートをサイレンス設定するタイミング...", - "Now": "現在", - "For...": "期間...", - "Until...": "期限...", - "{{duration}} from now": "今から {{duration}}", - "Start immediately": "すぐに開始", - "Alert labels": "アラートのラベル", - "Alerts with labels that match these selectors will be silenced instead of firing. Label values can be matched exactly or with a <2>": "以下のセレクターに一致するラベルの付いたアラートは、発行されずにサイレンス設定されます。ラベル値は、完全一致検索とすることも、<2> で一致検索することもできます", - "regular expression": "正規表現", - "Label name": "ラベル名", - "Label value": "ラベルの値", - "RegEx": "RegEx", - "Negative matcher": "ネガティブマッチャー", - "Remove": "削除", - "Add label": "ラベルの追加", - "Silence": "サイレンス", - "Overwriting current silence": "現在のサイレンスの上書き", - "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "変更が保存されると、現時点で既存のサイレンスが期限切れになり、新規の設定を使用した新規サイレンスがこの代わりとして実行されます。", + "Check to show gaps for missing data": "欠落データの不足分を表示するにはチェックを入れてください", + "No gaps found in the data": "データに不足分は見つかりませんでした", + "Disconnected": "切断", "<0>{{firstIndex}} - {{lastIndex}} of <3>{{itemCount}} {{itemsTitle}}": "<0>{{firstIndex}} - {{lastIndex}}/<3>{{itemCount}} {{itemsTitle}}", "Items per page": "1 ページの項目数", "per page": "ページあたり", @@ -200,20 +286,20 @@ "of": "/", "Up": "起動中", "Down": "停止中", - "Target details": "Target の詳細", - "Targets": "Target", + "Target details": "ターゲットの詳細", + "Targets": "ターゲット", "Error loading service monitor data": "サービスモニターデータの読み込みエラー", "Error loading pod monitor data": "Pod モニターデータの読み込みエラー", "Endpoint": "エンドポイント", - "Namespace": "Namespace", "Last scrape": "最後の収集日時", "Scrape failed": "収集に失敗しました", "Status": "ステータス", "Monitor": "モニター", "Last Scrape": "最後の収集日時", "Scrape Duration": "収集期間", + "No metrics targets found": "メトリクスターゲットはありません", "Metrics targets": "メトリクスターゲット", "Error loading latest targets data": "最新のターゲットデータの読み込み中にエラーが発生しました", "Search by endpoint or namespace...": "エンドポイントや namespace で検索...", "Text": "テキスト" -} +} \ No newline at end of file diff --git a/web/locales/ko/plugin__monitoring-plugin.json b/web/locales/ko/plugin__monitoring-plugin.json index 88ef084b3..4575e602c 100644 --- a/web/locales/ko/plugin__monitoring-plugin.json +++ b/web/locales/ko/plugin__monitoring-plugin.json @@ -1,100 +1,158 @@ { + "Recreate silence": "음소거 다시 생성", + "Edit silence": "음소거 편집", + "Expire silence": "음소거 만료", + "Starts": "시작", + "Ends": "종료", + "Expired": "만료", + "Name": "이름", + "Firing alerts": "알림 실행", + "State": "상태", + "Creator": "작성자", + "Alerts": "알림", + "Silences": "음소거", + "Alerting Rules": "알림 규칙", + "Alerting rules": "알림 규칙", + "Alerting": "알림", + "Severity": "심각도", + "Namespace": "네임 스페이스", + "Source": "소스", + "Cluster": "클러스터", + "Silence alert": "음소거 알림", + "User": "사용자", + "Platform": "플랫폼:", + "Export as CSV": "CSV로 내보내기", + "Total": "합계", + "Description": "설명", + "Active since": "다음 시간 이후 활성", + "Value": "값", + "{{name}} details": "{{name}} 세부 정보", + "Alerting rule details": "알림 규칙 세부 정보", + "Summary": "요약", + "Message": "메시지", + "Runbook": "Runbook", + "For": "기간", + "Expression": "표현식", + "Labels": "라벨", + "Active alerts": "활성 알림", + "None found": "찾을 수 없음", + "Alert State": "알림 상태", "Firing": "실행", "Pending": "보류", "Silenced": "음소거", "Not Firing": "실행하지 않음", - "Active": "활성", - "Expired": "만료", - "Ends": "종료", - "Since": "다음 시간 이후", - "Critical": "심각", - "Info": "정보", - "Warning": "경고", - "None": "없음", + "No alerting rules found": "알림 규칙 없음", + "Alert state": "알림 상태", + "Alert details": "알림 세부 정보", + "Alerting rule": "알림 규칙", + "Silenced by": "음소거", "Pending: ": "보류 중:", "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "알림은 활성화되어 있지만 실행되기 전에 알림 규칙에 지정된 기간 동안 대기 중입니다.", "Firing: ": "실행:", "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.": "알림 조건이 true이고 선택 사항인 'for' 기간이 경과되었기 때문에 알림이 실행됩니다. 알림은 조건이 true인 한 계속해서 발생합니다.", "Silenced: ": "음소거:", "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "정의된 기간 동안 알림이 음소거됩니다. 사용자가 정의한 레이블 선택기 세트에 따라 알림을 일시적으로 음소거합니다. 나열된 모든 값 또는 정규식과 일치하는 알림에 대해서는 알림이 전송되지 않습니다.", - "Critical: ": "심각:", + "No alerts found": "알림 없음", + "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Alertmanager에서 음소거를 로드하는 중 오류가 발생했습니다. 아래 알림 중 일부는 실제로 음소거될 수 있습니다.", + "Critical": "심각:", + "Info": "정보", + "Warning": "경고", + "None": "없음", + "Since": "다음 시간 이후", + "Inspect": "검사", "The condition that triggered the alert could have a critical impact. The alert requires immediate attention when fired and is typically paged to an individual or to a critical response team.": "알림을 트리거한 상태는 심각한 영향을 미칠 수 있습니다. 알림은 실행 시 즉각적인 주의가 필요하며 일반적으로 개인 또는 문제 대응팀으로 호출됩니다.", - "Warning: ": "경고:", "The alert provides a warning notification about something that might require attention in order to prevent a problem from occurring. Warnings are typically routed to a ticketing system for non-immediate review.": "이 알림은 문제가 발생하지 않도록 주의가 필요한 항목에 대한 경고 알림을 제공합니다. 일반적으로 경고는 나중 검토를 위해 티켓팅 시스템으로 라우팅됩니다.", - "Info: ": "정보:", "The alert is provided for informational purposes only.": "알림은 정보 목적으로만 제공됩니다.", - "None: ": "없음:", "The alert has no defined severity.": "알림에 정의된 심각도가 없습니다.", "You can also create custom severity definitions for user workload alerts.": "사용자 워크로드 알림에 대한 사용자 지정 심각도 정의를 생성 할 수도 있습니다.", "Platform: ": "플랫폼:", "Platform-level alerts relate only to OpenShift namespaces. OpenShift namespaces provide core OpenShift functionality.": "플랫폼 수준 알림은 OpenShift 네임 스페이스에만 관련됩니다. OpenShift 네임 스페이스는 핵심 OpenShift 기능을 제공합니다.", "User: ": "사용자: ", "User workload alerts relate to user-defined namespaces. These alerts are user-created and are customizable. User workload monitoring can be enabled post-installation to provide observability into your own services.": "사용자 워크로드 알림은 사용자 정의 네임 스페이스와 관련됩니다. 이러한 알림은 사용자가 생성하고 사용자 정의할 수 있습니다. 설치 후 사용자 워크로드 모니터링을 사용하도록 설정하여 사용자 서비스에 대한 모니터링 기능을 제공할 수 있습니다.", - "Starts": "시작", - "Platform": "플랫폼", - "User": "사용자", - "Name": "이름", - "Firing alerts": "알림 실행", - "State": "상태", - "Creator": "작성자", - "Silenced by": "음소거", - "Alerts": "알림", - "Alert details": "알림 세부 정보", - "Silence alert": "음소거 알림", - "Severity": "심각도", - "Description": "설명", - "Summary": "요약", - "Message": "메시지", - "Runbook": "Runbook", - "Source": "소스", - "Labels": "라벨", - "Alerting rule": "알림 규칙", - "Active since": "다음 시간 이후 활성", - "Value": "값", - "{{name}} details": "{{name}} 세부 정보", - "Alerting rules": "알림 규칙", - "Alerting rule details": "알림 규칙 세부 정보", - "For": "기간", - "Expression": "표현식", - "Active alerts": "활성 알림", - "None found": "찾을 수 없음", - "Expire silence": "음소거 만료", - "Are you sure you want to expire this silence?": "음소거를 종료하시겠습니까?", - "An error occurred": "오류가 발생했습니다", + "Create silence": "음소거 상태 만들기", + "Overwriting current silence": "현재 음소거 상태 덮어 쓰기", + "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "변경 사항이 저장되면 현재 음소거 상태가 만료되고 새 설정을 사용하여 새 음소거가 대신 적용됩니다.", + "Invalid date / time": "잘못된 날짜/시간", + "Datetime": "날짜 시간", + "Select the negative matcher option to update the label value to a not equals matcher.": "부정 일치 탐색기 옵션을 선택하여 레이블 값을 일치하지 않음으로 업데이트합니다.", + "If both the RegEx and negative matcher options are selected, the label value must not match the regular expression.": "정규식 및 부정 일치 탐색기 옵션을 둘 다 선택하면 레이블 값이 정규식과 일치하지 않아야 합니다.", + "30m": "30분", + "1h": "1시간", + "2h": "2시간", + "6h": "6시간", + "12h": "12시간", + "1d": "1일", + "2d": "2일", + "1w": "1주", + "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "사용자가 정의한 레이블 선택기 세트에 따라 알림을 일시적으로 음소거합니다. 나열된 모든 값 또는 정규식과 일치하는 알림에 대해서는 알림이 전송되지 않습니다.", + "Duration": "기간", + "Silence alert from...": "음소거 알림 ...", + "Now": "지금", + "For...": "기간...", + "Until...": "기한...", + "{{duration}} from now": "지금 부터 {{duration}}", + "Start immediately": "바로 시작", + "Alert labels": "알림 라벨", + "Alerts with labels that match these selectors will be silenced instead of firing. Label values can be matched exactly or with a <2>": "이러한 선택기와 일치하는 레이블이 있는 알림은 실행되지 않고 음소거됩니다. 레이블 값은 정확히 일치하거나<2>를 사용할 수 있습니다.", + "regular expression": "정규식", + "Label name": "라벨 이름", + "Label value": "라벨 값", + "Select all that apply:": "해당되는 모든 항목을 선택합니다:", + "RegEx": "정규 표현식", + "Negative matcher": "부정 일치 탐색기", + "Remove": "삭제", + "Add label": "라벨 추가", + "Required": "필수 항목", + "Comment": "댓글", + "Silence": "음소거", "Cancel": "취소", - "Recreate silence": "음소거 다시 생성", - "Edit silence": "음소거 편집", - "View alerting rule": "알림 규칙 보기", - "Silences": "음소거", "Silence details": "음소거 상세 정보", + "Actions": "동작", "Matchers": "일치 시항", "No matchers": "일치 시항 없음", "Last updated at": "마지막 업데이트", "Starts at": "시작", "Ends at": "종료", "Created by": "작성자", - "Comment": "댓글", - "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "Alertmanager에서 음소거를 로드하는 중 오류가 발생했습니다. 아래 알림 중 일부는 실제로 음소거될 수 있습니다.", - "Alert State": "알림 상태", - "Alert state": "알림 상태", - "Create silence": "음소거 상태 만들기", + "No Alerts found": "알림을 찾을 수 없음", + "View alerting rule": "알림 규칙 보기", "Silence State": "음소거 상태", + "Active": "활성", "Error loading silences from Alertmanager. Alertmanager may be unavailable.": "Alertmanager에서 음소거 상태를 로드하는 동안 오류가 발생했습니다. Alertmanager를 사용하지 못할 수 있습니다.", + "No silences found": "알림 중지 없음", "Error": "오류", - "Alerting": "알림", + "Expire {{count}} silence_one": "{{count}} 음소거 만료", + "Expire {{count}} silence_other": "{{count}} 음소거 만료", + "Expire Silence": "음소거 만료", + "Are you sure you want to expire this silence?": "음소거를 종료하시겠습니까?", + "An error occurred": "오류가 발생했습니다", + "Restricted access": "제한된 액세스", + "You don't have access to this section due to cluster policy": "클러스터 정책으로 인해 이 섹션에 액세스할 수 없음", + "Error details": "오류 정보", + "No {{label}} found": "{{label}}을/를 찾을 수 없음", + "Not found": "찾을 수 없음", + "Try again": "다시 시도", + "Error loading {{label}}": "{{label}} 로딩 중 오류 발생", + "404: Not Found": "404: 찾을 수 없음", + "{{labels}} content is not available in the catalog at this time due to loading failures.": "로드 실패로 인해 현재 카탈로그에서 {{labels}} 콘텐츠를 사용할 수 없습니다.", + "No datapoints found.": "데이터 포인트를 찾을 수 없습니다", + "Namespaces": "네임스페이스", + "Project": "프로젝트", + "Projects": "프로젝트", + "Create new option \"{{option}}\"": "새 옵션 \"{{option}}\" 만들기", + "Filter options": "필터 옵션", + "Clear input value": "입력 값 지우기", + "No results found": "결과 없음", "Custom time range": "사용자 지정 시간 범위", - "From": "기점", + "From": "에서", "To": "대상", "Save": "저장", - "Filter options": "필터 옵션", - "Select a dashboard from the dropdown": "드롭다운에서 대시보드 선택", - "Error loading options": "옵션을 로드하는 중에 오류가 발생했습니다.", - "Dashboard": "대시 보드", - "Refresh interval": "새로 고침 간격", "Dashboards": "대시 보드", - "Inspect": "검사", - "Error loading card": "카드 로드 중 오류 발생", "Metrics dashboards": "통계 대시 보드", + "Error Loading Dashboards": "대시보드 로드 중 오류 발생", + "Error loading card": "카드 로드 중 오류 발생", + "Error loading options": "옵션을 로드하는 중에 오류가 발생했습니다.", + "Select a dashboard from the dropdown": "드롭다운에서 대시보드 선택", "panel.styles attribute not found": "panel.styles 속성을 찾을 수 없습니다", "query results table": "쿼리 결과 테이블", "Last {{count}} minute_one": "마지막 {{count}} 분", @@ -106,47 +164,104 @@ "Last {{count}} week_one": "마지막 {{count}} 주", "Last {{count}} week_other": "마지막 {{count}} 주", "Time range": "시간 범위", + "Refresh interval": "새로 고침 간격", "Could not parse JSON data for dashboard \"{{dashboard}}\"": "대시 보드 “{{dashboard}}\"의 JSON 데이터를 구문 분석할 수 없습니다.", + "Dashboard Variables": "대시보드 변수", + "No matching datasource found": "일치하는 데이터 소스를 찾을 수 없음", + "No Dashboard Available in Selected Project": "선택한 프로젝트에서 사용할 수 있는 대시보드 없음", + "To explore data, create a dashboard for this project": "데이터를 탐색하려면 이 프로젝트에 대한 대시보드를 만듭니다", + "No Perses Project Available": "사용 가능한 Perses 프로젝트 없음", + "To explore data, create a Perses Project": "데이터를 탐색하려면 Perses 프로젝트를 생성합니다.", + "Empty Dashboard": "빈 대시 보드", + "To get started add something to your dashboard": "시작하려면 대시보드에 항목을 추가합니다", + "No projects found": "프로젝트를 찾을 수 없습니다.", + "No results match the filter criteria.": "필터 기준과 일치하는 결과가 없습니다.", + "Clear filters": "필터 지우기", + "Select project...": "프로젝트 선택...", + "Dashboard": "대시 보드", + "Refresh off": "새로 고침 해제", + "{{count}} second_one": "{{count}} 초", + "{{count}} second_other": "{{count}} 초", + "{{count}} minute_one": "{{count}} 분", + "{{count}} minute_other": "{{count}} 분", + "{{count}} hour_one": "{{count}} 시", + "{{count}} hour_other": "{{count}} 시", + "{{count}} day_one": "{{count}} 일", + "{{count}} day_other": "{{count}} 일", + "Alerts Timeline": "알림 타임라인", + "To view alerts, select an incident from the chart above or from the filters.": "알림을 보려면 위의 차트 또는 필터에서 인시던트를 선택합니다.", + "Alert Name": "알림 이름", + "Component": "구성 요소", + "Start": "시작", + "End": "종료", + "Resolved": "해결됨", + "Unknown": "알 수 없음", + "Incidents Timeline": "인시던트 타임라인", + "ID": "ID", + "Component(s)": "구성 요소", + "Alert": "알림", + "Incidents": "인시던트", + "Clear all filters": "필터 모두 지우기", + "Filter type selection": "필터 유형 선택", + "Incident ID": "인시던트 ID", + "Severity filters": "심각도 필터", + "State filters": "상태 필터", + "Incident ID filters": "인시던트 ID 필터", + "Last 1 day": "최근 1일", + "Last 3 days": "최근 3일", + "Last 7 days": "최근 7일", + "Last 15 days": "최근 15일", + "Show graph": "그래프 표시", + "Hide graph": "그래프 숨기기", + "No incident selected.": "선택된 인시던트가 없습니다.", + "The incident is critical.": "이 인시던트는 심각합니다.", + "The incident might lead to critical.": "이 인시던트는 심각한 문제로 이어질 수 있습니다.", + "Informative": "정보", + "The incident is not critical.": "이 인시던트는 심각하지 않습니다.", + "The incident is currently firing.": "현재 인시던트가 활성화되어 있습니다.", + "The incident is not currently firing.": "현재 인시던트가 활성화되어 있지 않습니다.", + "component": "구성 요소 추적:", + "components": "구성 요소", "No labels": "라벨 없음", + "Expression (press Shift+Enter for newlines)": "표현식 (Shift+Enter 줄 바꿈)", + "Access restricted.": "액세스가 제한되어 있습니다.", + "Failed to load metrics list.": "메트릭을 로드하지 못했습니다.", + "Clear query": "쿼리 지우기", + "Queries": "쿼리", + "Select query": "쿼리 선택", "Add query": "쿼리 추가", "Collapse all query tables": "모든 쿼리 테이블 축소", "Expand all query tables": "모든 쿼리 테이블 확장", "Delete all queries": "모든 쿼리 삭제", - "Show graph": "그래프 표시", - "Hide graph": "그래프 숨기기", - "Hide table": "표 숨기기", - "Show table": "표 보기", "Show series": "시리즈 보기", "Hide series": "시리즈 숨기기", "Disable query": "쿼리 비활성화", "Enable query": "쿼리 활성화", - "Query must be enabled": "쿼리를 활성화해야 함", "Hide all series": "모든 시리즈 숨기기", "Show all series": "모든 시리즈 보기", + "Query must be enabled": "쿼리를 활성화해야 함", "Delete query": "쿼리 삭제", "Duplicate query": "중복 쿼리", "Error loading values": "값을 로드하는 동안 오류 발생", - "No datapoints found.": "데이터 포인트를 찾을 수 없습니다", "Unselect all": "모두 선택 취소", "Select all": "모두 선택", + "Error loading custom data source": "사용자 정의 데이터 소스 로드 중 오류 발생", + "An error occurred while loading the custom data source.": "사용자 정의 데이터 소스를 로드하는 동안 오류가 발생했습니다.", "No query entered": "쿼리가 입력되어 있지 않습니다", "Enter a query in the box below to explore metrics for this cluster.": "이 클러스터의 메트릭을 살펴보려면 아래 상자에 쿼리를 입력합니다.", "Insert example query": "예제 쿼리 삽입", "Run queries": "쿼리 실행", + "Bytes Binary (KiB, MiB)": "바이트 바이너리 (KiB, MiB)", + "Bytes Decimal (kb, MB)": "바이트 10진수 (kb, MB)", + "Bytes Binary Per Second (KiB/s, MiB/s)": "초당 바이트 바이너리 (KiB/s, MiB/s)", + "Bytes Decimal Per Second (kB/s, MB/s)": "초당 바이트 10진수(kB/s, MB/s)", + "Packets Per Second": "초당 패킷 수", + "Miliseconds": "밀리초", + "Seconds": " 초", + "Percentage": "백분율", + "No Units": "단위 없음", "Metrics": "메트릭", - "Refresh off": "새로 고침 해제", - "{{count}} second_one": "{{count}} 초", - "{{count}} second_other": "{{count}} 초", - "{{count}} minute_one": "{{count}} 분", - "{{count}} minute_other": "{{count}} 분", - "{{count}} hour_one": "{{count}} 시", - "{{count}} hour_other": "{{count}} 시", - "{{count}} day_one": "{{count}} 일", - "{{count}} day_other": "{{count}} 일", - "Expression (press Shift+Enter for newlines)": "표현식 (Shift+Enter 줄 바꿈)", - "Access restricted.": "액세스가 제한되어 있습니다.", - "Failed to load metrics list.": "메트릭을 로드하지 못했습니다.", - "Clear query": "쿼리 지우기", + "This dropdown only formats results.": "이 드롭다운은 결과의 형식만 지정합니다.", "graph timespan": "그래프 시간 범위", "Reset zoom": "확대/축소 재설정", "Displaying with reduced resolution due to large dataset.": "큰 데이터 세트로 인해 해상도가 저하된 상태로 표시됩니다.", @@ -156,38 +271,9 @@ "Query result is a string, which cannot be graphed.": "쿼리 결과는 그래프로 표시 할 수없는 문자열입니다.", "The resulting dataset is too large to graph.": "결과 데이터 세트가 너무 커서 그래프로 표시할 수 없습니다.", "Stacked": "스택", - "Invalid date / time": "잘못된 날짜/시간", - "Datetime": "날짜 시간", - "Select the negative matcher option to update the label value to a not equals matcher.": "부정 일치 탐색기 옵션을 선택하여 레이블 값을 일치하지 않음으로 업데이트합니다.", - "If both the RegEx and negative matcher options are selected, the label value must not match the regular expression.": "정규식 및 부정 일치 탐색기 옵션을 둘 다 선택하면 레이블 값이 정규식과 일치하지 않아야 합니다.", - "30m": "30분", - "1h": "1시간", - "2h": "2시간", - "6h": "6시간", - "12h": "12시간", - "1d": "1일", - "2d": "2일", - "1w": "1주", - "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "사용자가 정의한 레이블 선택기 세트에 따라 알림을 일시적으로 음소거합니다. 나열된 모든 값 또는 정규식과 일치하는 알림에 대해서는 알림이 전송되지 않습니다.", - "Duration": "기간", - "Silence alert from...": "음소거 알림 ...", - "Now": "지금", - "For...": "기간...", - "Until...": "기한...", - "{{duration}} from now": "지금 부터 {{duration}}", - "Start immediately": "바로 시작", - "Alert labels": "알림 라벨", - "Alerts with labels that match these selectors will be silenced instead of firing. Label values can be matched exactly or with a <2>": "이러한 선택기와 일치하는 레이블이 있는 알림은 실행되지 않고 음소거됩니다. 레이블 값은 정확히 일치하거나<2>를 사용할 수 있습니다.", - "regular expression": "정규식", - "Label name": "라벨 이름", - "Label value": "라벨 값", - "RegEx": "정규 표현식", - "Negative matcher": "부정 일치 탐색기", - "Remove": "삭제", - "Add label": "라벨 추가", - "Silence": "음소거", - "Overwriting current silence": "현재 음소거 상태 덮어 쓰기", - "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "변경 사항이 저장되면 현재 음소거 상태가 만료되고 새 설정을 사용하여 새 음소거가 대신 적용됩니다.", + "Check to show gaps for missing data": "누락된 데이터의 공백을 표시하려면 선택", + "No gaps found in the data": "데이터에서 공백이 발견되지 않았습니다", + "Disconnected": "연결 끊김", "<0>{{firstIndex}} - {{lastIndex}} of <3>{{itemCount}} {{itemsTitle}}": "<0>{{firstIndex}} - {{lastIndex}} / <3>{{itemCount}} {{itemsTitle}}", "Items per page": "페이지 당 항목", "per page": "페이지 당", @@ -205,15 +291,15 @@ "Error loading service monitor data": "서비스 모니터 데이터 로드 중 오류 발생", "Error loading pod monitor data": "Pod 모니터 데이터를 로드하는 중 오류 발생", "Endpoint": "엔드포인트", - "Namespace": "네임 스페이스", "Last scrape": "마지막 스크랩", "Scrape failed": "스크랩 실패", "Status": "상태", "Monitor": "모니터", "Last Scrape": "마지막 스크랩", "Scrape Duration": "스크랩 시간", + "No metrics targets found": "메트릭 대상 없음", "Metrics targets": "메트릭 대상", "Error loading latest targets data": "최신 대상 데이터를 로드하는 동안 오류가 발생했습니다.", "Search by endpoint or namespace...": "엔드포인트 또는 네임스페이스로 검색...", "Text": "텍스트" -} +} \ No newline at end of file diff --git a/web/locales/zh/plugin__monitoring-plugin.json b/web/locales/zh/plugin__monitoring-plugin.json index 16a22cd5e..c75af4f22 100644 --- a/web/locales/zh/plugin__monitoring-plugin.json +++ b/web/locales/zh/plugin__monitoring-plugin.json @@ -1,100 +1,158 @@ { + "Recreate silence": "重新创建静默", + "Edit silence": "编辑静默", + "Expire silence": "使静默过期", + "Starts": "开始", + "Ends": "结束", + "Expired": "过期", + "Name": "名称", + "Firing alerts": "发出警报", + "State": "状态", + "Creator": "创建者", + "Alerts": "警报", + "Silences": "静默", + "Alerting Rules": "警报规则", + "Alerting rules": "警报规则", + "Alerting": "警报", + "Severity": "严重性", + "Namespace": "命名空间", + "Source": "源", + "Cluster": "集群", + "Silence alert": "静默警报", + "User": "用户", + "Platform": "平台", + "Export as CSV": "导出为 CSV", + "Total": "总计", + "Description": "描述", + "Active since": "活跃自", + "Value": "值", + "{{name}} details": "{{name}}详情", + "Alerting rule details": "警报规则详情", + "Summary": "概述", + "Message": "消息", + "Runbook": "Runbook", + "For": "对于", + "Expression": "表达式", + "Labels": "标签", + "Active alerts": "活跃的警报", + "None found": "找不到", + "Alert State": "警报状态", "Firing": "触发", - "Pending": "待定", + "Pending": "待处理", "Silenced": "静默", "Not Firing": "未触发", - "Active": "活跃", - "Expired": "过期", - "Ends": "结束", - "Since": "自", - "Critical": "关键", - "Info": "信息", - "Warning": "警告", - "None": "无", + "No alerting rules found": "未找到警报规则", + "Alert state": "警报状态", + "Alert details": "警报详细信息", + "Alerting rule": "警报规则", + "Silenced by": "静默于", "Pending: ": "待定:", - "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "该警报处于活跃状态,但正在等待警报规则中指定的持续时间,然后再触发警报。", + "The alert is active but is waiting for the duration that is specified in the alerting rule before it fires.": "该警报处于活跃状态,但正在等待警报规则中指定的持续时间,然后再触发。", "Firing: ": "触发:", "The alert is firing because the alert condition is true and the optional `for` duration has passed. The alert will continue to fire as long as the condition remains true.": "警报正在触发,因为满足警报条件,且可选的 'for' 持续时间已过。只要条件满足,警报将继续触发。", "Silenced: ": "静默:", "The alert is now silenced for a defined time period. Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "这个警报现在会在定义的时间段内静默。静默会根据您定义的一组标签选择器临时将警报静音。对于与所列出的值或正则表达式匹配的警报,不会发送相关的通知。", - "Critical: ": "关键:", + "No alerts found": "未找到警报", + "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "从 Alertmanager 加载静默时出错。以下一些警报实际上可能会被静默。", + "Critical": "关键", + "Info": "信息", + "Warning": "警告", + "None": "无", + "Since": "自", + "Inspect": "检查", "The condition that triggered the alert could have a critical impact. The alert requires immediate attention when fired and is typically paged to an individual or to a critical response team.": "触发警报的条件具有严重的影响。该警报在触发时需要立即关注,并且通常会传给个人或关键响应团队。", - "Warning: ": "警告:", "The alert provides a warning notification about something that might require attention in order to prevent a problem from occurring. Warnings are typically routed to a ticketing system for non-immediate review.": "该警报针对可能需要注意的事件提供警告通知,以防止问题的发生。警告信息通常会发送到一个不会被马上审阅的一个问题单系统。", - "Info: ": "信息:", "The alert is provided for informational purposes only.": "该警报仅用于提供信息。", - "None: ": "无: ", "The alert has no defined severity.": "该警报没有定义的严重性。", "You can also create custom severity definitions for user workload alerts.": "您还可以为用户工作负载的警报创建自定义的严重性。", "Platform: ": "平台:", "Platform-level alerts relate only to OpenShift namespaces. OpenShift namespaces provide core OpenShift functionality.": "平台级别的警报仅与 OpenShift 命名空间相关。OpenShift 命名空间提供 OpenShift 的核心功能。", "User: ": "用户:", "User workload alerts relate to user-defined namespaces. These alerts are user-created and are customizable. User workload monitoring can be enabled post-installation to provide observability into your own services.": "用户工作负载警报与用户定义的命名空间相关。这些警报是用户创建的,并可自定义。用户工作负载监控可以在安装后启用,以对您自己的服务提供可观察性。", - "Starts": "开始", - "Platform": "平台", - "User": "用户", - "Name": "名称", - "Firing alerts": "发出警报", - "State": "状态", - "Creator": "创建者", - "Silenced by": "静默于", - "Alerts": "警报", - "Alert details": "警报详细信息", - "Silence alert": "静默警报", - "Severity": "严重性", - "Description": "描述", - "Summary": "概述", - "Message": "消息", - "Runbook": "Runbook", - "Source": "源", - "Labels": "标签", - "Alerting rule": "警报规则", - "Active since": "活跃自", - "Value": "值", - "{{name}} details": "{{name}}详情", - "Alerting rules": "警报规则", - "Alerting rule details": "警报规则详情", - "For": "对于", - "Expression": "表达式", - "Active alerts": "活跃的警报", - "None found": "找不到", - "Expire silence": "使静默过期", - "Are you sure you want to expire this silence?": "您确定要使这个静默过期吗?", - "An error occurred": "发生错误", + "Create silence": "创建静默", + "Overwriting current silence": "覆盖当前的静默", + "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "保存更改后,当前存在的静默将会到期,新配置的新静默将会生效。", + "Invalid date / time": "无效的日期/时间", + "Datetime": "日期时间", + "Select the negative matcher option to update the label value to a not equals matcher.": "选择负匹配器选项,更新与匹配器不匹配的标签。", + "If both the RegEx and negative matcher options are selected, the label value must not match the regular expression.": "如果同时选择了正则表达式和负匹配器选项,标签值需要与正则表达式不匹配。", + "30m": "30 分钟", + "1h": "1 小时", + "2h": "2 小时", + "6h": "6 小时", + "12h": "12 小时", + "1d": "1 天", + "2d": "2 天", + "1w": "1 周", + "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "静默会根据您定义的一组标签选择器临时将警报静音。对于与所有列出的值或正则表达式匹配的警报,不会发送通知。", + "Duration": "持续时间", + "Silence alert from...": "静默警报......", + "Now": "现在", + "For...": "时长......", + "Until...": "直到......", + "{{duration}} from now": "从现在开始 {{duration}}", + "Start immediately": "立即启动", + "Alert labels": "警报标签", + "Alerts with labels that match these selectors will be silenced instead of firing. Label values can be matched exactly or with a <2>": "带有与所有这些选择器匹配的标签的警报将被静默而不是被触发。标签值可以完全匹配,也可以使用一个<2>", + "regular expression": "正则表达式", + "Label name": "标签名称", + "Label value": "标签值", + "Select all that apply:": "选择所有适用项:", + "RegEx": "RegEx", + "Negative matcher": "负匹配器", + "Remove": "删除", + "Add label": "添加标签", + "Required": "必需", + "Comment": "评论", + "Silence": "静默", "Cancel": "取消", - "Recreate silence": "重新创建静默", - "Edit silence": "编辑静默", - "View alerting rule": "查看警报规则", - "Silences": "静默", "Silence details": "沉默详情", + "Actions": "行动", "Matchers": "匹配器", "No matchers": "没有匹配器", "Last updated at": "最后更新于", "Starts at": "开始于", "Ends at": "结束于", "Created by": "由创建", - "Comment": "评论", - "Error loading silences from Alertmanager. Some of the alerts below may actually be silenced.": "从 Alertmanager 加载静默时出错。以下一些警报实际上可能会被静默。", - "Alert State": "警报状态", - "Alert state": "警报状态", - "Create silence": "创建静默", + "No Alerts found": "未找到警报", + "View alerting rule": "查看警报规则", "Silence State": "静默状态", + "Active": "活跃", "Error loading silences from Alertmanager. Alertmanager may be unavailable.": "从 Alertmanager 加载静默时出错。Alertmanager 可能不可用。", + "No silences found": "未找到静默", "Error": "错误", - "Alerting": "警报", + "Expire {{count}} silence_one": "{{count}} 个禁言到期", + "Expire {{count}} silence_other": "{{count}} 个禁言到期", + "Expire Silence": "过期静默", + "Are you sure you want to expire this silence?": "您确定要使这个静默过期吗?", + "An error occurred": "发生错误", + "Restricted access": "限制的访问", + "You don't have access to this section due to cluster policy": "由于集群策略您无法访问此部分", + "Error details": "错误详情", + "No {{label}} found": "没有找到 {{label}}", + "Not found": "没有找到", + "Try again": "再次尝试", + "Error loading {{label}}": "错误加载 {{label}}", + "404: Not Found": "404: Not Found", + "{{labels}} content is not available in the catalog at this time due to loading failures.": "{{labels}} 内容在目录中不可用,因为加载失败。", + "No datapoints found.": "找不到数据点。", + "Namespaces": "命名空间", + "Project": "项目", + "Projects": "项目", + "Create new option \"{{option}}\"": "创建新选项 \"{{option}}\"", + "Filter options": "选择过滤选项", + "Clear input value": "清除输入值", + "No results found": "未找到结果", "Custom time range": "自定义时间范围", "From": "从", "To": "到", "Save": "保存", - "Filter options": "过滤选项", - "Select a dashboard from the dropdown": "从下拉菜单中选择一个仪表板", - "Error loading options": "加载选项出错", - "Dashboard": "仪表板", - "Refresh interval": "刷新间隔", "Dashboards": "仪表板", - "Inspect": "检查", - "Error loading card": "错误加载卡", "Metrics dashboards": "指标仪表板", + "Error Loading Dashboards": "加载仪表板错误", + "Error loading card": "错误加载卡", + "Error loading options": "加载选项出错", + "Select a dashboard from the dropdown": "从下拉菜单中选择一个仪表板", "panel.styles attribute not found": "未找到 panel.styles 属性", "query results table": "查询结果表", "Last {{count}} minute_one": "最后 {{count}} 分钟", @@ -106,47 +164,104 @@ "Last {{count}} week_one": "最后 {{count}} 周", "Last {{count}} week_other": "最后 {{count}} 周", "Time range": "时间范围", + "Refresh interval": "刷新间隔", "Could not parse JSON data for dashboard \"{{dashboard}}\"": "无法为仪表板 \"{{dashboard}}\" 解析 JSON 数据", + "Dashboard Variables": "仪表板变量", + "No matching datasource found": "未找到匹配的数据源", + "No Dashboard Available in Selected Project": "选择的项目中没有可用的仪表板", + "To explore data, create a dashboard for this project": "要探索数据,为此项目创建一个仪表板", + "No Perses Project Available": "没有可用的 Perses 项目", + "To explore data, create a Perses Project": "要探索数据,创建一个 Perses 项目", + "Empty Dashboard": "空仪表板", + "To get started add something to your dashboard": "开始在仪表板中添加内容", + "No projects found": "没有找到项目", + "No results match the filter criteria.": "没有符合过滤条件的结果。", + "Clear filters": "清除过滤", + "Select project...": "选择项目......", + "Dashboard": "仪表板", + "Refresh off": "刷新", + "{{count}} second_one": "{{count}} 秒", + "{{count}} second_other": "{{count}} 秒", + "{{count}} minute_one": "{{count}} 分钟", + "{{count}} minute_other": "{{count}} 分钟", + "{{count}} hour_one": "{{count}} 小时", + "{{count}} hour_other": "{{count}} 小时", + "{{count}} day_one": "{{count}} 天", + "{{count}} day_other": "{{count}} 天", + "Alerts Timeline": "警报时间表", + "To view alerts, select an incident from the chart above or from the filters.": "要查看警报,从上面的图表或过滤中选择一个事件。", + "Alert Name": "警报名称", + "Component": "组件", + "Start": "开始", + "End": "结束", + "Resolved": "已解决", + "Unknown": "未知", + "Incidents Timeline": "事件时间表", + "ID": "ID", + "Component(s)": "组件", + "Alert": "警报", + "Incidents": "事件", + "Clear all filters": "清除所有过滤器", + "Filter type selection": "过滤类型选择", + "Incident ID": "事件 ID", + "Severity filters": "严重性过滤", + "State filters": "状态过滤", + "Incident ID filters": "事件 ID 过滤", + "Last 1 day": "最后 1 天", + "Last 3 days": "最后 3 天", + "Last 7 days": "最后 7 天", + "Last 15 days": "最后 15 天", + "Show graph": "显示图", + "Hide graph": "隐藏图", + "No incident selected.": "没有选择事件。", + "The incident is critical.": "事件是关键的。", + "The incident might lead to critical.": "事件可能会导致关键状态。", + "Informative": "信息性", + "The incident is not critical.": "事件并不是关键的。", + "The incident is currently firing.": "事件是当前触发的。", + "The incident is not currently firing.": "事件不是当前触发的。", + "component": "组件", + "components": "组件", "No labels": "没有标签", + "Expression (press Shift+Enter for newlines)": "表达式(按 Shift+Enter 键进入新行)", + "Access restricted.": "限制的访问。", + "Failed to load metrics list.": "加载指标列表失败。", + "Clear query": "清除查询", + "Queries": "查询", + "Select query": "选择查询", "Add query": "添加查询", "Collapse all query tables": "折叠所有查询表", "Expand all query tables": "展开所有查询表", "Delete all queries": "删除所有查询", - "Show graph": "显示图", - "Hide graph": "隐藏图", - "Hide table": "隐藏表", - "Show table": "显示表", "Show series": "显示系列", "Hide series": "隐藏系列", "Disable query": "禁用查询", "Enable query": "启用查询", - "Query must be enabled": "需要启用的查询", "Hide all series": "隐藏所有系列", "Show all series": "显示所有系列", + "Query must be enabled": "需要启用的查询", "Delete query": "删除查询", "Duplicate query": "重复查询", "Error loading values": "加载值错误", - "No datapoints found.": "找不到数据点。", "Unselect all": "取消选择所有", "Select all": "选择所有", + "Error loading custom data source": "加载自定义数据源时出错", + "An error occurred while loading the custom data source.": "加载自定义数据源时出错。", "No query entered": "没有输入查询", "Enter a query in the box below to explore metrics for this cluster.": "在下面的框中输入查询来浏览此集群的指标。", "Insert example query": "插入示例查询", "Run queries": "运行查询", + "Bytes Binary (KiB, MiB)": "字节二进制 (KiB、MiB)", + "Bytes Decimal (kb, MB)": "字节十进制 (kb, MB)", + "Bytes Binary Per Second (KiB/s, MiB/s)": "字节二进制每秒 (KiB/s, MiB/s)", + "Bytes Decimal Per Second (kB/s, MB/s)": "字节十进制每秒 (kB/s, MB/s)", + "Packets Per Second": "数据包每秒", + "Miliseconds": "毫秒", + "Seconds": "秒", + "Percentage": "百分比", + "No Units": "没有单元", "Metrics": "指标", - "Refresh off": "刷新", - "{{count}} second_one": "{{count}} 秒", - "{{count}} second_other": "{{count}} 秒", - "{{count}} minute_one": "{{count}} 分钟", - "{{count}} minute_other": "{{count}} 分钟", - "{{count}} hour_one": "{{count}} 小时", - "{{count}} hour_other": "{{count}} 小时", - "{{count}} day_one": "{{count}} 天", - "{{count}} day_other": "{{count}} 天", - "Expression (press Shift+Enter for newlines)": "表达式(按 Shift+Enter 键进入新行)", - "Access restricted.": "限制的访问。", - "Failed to load metrics list.": "加载指标列表失败。", - "Clear query": "清除查询", + "This dropdown only formats results.": "此下拉菜单仅格式化结果。", "graph timespan": "图形化时间跨度", "Reset zoom": "重设缩放", "Displaying with reduced resolution due to large dataset.": "因为数据集太大,以较低的解析度显示。", @@ -156,39 +271,10 @@ "Query result is a string, which cannot be graphed.": "查询结果是一个字符串,无法被图形化。", "The resulting dataset is too large to graph.": "结果数据集太大无法进行图形化。", "Stacked": "堆栈", - "Invalid date / time": "无效的日期/时间", - "Datetime": "日期时间", - "Select the negative matcher option to update the label value to a not equals matcher.": "选择负匹配器选项,更新与匹配器不匹配的标签。", - "If both the RegEx and negative matcher options are selected, the label value must not match the regular expression.": "如果同时选择了正则表达式和负匹配器选项,标签值需要与正则表达式不匹配。", - "30m": "30 分钟", - "1h": "1 小时", - "2h": "2 小时", - "6h": "6 小时", - "12h": "12 小时", - "1d": "1 天", - "2d": "2 天", - "1w": "1 周", - "Silences temporarily mute alerts based on a set of label selectors that you define. Notifications will not be sent for alerts that match all the listed values or regular expressions.": "静默会根据您定义的一组标签选择器临时将警报静音。对于与所有列出的值或正则表达式匹配的警报,不会发送通知。", - "Duration": "持续时间", - "Silence alert from...": "静默警报......", - "Now": "现在", - "For...": "时长......", - "Until...": "直到......", - "{{duration}} from now": "从现在开始 {{duration}}", - "Start immediately": "立即启动", - "Alert labels": "警报标签", - "Alerts with labels that match these selectors will be silenced instead of firing. Label values can be matched exactly or with a <2>": "带有与所有这些选择器匹配的标签的警报将被静默而不是被触发。标签值可以完全匹配,也可以使用一个<2>", - "regular expression": "正则表达式", - "Label name": "标签名称", - "Label value": "标签值", - "RegEx": "RegEx", - "Negative matcher": "负匹配器", - "Remove": "删除", - "Add label": "添加标签", - "Silence": "静默", - "Overwriting current silence": "覆盖当前的静默", - "When changes are saved, the currently existing silence will be expired and a new silence with the new configuration will take its place.": "保存更改后,当前存在的静默将会到期,新配置的新静默将会生效。", - "<0>{{firstIndex}} - {{lastIndex}} of <3>{{itemCount}} {{itemsTitle}}": "<0>{{firstIndex}} - {{lastIndex}}(共 <3>{{itemCount}} 个 {{itemsTitle}})", + "Check to show gaps for missing data": "检查以显示缺少数据的差距", + "No gaps found in the data": "在数据中没有找到任何差距", + "Disconnected": "断开连接", + "<0>{{firstIndex}} - {{lastIndex}} of <3>{{itemCount}} {{itemsTitle}}": "<0>{{firstIndex}} - {{lastIndex}}(共 <3>{{itemCount}} {{itemsTitle}})", "Items per page": "每页的项", "per page": "每页", "Go to first page": "转至第一页", @@ -205,7 +291,6 @@ "Error loading service monitor data": "加载服务监控数据时出错", "Error loading pod monitor data": "加载 pod 监控数据时出错", "Endpoint": "端点", - "Namespace": "命名空间", "Last scrape": "最后刮削", "Scrape failed": "刮削失败", "Status": "状态", @@ -213,7 +298,8 @@ "Last Scrape": "最后刮削", "Scrape Duration": "刮削持续时间", "Metrics targets": "指标目标", + "No metrics targets found": "未找到指标目标", "Error loading latest targets data": "加载最新目标数据时出错", "Search by endpoint or namespace...": "按端点或命名空间搜索......", "Text": "内容" -} +} \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 7501a724e..12ed08ccd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -26,42 +26,22 @@ "@openshift-console/dynamic-plugin-sdk-internal": "^4.19.0-prerelease.2", "@openshift-console/dynamic-plugin-sdk-webpack": "^4.19.0", "@patternfly/react-charts": "^8.2.0", - "@patternfly/react-core": "^6.2.0", + "@patternfly/react-code-editor": "^6.4.1", + "@patternfly/react-core": "^6.4.1", "@patternfly/react-data-view": "^6.1.0", "@patternfly/react-icons": "^6.2.0", "@patternfly/react-table": "^6.2.0", "@patternfly/react-templates": "^6.2.0", - "@perses-dev/bar-chart-plugin": "^0.9.0", - "@perses-dev/components": "^0.52.0", - "@perses-dev/core": "^0.52.0", - "@perses-dev/dashboards": "^0.52.0", - "@perses-dev/datasource-variable-plugin": "^0.3.2", - "@perses-dev/explore": "^0.52.0", - "@perses-dev/flame-chart-plugin": "^0.3.0", - "@perses-dev/gauge-chart-plugin": "^0.9.0", - "@perses-dev/heatmap-chart-plugin": "^0.2.1", - "@perses-dev/histogram-chart-plugin": "^0.9.0", - "@perses-dev/loki-plugin": "^0.1.1", - "@perses-dev/markdown-plugin": "^0.9.0", - "@perses-dev/pie-chart-plugin": "^0.9.0", - "@perses-dev/plugin-system": "^0.52.0", - "@perses-dev/prometheus-plugin": "^0.53.3", - "@perses-dev/pyroscope-plugin": "^0.3.1", - "@perses-dev/scatter-chart-plugin": "^0.8.0", - "@perses-dev/stat-chart-plugin": "^0.9.0", - "@perses-dev/static-list-variable-plugin": "^0.5.1", - "@perses-dev/status-history-chart-plugin": "^0.9.0", - "@perses-dev/table-plugin": "^0.8.0", - "@perses-dev/tempo-plugin": "^0.53.1", - "@perses-dev/timeseries-chart-plugin": "^0.10.1", - "@perses-dev/timeseries-table-plugin": "^0.9.0", - "@perses-dev/trace-table-plugin": "^0.8.1", - "@perses-dev/tracing-gantt-chart-plugin": "^0.9.2", + "@perses-dev/components": "0.53.0-rc.2", + "@perses-dev/core": "0.53.0-rc.1", + "@perses-dev/dashboards": "0.53.0-rc.2", + "@perses-dev/explore": "0.53.0-rc.2", + "@perses-dev/plugin-system": "0.53.0-rc.2", "@prometheus-io/codemirror-promql": "^0.37.0", "@tanstack/react-query": "^4.36.1", "@types/ajv": "^0.0.5", "@types/js-yaml": "^4.0.9", - "ajv": "^8.17.1", + "ajv": "^8.18.0", "classnames": "2.x", "fuzzysearch": "1.0.x", "i18next": "^21.8.14", @@ -74,6 +54,7 @@ "murmurhash-js": "1.0.x", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-hook-form": "^7.66.0", "react-i18next": "^11.8.11", "react-linkify": "^0.2.2", "react-modal": "^3.12.1", @@ -87,13 +68,14 @@ "@cypress/grep": "^4.1.0", "@cypress/webpack-preprocessor": "^6.0.2", "@date-fns/tz": "^1.4.1", + "@swc/core": "^1.15.3", + "@swc/helpers": "^0.5.17", "@types/classnames": "^2.2.7", "@types/jest": "^30.0.0", "@types/lodash-es": "^4.17.12", "@types/node": "^22.17.2", "@types/react": "17.0.83", "@types/react-router-dom": "^5.3.2", - "@types/webpack-dev-server": "^4.7.2", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", "copy-webpack-plugin": "^11.0.0", @@ -102,6 +84,7 @@ "cypress": "^14.2.1", "cypress-multi-reporters": "^1.4.0", "cypress-wait-until": "^3.0.2", + "esbuild-loader": "^4.4.2", "eslint": "^8.44.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", @@ -122,8 +105,8 @@ "sass": "^1.42.1", "sass-loader": "^10.1.1", "style-loader": "^3.3.1", + "swc-loader": "^0.2.6", "ts-jest": "^29.4.4", - "ts-loader": "^9.2.8", "ts-node": "^10.7.0", "typescript": "^5.9.2", "webpack": "^5.94.0", @@ -134,24 +117,10 @@ "react-router-dom": "<7" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@atlaskit/pragmatic-drag-and-drop": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.4.tgz", - "integrity": "sha512-lZHnO9BJdHPKnwB0uvVUCyDnIhL+WAHzXQ2EXX0qacogOsnvIUiCgY0BLKhBqTCWln3/f/Ox5jU54MKO6ayh9A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.7.tgz", + "integrity": "sha512-jX+68AoSTqO/fhCyJDTZ38Ey6/wyL2Iq+J/moanma0YyktpnoHxevjY1UNJHYp0NCburdQDZSL1ZFac1mO1osQ==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.0.0", @@ -184,9 +153,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -194,22 +163,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -225,13 +194,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -281,20 +250,27 @@ "yallist": "^3.0.2" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "engines": { @@ -305,15 +281,15 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -341,6 +317,28 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -351,15 +349,15 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -379,15 +377,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -483,9 +481,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -502,42 +500,42 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", - "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -547,15 +545,15 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -618,15 +616,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -996,9 +994,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", "dev": true, "license": "MIT", "peer": true, @@ -1031,14 +1029,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -1049,9 +1047,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", - "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "dev": true, "license": "MIT", "peer": true, @@ -1061,7 +1059,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1089,15 +1087,15 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1195,9 +1193,9 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", "dev": true, "license": "MIT", "peer": true, @@ -1300,9 +1298,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", "dev": true, "license": "MIT", "peer": true, @@ -1370,17 +1368,17 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1477,9 +1475,9 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "dev": true, "license": "MIT", "peer": true, @@ -1488,7 +1486,7 @@ "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1533,9 +1531,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", "dev": true, "license": "MIT", "peer": true, @@ -1622,9 +1620,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", - "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "dev": true, "license": "MIT", "peer": true, @@ -1831,22 +1829,22 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", - "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/compat-data": "^7.28.0", + "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", @@ -1855,42 +1853,42 @@ "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1933,9 +1931,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1956,17 +1954,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -1974,13 +1972,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1994,9 +1992,9 @@ "license": "MIT" }, "node_modules/@codemirror/autocomplete": { - "version": "6.18.6", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", - "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz", + "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -2006,9 +2004,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", - "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", + "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -2028,9 +2026,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", - "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -2042,9 +2040,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.8.5", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", - "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz", + "integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -2085,9 +2083,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", - "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -2121,9 +2119,9 @@ } }, "node_modules/@cypress/grep": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@cypress/grep/-/grep-4.1.0.tgz", - "integrity": "sha512-yUscMiUgM28VDPrNxL19/BhgHZOVrAPrzVsuEcy6mqPqDYt8H8fIaHeeGQPW4CbMu/ry9sehjH561WDDBIXOIg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cypress/grep/-/grep-4.1.1.tgz", + "integrity": "sha512-KDM5kOJIQwdn7BGrmejCT34XCMLt8Bahd8h6RlRTYahs2gdc1wHq6XnrqlasF72GzHw0yAzCaH042hRkqu1gFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2136,9 +2134,9 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", - "integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", + "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2155,7 +2153,7 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.14.0", + "qs": "~6.14.1", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", @@ -2185,9 +2183,9 @@ } }, "node_modules/@cypress/webpack-preprocessor/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -2253,38 +2251,76 @@ "node": ">=14.17.0" } }, - "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", - "dev": true, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", "license": "MIT", - "optional": true, + "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "optional": true, + "peer": true, "dependencies": { - "tslib": "^2.4.0" + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", "license": "MIT", - "optional": true, + "peer": true, + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "peer": true, "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" } }, "node_modules/@emotion/babel-plugin": { @@ -2332,9 +2368,9 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" @@ -2439,74 +2475,6 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", @@ -2514,6 +2482,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2524,9 +2493,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], @@ -2541,9 +2510,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], @@ -2558,9 +2527,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], @@ -2575,9 +2544,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], @@ -2592,9 +2561,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], @@ -2609,9 +2578,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], @@ -2626,9 +2595,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], @@ -2643,9 +2612,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], @@ -2660,9 +2629,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], @@ -2677,9 +2646,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], @@ -2694,9 +2663,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], @@ -2711,9 +2680,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -2728,9 +2697,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], @@ -2745,9 +2714,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], @@ -2762,9 +2731,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], @@ -2779,9 +2748,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], @@ -2796,9 +2765,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -2813,9 +2782,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], @@ -2830,9 +2799,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], @@ -2847,9 +2816,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], @@ -2864,9 +2833,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], @@ -2881,9 +2850,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2900,9 +2869,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -2934,9 +2903,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2950,12 +2919,46 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } }, "node_modules/@eslint/js": { "version": "8.57.1", @@ -2973,24 +2976,6 @@ "integrity": "sha512-2hYR6r661Cq9B8zugtu6yxuOKqrVhAgfOSaPSq8XoxbC4ebsl0KOTy/vPoP+9U7JuQVLfrmikirW4a9Z0nDUug==", "license": "MIT" }, - "node_modules/@grafana/lezer-logql": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@grafana/lezer-logql/-/lezer-logql-0.2.8.tgz", - "integrity": "sha512-GbWKZ8BdLUFyh5ZMwOo6sZXaYcOTYFkFhBLGJ5law6V78nKoLAn77aqIEBs9mJsJ34lBsApmK77FJOFqJ8fnbg==", - "license": "Apache-2.0", - "peerDependencies": { - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@grafana/lezer-traceql": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@grafana/lezer-traceql/-/lezer-traceql-0.0.20.tgz", - "integrity": "sha512-AqHLlceOEqDmZWV1FISBIR/l34rATHlPBuNGDA+2rmlvARHd+MS/DHm/K/53x0W+qZULF24JHzDrVPCHxQZ7cg==", - "license": "Apache-2.0", - "peerDependencies": { - "@lezer/lr": "^1.3.0" - } - }, "node_modules/@gulpjs/to-absolute-glob": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", @@ -3029,6 +3014,30 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -3070,9 +3079,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -3083,9 +3092,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -3121,9 +3130,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -3181,20 +3190,10 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -3620,16 +3619,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@jest/reporters/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3668,9 +3657,9 @@ "license": "MIT" }, "node_modules/@jest/reporters/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -3698,55 +3687,6 @@ "node": ">=8" } }, - "node_modules/@jest/reporters/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@jest/reporters/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4111,15 +4051,26 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -4130,9 +4081,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", - "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -4140,15 +4091,15 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -4173,9 +4124,9 @@ } }, "node_modules/@jsonjoy.com/buffers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", - "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4207,19 +4158,20 @@ } }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz", - "integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/buffers": "^1.2.0", "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.1", + "@jsonjoy.com/json-pointer": "^1.0.2", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0" + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" }, "engines": { "node": ">=10.0" @@ -4288,18 +4240,18 @@ "license": "MIT" }, "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", + "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", "license": "MIT" }, "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/json": { @@ -4314,9 +4266,9 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.3.tgz", + "integrity": "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -4328,557 +4280,101 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, - "node_modules/@modern-js/node-bundle-require": { - "version": "2.68.2", - "resolved": "https://registry.npmjs.org/@modern-js/node-bundle-require/-/node-bundle-require-2.68.2.tgz", - "integrity": "sha512-MWk/pYx7KOsp+A/rN0as2ji/Ba8x0m129aqZ3Lj6T6CCTWdz0E/IsamPdTmF9Jnb6whQoBKtWSaLTCQlmCoY0Q==", + "node_modules/@module-federation/bridge-react-webpack-plugin": { + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.21.6.tgz", + "integrity": "sha512-lJMmdhD4VKVkeg8RHb+Jwe6Ou9zKVgjtb1inEURDG/sSS2ksdZA8pVKLYbRPRbdmjr193Y8gJfqFbI2dqoyc/g==", "license": "MIT", "dependencies": { - "@modern-js/utils": "2.68.2", - "@swc/helpers": "^0.5.17", - "esbuild": "0.25.5" + "@module-federation/sdk": "0.21.6", + "@types/semver": "7.5.8", + "semver": "7.6.3" } }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], + "node_modules/@module-federation/bridge-react-webpack-plugin/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@module-federation/cli": { + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-0.21.6.tgz", + "integrity": "sha512-qNojnlc8pTyKtK7ww3i/ujLrgWwgXqnD5DcDPsjADVIpu7STaoaVQ0G5GJ7WWS/ajXw6EyIAAGW/AMFh4XUxsQ==", "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@module-federation/dts-plugin": "0.21.6", + "@module-federation/sdk": "0.21.6", + "chalk": "3.0.0", + "commander": "11.1.0", + "jiti": "2.4.2" + }, + "bin": { + "mf": "bin/mf.js" + }, "engines": { - "node": ">=18" + "node": ">=16.0.0" } }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], + "node_modules/@module-federation/cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], + "node_modules/@module-federation/cli/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], + "node_modules/@module-federation/cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=18" + "node": ">=7.0.0" } }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], + "node_modules/@module-federation/cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/@module-federation/cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@modern-js/node-bundle-require/node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" - } - }, - "node_modules/@modern-js/utils": { - "version": "2.68.2", - "resolved": "https://registry.npmjs.org/@modern-js/utils/-/utils-2.68.2.tgz", - "integrity": "sha512-revom/i/EhKfI0STNLo/AUbv7gY0JY0Ni2gO6P/Z4cTyZZRgd5j90678YB2DGn+LtmSrEWtUphyDH5Jn1RKjgg==", - "license": "MIT", - "dependencies": { - "@swc/helpers": "^0.5.17", - "caniuse-lite": "^1.0.30001520", - "lodash": "^4.17.21", - "rslog": "^1.1.0" - } - }, - "node_modules/@module-federation/bridge-react-webpack-plugin": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.19.1.tgz", - "integrity": "sha512-D+iFESodr/ohaXjmTOWBSFdjAz/WfN5Y5lIKB5Axh19FBUxvCy6Pj/We7C5JXc8CD9puqxXFOBNysJ7KNB89iw==", - "license": "MIT", - "dependencies": { - "@module-federation/sdk": "0.19.1", - "@types/semver": "7.5.8", - "semver": "7.6.3" - } - }, - "node_modules/@module-federation/bridge-react-webpack-plugin/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@module-federation/cli": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-0.19.1.tgz", - "integrity": "sha512-WHEnqGLLtK3jFdAhhW5WMqF5TO4FUfgp6+ujuZLrB1iOnjJXwg/+3F/qjWQtfUPIUCJSAC+58TSKXo8FjNcxPA==", - "license": "MIT", - "dependencies": { - "@modern-js/node-bundle-require": "2.68.2", - "@module-federation/dts-plugin": "0.19.1", - "@module-federation/sdk": "0.19.1", - "chalk": "3.0.0", - "commander": "11.1.0" - }, - "bin": { - "mf": "bin/mf.js" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@module-federation/cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@module-federation/cli/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@module-federation/cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@module-federation/cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/@module-federation/cli/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/@module-federation/cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=8" } }, "node_modules/@module-federation/cli/node_modules/supports-color": { @@ -4894,13 +4390,13 @@ } }, "node_modules/@module-federation/data-prefetch": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/data-prefetch/-/data-prefetch-0.19.1.tgz", - "integrity": "sha512-EXtEhYBw5XSHmtLp8Nu0sK2MMkdBtmvWQFfWmLDjPGGTeJHNE+fIHmef9hDbqXra8RpCyyZgwfTCUMZcwAGvzQ==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/data-prefetch/-/data-prefetch-0.21.6.tgz", + "integrity": "sha512-8HD7ZhtWZ9vl6i3wA7M8cEeCRdtvxt09SbMTfqIPm+5eb/V4ijb8zGTYSRhNDb5RCB+BAixaPiZOWKXJ63/rVw==", "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.19.1", - "@module-federation/sdk": "0.19.1", + "@module-federation/runtime": "0.21.6", + "@module-federation/sdk": "0.21.6", "fs-extra": "9.1.0" }, "peerDependencies": { @@ -4909,22 +4405,22 @@ } }, "node_modules/@module-federation/dts-plugin": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-0.19.1.tgz", - "integrity": "sha512-/MV5gbEsiQiDwPmEq8WS24P/ibDtRwM7ejRKwZ+vWqv11jg75FlxHdzl71CMt5AatoPiUkrsPDQDO1EmKz/NXQ==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-0.21.6.tgz", + "integrity": "sha512-YIsDk8/7QZIWn0I1TAYULniMsbyi2LgKTi9OInzVmZkwMC6644x/ratTWBOUDbdY1Co+feNkoYeot1qIWv2L7w==", "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.19.1", - "@module-federation/managers": "0.19.1", - "@module-federation/sdk": "0.19.1", - "@module-federation/third-party-dts-extractor": "0.19.1", + "@module-federation/error-codes": "0.21.6", + "@module-federation/managers": "0.21.6", + "@module-federation/sdk": "0.21.6", + "@module-federation/third-party-dts-extractor": "0.21.6", "adm-zip": "^0.5.10", "ansi-colors": "^4.1.3", - "axios": "^1.11.0", + "axios": "^1.12.0", "chalk": "3.0.0", "fs-extra": "9.1.0", "isomorphic-ws": "5.0.0", - "koa": "3.0.1", + "koa": "3.0.3", "lodash.clonedeepwith": "4.5.0", "log4js": "6.9.1", "node-schedule": "2.1.1", @@ -5008,44 +4504,23 @@ "node": ">=8" } }, - "node_modules/@module-federation/dts-plugin/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@module-federation/enhanced": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-0.19.1.tgz", - "integrity": "sha512-cSNbV5IFZRECpKEdIhIGNW9dNPjyDmSFlPIV0OG7aP4zAmUtz/oizpYtEE5r7hLAGxzWwBnj7zQIIxvmKgrSAQ==", - "license": "MIT", - "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "0.19.1", - "@module-federation/cli": "0.19.1", - "@module-federation/data-prefetch": "0.19.1", - "@module-federation/dts-plugin": "0.19.1", - "@module-federation/error-codes": "0.19.1", - "@module-federation/inject-external-runtime-core-plugin": "0.19.1", - "@module-federation/managers": "0.19.1", - "@module-federation/manifest": "0.19.1", - "@module-federation/rspack": "0.19.1", - "@module-federation/runtime-tools": "0.19.1", - "@module-federation/sdk": "0.19.1", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-0.21.6.tgz", + "integrity": "sha512-8PFQxtmXc6ukBC4CqGIoc96M2Ly9WVwCPu4Ffvt+K/SB6rGbeFeZoYAwREV1zGNMJ5v5ly6+AHIEOBxNuSnzSg==", + "license": "MIT", + "dependencies": { + "@module-federation/bridge-react-webpack-plugin": "0.21.6", + "@module-federation/cli": "0.21.6", + "@module-federation/data-prefetch": "0.21.6", + "@module-federation/dts-plugin": "0.21.6", + "@module-federation/error-codes": "0.21.6", + "@module-federation/inject-external-runtime-core-plugin": "0.21.6", + "@module-federation/managers": "0.21.6", + "@module-federation/manifest": "0.21.6", + "@module-federation/rspack": "0.21.6", + "@module-federation/runtime-tools": "0.21.6", + "@module-federation/sdk": "0.21.6", "btoa": "^1.2.1", "schema-utils": "^4.3.0", "upath": "2.0.1" @@ -5071,40 +4546,40 @@ } }, "node_modules/@module-federation/error-codes": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.19.1.tgz", - "integrity": "sha512-XtrOfaYPBD9UbdWb7O+gk295/5EFfC2/R6JmhbQmM2mt2axlrwUoy29LAEMSpyMkAD0NfRfQ3HaOsJQiUIy+Qg==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.21.6.tgz", + "integrity": "sha512-MLJUCQ05KnoVl8xd6xs9a5g2/8U+eWmVxg7xiBMeR0+7OjdWUbHwcwgVFatRIwSZvFgKHfWEiI7wsU1q1XbTRQ==", "license": "MIT" }, "node_modules/@module-federation/inject-external-runtime-core-plugin": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-0.19.1.tgz", - "integrity": "sha512-yOErRSKR60H4Zyk4nUqsc7u7eLaZ5KX3FXAyKxdGwIJ1B8jJJS+xRiQM8bwRansoF23rv7XWO62K5w/qONiTuQ==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-0.21.6.tgz", + "integrity": "sha512-DJQne7NQ988AVi3QB8byn12FkNb+C2lBeU1NRf8/WbL0gmHsr6kW8hiEJCm8LYaURwtsQqtsEV7i+8+51qjSmQ==", "license": "MIT", "peerDependencies": { - "@module-federation/runtime-tools": "0.19.1" + "@module-federation/runtime-tools": "0.21.6" } }, "node_modules/@module-federation/managers": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-0.19.1.tgz", - "integrity": "sha512-bZwiRqc0Cy76xSgKw8dFpVc0tpu6EG+paL0bAtHU5Kj9SBRGyCZ1JQY2W+S8z5tS/7M+gDNl9iIgQim+Kq6isg==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-0.21.6.tgz", + "integrity": "sha512-BeV6m2/7kF5MDVz9JJI5T8h8lMosnXkH2bOxxFewcra7ZjvDOgQu7WIio0mgk5l1zjNPvnEVKhnhrenEdcCiWg==", "license": "MIT", "dependencies": { - "@module-federation/sdk": "0.19.1", + "@module-federation/sdk": "0.21.6", "find-pkg": "2.0.0", "fs-extra": "9.1.0" } }, "node_modules/@module-federation/manifest": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-0.19.1.tgz", - "integrity": "sha512-6QruFQRpedVpHq2JpsYFMrFQvSbqe4QcGjk6zYWQCx+kcUvxYuKwfRzhyJt/Sorqz2rW92I2ckmlHKufCLOmTg==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-0.21.6.tgz", + "integrity": "sha512-yg93+I1qjRs5B5hOSvjbjmIoI2z3th8/yst9sfwvx4UDOG1acsE3HHMyPN0GdoIGwplC/KAnU5NmUz4tREUTGQ==", "license": "MIT", "dependencies": { - "@module-federation/dts-plugin": "0.19.1", - "@module-federation/managers": "0.19.1", - "@module-federation/sdk": "0.19.1", + "@module-federation/dts-plugin": "0.21.6", + "@module-federation/managers": "0.21.6", + "@module-federation/sdk": "0.21.6", "chalk": "3.0.0", "find-pkg": "2.0.0" } @@ -5177,18 +4652,18 @@ } }, "node_modules/@module-federation/rspack": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-0.19.1.tgz", - "integrity": "sha512-H/bmdHhK91JIar9juyxdGQkjk5fLwbfugoBwFzxCx0PybwKObs+ZHW7yZ1ZoVBsRkYmvV79R2Squgtn/aGReCA==", - "license": "MIT", - "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "0.19.1", - "@module-federation/dts-plugin": "0.19.1", - "@module-federation/inject-external-runtime-core-plugin": "0.19.1", - "@module-federation/managers": "0.19.1", - "@module-federation/manifest": "0.19.1", - "@module-federation/runtime-tools": "0.19.1", - "@module-federation/sdk": "0.19.1", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-0.21.6.tgz", + "integrity": "sha512-SB+z1P+Bqe3R6geZje9dp0xpspX6uash+zO77nodmUy8PTTBlkL7800Cq2FMLKUdoTZHJTBVXf0K6CqQWSlItg==", + "license": "MIT", + "dependencies": { + "@module-federation/bridge-react-webpack-plugin": "0.21.6", + "@module-federation/dts-plugin": "0.21.6", + "@module-federation/inject-external-runtime-core-plugin": "0.21.6", + "@module-federation/managers": "0.21.6", + "@module-federation/manifest": "0.21.6", + "@module-federation/runtime-tools": "0.21.6", + "@module-federation/sdk": "0.21.6", "btoa": "1.2.1" }, "peerDependencies": { @@ -5206,46 +4681,46 @@ } }, "node_modules/@module-federation/runtime": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.19.1.tgz", - "integrity": "sha512-eSXexdGGPpZnhiWCVfRlVLNWj7gHKp65beC4b8wddTvMBIrxnsdl9ae1ebwcIpbe9gOGDbaXBFtc3r5MH6l6Jg==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.21.6.tgz", + "integrity": "sha512-+caXwaQqwTNh+CQqyb4mZmXq7iEemRDrTZQGD+zyeH454JAYnJ3s/3oDFizdH6245pk+NiqDyOOkHzzFQorKhQ==", "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.19.1", - "@module-federation/runtime-core": "0.19.1", - "@module-federation/sdk": "0.19.1" + "@module-federation/error-codes": "0.21.6", + "@module-federation/runtime-core": "0.21.6", + "@module-federation/sdk": "0.21.6" } }, "node_modules/@module-federation/runtime-core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.19.1.tgz", - "integrity": "sha512-NLSlPnIzO2RoF6W1xq/x3t1j7jcglMaPSv2EIVOFvs5/ah7BeJmRhtH494tmjIwV0q+j1QEGGhijHxXZLK1HMQ==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.21.6.tgz", + "integrity": "sha512-5Hd1Y5qp5lU/aTiK66lidMlM/4ji2gr3EXAtJdreJzkY+bKcI5+21GRcliZ4RAkICmvdxQU5PHPL71XmNc7Lsw==", "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.19.1", - "@module-federation/sdk": "0.19.1" + "@module-federation/error-codes": "0.21.6", + "@module-federation/sdk": "0.21.6" } }, "node_modules/@module-federation/runtime-tools": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.19.1.tgz", - "integrity": "sha512-WjLZcuP7U5pSQobMEvaMH9pFrvfV3Kk2dfOUNza0tpj6vYtAxk6FU6TQ8WDjqG7yuglyAzq0bVEKVrdIB4Vd9Q==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.21.6.tgz", + "integrity": "sha512-fnP+ZOZTFeBGiTAnxve+axGmiYn2D60h86nUISXjXClK3LUY1krUfPgf6MaD4YDJ4i51OGXZWPekeMe16pkd8Q==", "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.19.1", - "@module-federation/webpack-bundler-runtime": "0.19.1" + "@module-federation/runtime": "0.21.6", + "@module-federation/webpack-bundler-runtime": "0.21.6" } }, "node_modules/@module-federation/sdk": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.19.1.tgz", - "integrity": "sha512-0JTkYaa4qNLtYGc6ZQQ50BinWh4bAOgT8t17jB/6BqcWiza6fKz647wN0AK+VX3rtl6kvGAjhtqqZtRBc8aeiw==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.21.6.tgz", + "integrity": "sha512-x6hARETb8iqHVhEsQBysuWpznNZViUh84qV2yE7AD+g7uIzHKiYdoWqj10posbo5XKf/147qgWDzKZoKoEP2dw==", "license": "MIT" }, "node_modules/@module-federation/third-party-dts-extractor": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-0.19.1.tgz", - "integrity": "sha512-XBuujPLWgJjljm/QfShtI0pErqRL28iiJ7AsUpFsNbSRJiBlcXTDPKqFWiZXmp/lGmJigLV2wDgyK0cyKqoWcg==", + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-0.21.6.tgz", + "integrity": "sha512-Il6x4hLsvCgZNk1DFwuMBNeoxD1BsZ5AW2BI/nUgu0k5FiAvfcz1OFawRFEHtaM/kVrCsymMOW7pCao90DaX3A==", "license": "MIT", "dependencies": { "find-pkg": "2.0.0", @@ -5253,31 +4728,37 @@ "resolve": "1.22.8" } }, - "node_modules/@module-federation/third-party-dts-extractor/node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.21.6.tgz", + "integrity": "sha512-7zIp3LrcWbhGuFDTUMLJ2FJvcwjlddqhWGxi/MW3ur1a+HaO8v5tF2nl+vElKmbG1DFLU/52l3PElVcWf/YcsQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@module-federation/runtime": "0.21.6", + "@module-federation/sdk": "0.21.6" } }, - "node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.19.1.tgz", - "integrity": "sha512-pr9kgwvBoe8tvXELDCqu8ihvLJYwS+cfwJmvk99MTbespzK0nuOepkeRCy2gOpeATDNiWdy/2DJcw34qeAmhJw==", + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.19.1", - "@module-federation/sdk": "0.19.1" + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@mui/core-downloads-tracker": { @@ -5476,58 +4957,14 @@ }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/x-data-grid": { - "version": "7.29.9", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.29.9.tgz", - "integrity": "sha512-RfK7Fnuu4eyv/4eD3MEB1xxZsx0xRBsofb1kifghIjyQV1EKAeRcwvczyrzQggj7ZRT5AqkwCzhLsZDvE5O0nQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0", - "@mui/x-internals": "7.29.0", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "reselect": "^5.1.1", - "use-sync-external-store": "^1.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", - "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { "optional": true } } }, - "node_modules/@mui/x-data-grid/node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" - }, "node_modules/@mui/x-date-pickers": { "version": "7.29.4", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.29.4.tgz", @@ -5614,19 +5051,6 @@ "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, "node_modules/@nexucis/fuzzy": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@nexucis/fuzzy/-/fuzzy-0.5.1.tgz", @@ -5724,9 +5148,9 @@ } }, "node_modules/@openshift-console/dynamic-plugin-sdk": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@openshift-console/dynamic-plugin-sdk/-/dynamic-plugin-sdk-4.19.0.tgz", - "integrity": "sha512-v1pNt2eDCyXwRZX+66QDnaYtrwbLapRMtCQql65pgePsE5/5aoOSGIsTjIB812j+oagAB1yk4r7OUlopA2F8kw==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@openshift-console/dynamic-plugin-sdk/-/dynamic-plugin-sdk-4.19.1.tgz", + "integrity": "sha512-4Ond6LRb4nqq03UDio8NHHSC5RHliFK7zaKGb/s86Hf35INdix3F7gayGPfRs3u3Lr9yGCMaT+NaLV79kA7qbg==", "license": "Apache-2.0", "dependencies": { "@patternfly/react-topology": "^6.2.0", @@ -5746,9 +5170,9 @@ } }, "node_modules/@openshift-console/dynamic-plugin-sdk-internal": { - "version": "4.19.0-prerelease.2", - "resolved": "https://registry.npmjs.org/@openshift-console/dynamic-plugin-sdk-internal/-/dynamic-plugin-sdk-internal-4.19.0-prerelease.2.tgz", - "integrity": "sha512-IqjaAgOBnaAXB8ff7JWFoI+0gD30Nr/84CAGO60dCe1kGkgefWD7ABdraULEjrce3V/ZDifZgO00yscMOa80Eg==", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@openshift-console/dynamic-plugin-sdk-internal/-/dynamic-plugin-sdk-internal-4.19.0.tgz", + "integrity": "sha512-OnC/2CV/oejtOzXQujYk/0JGSPuFjR6F51KsKKPelX9KqF+A2G2GW/yESR3n/FkGMtXxl6ZCUXUBCDMLXVRH/g==", "license": "Apache-2.0", "dependencies": { "@patternfly/react-topology": "^6.2.0", @@ -5764,9 +5188,9 @@ } }, "node_modules/@openshift-console/dynamic-plugin-sdk-webpack": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@openshift-console/dynamic-plugin-sdk-webpack/-/dynamic-plugin-sdk-webpack-4.19.0.tgz", - "integrity": "sha512-a4zxkrEe4qhKir5R21OK/wJVCn8Gzm/y3smLxRDi/BjHFgcnNQKEgCMP4IhvTlT0NRJQ4Q9AmPT1WXBlV3ip3g==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@openshift-console/dynamic-plugin-sdk-webpack/-/dynamic-plugin-sdk-webpack-4.20.0.tgz", + "integrity": "sha512-cTpBy8Jz2C65Qy/3fXIRv8IehvKRkms8rMcjz0KGo7DRoaDPnSyMXi5gH0BIA01nr5U6sBncNSbaD/3PXaZaig==", "license": "Apache-2.0", "dependencies": { "@openshift/dynamic-plugin-sdk-webpack": "^4.0.2", @@ -5785,9 +5209,9 @@ } }, "node_modules/@openshift-console/dynamic-plugin-sdk-webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5824,9 +5248,9 @@ } }, "node_modules/@openshift/dynamic-plugin-sdk-webpack/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5872,20 +5296,41 @@ "@parcel/watcher-win32-x64": "2.5.1" } }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@patternfly/react-charts": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-charts/-/react-charts-8.3.0.tgz", - "integrity": "sha512-KD0KJonICtYr/73yZYDMa5yMgmE9Bg50gJ6BEpYavR/nPwWYAcZWQswDndNtyVgDUc9GoEYi3ly7nbLEtylbdA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-charts/-/react-charts-8.4.0.tgz", + "integrity": "sha512-lxfH2gVDg4Pd+D6TQ2SSqc5fQPk1UvhHbuP+7YZJdPhk2PzhhbaT3CE+kp5ZEU2y/lJb8L5kZ5lOk8tvPn6PQw==", "license": "MIT", "dependencies": { - "@patternfly/react-styles": "^6.3.0", - "@patternfly/react-tokens": "^6.3.0", + "@patternfly/react-styles": "^6.4.0", + "@patternfly/react-tokens": "^6.4.0", "hoist-non-react-statics": "^3.3.2", "lodash": "^4.17.21", "tslib": "^2.8.1" }, "peerDependencies": { - "echarts": "^5.6.0", + "echarts": "^5.6.0 || ^6.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19", "victory-area": "^37.3.6", @@ -5963,786 +5408,381 @@ } } }, - "node_modules/@patternfly/react-component-groups": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-6.3.0.tgz", - "integrity": "sha512-W8vSYD4KrAhDnjRLCPK+irVhG9GORQ7PveBFJ9FAvjCc4lGv73smDY4M1Lv2peNHQaXQpn6DSPsuynaReRvIhg==", - "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^6.0.0", - "@patternfly/react-icons": "^6.0.0", - "@patternfly/react-styles": "^6.0.0", - "@patternfly/react-table": "^6.0.0", - "react-jss": "^10.10.0" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, - "node_modules/@patternfly/react-core": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.3.0.tgz", - "integrity": "sha512-TM+pLwLd5DzaDlOQhqeju9H9QUFQypQiNwXQLNIxOV5r3fmKh4NTp2Av/8WmFkpCj8mejDOfp4TNxoU1zdjCkQ==", - "license": "MIT", - "dependencies": { - "@patternfly/react-icons": "^6.3.0", - "@patternfly/react-styles": "^6.3.0", - "@patternfly/react-tokens": "^6.3.0", - "focus-trap": "7.6.4", - "react-dropzone": "^14.3.5", - "tslib": "^2.8.1" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, - "node_modules/@patternfly/react-data-view": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.3.0.tgz", - "integrity": "sha512-0bdJl/BWDYmHQH8CyLoI59FTYVnEQ3e50ero8RvCYe+k5qnX1t/1KC/bJWosIxGROUBNtOWn6OEOKzfd8+BCTQ==", - "license": "MIT", - "dependencies": { - "@patternfly/react-component-groups": "^6.1.0", - "@patternfly/react-core": "^6.0.0", - "@patternfly/react-icons": "^6.0.0", - "@patternfly/react-table": "^6.0.0", - "clsx": "^2.1.1", - "react-jss": "^10.10.0" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, - "node_modules/@patternfly/react-icons": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.3.0.tgz", - "integrity": "sha512-W39JyqKW1UL6/YGuinDnpjbhmmLAfuxVrgDcdFBaK4D7D1iqkkqrDMV8zIzmV/RkodJ79xRnucYhYb2RukG4RA==", - "license": "MIT", - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, - "node_modules/@patternfly/react-styles": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.3.0.tgz", - "integrity": "sha512-FvuyNsY2oN8f2dvCl4Hx8CxBWCIF3BC9JE3Ay1lCuVqY1WYkvW4AQn3/0WVRINCxB9FkQxVNkSjARdwHNCEulw==", - "license": "MIT" - }, - "node_modules/@patternfly/react-table": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.3.0.tgz", - "integrity": "sha512-klC0HKZvKhrRRJX8j1bLmfvzJyMeqpwbKeV7np9Ggvvh79IxWpBBKX2XbJkjHtl+sCwIVN1VZatil3HGba5CZQ==", - "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^6.3.0", - "@patternfly/react-icons": "^6.3.0", - "@patternfly/react-styles": "^6.3.0", - "@patternfly/react-tokens": "^6.3.0", - "lodash": "^4.17.21", - "tslib": "^2.8.1" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, - "node_modules/@patternfly/react-templates": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-templates/-/react-templates-6.3.0.tgz", - "integrity": "sha512-7z5uR4m4st52b0652szAcoNH363Xv24SFfxzFXRALw4UQaUJsGnrF+atBZk1vNXomzbf8r0VazF4xXWaZL6mfA==", + "node_modules/@patternfly/react-code-editor": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.4.1.tgz", + "integrity": "sha512-308BhliLV+DatYkewaS71RDrYAmgLzDL2lAE+txGtztVeq/yaEgWBr1GStsSLrGfC+8zZL/JuW7gt8CqxUKpNQ==", "license": "MIT", "dependencies": { - "@patternfly/react-core": "^6.3.0", - "@patternfly/react-icons": "^6.3.0", - "@patternfly/react-styles": "^6.3.0", - "@patternfly/react-tokens": "^6.3.0", + "@monaco-editor/react": "^4.6.0", + "@patternfly/react-core": "^6.4.1", + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-styles": "^6.4.0", + "react-dropzone": "14.3.5", "tslib": "^2.8.1" }, "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, - "node_modules/@patternfly/react-tokens": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.3.0.tgz", - "integrity": "sha512-yWStfkbxg4RWAExFKS/JRGScyadOy35yr4DFispNeHrkZWMp4pwKf0VdwlQZ7+ZtSgEWtzzy1KFxMLmWh3mEqA==", - "license": "MIT" - }, - "node_modules/@patternfly/react-topology": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-6.3.0.tgz", - "integrity": "sha512-v0P8khAJqS01haEl9abw3L/MzdpIY+PsEVGBAMgA/tBhU/+9Ieco8fmmjW+7CfRmXhSC8NUjYgasl+9ZJVd3rA==", - "license": "MIT", - "dependencies": { - "@dagrejs/dagre": "1.1.2", - "@patternfly/react-core": "^6.0.0", - "@patternfly/react-icons": "^6.0.0", - "@patternfly/react-styles": "^6.0.0", - "@types/d3": "^7.4.0", - "@types/d3-force": "^1.2.1", - "d3": "^7.8.0", - "mobx": "^6.9.0", - "mobx-react": "^7.6.0", - "point-in-svg-path": "^1.0.1", - "popper.js": "^1.16.1", - "tslib": "^2.0.0", - "webcola": "3.4.0" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - } - }, - "node_modules/@perses-dev/bar-chart-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@perses-dev/bar-chart-plugin/-/bar-chart-plugin-0.9.0.tgz", - "integrity": "sha512-jx6QT74zZxC4xtID7mn95858xFJC+kk0lNYjH6sGPtPH0FEh/hbbWUNEip7RpSuVm2CcZIeYJ+lS5CdtWIwtkg==", - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" - } - }, - "node_modules/@perses-dev/components": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@perses-dev/components/-/components-0.52.0.tgz", - "integrity": "sha512-SPWHI/DKdUFiP4b3tP1+MgU+N4Y2ZGMWIXN7Fd+qRvU0vU0J7RWTjvszKrznLBboo1pPZQpoFE+Y6V3A2TFgxA==", - "license": "Apache-2.0", - "dependencies": { - "@atlaskit/pragmatic-drag-and-drop": "^1.4.0", - "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", - "@codemirror/lang-json": "^6.0.1", - "@fontsource/lato": "^4.5.10", - "@mui/x-date-pickers": "^7.23.1", - "@perses-dev/core": "0.52.0", - "@tanstack/react-table": "^8.20.5", - "@uiw/react-codemirror": "^4.19.1", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "mathjs": "^10.6.4", - "mdi-material-ui": "^7.9.2", - "notistack": "^3.0.2", - "react-colorful": "^5.6.1", - "react-error-boundary": "^3.1.4", - "react-hook-form": "^7.51.3", - "react-virtuoso": "^4.12.2" - }, - "peerDependencies": { - "@mui/material": "^6.1.10", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" - } - }, - "node_modules/@perses-dev/core": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@perses-dev/core/-/core-0.52.0.tgz", - "integrity": "sha512-dtaNSgVx4YH3kmQLdHyFun+C2WH1Fp85lLKA2ZEyAMX5hQZ3GwL1cusb2J51R4SPHRJ79YCtkbwD7dYnJBnrsw==", - "license": "Apache-2.0", - "dependencies": { - "date-fns": "^4.1.0", - "lodash": "^4.17.21", - "mathjs": "^10.6.4", - "numbro": "^2.3.6", - "zod": "^3.21.4" - }, - "peerDependencies": { - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" - } - }, - "node_modules/@perses-dev/dashboards": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@perses-dev/dashboards/-/dashboards-0.52.0.tgz", - "integrity": "sha512-InOalnIIUJxqOaJI8t1+FPOXf0OhQ0+BczNU+CP28THUpIPAxliOZUzvLGQ/OqEFepnapb+RlOYCNKF04Cdcpw==", - "license": "Apache-2.0", - "dependencies": { - "@perses-dev/components": "0.52.0", - "@perses-dev/core": "0.52.0", - "@perses-dev/plugin-system": "0.52.0", - "@types/react-grid-layout": "^1.3.2", - "date-fns": "^4.1.0", - "immer": "^10.1.1", - "mdi-material-ui": "^7.9.2", - "react-grid-layout": "^1.3.4", - "react-hook-form": "^7.46.1", - "react-intersection-observer": "^9.4.0", - "use-immer": "^0.11.0", - "use-query-params": "^2.2.1", - "use-resize-observer": "^9.0.0", - "yaml": "^2.7.0", - "zustand": "^4.3.3" - }, - "peerDependencies": { - "@mui/material": "^6.1.10", - "@tanstack/react-query": "^4.39.1", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" - } - }, - "node_modules/@perses-dev/datasource-variable-plugin": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@perses-dev/datasource-variable-plugin/-/datasource-variable-plugin-0.3.2.tgz", - "integrity": "sha512-GOmL55qmumdzvYqrvFmnlVveEwiUJ8IFB58uRyTwpyRDRQDv37iFlD4CeJeVGy7KUHiGqZNwLPPgGt7JDtzb7Q==", - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0", - "@perses-dev/core": "^0.52.0", - "@perses-dev/plugin-system": "^0.52.0", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" - } - }, - "node_modules/@perses-dev/explore": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@perses-dev/explore/-/explore-0.52.0.tgz", - "integrity": "sha512-J5ur8+LhRasOf57E5txGFrEurYC5wihkk019BHgPnBxPlusxYxuwdp/s25PGtRP/pXFHxQejogIS9GnyhX+xcA==", - "license": "Apache-2.0", - "dependencies": { - "@nexucis/fuzzy": "^0.5.1", - "@perses-dev/components": "0.52.0", - "@perses-dev/core": "0.52.0", - "@perses-dev/dashboards": "0.52.0", - "@perses-dev/plugin-system": "0.52.0", - "@types/react-grid-layout": "^1.3.2", - "date-fns": "^4.1.0", - "immer": "^10.1.1", - "mdi-material-ui": "^7.9.2", - "qs": "^6.14.0", - "react-grid-layout": "^1.3.4", - "react-hook-form": "^7.46.1", - "react-intersection-observer": "^9.4.0", - "react-virtuoso": "^4.12.2", - "use-immer": "^0.11.0", - "use-query-params": "^2.2.1", - "use-resize-observer": "^9.0.0", - "zod": "^3.21.4", - "zustand": "^4.3.3" - }, - "peerDependencies": { - "@mui/material": "^6.1.10", - "@tanstack/react-query": "^4.39.1", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" - } - }, - "node_modules/@perses-dev/flame-chart-plugin": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@perses-dev/flame-chart-plugin/-/flame-chart-plugin-0.3.0.tgz", - "integrity": "sha512-K4mTk2HgBvxVhfYNoE3VZ1VJDPNgKwfmZQudqFc+3h8584V4DUTlDfNzc+Rv/iS/gcyGT+R4gazqokygGKXVlA==", - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/gauge-chart-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@perses-dev/gauge-chart-plugin/-/gauge-chart-plugin-0.9.0.tgz", - "integrity": "sha512-EDYCqY91XkgxKR3mJLPbqYE1ajsyZLBnx+BN1l2VELcaCXfRBKuqTk90XlbAMeyt/FzIt+ClJiTh+2s3QyDUgQ==", + "node_modules/@patternfly/react-code-editor/node_modules/react-dropzone": { + "version": "14.3.5", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz", + "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "react": ">= 16.8 || 18.0.0" } }, - "node_modules/@perses-dev/heatmap-chart-plugin": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@perses-dev/heatmap-chart-plugin/-/heatmap-chart-plugin-0.2.1.tgz", - "integrity": "sha512-4+izLo5i7e6M3XxXUegK9bx7xzOI05xKwt5/2+UsDWEj6r/PMTsJvdrXx++oN0gHdbBim8ot7HNSGCjDcBMjbw==", + "node_modules/@patternfly/react-component-groups": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-6.4.0.tgz", + "integrity": "sha512-vg0761nQ/7hfggbp6+XowRcQQSd9oIToh77+4lmsyrs41MkA5ppQIPBCZ4lUZW87kmEPhkHqglpJcVfsrrIM/g==", + "license": "MIT", + "dependencies": { + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "@patternfly/react-table": "^6.0.0", + "react-jss": "^10.10.0" + }, "peerDependencies": { - "@perses-dev/components": "^0.52.0", - "@perses-dev/core": "^0.52.0", - "@perses-dev/plugin-system": "^0.52.0", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" + "@patternfly/react-drag-drop": "^6.0.0", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/histogram-chart-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@perses-dev/histogram-chart-plugin/-/histogram-chart-plugin-0.9.0.tgz", - "integrity": "sha512-0rMgYWHVVn3RM8PuclUNUsgVX9NPdXcStMWdgbtE8Lr1upleTtme6PVNEvfxl4ntOBZjptFzTX+AsIcY65Zypg==", + "node_modules/@patternfly/react-core": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.4.1.tgz", + "integrity": "sha512-EUSV76Eifkt4R3q2JIaiB6/FHeQqOCttK1DQMXNoOCNa3ODkZ7H+KlMdminufMDfRzhwAgTVihZ62K9PFfc8Vg==", + "license": "MIT", + "dependencies": { + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-styles": "^6.4.0", + "@patternfly/react-tokens": "^6.4.0", + "focus-trap": "7.6.4", + "react-dropzone": "^14.3.5", + "tslib": "^2.8.1" + }, "peerDependencies": { - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/loki-plugin": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@perses-dev/loki-plugin/-/loki-plugin-0.1.1.tgz", - "integrity": "sha512-yCei+vE0efLqYR+K/ehqXYsGF6C7hFHQxzpKqN2rmbn5cAKiB/kgx+SNq+ZF0MItUi089iqNQ4rKkxbJqawVVQ==", + "node_modules/@patternfly/react-data-view": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.4.0.tgz", + "integrity": "sha512-AYIJvWLSoZaf3askvBjyyFQEvSCiquw5PFzEOiTsNoM2pDYkRagzppjclpI+MRJr44ZrfpljC6ZKE4f5Ni2p+w==", + "license": "MIT", "dependencies": { - "@grafana/lezer-logql": "^0.2.8" + "@patternfly/react-component-groups": "^6.1.0", + "@patternfly/react-core": "^6.4.0", + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-table": "^6.4.0", + "clsx": "^2.1.1", + "react-jss": "^10.10.0" }, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/dashboards": "^0.52.0-beta.5", - "@perses-dev/explore": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "@tanstack/react-query": "^4.39.1", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "react-hook-form": "^7.52.2", - "use-resize-observer": "^9.0.0" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/markdown-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@perses-dev/markdown-plugin/-/markdown-plugin-0.9.0.tgz", - "integrity": "sha512-CoXfSk7x+xo2jUEMd24zfhYzNHI+G/sL7kFsAkBRloHdZIWt2OQ7uZ5C3TJUf1iqBPjdtdeblY1lSynd8Cj2Ag==", + "node_modules/@patternfly/react-drag-drop": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.4.0.tgz", + "integrity": "sha512-571HWmMbfwxCHC7KWPuazFHpgwvGJkxBg3i+4K/Ie8bQz8M/z2psaEnIOQCBL2tCGO6xNkfeojPXXjSClHLhzQ==", + "license": "MIT", + "peer": true, "dependencies": { - "dompurify": "^3.2.3", - "marked": "^15.0.6" + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@patternfly/react-core": "^6.4.0", + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-styles": "^6.4.0", + "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/pie-chart-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@perses-dev/pie-chart-plugin/-/pie-chart-plugin-0.9.0.tgz", - "integrity": "sha512-aSE2/pG42LF3cx/2kHxxE0uyB/Zi20jNrxK90fMLonc8DAawgqoYN3GmMk8s9LPIjDb4mqKcXY5jjF9daTIUjA==", + "node_modules/@patternfly/react-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", + "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "license": "MIT", "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/plugin-system": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@perses-dev/plugin-system/-/plugin-system-0.52.0.tgz", - "integrity": "sha512-6wNTZQwlcpX3sCc/Fq1N9/waIh9YGmniCngkqnAw9zhu04UVNKT1ESFAg7IGjJNnIg70Ezx8L7lrI5gSdtjiMg==", - "license": "Apache-2.0", + "node_modules/@patternfly/react-styles": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.4.0.tgz", + "integrity": "sha512-EXmHA67s5sy+Wy/0uxWoUQ52jr9lsH2wV3QcgtvVc5zxpyBX89gShpqv4jfVqaowznHGDoL6fVBBrSe9BYOliQ==", + "license": "MIT" + }, + "node_modules/@patternfly/react-table": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.4.0.tgz", + "integrity": "sha512-yv0sFOLGts8a2q9C1xUegjp50ayYyVRe0wKjMf+aMSNIK8sVYu8qu0yfBsCDybsUCldue7+qsYKRLFZosTllWQ==", + "license": "MIT", "dependencies": { - "@module-federation/enhanced": "^0.19.1", - "@perses-dev/components": "0.52.0", - "@perses-dev/core": "0.52.0", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "immer": "^10.1.1", - "react-hook-form": "^7.46.1", - "use-immer": "^0.11.0", - "use-query-params": "^2.2.1", - "zod": "^3.22.2" + "@patternfly/react-core": "^6.4.0", + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-styles": "^6.4.0", + "@patternfly/react-tokens": "^6.4.0", + "lodash": "^4.17.21", + "tslib": "^2.8.1" }, "peerDependencies": { - "@mui/material": "^6.1.10", - "@tanstack/react-query": "^4.39.1", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/prometheus-plugin": { - "version": "0.53.3", - "resolved": "https://registry.npmjs.org/@perses-dev/prometheus-plugin/-/prometheus-plugin-0.53.3.tgz", - "integrity": "sha512-qyNJwDi74XSKGiU2qgWH71V6gn+4xbVUVlQoeegCY36D27Rl5xNnRggdyIkgB60GmhKfkAC04BXPsF9TAdeT+w==", + "node_modules/@patternfly/react-templates": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-templates/-/react-templates-6.4.0.tgz", + "integrity": "sha512-n3/CWJ3jEv7d7ZjDa6g0B+k1N9kdw6WV259O44GqGSUd/cgMNZp+B9iIcOKQhekvCEPqvqzsAJT2b9X3YQNwkg==", + "license": "MIT", "dependencies": { - "@nexucis/fuzzy": "^0.5.1", - "@prometheus-io/codemirror-promql": "^0.45.6", - "color-hash": "^2.0.2", - "qs": "^6.13.0", - "react-virtuoso": "^4.12.2" + "@patternfly/react-core": "^6.4.0", + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-styles": "^6.4.0", + "@patternfly/react-tokens": "^6.4.0", + "tslib": "^2.8.1" }, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/dashboards": "^0.52.0-beta.5", - "@perses-dev/explore": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "@tanstack/react-query": "^4.39.1", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "react-hook-form": "^7.52.2", - "react-router-dom": "^5 || ^6 || ^7", - "use-resize-observer": "^9.0.0" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/prometheus-plugin/node_modules/@prometheus-io/codemirror-promql": { - "version": "0.45.6", - "resolved": "https://registry.npmjs.org/@prometheus-io/codemirror-promql/-/codemirror-promql-0.45.6.tgz", - "integrity": "sha512-/MLafOGFWFE4vGNDf5k0UodF16Ej7M22WO4q19I6DbncuYHsQAe3fKFDuA3B7noAitbi/XYoXL9kbOuq1VbI6g==", - "license": "Apache-2.0", + "node_modules/@patternfly/react-tokens": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.4.0.tgz", + "integrity": "sha512-iZthBoXSGQ/+PfGTdPFJVulaJZI3rwE+7A/whOXPGp3Jyq3k6X52pr1+5nlO6WHasbZ9FyeZGqXf4fazUZNjbw==", + "license": "MIT" + }, + "node_modules/@patternfly/react-topology": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-6.4.0.tgz", + "integrity": "sha512-Uy2ofRnI0apYiPWUYIAlXifl+4QPx/sqsVku1WglCkqjMPwuR8vC/GTIJEq2qW7mucDAcWSTFAPc5+zqyPRrlQ==", + "license": "MIT", "dependencies": { - "@prometheus-io/lezer-promql": "0.45.6", - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=12.0.0" + "@dagrejs/dagre": "1.1.2", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "@types/d3": "^7.4.0", + "@types/d3-force": "^1.2.1", + "d3": "^7.8.0", + "mobx": "^6.9.0", + "mobx-react": "^7.6.0", + "point-in-svg-path": "^1.0.1", + "popper.js": "^1.16.1", + "tslib": "^2.0.0", + "webcola": "3.4.0" }, "peerDependencies": { - "@codemirror/autocomplete": "^6.4.0", - "@codemirror/language": "^6.3.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/state": "^6.1.1", - "@codemirror/view": "^6.4.0", - "@lezer/common": "^1.0.1" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/@perses-dev/prometheus-plugin/node_modules/@prometheus-io/lezer-promql": { - "version": "0.45.6", - "resolved": "https://registry.npmjs.org/@prometheus-io/lezer-promql/-/lezer-promql-0.45.6.tgz", - "integrity": "sha512-IIShcInrCT+pBFjKqvgfM9ylC3LVdgtLEt9HxbwDJEn7yRHRFmKZdmoSavsyPrzajcxQ+7vyRKw7qX9It4J/Zg==", + "node_modules/@perses-dev/components": { + "version": "0.53.0-rc.2", + "resolved": "https://registry.npmjs.org/@perses-dev/components/-/components-0.53.0-rc.2.tgz", + "integrity": "sha512-faH+rdmUIVjpkc4tJAWnOw0dx6DAHqikKh/ODCuVAnWUmZ8X25K1MBueLWreOABto6vzzL1ZVYkUaO2DuPjp0w==", "license": "Apache-2.0", - "peerDependencies": { - "@lezer/highlight": "^1.1.2", - "@lezer/lr": "^1.2.3" - } - }, - "node_modules/@perses-dev/pyroscope-plugin": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@perses-dev/pyroscope-plugin/-/pyroscope-plugin-0.3.1.tgz", - "integrity": "sha512-lp1LAb/440ekGbsWckAMujrUH3ii3+8ybiy0FbhWwQYLVsX4T2u1vpSAw8bOl/zHUGeX/93XLJ1CkI63jhoCZA==", "dependencies": { - "@codemirror/autocomplete": "^6.18.4", - "@lezer/highlight": "^1.2.1x" - }, - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/dashboards": "^0.52.0-beta.5", - "@perses-dev/explore": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "@tanstack/react-query": "^4.39.1", + "@atlaskit/pragmatic-drag-and-drop": "^1.4.0", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", + "@codemirror/lang-json": "^6.0.1", + "@fontsource/lato": "^4.5.10", + "@mui/x-date-pickers": "^7.23.1", + "@perses-dev/core": "0.53.0-rc.0", + "@tanstack/react-table": "^8.20.5", "@uiw/react-codemirror": "^4.19.1", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "echarts": "5.5.0", "immer": "^10.1.1", "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "react-hook-form": "^7.52.2", - "use-resize-observer": "^9.0.0" - } - }, - "node_modules/@perses-dev/scatter-chart-plugin": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@perses-dev/scatter-chart-plugin/-/scatter-chart-plugin-0.8.0.tgz", - "integrity": "sha512-1tDnpsQdu8XuWsKDUYlrReXY8o9zhylHYyPZTQ3gs8LVVEgOXWDqBUgjxzkAt6mNoaCL8TuJk3g2kpAJlMxVKw==", - "dependencies": { + "mathjs": "^10.6.4", + "mdi-material-ui": "^7.9.2", + "notistack": "^3.0.2", + "react-colorful": "^5.6.1", + "react-error-boundary": "^3.1.4", + "react-hook-form": "^7.51.3", "react-virtuoso": "^4.12.2" }, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", + "@mui/material": "^6.1.10", "lodash": "^4.17.21", "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "react-dom": "^17.0.2 || ^18.0.0" } }, - "node_modules/@perses-dev/stat-chart-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@perses-dev/stat-chart-plugin/-/stat-chart-plugin-0.9.0.tgz", - "integrity": "sha512-uFpabWAGA7kh0bOKDQbYHjAYrPEp71M+EgKBKNM6BRC1W4FLuYL1V1aQPud3VBBEbbS9Q8ECnC2EV9MkYdgJRA==", - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", + "node_modules/@perses-dev/components/node_modules/@perses-dev/core": { + "version": "0.53.0-rc.0", + "resolved": "https://registry.npmjs.org/@perses-dev/core/-/core-0.53.0-rc.0.tgz", + "integrity": "sha512-hcFY/l7PlQZ9lz/uAh0J8Txw0l+W2mkalNcDu+CGvusc2sgQznrCyn7hh/UppSN7ls1JgwaCqjjVeqBEHEjsrQ==", + "license": "Apache-2.0", + "dependencies": { "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "mathjs": "^10.6.4", + "numbro": "^2.3.6", + "zod": "^3.21.4" } }, - "node_modules/@perses-dev/static-list-variable-plugin": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@perses-dev/static-list-variable-plugin/-/static-list-variable-plugin-0.5.1.tgz", - "integrity": "sha512-6wkGx/CkF/UjnHzxAq9qq0A9RUY4NwvxORLrQZSliBt9tPfd4ZGJq7DXogNQqtZUZTlZFnWFM1qRmFRvBSYvxQ==", + "node_modules/@perses-dev/core": { + "version": "0.53.0-rc.1", + "resolved": "https://registry.npmjs.org/@perses-dev/core/-/core-0.53.0-rc.1.tgz", + "integrity": "sha512-jc8iaQ0N3GfA0LxBR88Wmv4pKIJH6TXYPRZeYDxKjKPIkZi4t0c/yYMVu6CV2BAFeKzPxc+R7wZ8LFVWj7SCPQ==", + "license": "Apache-2.0", "dependencies": { - "color-hash": "^2.0.2" - }, - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "mathjs": "^10.6.4", + "numbro": "^2.3.6", + "zod": "^3.21.4" } }, - "node_modules/@perses-dev/status-history-chart-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@perses-dev/status-history-chart-plugin/-/status-history-chart-plugin-0.9.0.tgz", - "integrity": "sha512-UPrrnMt8WMUId6WabxlGDdAnSpEBYuHm7kBbUCyy2ZdmjMRH4QDcH6dLyA5IntCi1emuYs+Mn53OD2qQUetmhw==", - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", + "node_modules/@perses-dev/dashboards": { + "version": "0.53.0-rc.2", + "resolved": "https://registry.npmjs.org/@perses-dev/dashboards/-/dashboards-0.53.0-rc.2.tgz", + "integrity": "sha512-MNDgv3gGBa5hb3LZu/dlKRNoL1x2bQWnXxQlma7yOQ1J/m/W5ZB2DpkaZ2ttxori7yJyRvYYEFoT9fnVJOlZ/Q==", + "license": "Apache-2.0", + "dependencies": { + "@perses-dev/components": "0.53.0-rc.2", + "@perses-dev/core": "0.53.0-rc.0", + "@perses-dev/plugin-system": "0.53.0-rc.2", + "@types/react-grid-layout": "^1.3.2", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", "immer": "^10.1.1", - "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" - } - }, - "node_modules/@perses-dev/table-plugin": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@perses-dev/table-plugin/-/table-plugin-0.8.0.tgz", - "integrity": "sha512-wtE85W2nuD5mjphcIv0SJfejAsvzAowzJh9qUOGm9G7sw6W5HnzqWlP1PcVCPLMihblRx4FxeXznJFlw9hddQw==", + "mdi-material-ui": "^7.9.2", + "react-grid-layout": "^1.3.4", + "react-hook-form": "^7.46.1", + "react-intersection-observer": "^9.4.0", + "use-immer": "^0.11.0", + "use-query-params": "^2.2.1", + "use-resize-observer": "^9.0.0", + "yaml": "^2.7.0", + "zustand": "^4.3.3" + }, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/dashboards": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "lodash": "^4.17.21", + "@mui/material": "^6.1.10", + "@tanstack/react-query": "^4.39.1", "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "react-dom": "^17.0.2 || ^18.0.0" } }, - "node_modules/@perses-dev/tempo-plugin": { - "version": "0.53.1", - "resolved": "https://registry.npmjs.org/@perses-dev/tempo-plugin/-/tempo-plugin-0.53.1.tgz", - "integrity": "sha512-dx0vqVpq61czGldBECkMealfI67dq33wNwG7Tp14DyN0Jk1exghaSdBiMwinNwyD4B2YxBN3mcP2bmAqJE29mA==", + "node_modules/@perses-dev/dashboards/node_modules/@perses-dev/core": { + "version": "0.53.0-rc.0", + "resolved": "https://registry.npmjs.org/@perses-dev/core/-/core-0.53.0-rc.0.tgz", + "integrity": "sha512-hcFY/l7PlQZ9lz/uAh0J8Txw0l+W2mkalNcDu+CGvusc2sgQznrCyn7hh/UppSN7ls1JgwaCqjjVeqBEHEjsrQ==", + "license": "Apache-2.0", "dependencies": { - "@codemirror/autocomplete": "^6.18.4", - "@grafana/lezer-traceql": "^0.0.20", - "@lezer/highlight": "^1.2.1x" - }, - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/dashboards": "^0.52.0-beta.5", - "@perses-dev/explore": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "@tanstack/react-query": "^4.39.1", - "@uiw/react-codemirror": "^4.19.1", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "immer": "^10.1.1", "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "react-hook-form": "^7.52.2", - "use-resize-observer": "^9.0.0" + "mathjs": "^10.6.4", + "numbro": "^2.3.6", + "zod": "^3.21.4" } }, - "node_modules/@perses-dev/timeseries-chart-plugin": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@perses-dev/timeseries-chart-plugin/-/timeseries-chart-plugin-0.10.1.tgz", - "integrity": "sha512-rvU5V7d27HUFpAxGjM/I5z0gi/Q7hi+jmC4JesjdZCEOltYWWld1mgPbzOn+mf/Qv86J373nw/zmalEP9VCEcw==", + "node_modules/@perses-dev/explore": { + "version": "0.53.0-rc.2", + "resolved": "https://registry.npmjs.org/@perses-dev/explore/-/explore-0.53.0-rc.2.tgz", + "integrity": "sha512-mehc1IxBcAFwzBR7vyG/AbrYprLK75FhgXzBIfhnQSIIQJFpjRyspcY3M2TTLaXxTUld+Jc5mlLYJFu1o10U8A==", + "license": "Apache-2.0", "dependencies": { - "color-hash": "^2.0.2" - }, - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", + "@nexucis/fuzzy": "^0.5.1", + "@perses-dev/components": "0.53.0-rc.2", + "@perses-dev/core": "0.53.0-rc.0", + "@perses-dev/dashboards": "0.53.0-rc.2", + "@perses-dev/plugin-system": "0.53.0-rc.2", + "@types/react-grid-layout": "^1.3.2", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", "immer": "^10.1.1", - "lodash": "^4.17.21", + "mdi-material-ui": "^7.9.2", + "qs": "^6.14.0", + "react-grid-layout": "^1.3.4", + "react-hook-form": "^7.46.1", + "react-intersection-observer": "^9.4.0", + "react-virtuoso": "^4.12.2", + "use-immer": "^0.11.0", + "use-query-params": "^2.2.1", + "use-resize-observer": "^9.0.0", + "zod": "^3.21.4", + "zustand": "^4.3.3" + }, + "peerDependencies": { + "@mui/material": "^6.1.10", + "@tanstack/react-query": "^4.39.1", "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "react-dom": "^17.0.2 || ^18.0.0" } }, - "node_modules/@perses-dev/timeseries-table-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@perses-dev/timeseries-table-plugin/-/timeseries-table-plugin-0.9.0.tgz", - "integrity": "sha512-3CZ5M6rSaodR7mQao7rEkSCg8EEi6uskzRJR9926CoziX2DqfDtM6nH2LOBZue8zTRlYwcJHTqVGnd+O/2eD1A==", - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/dashboards": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", + "node_modules/@perses-dev/explore/node_modules/@perses-dev/core": { + "version": "0.53.0-rc.0", + "resolved": "https://registry.npmjs.org/@perses-dev/core/-/core-0.53.0-rc.0.tgz", + "integrity": "sha512-hcFY/l7PlQZ9lz/uAh0J8Txw0l+W2mkalNcDu+CGvusc2sgQznrCyn7hh/UppSN7ls1JgwaCqjjVeqBEHEjsrQ==", + "license": "Apache-2.0", + "dependencies": { "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "mathjs": "^10.6.4", + "numbro": "^2.3.6", + "zod": "^3.21.4" } }, - "node_modules/@perses-dev/trace-table-plugin": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@perses-dev/trace-table-plugin/-/trace-table-plugin-0.8.1.tgz", - "integrity": "sha512-tOs+YGl3SirWYRyxE/rPtfvtCODvLsQxWhjiSLop3fPT9Ue9CKwLou653o6S2l9p17FHuRKFfqiNhJLrPQ8JMQ==", + "node_modules/@perses-dev/plugin-system": { + "version": "0.53.0-rc.2", + "resolved": "https://registry.npmjs.org/@perses-dev/plugin-system/-/plugin-system-0.53.0-rc.2.tgz", + "integrity": "sha512-+Gr96xBt4+pf6MsKy1ZiEy1beSEJ2OJ7V7y1W2rEkO+pek0afu90EyEDWLUNfVS+3k5YcXf4BLUgKTA6XtQExQ==", + "license": "Apache-2.0", "dependencies": { - "@mui/x-data-grid": "^7.20.0" + "@module-federation/enhanced": "^0.21.4", + "@perses-dev/components": "0.53.0-rc.2", + "@perses-dev/core": "0.53.0-rc.0", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "immer": "^10.1.1", + "react-hook-form": "^7.46.1", + "use-immer": "^0.11.0", + "use-query-params": "^2.2.1", + "zod": "^3.22.2" }, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", - "lodash": "^4.17.21", + "@mui/material": "^6.1.10", + "@tanstack/react-query": "^4.39.1", "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "react-dom": "^17.0.2 || ^18.0.0" } }, - "node_modules/@perses-dev/tracing-gantt-chart-plugin": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@perses-dev/tracing-gantt-chart-plugin/-/tracing-gantt-chart-plugin-0.9.2.tgz", - "integrity": "sha512-HBWVDqOYuhfRHuehZp5awPqsIuY3Ta9vAgwiGR8SoYLqzCIDUkSHVEr+meGocN9NwP+mnI3yp9RhUPdhpdhr5w==", + "node_modules/@perses-dev/plugin-system/node_modules/@perses-dev/core": { + "version": "0.53.0-rc.0", + "resolved": "https://registry.npmjs.org/@perses-dev/core/-/core-0.53.0-rc.0.tgz", + "integrity": "sha512-hcFY/l7PlQZ9lz/uAh0J8Txw0l+W2mkalNcDu+CGvusc2sgQznrCyn7hh/UppSN7ls1JgwaCqjjVeqBEHEjsrQ==", + "license": "Apache-2.0", "dependencies": { - "color-hash": "^2.0.2" - }, - "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.52.0-beta.5", - "@perses-dev/core": "^0.52.0-beta.5", - "@perses-dev/plugin-system": "^0.52.0-beta.5", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", "lodash": "^4.17.21", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0", - "use-resize-observer": "^9.0.0" + "mathjs": "^10.6.4", + "numbro": "^2.3.6", + "zod": "^3.21.4" } }, "node_modules/@pkgjs/parseargs": { @@ -6820,37 +5860,37 @@ } }, "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" } }, "node_modules/@rspack/binding": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.5.5.tgz", - "integrity": "sha512-JkB943uBU0lABnKG/jdO+gg3/eeO9CEQMR/1dL6jSU9GTxaNf3XIVc05RhRC7qoVsiXuhSMMFxWyV0hyHxp2bA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.6.1.tgz", + "integrity": "sha512-6duvh3CbDA3c4HpNkzIOP9z1wn/mKY1Mrxj+AqgcNvsE0ppp1iKlMsJCDgl7SlUauus2AgtM1dIEU+0sRajmwQ==", "license": "MIT", "peer": true, "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.5.5", - "@rspack/binding-darwin-x64": "1.5.5", - "@rspack/binding-linux-arm64-gnu": "1.5.5", - "@rspack/binding-linux-arm64-musl": "1.5.5", - "@rspack/binding-linux-x64-gnu": "1.5.5", - "@rspack/binding-linux-x64-musl": "1.5.5", - "@rspack/binding-wasm32-wasi": "1.5.5", - "@rspack/binding-win32-arm64-msvc": "1.5.5", - "@rspack/binding-win32-ia32-msvc": "1.5.5", - "@rspack/binding-win32-x64-msvc": "1.5.5" + "@rspack/binding-darwin-arm64": "1.6.1", + "@rspack/binding-darwin-x64": "1.6.1", + "@rspack/binding-linux-arm64-gnu": "1.6.1", + "@rspack/binding-linux-arm64-musl": "1.6.1", + "@rspack/binding-linux-x64-gnu": "1.6.1", + "@rspack/binding-linux-x64-musl": "1.6.1", + "@rspack/binding-wasm32-wasi": "1.6.1", + "@rspack/binding-win32-arm64-msvc": "1.6.1", + "@rspack/binding-win32-ia32-msvc": "1.6.1", + "@rspack/binding-win32-x64-msvc": "1.6.1" } }, "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.5.5.tgz", - "integrity": "sha512-Kg3ywEZHLX+aROfTQ5tMOv+Ud+8b4jk8ruUgsi0W8oBkEkR5xBdhFa9vcf6pzy+gfoLCnEI68U9i8ttm+G0csA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.6.1.tgz", + "integrity": "sha512-am7gVsqicKY/FhDfNa/InHxrBd3wRt6rI7sFTaunKaPbPERjWSKr/sI47tB3t8uNYmLQFFhWFijomAhDyrlHMg==", "cpu": [ "arm64" ], @@ -6862,14 +5902,14 @@ "peer": true }, "node_modules/@rspack/core": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.5.5.tgz", - "integrity": "sha512-AOIuMktK6X/xHAjJ/0QJ2kbSkILXj641GCPE+EOfWO27ODA8fHPArKbyz5AVGVePV3aUfEo2VFcsNzP67VBEPA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.6.1.tgz", + "integrity": "sha512-hZVrmiZoBTchWUdh/XbeJ5z+GqHW5aPYeufBigmtUeyzul8uJtHlWKmQhpG+lplMf6o1RESTjjxl632TP/Cfhg==", "license": "MIT", "peer": true, "dependencies": { - "@module-federation/runtime-tools": "0.18.0", - "@rspack/binding": "1.5.5", + "@module-federation/runtime-tools": "0.21.2", + "@rspack/binding": "1.6.1", "@rspack/lite-tapable": "1.0.1" }, "engines": { @@ -6885,62 +5925,62 @@ } }, "node_modules/@rspack/core/node_modules/@module-federation/error-codes": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.18.0.tgz", - "integrity": "sha512-Woonm8ehyVIUPXChmbu80Zj6uJkC0dD9SJUZ/wOPtO8iiz/m+dkrOugAuKgoiR6qH4F+yorWila954tBz4uKsQ==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.21.2.tgz", + "integrity": "sha512-mGbPAAApgjmQUl4J7WAt20aV04a26TyS21GDEpOGXFEQG5FqmZnSJ6FqB8K19HgTKioBT1+fF/Ctl5bGGao/EA==", "license": "MIT", "peer": true }, "node_modules/@rspack/core/node_modules/@module-federation/runtime": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.18.0.tgz", - "integrity": "sha512-+C4YtoSztM7nHwNyZl6dQKGUVJdsPrUdaf3HIKReg/GQbrt9uvOlUWo2NXMZ8vDAnf/QRrpSYAwXHmWDn9Obaw==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.21.2.tgz", + "integrity": "sha512-97jlOx4RAnAHMBTfgU5FBK6+V/pfT6GNX0YjSf8G+uJ3lFy74Y6kg/BevEkChTGw5waCLAkw/pw4LmntYcNN7g==", "license": "MIT", "peer": true, "dependencies": { - "@module-federation/error-codes": "0.18.0", - "@module-federation/runtime-core": "0.18.0", - "@module-federation/sdk": "0.18.0" + "@module-federation/error-codes": "0.21.2", + "@module-federation/runtime-core": "0.21.2", + "@module-federation/sdk": "0.21.2" } }, "node_modules/@rspack/core/node_modules/@module-federation/runtime-core": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.18.0.tgz", - "integrity": "sha512-ZyYhrDyVAhUzriOsVfgL6vwd+5ebYm595Y13KeMf6TKDRoUHBMTLGQ8WM4TDj8JNsy7LigncK8C03fn97of0QQ==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.21.2.tgz", + "integrity": "sha512-LtDnccPxjR8Xqa3daRYr1cH/6vUzK3mQSzgvnfsUm1fXte5syX4ftWw3Eu55VdqNY3yREFRn77AXdu9PfPEZRw==", "license": "MIT", "peer": true, "dependencies": { - "@module-federation/error-codes": "0.18.0", - "@module-federation/sdk": "0.18.0" + "@module-federation/error-codes": "0.21.2", + "@module-federation/sdk": "0.21.2" } }, "node_modules/@rspack/core/node_modules/@module-federation/runtime-tools": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.18.0.tgz", - "integrity": "sha512-fSga9o4t1UfXNV/Kh6qFvRyZpPp3EHSPRISNeyT8ZoTpzDNiYzhtw0BPUSSD8m6C6XQh2s/11rI4g80UY+d+hA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.21.2.tgz", + "integrity": "sha512-SgG9NWTYGNYcHSd5MepO3AXf6DNXriIo4sKKM4mu4RqfYhHyP+yNjnF/gvYJl52VD61g0nADmzLWzBqxOqk2tg==", "license": "MIT", "peer": true, "dependencies": { - "@module-federation/runtime": "0.18.0", - "@module-federation/webpack-bundler-runtime": "0.18.0" + "@module-federation/runtime": "0.21.2", + "@module-federation/webpack-bundler-runtime": "0.21.2" } }, "node_modules/@rspack/core/node_modules/@module-federation/sdk": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.18.0.tgz", - "integrity": "sha512-Lo/Feq73tO2unjmpRfyyoUkTVoejhItXOk/h5C+4cistnHbTV8XHrW/13fD5e1Iu60heVdAhhelJd6F898Ve9A==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.21.2.tgz", + "integrity": "sha512-t2vHSJ1a9zjg7LLJoEghcytNLzeFCqOat5TbXTav5dgU0xXw82Cf0EfLrxiJL6uUpgbtyvUdqqa2DVAvMPjiiA==", "license": "MIT", "peer": true }, "node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.18.0.tgz", - "integrity": "sha512-TEvErbF+YQ+6IFimhUYKK3a5wapD90d90sLsNpcu2kB3QGT7t4nIluE25duXuZDVUKLz86tEPrza/oaaCWTpvQ==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.21.2.tgz", + "integrity": "sha512-06R/NDY6Uh5RBIaBOFwYWzJCf1dIiQd/DFHToBVhejUT3ZFG7GzHEPIIsAGqMzne/JSmVsvjlXiJu7UthQ6rFA==", "license": "MIT", "peer": true, "dependencies": { - "@module-federation/runtime": "0.18.0", - "@module-federation/sdk": "0.18.0" + "@module-federation/runtime": "0.21.2", + "@module-federation/sdk": "0.21.2" } }, "node_modules/@rspack/lite-tapable": { @@ -6980,19 +6020,93 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@swc/core": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz", + "integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.3", + "@swc/core-darwin-x64": "1.15.3", + "@swc/core-linux-arm-gnueabihf": "1.15.3", + "@swc/core-linux-arm64-gnu": "1.15.3", + "@swc/core-linux-arm64-musl": "1.15.3", + "@swc/core-linux-x64-gnu": "1.15.3", + "@swc/core-linux-x64-musl": "1.15.3", + "@swc/core-win32-arm64-msvc": "1.15.3", + "@swc/core-win32-ia32-msvc": "1.15.3", + "@swc/core-win32-x64-msvc": "1.15.3" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz", + "integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tanstack/query-core": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.40.0.tgz", - "integrity": "sha512-7MJTtZkCSuehMC7IxMOCGsLvHS3jHx4WjveSrGsG1Nc1UQLjaFwwkpLA2LmPfvOAxnH4mszMOBFD6LlZE+aB+Q==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.41.0.tgz", + "integrity": "sha512-193R4Jp9hjvlij6LryxrB5Mpbffd2L9PeWh3KlIy/hJV4SkBOfiQZ+jc5qAZLDCrdbkA5FjGj+UoDYw6TcNnyA==", "license": "MIT", "funding": { "type": "github", @@ -7000,12 +6114,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.40.1.tgz", - "integrity": "sha512-mgD07S5N8e5v81CArKDWrHE4LM7HxZ9k/KLeD3+NUD9WimGZgKIqojUZf/rXkfAMYZU9p0Chzj2jOXm7xpgHHQ==", + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.42.0.tgz", + "integrity": "sha512-j0tiofkzE3CSrYKmVRaKuwGgvCE+P2OOEDlhmfjeZf5ufcuFHwYwwgw3j08n4WYPVZ+OpsHblcFYezhKA3jDwg==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "4.40.0", + "@tanstack/query-core": "4.41.0", "use-sync-external-store": "^1.2.0" }, "funding": { @@ -7013,8 +6127,8 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-native": "*" }, "peerDependenciesMeta": { @@ -7087,17 +6201,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/ajv": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@types/ajv/-/ajv-0.0.5.tgz", @@ -7240,9 +6343,9 @@ } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-axis": { @@ -7481,35 +6584,22 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", "dev": true, "license": "MIT", "dependencies": { @@ -7559,9 +6649,9 @@ "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "dev": true, "license": "MIT", "dependencies": { @@ -7649,18 +6739,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.17.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz", - "integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/node-forge": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.13.tgz", - "integrity": "sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", "dev": true, "license": "MIT", "dependencies": { @@ -7783,13 +6873,12 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -7804,15 +6893,26 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", - "@types/send": "*" + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/@types/sinonjs__fake-timers": { @@ -7823,9 +6923,9 @@ "license": "MIT" }, "node_modules/@types/sizzle": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", - "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", "dev": true, "license": "MIT" }, @@ -7858,18 +6958,8 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true - }, - "node_modules/@types/webpack-dev-server": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-4.7.2.tgz", - "integrity": "sha512-Y3p0Fmfvp0MHBDoCzo+xFJaWTw0/z37mWIo6P15j+OtmUDLvznJWdZNeD7Q004R+MpQlys12oXbXsrXRmxwg4Q==", - "deprecated": "This is a stub types definition. webpack-dev-server provides its own type definitions, so you do not need this installed.", - "dev": true, - "license": "MIT", - "dependencies": { - "webpack-dev-server": "*" - } + "optional": true, + "peer": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -7882,9 +6972,9 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", "dev": true, "license": "MIT", "dependencies": { @@ -7910,21 +7000,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", - "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/type-utils": "8.39.1", - "@typescript-eslint/utils": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7934,33 +7023,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.1", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", - "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7970,20 +7049,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", - "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.1", - "@typescript-eslint/types": "^8.39.1", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7997,159 +7076,29 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", - "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", - "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", - "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/utils": "8.39.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", - "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", - "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.39.1", - "@typescript-eslint/tsconfig-utils": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", - "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -8158,19 +7107,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", - "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8178,348 +7129,210 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.24.2.tgz", - "integrity": "sha512-wW/gjLRvVUeYyhdh2TApn25cvdcR+Rhg6R/j3eTOvXQzU1HNzNYCVH4YKVIfgtfdM/Xs+N8fkk+rbr1YvBppCg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, "license": "MIT", "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@codemirror/autocomplete": ">=6.0.0", - "@codemirror/commands": ">=6.0.0", - "@codemirror/language": ">=6.0.0", - "@codemirror/lint": ">=6.0.0", - "@codemirror/search": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@uiw/react-codemirror": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.24.2.tgz", - "integrity": "sha512-kp7DhTq4RR+M2zJBQBrHn1dIkBrtOskcwJX4vVsKGByReOvfMrhqRkGTxYMRDTX6x75EG2mvBJPDKYcUQcHWBw==", - "license": "MIT", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@babel/runtime": "^7.18.6", - "@codemirror/commands": "^6.1.0", - "@codemirror/state": "^6.1.1", - "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.24.2", - "codemirror": "^6.0.0" + "brace-expansion": "^5.0.2" }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" + "engines": { + "node": "18 || 20 || >=22" }, - "peerDependencies": { - "@babel/runtime": ">=7.11.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/theme-one-dark": ">=6.0.0", - "@codemirror/view": ">=6.0.0", - "codemirror": ">=6.0.0", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": ">=14.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.3.tgz", + "integrity": "sha512-F1doRyD50CWScwGHG2bBUtUpwnOv/zqSnzkZqJcX5YAHQx6Z1CuX8jdnFMH6qktRrPU1tfpNYftTWu3QIoHiMA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.3.tgz", + "integrity": "sha512-1wtBZTXPIp8u6F/xjHvsUAYlEeF5Dic4xZBnqJyLzv7o7GjGYEUfSz9Z7bo9aK9GAx2uojG/AuBMfhA4uhvIVQ==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.3", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ] }, "node_modules/@webassemblyjs/ast": { @@ -8753,15 +7566,6 @@ "node": ">= 0.6" } }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -8833,9 +7637,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -8865,6 +7669,18 @@ } } }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -9262,29 +8078,16 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, - "node_modules/axios/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -9436,23 +8239,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-loader/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/babel-loader/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -9614,12 +8400,19 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", - "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "dev": true, "license": "Apache-2.0", - "optional": true + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -9642,6 +8435,18 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -9764,24 +8569,24 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -9798,6 +8603,27 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9811,6 +8637,16 @@ "node": ">=0.10.0" } }, + "node_modules/body-parser/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -9818,20 +8654,18 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "node_modules/body-parser/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "side-channel": "^1.0.6" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, "node_modules/bonjour-service": { @@ -9853,13 +8687,26 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -9970,9 +8817,9 @@ "peer": true }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -9989,10 +8836,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -10182,22 +9030,19 @@ } }, "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", "funding": [ { "type": "opencollective", @@ -10344,9 +9189,9 @@ } }, "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -10360,9 +9205,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", + "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", "dev": true, "license": "MIT" }, @@ -10527,9 +9372,9 @@ } }, "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, "license": "MIT" }, @@ -10542,12 +9387,6 @@ "color-name": "1.1.3" } }, - "node_modules/color-hash": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/color-hash/-/color-hash-2.0.2.tgz", - "integrity": "sha512-6exeENAqBTuIR1wIo36mR8xVVBv6l1hSLd7Qmvf6158Ld1L15/dbahR9VUOiX7GmGJBCnQyS0EY+I8x+wa7egg==", - "license": "MIT" - }, "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", @@ -10584,26 +9423,23 @@ } }, "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=16" } }, "node_modules/comment-json": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", - "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", + "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", "license": "MIT", "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", - "esprima": "^4.0.1", - "has-own-prop": "^2.0.0", - "repeat-string": "^1.6.1" + "esprima": "^4.0.1" }, "engines": { "node": ">= 6" @@ -10681,6 +9517,16 @@ "dev": true, "license": "MIT" }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -10800,6 +9646,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/copy-webpack-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/copy-webpack-plugin/node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -10814,14 +9670,14 @@ } }, "node_modules/core-js-compat": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz", - "integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", + "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "browserslist": "^4.25.1" + "browserslist": "^4.26.3" }, "funding": { "type": "opencollective", @@ -10966,9 +9822,9 @@ } }, "node_modules/css-loader/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -11038,9 +9894,9 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "14.5.3", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.3.tgz", - "integrity": "sha512-syLwKjDeMg77FRRx68bytLdlqHXDT4yBVh0/PPkcgesChYDjUZbwxLqMXuryYKzAyJsPsQHUDW1YU74/IYEUIA==", + "version": "14.5.4", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.4.tgz", + "integrity": "sha512-0Dhm4qc9VatOcI1GiFGVt8osgpPdqJLHzRwcAB5MSD/CAAts3oybvPUPawHyvJZUd8osADqZe/xzMsZ8sDTjXw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -11194,6 +10050,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cypress/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/cypress/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -11204,10 +10070,17 @@ "node": ">=8" } }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true, + "license": "MIT" + }, "node_modules/cypress/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -11755,16 +10628,16 @@ } }, "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "devOptional": true, "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -11788,763 +10661,1249 @@ "engines": { "node": ">=10" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delaunay-find": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/delaunay-find/-/delaunay-find-0.0.6.tgz", + "integrity": "sha512-1+almjfrnR7ZamBk0q3Nhg6lqSe6Le4vL0WJDSMx4IDbQwTpUTXPjxC00lqLBT8MYsJpPCbI16sIkw9cPsbi7Q==", + "license": "ISC", + "dependencies": { + "delaunator": "^4.0.0" + } + }, + "node_modules/delaunay-find/node_modules/delaunator": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", + "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==", + "license": "ISC" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, - "node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } + "engines": { + "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", - "license": "MIT" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "utila": "~0.4" } }, - "node_modules/default-browser": { + "node_modules/dom-helpers": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dev": true, + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "license": "MIT", "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" } }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, - "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "domelementtype": "^2.3.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", "dependencies": { - "robust-predicates": "^3.0.2" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, - "node_modules/delaunay-find": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/delaunay-find/-/delaunay-find-0.0.6.tgz", - "integrity": "sha512-1+almjfrnR7ZamBk0q3Nhg6lqSe6Le4vL0WJDSMx4IDbQwTpUTXPjxC00lqLBT8MYsJpPCbI16sIkw9cPsbi7Q==", - "license": "ISC", + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", "dependencies": { - "delaunator": "^4.0.0" + "tslib": "2.3.0", + "zrender": "5.6.1" } }, - "node_modules/delaunay-find/node_modules/delaunator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", - "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==", + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "license": "ISC" }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">= 4" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, - "node_modules/depd": { + "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "devOptional": true, "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "dependencies": { + "iconv-lite": "^0.6.2" } }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" }, "engines": { - "node": ">=0.10" + "node": ">=10.13.0" } }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "license": "BSD-3-Clause", - "peer": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=0.3.1" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/envinfo": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.20.0.tgz", + "integrity": "sha512-+zUomDcLXsVkQ37vUqWBvQwLaLlj8eZPSi61llaEFAVBY5mhcXdaSw1pSJVl4yTYD5g/gEfpNl28YYk4IPvrrg==", "dev": true, "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" + "bin": { + "envinfo": "dist/cli.js" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" }, "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "esutils": "^2.0.2" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.4" } }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { - "utila": "~0.4" + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "hasown": "^2.0.2" }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "engines": { + "node": ">= 0.4" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { - "node": ">= 4" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dompurify": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", - "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", - "license": "(MPL-2.0 OR Apache-2.0)", + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, "optionalDependencies": { - "@types/trusted-types": "^2.0.7" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "node_modules/esbuild-loader": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.4.2.tgz", + "integrity": "sha512-8LdoT9sC7fzfvhxhsIAiWhzLJr9yT3ggmckXxsgvM07wgrRxhuT98XhLn3E7VczU5W5AFsPKv9DdWcZIubbWkQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "esbuild": "^0.27.1", + "get-tsconfig": "^4.10.1", + "loader-utils": "^2.0.4", + "webpack-sources": "^1.4.3" }, "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "url": "https://github.com/privatenumber/esbuild-loader?sponsor=1" + }, + "peerDependencies": { + "webpack": "^4.40.0 || ^5.0.0" } }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "node_modules/esbuild-loader/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/esbuild-loader/node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "node_modules/esbuild-loader/node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/echarts": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", - "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "2.3.0", - "zrender": "5.6.1" + "node_modules/esbuild-loader/node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/echarts/node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "license": "0BSD" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", - "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==", - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "node_modules/esbuild-loader/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" + "node": ">=18" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/esbuild-loader/node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 4" + "node": ">=18" } }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/esbuild-loader/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.8" + "node": ">=18" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "devOptional": true, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "node_modules/esbuild-loader/node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "node_modules/esbuild-loader/node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "once": "^1.4.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "node_modules/esbuild-loader/node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.13.0" + "node": ">=18" } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "node_modules/esbuild-loader/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.6" + "node": ">=18" } }, - "node_modules/ensure-posix-path": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", - "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "node_modules/esbuild-loader/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "ISC" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "node_modules/esbuild-loader/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=18" } }, - "node_modules/envinfo": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.15.0.tgz", - "integrity": "sha512-chR+t7exF6y59kelhXw5I3849nTy7KIRO+ePdLMhCD+JRP/JvmkenDWP7QSFGlsHX+kxGxdDutOPrmj5j1HR6g==", + "node_modules/esbuild-loader/node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/eol": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", - "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "node_modules/esbuild-loader/node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "node_modules/esbuild-loader/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/esbuild-loader/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/esbuild-loader/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "node_modules/esbuild-loader/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/esbuild-loader/node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/esbuild-loader/node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "node_modules/esbuild-loader/node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "node_modules/esbuild-loader/node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "node_modules/esbuild-loader/node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -12555,49 +11914,53 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" - } - }, - "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", - "cpu": [ - "arm64" - ], + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/esbuild-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "BSD-3-Clause", "engines": { - "node": ">=18" + "node": ">=0.10.0" + } + }, + "node_modules/esbuild-loader/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" } }, "node_modules/escalade": { @@ -12771,6 +12134,17 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -12784,6 +12158,19 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -12803,25 +12190,20 @@ } }, "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { @@ -12838,9 +12220,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -12870,6 +12252,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -12907,23 +12300,6 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -12951,6 +12327,16 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -12974,20 +12360,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "yocto-queue": "^0.1.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "*" } }, "node_modules/eslint/node_modules/p-locate": { @@ -13137,6 +12520,16 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -13221,40 +12614,40 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -13273,8 +12666,18 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, "node_modules/express/node_modules/ms": { @@ -13291,20 +12694,18 @@ "dev": true, "license": "MIT" }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "node_modules/express/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "side-channel": "^1.0.6" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, "node_modules/extend": { @@ -13415,9 +12816,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { "type": "github", @@ -13583,6 +12984,16 @@ "dev": true, "license": "MIT" }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-file-up": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-2.0.1.tgz", @@ -13614,9 +13025,9 @@ "license": "MIT" }, "node_modules/find-test-names": { - "version": "1.29.17", - "resolved": "https://registry.npmjs.org/find-test-names/-/find-test-names-1.29.17.tgz", - "integrity": "sha512-JyZJ1EqH6+3/eXtViY7A79BTggAmcKu0rmXu89oJPobwbk8MmFhVz06sF1L2r6TLT7477iVtCmftBQ0pl2K/kg==", + "version": "1.29.19", + "resolved": "https://registry.npmjs.org/find-test-names/-/find-test-names-1.29.19.tgz", + "integrity": "sha512-fSO2GXgOU6dH+FdffmRXYN/kLdnd8zkBGIZrKsmAdfLSFUUDLpDFF7+F/h+wjmjDWQmMgD8hPfJZR+igiEUQHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13624,8 +13035,8 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "acorn-walk": "^8.2.0", "debug": "^4.3.3", - "globby": "^11.0.4", - "simple-bin-help": "^1.8.0" + "simple-bin-help": "^1.8.0", + "tinyglobby": "^0.2.13" }, "bin": { "find-test-names": "bin/find-test-names.js", @@ -13763,9 +13174,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -13979,6 +13390,16 @@ "integrity": "sha512-s+kNWQuI3mo9OALw0HJ6YGmMbLqEufCh2nX/zzV5CrICQ/y4AwPxM+6TIiF9ItFCHXFCyM/BfCCmN57NTIJuPg==", "license": "MIT" }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -14080,6 +13501,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getos": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", @@ -14235,9 +13669,9 @@ } }, "node_modules/glob-to-regex.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", - "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -14257,6 +13691,28 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", @@ -14375,10 +13831,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/goober": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", - "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", "license": "MIT", "peerDependencies": { "csstype": "^3.0.10" @@ -14480,15 +13946,6 @@ "node": ">=4" } }, - "node_modules/has-own-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", - "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -14748,9 +14205,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", - "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", + "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", "dev": true, "license": "MIT", "dependencies": { @@ -14883,6 +14340,15 @@ "node": ">= 0.8" } }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-parser-js": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", @@ -15064,9 +14530,10 @@ } }, "node_modules/i18next-parser": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-9.3.0.tgz", - "integrity": "sha512-VaQqk/6nLzTFx1MDiCZFtzZXKKyBV6Dv0cJMFM/hOt4/BWHWRgYafzYfVQRUzotwUwjqeNCprWnutzD/YAGczg==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-9.4.0.tgz", + "integrity": "sha512-SLQJGDj/baBIB9ALmJVXSOXWh3Zn9+wH7J2IuQ4rvx8yuQYpUWitmt8cHFjj6FExjgr8dHfd1SGeQgkowXDO1Q==", + "deprecated": "Project is deprecated, use i18next-cli instead", "dev": true, "license": "MIT", "dependencies": { @@ -15108,9 +14575,9 @@ } }, "node_modules/i18next-parser/node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", "dev": true, "license": "MIT", "dependencies": { @@ -15123,9 +14590,9 @@ } }, "node_modules/i18next-parser/node_modules/i18next": { - "version": "23.16.8", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", - "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "version": "24.2.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", + "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", "dev": true, "funding": [ { @@ -15143,7 +14610,15 @@ ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.2" + "@babel/runtime": "^7.26.10" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/iconv-lite": { @@ -15193,9 +14668,9 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -15203,9 +14678,9 @@ } }, "node_modules/immer": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", - "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -15571,14 +15046,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -16035,9 +15511,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -16243,22 +15719,6 @@ "node": ">=10.17.0" } }, - "node_modules/jest-changed-files/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-circus": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", @@ -16354,22 +15814,6 @@ "node": ">=8" } }, - "node_modules/jest-circus/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-circus/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -16560,16 +16004,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-config/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/jest-config/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -16608,9 +16042,9 @@ "license": "MIT" }, "node_modules/jest-config/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -16638,22 +16072,6 @@ "node": ">=8" } }, - "node_modules/jest-config/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest-config/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -16909,49 +16327,6 @@ "fsevents": "^2.3.3" } }, - "node_modules/jest-haste-map/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-haste-map/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-haste-map/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jest-leak-detector": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", @@ -17385,94 +16760,24 @@ "color-name": "~1.1.4" }, "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runner/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-runner/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=7.0.0" } }, - "node_modules/jest-runner/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/jest-runner/node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "engines": { + "node": ">=8" } }, "node_modules/jest-runner/node_modules/supports-color": { @@ -17538,16 +16843,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/jest-runtime/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -17586,9 +16881,9 @@ "license": "MIT" }, "node_modules/jest-runtime/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -17616,22 +16911,6 @@ "node": ">=8" } }, - "node_modules/jest-runtime/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest-runtime/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17742,9 +17021,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -17908,6 +17187,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-validate/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -18065,23 +17357,27 @@ } }, "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^8.1.1" }, "engines": { - "node": ">= 10.13.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18091,6 +17387,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -18102,6 +17399,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -18109,9 +17415,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -18192,9 +17498,9 @@ } }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -18473,9 +17779,9 @@ } }, "node_modules/koa": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/koa/-/koa-3.0.1.tgz", - "integrity": "sha512-oDxVkRwPOHhGlxKIDiDB2h+/l05QPtefD7nSqRgDfZt8P+QVYFWjfeK8jANf5O2YXjk8egd7KntvXKYx82wOag==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.0.3.tgz", + "integrity": "sha512-MeuwbCoN1daWS32/Ni5qkzmrOtQO2qrnfdxDHjrm6s4b59yG4nexAJ0pTEFyzjLp0pBVO80CZp0vW8Ze30Ebow==", "license": "MIT", "dependencies": { "accepts": "^1.3.8", @@ -18507,15 +17813,6 @@ "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", "license": "MIT" }, - "node_modules/koa/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/koa/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -18526,35 +17823,25 @@ } }, "node_modules/koa/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/koa/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" + "node": ">=18" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/launch-editor": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.0.tgz", - "integrity": "sha512-R/PIF14L6e2eHkhvQPu7jDRCr0msfCYCxbYiLgkkAGi0dVPWuM+RrsPu0a5dpuNe0KWGL3jpAkOlv53xGfPheQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -18663,12 +17950,16 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -18699,15 +17990,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": { @@ -19018,12 +18309,6 @@ "node": ">=10" } }, - "node_modules/lru-cache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -19050,9 +18335,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -19080,10 +18365,11 @@ } }, "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -19105,6 +18391,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/matcher-collection/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/matcher-collection/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -19160,19 +18470,18 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/memfs": { - "version": "4.47.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.47.0.tgz", - "integrity": "sha512-Xey8IZA57tfotV/TN4d6BmccQuhFP+CqRiI7TTNdipZdZBzF2WnzUcH//Cudw6X4zJiUbo/LTuU/HPA/iC/pNg==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.50.0.tgz", + "integrity": "sha512-N0LUYQMUA1yS5tJKmMtU9yprPm6ZIg24yr/OVv/7t6q0kKDIho4cBbXRi1XKttUmNYDYgF/q45qrKE/UhGO0CA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -19290,15 +18599,19 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", + "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -19335,19 +18648,19 @@ } }, "node_modules/mktemp": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", - "integrity": "sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-2.0.2.tgz", + "integrity": "sha512-Q9wJ/xhzeD9Wua1MwDN2v3ah3HENsUVSlzzL9Qw149cL9hHZkXtQGl3Eq36BbdLV+/qUwaP1WtJQ+H/+Oxso8g==", "dev": true, "license": "MIT", "engines": { - "node": ">0.9" + "node": "20 || 22 || 24" } }, "node_modules/mobx": { - "version": "6.13.7", - "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", - "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz", + "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", "license": "MIT", "funding": { "type": "opencollective", @@ -19402,9 +18715,9 @@ } }, "node_modules/mocha": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", - "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "license": "MIT", "peer": true, @@ -19417,6 +18730,7 @@ "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", @@ -19495,17 +18809,6 @@ "node": ">=4" } }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/mocha/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -19525,9 +18828,9 @@ } }, "node_modules/mocha/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "peer": true, @@ -19574,40 +18877,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mocha/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -19643,9 +18912,9 @@ } }, "node_modules/mochawesome": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/mochawesome/-/mochawesome-7.1.3.tgz", - "integrity": "sha512-Vkb3jR5GZ1cXohMQQ73H3cZz7RoxGjjUo0G5hu0jLaW+0FdUxUwg3Cj29bqQdh0rFcnyV06pWmqmi5eBPnEuNQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/mochawesome/-/mochawesome-7.1.4.tgz", + "integrity": "sha512-fucGSh8643QkSvNRFOaJ3+kfjF0FhA/YtvDncnRAG0A4oCtAzHIFkt/+SgsWil1uwoeT+Nu5fsAnrKkFtnPcZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19656,7 +18925,7 @@ "lodash.isfunction": "^3.0.9", "lodash.isobject": "^3.0.2", "lodash.isstring": "^4.0.1", - "mochawesome-report-generator": "^6.2.0", + "mochawesome-report-generator": "^6.3.0", "strip-ansi": "^6.0.1", "uuid": "^8.3.2" }, @@ -19698,16 +18967,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/mochawesome-merge/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/mochawesome-merge/node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -19845,9 +19104,9 @@ } }, "node_modules/mochawesome-report-generator": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/mochawesome-report-generator/-/mochawesome-report-generator-6.2.0.tgz", - "integrity": "sha512-Ghw8JhQFizF0Vjbtp9B0i//+BOkV5OWcQCPpbO0NGOoxV33o+gKDYU0Pr2pGxkIHnqZ+g5mYiXF7GMNgAcDpSg==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/mochawesome-report-generator/-/mochawesome-report-generator-6.3.2.tgz", + "integrity": "sha512-iB6iyOUMyMr8XOUYTNfrqYuZQLZka3K/Gr2GPc6CHPe7t2ZhxxfcoVkpMLOtyDKnWbY1zgu1/7VNRsigrvKnOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19861,7 +19120,6 @@ "prop-types": "^15.7.2", "tcomb": "^3.2.17", "tcomb-validation": "^3.3.0", - "validator": "^13.6.0", "yargs": "^17.2.1" }, "bin": { @@ -20013,9 +19271,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": { @@ -20045,6 +19303,17 @@ "node": ">=8" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -20097,9 +19366,9 @@ } }, "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, "license": "MIT", "bin": { @@ -20120,10 +19389,9 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -20175,9 +19443,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -20192,9 +19460,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/node-schedule": { @@ -20567,6 +19835,34 @@ } }, "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", @@ -20581,18 +19877,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -21241,10 +20525,9 @@ } }, "node_modules/proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, "node_modules/pump": { @@ -21285,9 +20568,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.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -21321,29 +20604,53 @@ "license": "MIT" }, "node_modules/quick-temp": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", - "integrity": "sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.9.tgz", + "integrity": "sha512-yI0h7tIhKVObn03kD+Ln9JFi4OljD28lfaOsTdfpTR0xzrhGOod+q66CjGafUqYX2juUfT9oHIGrTBBo22mkRA==", "dev": true, "license": "MIT", "dependencies": { - "mktemp": "~0.4.0", - "rimraf": "^2.5.4", - "underscore.string": "~3.3.4" + "mktemp": "^2.0.1", + "rimraf": "^5.0.10", + "underscore.string": "~3.3.6" + } + }, + "node_modules/quick-temp/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/quick-temp/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "dev": true, "license": "ISC", "dependencies": { - "glob": "^7.1.3" + "glob": "^10.3.7" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/raf-schd": { @@ -21378,19 +20685,40 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body/node_modules/iconv-lite": { @@ -21521,9 +20849,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.62.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", - "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "version": "7.66.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", + "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -21574,9 +20902,9 @@ } }, "node_modules/react-is": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", - "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", "license": "MIT" }, "node_modules/react-jss": { @@ -21732,14 +21060,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" @@ -21760,12 +21088,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" @@ -21797,9 +21125,9 @@ } }, "node_modules/react-virtuoso": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.13.0.tgz", - "integrity": "sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.14.1.tgz", + "integrity": "sha512-NRUF1ak8lY+Tvc6WN9cce59gU+lilzVtOozP+pm9J7iHshLGGjsiAB4rB2qlBPHjFbcXOQpT+7womNHGDUql8w==", "license": "MIT", "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", @@ -21938,9 +21266,9 @@ "peer": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "dev": true, "license": "MIT", "peer": true, @@ -21973,19 +21301,19 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -22000,33 +21328,19 @@ "peer": true }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "dev": true, "license": "BSD-2-Clause", "peer": true, "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -22151,15 +21465,6 @@ "entities": "^2.0.0" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/replace-ext": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", @@ -22226,21 +21531,18 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -22309,6 +21611,16 @@ "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", "license": "MIT" }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -22373,12 +21685,6 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, - "node_modules/rslog": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rslog/-/rslog-1.2.11.tgz", - "integrity": "sha512-YgMMzQf6lL9q4rD9WS/lpPWxVNJ1ttY9+dOXJ0+7vJrKCAOT4GH0EiRnBi9mKOitcHiOwjqJPV1n/HRqqgZmOQ==", - "license": "MIT" - }, "node_modules/rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -22538,9 +21844,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.89.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", - "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", + "version": "1.94.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.0.tgz", + "integrity": "sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ==", "dev": true, "license": "MIT", "dependencies": { @@ -22597,9 +21903,9 @@ } }, "node_modules/sass-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -22650,9 +21956,9 @@ } }, "node_modules/sass-loader/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -22663,9 +21969,9 @@ } }, "node_modules/sass/node_modules/immutable": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", - "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "dev": true, "license": "MIT" }, @@ -22680,9 +21986,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -22698,18 +22004,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -22798,6 +22092,16 @@ "node": ">= 0.8" } }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -23216,6 +22520,13 @@ "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==", "license": "MIT" }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -23236,9 +22547,10 @@ } }, "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -23249,6 +22561,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -23281,9 +22594,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "license": "CC0-1.0" }, "node_modules/spdy": { @@ -23389,10 +22702,16 @@ "node": ">=8" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -23469,17 +22788,15 @@ } }, "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "dev": true, "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -23720,9 +23037,9 @@ } }, "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "license": "MIT" }, "node_modules/stylis": { @@ -23755,6 +23072,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swc-loader": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", + "integrity": "sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, "node_modules/symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -23788,18 +23119,22 @@ } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tcomb": { @@ -23830,13 +23165,13 @@ } }, "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -23848,9 +23183,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", @@ -23881,12 +23216,69 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -23902,6 +23294,30 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-decoder": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", @@ -23912,6 +23328,21 @@ "b4a": "^1.6.4" } }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -24001,16 +23432,64 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", - "license": "MIT" + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/tlds": { - "version": "1.259.0", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.259.0.tgz", - "integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==", + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", "license": "MIT", "bin": { "tlds": "bin.js" @@ -24141,9 +23620,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -24154,9 +23633,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz", - "integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==", + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "dev": true, "license": "MIT", "dependencies": { @@ -24166,7 +23645,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.2", + "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -24207,9 +23686,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -24232,126 +23711,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ts-loader/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ts-loader/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ts-loader/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -24397,9 +23756,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": { @@ -24478,19 +23837,44 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -24587,9 +23971,9 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -24653,9 +24037,9 @@ } }, "node_modules/undici": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.13.0.tgz", - "integrity": "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==", + "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": { @@ -24695,9 +24079,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "dev": true, "license": "MIT", "peer": true, @@ -24706,9 +24090,9 @@ } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "dev": true, "license": "MIT", "peer": true, @@ -24791,9 +24175,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -24876,9 +24260,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -24950,16 +24334,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -25638,6 +25012,30 @@ "node": "8.* || >= 10.*" } }, + "node_modules/walk-sync/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/walk-sync/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -25658,9 +25056,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -25742,9 +25140,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.101.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.0.tgz", - "integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==", + "version": "5.105.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", + "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -25755,22 +25153,22 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.2", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { @@ -25953,19 +25351,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, "node_modules/webpack-dev-server/node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -26041,6 +25426,28 @@ "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -26243,9 +25650,9 @@ "license": "MIT" }, "node_modules/workerpool": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", - "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true, "license": "Apache-2.0", "peer": true @@ -26393,9 +25800,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -26504,16 +25911,15 @@ } }, "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -26568,6 +25974,20 @@ "node": ">=10" } }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yargs-unparser/node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", diff --git a/web/package.json b/web/package.json index ee4e316da..2deb9c0a7 100644 --- a/web/package.json +++ b/web/package.json @@ -28,16 +28,22 @@ "test": "npm run cypress:run:ci", "test-cypress-console": "./node_modules/.bin/cypress open --browser chrome", "test-cypress-console-headless": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless", - "test-cypress-monitoring-regression": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/monitoring/regression/*cy.ts", - "test-cypress-monitoring-bvt": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/monitoring/*bvt*cy.ts", - "test-cypress-monitoring": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/monitoring/**", - "test-cypress-monitoring-alerts": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/monitoring/regression/*.reg_alerts*.cy.ts", - "test-cypress-monitoring-metrics": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/monitoring/regression/*.reg_metrics*.cy.ts", - "test-cypress-monitoring-legacy-dashboards": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/monitoring/regression/*.reg_legacy_dashboards*.cy.ts", - "test-cypress-coo-bvt": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/coo/01.coo_bvt.cy.ts", - "test-cypress-coo-ivt": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/coo/01.coo_ivt.cy.ts", - "test-cypress-coo": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/coo/*cy.ts", - "test-cypress-incidents": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --spec cypress/e2e/incidents/*cy.ts", + "test-cypress-monitoring": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@monitoring --@flaky --@demo'", + "test-cypress-monitoring-dev": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@monitoring-dev --@demo'", + "test-cypress-monitoring-bvt": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@monitoring+@smoke --@demo'", + "test-cypress-monitoring-regression": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@monitoring --@smoke --@flaky --@demo'", + "test-cypress-alerts": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@alerts --@flaky --@demo'", + "test-cypress-metrics": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@metrics --@flaky --@demo'", + "test-cypress-dashboards": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@dashboards --@flaky --@demo'", + "test-cypress-coo": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@coo --@flaky --@demo'", + "test-cypress-coo-bvt": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@coo+@smoke --@demo'", + "test-cypress-virtualization": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@virtualization --@flaky --@demo'", + "test-cypress-incidents": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@incidents --@flaky --@demo'", + "test-cypress-incidents-e2e": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@incidents+@e2e-real --@flaky --@demo'", + "test-cypress-smoke": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@smoke --@flaky --@demo'", + "test-cypress-fast": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@smoke --@slow --@demo --@flaky'", + "test-cypress-perses-dev": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@perses-dev --@demo'", + "test-cypress-perses": "node --max-old-space-size=4096 ./node_modules/.bin/cypress run --browser chrome --headless --env grepTags='@perses --@smoke --@flaky --@demo'", "ts-node": "ts-node -O '{\"module\":\"commonjs\"}'" }, "dependencies": { @@ -59,42 +65,22 @@ "@openshift-console/dynamic-plugin-sdk-internal": "^4.19.0-prerelease.2", "@openshift-console/dynamic-plugin-sdk-webpack": "^4.19.0", "@patternfly/react-charts": "^8.2.0", - "@patternfly/react-core": "^6.2.0", + "@patternfly/react-code-editor": "^6.4.1", + "@patternfly/react-core": "^6.4.1", "@patternfly/react-data-view": "^6.1.0", "@patternfly/react-icons": "^6.2.0", "@patternfly/react-table": "^6.2.0", "@patternfly/react-templates": "^6.2.0", - "@perses-dev/bar-chart-plugin": "^0.9.0", - "@perses-dev/components": "^0.52.0", - "@perses-dev/core": "^0.52.0", - "@perses-dev/dashboards": "^0.52.0", - "@perses-dev/datasource-variable-plugin": "^0.3.2", - "@perses-dev/explore": "^0.52.0", - "@perses-dev/flame-chart-plugin": "^0.3.0", - "@perses-dev/gauge-chart-plugin": "^0.9.0", - "@perses-dev/heatmap-chart-plugin": "^0.2.1", - "@perses-dev/histogram-chart-plugin": "^0.9.0", - "@perses-dev/loki-plugin": "^0.1.1", - "@perses-dev/markdown-plugin": "^0.9.0", - "@perses-dev/pie-chart-plugin": "^0.9.0", - "@perses-dev/plugin-system": "^0.52.0", - "@perses-dev/prometheus-plugin": "^0.53.3", - "@perses-dev/pyroscope-plugin": "^0.3.1", - "@perses-dev/scatter-chart-plugin": "^0.8.0", - "@perses-dev/stat-chart-plugin": "^0.9.0", - "@perses-dev/static-list-variable-plugin": "^0.5.1", - "@perses-dev/status-history-chart-plugin": "^0.9.0", - "@perses-dev/table-plugin": "^0.8.0", - "@perses-dev/tempo-plugin": "^0.53.1", - "@perses-dev/timeseries-chart-plugin": "^0.10.1", - "@perses-dev/timeseries-table-plugin": "^0.9.0", - "@perses-dev/trace-table-plugin": "^0.8.1", - "@perses-dev/tracing-gantt-chart-plugin": "^0.9.2", + "@perses-dev/components": "0.53.0-rc.2", + "@perses-dev/core": "0.53.0-rc.1", + "@perses-dev/dashboards": "0.53.0-rc.2", + "@perses-dev/explore": "0.53.0-rc.2", + "@perses-dev/plugin-system": "0.53.0-rc.2", "@prometheus-io/codemirror-promql": "^0.37.0", "@tanstack/react-query": "^4.36.1", "@types/ajv": "^0.0.5", "@types/js-yaml": "^4.0.9", - "ajv": "^8.17.1", + "ajv": "^8.18.0", "classnames": "2.x", "fuzzysearch": "1.0.x", "i18next": "^21.8.14", @@ -107,6 +93,7 @@ "murmurhash-js": "1.0.x", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-hook-form": "^7.66.0", "react-i18next": "^11.8.11", "react-linkify": "^0.2.2", "react-modal": "^3.12.1", @@ -120,13 +107,14 @@ "@cypress/grep": "^4.1.0", "@cypress/webpack-preprocessor": "^6.0.2", "@date-fns/tz": "^1.4.1", + "@swc/core": "^1.15.3", + "@swc/helpers": "^0.5.17", "@types/classnames": "^2.2.7", "@types/jest": "^30.0.0", "@types/lodash-es": "^4.17.12", "@types/node": "^22.17.2", "@types/react": "17.0.83", "@types/react-router-dom": "^5.3.2", - "@types/webpack-dev-server": "^4.7.2", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", "copy-webpack-plugin": "^11.0.0", @@ -135,6 +123,7 @@ "cypress": "^14.2.1", "cypress-multi-reporters": "^1.4.0", "cypress-wait-until": "^3.0.2", + "esbuild-loader": "^4.4.2", "eslint": "^8.44.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", @@ -155,8 +144,8 @@ "sass": "^1.42.1", "sass-loader": "^10.1.1", "style-loader": "^3.3.1", + "swc-loader": "^0.2.6", "ts-jest": "^29.4.4", - "ts-loader": "^9.2.8", "ts-node": "^10.7.0", "typescript": "^5.9.2", "webpack": "^5.94.0", @@ -167,7 +156,8 @@ "react-router-dom": "<7" }, "overrides": { - "echarts": "^5.6.0" + "echarts": "^5.6.0", + "qs": "^6.14.1" }, "consolePlugin": { "name": "monitoring-plugin", @@ -175,7 +165,8 @@ "displayName": "OpenShift console monitoring plugin", "description": "This plugin adds the monitoring UI to the OpenShift web console", "exposedModules": { - "DashboardsPage": "./components/dashboards/perses/dashboard-page", + "DashboardListPage": "./components/dashboards/perses/dashboard-list-page", + "DashboardPage": "./components/dashboards/perses/dashboard-page", "LegacyDashboardsPage": "./components/dashboards/legacy/legacy-dashboard-page", "SilencesPage": "./components/alerting/SilencesPage", "SilencesDetailsPage": "./components/alerting/SilencesDetailsPage", @@ -190,11 +181,12 @@ "MonitoringReducer": "./store/reducers", "IncidentsPage": "./components/Incidents/IncidentsPage", "TargetsPage": "./components/targets-page", - "PrometheusRedirectPage": "./components/prometheus-redirect-page", + "PrometheusRedirectPage": "./components/redirects/prometheus-redirect-page", + "DevRedirects": "./components/redirects/dev-redirects", "MonitoringContext": "./contexts/MonitoringContext" }, "dependencies": { "@console/pluginAPI": "*" } } -} \ No newline at end of file +} diff --git a/web/src/components/Incidents/AlertsChart/AlertsChart.tsx b/web/src/components/Incidents/AlertsChart/AlertsChart.tsx index 5413a21ca..1705b2e14 100644 --- a/web/src/components/Incidents/AlertsChart/AlertsChart.tsx +++ b/web/src/components/Incidents/AlertsChart/AlertsChart.tsx @@ -32,6 +32,7 @@ import { generateDateArray, generateAlertsDateArray, getCurrentTime, + roundDateToInterval, } from '../utils'; import { dateTimeFormatter, timeFormatter } from '../../console/utils/datetime'; import { useTranslation } from 'react-i18next'; @@ -76,7 +77,7 @@ const AlertsChart = ({ theme }: { theme: 'light' | 'dark' }) => { }, [alertsData]); useEffect(() => { - setChartContainerHeight(chartData?.length < 5 ? 300 : chartData?.length * 60); + setChartContainerHeight(chartData?.length < 5 ? 300 : chartData?.length * 55); setChartHeight(chartData?.length < 5 ? 250 : chartData?.length * 55); }, [chartData]); @@ -164,7 +165,9 @@ const AlertsChart = ({ theme }: { theme: 'light' | 'dark' }) => { const endDate = datum.alertstate === 'firing' ? '---' - : dateTimeFormatter(i18n.language).format(new Date(datum.y)); + : dateTimeFormatter(i18n.language).format( + roundDateToInterval(new Date(datum.y)), + ); const alertName = datum.silenced ? `${datum.name} (silenced)` : datum.name; diff --git a/web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx b/web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx index b5d27ad1e..51c7a52a8 100644 --- a/web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx +++ b/web/src/components/Incidents/IncidentsChart/IncidentsChart.tsx @@ -33,6 +33,7 @@ import { calculateIncidentsChartDomain, createIncidentsChartBars, generateDateArray, + roundDateToInterval, } from '../utils'; import { dateTimeFormatter, timeFormatter } from '../../console/utils/datetime'; import { useTranslation } from 'react-i18next'; @@ -178,7 +179,9 @@ const IncidentsChart = ({ const startDate = dateTimeFormatter(i18n.language).format(new Date(datum.y0)); const endDate = datum.firing ? '---' - : dateTimeFormatter(i18n.language).format(new Date(datum.y)); + : dateTimeFormatter(i18n.language).format( + roundDateToInterval(new Date(datum.y)), + ); const components = formatComponentList(datum.componentList); return `${t('Severity')}: ${t(datum.name)} diff --git a/web/src/components/Incidents/IncidentsDetailsRowTable.tsx b/web/src/components/Incidents/IncidentsDetailsRowTable.tsx index 793e8c199..e29e8a095 100644 --- a/web/src/components/Incidents/IncidentsDetailsRowTable.tsx +++ b/web/src/components/Incidents/IncidentsDetailsRowTable.tsx @@ -17,7 +17,7 @@ interface IncidentsDetailsRowTableProps { } const IncidentsDetailsRowTable = ({ alerts }: IncidentsDetailsRowTableProps) => { - const [namespace, setNamespace] = useActiveNamespace(); + const [, setNamespace] = useActiveNamespace(); const { perspective } = usePerspective(); const { t } = useTranslation(process.env.I18N_NAMESPACE); @@ -34,7 +34,7 @@ const IncidentsDetailsRowTable = ({ alerts }: IncidentsDetailsRowTableProps) => setNamespace(ALL_NAMESPACES_KEY)} > {alertDetails.alertname} @@ -63,7 +63,7 @@ const IncidentsDetailsRowTable = ({ alerts }: IncidentsDetailsRowTableProps) => } return null; - }, [alerts, perspective, namespace, setNamespace]); + }, [alerts, perspective, setNamespace]); return ( diff --git a/web/src/components/Incidents/IncidentsPage.tsx b/web/src/components/Incidents/IncidentsPage.tsx index f098068df..8cb739c48 100644 --- a/web/src/components/Incidents/IncidentsPage.tsx +++ b/web/src/components/Incidents/IncidentsPage.tsx @@ -20,7 +20,10 @@ import { ToolbarGroup, Flex, FlexItem, + Alert, + AlertActionCloseButton, } from '@patternfly/react-core'; +import { AccessDenied } from '../console/console-shared/src/components/empty-state/AccessDenied'; import { IncidentsTable } from './IncidentsTable'; import { getIncidentsTimeRanges, @@ -65,7 +68,6 @@ import IncidentFilterToolbarItem, { useStateOptions, } from './ToolbarItemFilter'; import { MonitoringProvider } from '../../contexts/MonitoringContext'; -import { ALL_NAMESPACES_KEY } from '../utils'; import { isEmpty } from 'lodash-es'; import { DataTestIDs } from '../data-test'; import { DocumentTitle } from '@openshift-console/dynamic-plugin-sdk'; @@ -75,10 +77,11 @@ const IncidentsPage = () => { const dispatch = useDispatch(); const location = useLocation(); const urlParams = useMemo(() => parseUrlParams(location.search), [location.search]); - const { rules } = useAlerts({ overrideNamespace: ALL_NAMESPACES_KEY }); + const { rules } = useAlerts({ dontUseTenancy: true }); const { theme } = usePatternFlyTheme(); // loading states const [incidentsAreLoading, setIncidentsAreLoading] = useState(true); + const [loadError, setLoadError] = useState(null); // days span is where we store the value for creating time ranges for // fetch incidents/alerts based on the length of time ranges // when days filter changes we set a new days span -> calculate new time range and fetch new data @@ -90,6 +93,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, @@ -132,9 +140,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, ); @@ -240,15 +245,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 { @@ -258,24 +271,18 @@ const IncidentsPage = () => { .catch((err) => { // eslint-disable-next-line no-console console.log(err); + + dispatch(setAlertsAreLoading({ alertsAreLoading: false })); + setLoadError(err); }); })(); }, [incidentForAlertProcessing]); - useEffect(() => { - if (rules && alertsData) { - dispatch( - setAlertsTableData({ - alertsTableData: groupAlertsForTable(alertsData, rules), - }), - ); - } - }, [alertsData, rules]); - useEffect(() => { if (!isInitialized) return; setIncidentsAreLoading(true); + setLoadError(null); // Set refresh time before making queries const currentTime = getCurrentTime(); @@ -327,6 +334,10 @@ const IncidentsPage = () => { .catch((err) => { // eslint-disable-next-line no-console console.log(err); + + setIncidentsAreLoading(false); + dispatch(setAlertsAreLoading({ alertsAreLoading: false })); + setLoadError(err); }); }, [isInitialized, incidentsActiveFilters.days, selectedGroupId]); @@ -356,6 +367,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(); @@ -386,7 +409,9 @@ const IncidentsPage = () => { return ( <> {title} - {alertsAreLoading && incidentsAreLoading ? ( + {loadError ? ( + + ) : alertsAreLoading && incidentsAreLoading ? ( { /> ) : ( - - { - closeDropDownFilters(); - dispatch( - setIncidentsActiveFilters({ - incidentsActiveFilters: { - ...incidentsActiveFilters, - severity: [], - state: [], - groupId: [], - }, - }), - ); - dispatch(setAlertsAreLoading({ alertsAreLoading: true })); - }} - > - - - - + setFiltersExpanded((prev) => ({ ...prev, filterType: isOpen })) + } + onSelect={(event, selection) => { + dispatch(setIncidentPageFilterType({ incidentPageFilterType: selection })); + setFilterTypeExpanded((prev) => ({ ...prev, filterType: false })); + }} + shouldFocusToggleOnSelect + toggle={(toggleRef) => ( + onFilterToggle(ev, 'filterType', setFilterTypeExpanded)} + isExpanded={filterTypeExpanded.filterType} + icon={} + data-test={DataTestIDs.IncidentsPage.FiltersSelectToggle} + > + {t(incidentPageFilterTypeSelected)} + + )} + style={{ width: '145px' }} + > + + + {t('Severity')} + + + {t('State')} + + + {t('Incident ID')} + + + + + + + setFiltersExpanded((prev) => ({ ...prev, severity: isExpanded })) + } + onIncidentFilterToggle={(ev) => + onFilterToggle(ev, 'severity', setFiltersExpanded) + } + dispatch={dispatch} + showToolbarItem={incidentPageFilterTypeSelected?.includes('Severity')} + /> + + + + setFiltersExpanded((prev) => ({ ...prev, state: isExpanded })) + } + onIncidentFilterToggle={(ev) => + onFilterToggle(ev, 'state', setFiltersExpanded) + } + dispatch={dispatch} + showToolbarItem={incidentPageFilterTypeSelected?.includes('State')} + /> + + + + setFiltersExpanded((prev) => ({ ...prev, groupId: isExpanded })) + } + onIncidentFilterToggle={(ev) => + onFilterToggle(ev, 'groupId', setFiltersExpanded) + } + dispatch={dispatch} + showToolbarItem={incidentPageFilterTypeSelected?.includes('Incident ID')} + /> + + + + - - - setFiltersExpanded((prev) => ({ ...prev, severity: isExpanded })) - } - onIncidentFilterToggle={(ev) => - onFilterToggle(ev, 'severity', setFiltersExpanded) - } - dispatch={dispatch} - showToolbarItem={incidentPageFilterTypeSelected?.includes('Severity')} - /> - - - - setFiltersExpanded((prev) => ({ ...prev, state: isExpanded })) - } - onIncidentFilterToggle={(ev) => onFilterToggle(ev, 'state', setFiltersExpanded)} - dispatch={dispatch} - showToolbarItem={incidentPageFilterTypeSelected?.includes('State')} - /> - - - - setFiltersExpanded((prev) => ({ ...prev, groupId: isExpanded })) - } - onIncidentFilterToggle={(ev) => - onFilterToggle(ev, 'groupId', setFiltersExpanded) - } - dispatch={dispatch} - showToolbarItem={incidentPageFilterTypeSelected?.includes('Incident ID')} - /> - - - - - - - - - - - - - - - - {!hideCharts && ( - <> - - - - - - - - )} - - - - - + {hideCharts ? t('Show graph') : t('Hide graph')} + + + + + {!hideCharts && ( + <> + + + + + + + + )} + + + + + + ) )} ); diff --git a/web/src/components/Incidents/api.spec.ts b/web/src/components/Incidents/api.spec.ts new file mode 100644 index 000000000..e9b588ed5 --- /dev/null +++ b/web/src/components/Incidents/api.spec.ts @@ -0,0 +1,150 @@ +// Setup global.window before importing modules that use it +(global as any).window = { + SERVER_FLAGS: { + prometheusBaseURL: '/api/prometheus', + prometheusTenancyBaseURL: '/api/prometheus-tenancy', + alertManagerBaseURL: '/api/alertmanager', + }, +}; + +import { createAlertsQuery, fetchDataForIncidentsAndAlerts } from './api'; +import { PrometheusResponse, consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; +import { buildPrometheusUrl } from '../utils'; + +// Mock the SDK +jest.mock('@openshift-console/dynamic-plugin-sdk', () => ({ + PrometheusEndpoint: { + QUERY_RANGE: 'api/v1/query_range', + }, + consoleFetchJSON: jest.fn(), +})); + +// Mock the global utils to avoid window access side effects +jest.mock('../utils', () => ({ + getPrometheusBasePath: jest.fn(), + buildPrometheusUrl: jest.fn(), +})); + +describe('createAlertsQuery', () => { + it('should create a valid alerts query', () => { + const alertsQuery = createAlertsQuery([ + { + src_alertname: 'test', + src_severity: 'critical', + src_namespace: 'test', + src_silenced: 'false', + }, + { + src_alertname: 'test2', + src_severity: 'warning', + src_namespace: 'test2', + src_silenced: 'false', + }, + { + src_alertname: 'test2', + src_severity: 'warning', + src_namespace: 'test2', + src_silenced: 'true', + }, + ]); + expect(alertsQuery).toEqual([ + 'ALERTS{alertname="test", severity="critical", namespace="test"} or ALERTS{alertname="test2", severity="warning", namespace="test2"}', + ]); + }); + it('should create valid alerts queries array', () => { + const alertsQuery = createAlertsQuery( + [ + { + src_alertname: 'test', + src_severity: 'critical', + src_namespace: 'test', + src_silenced: 'false', + }, + { + src_alertname: 'test2', + src_severity: 'warning', + src_namespace: 'test2', + src_silenced: 'false', + }, + { + src_alertname: 'test2', + src_severity: 'warning', + src_namespace: 'test2', + src_silenced: 'true', + }, + ], + 100, + ); + expect(alertsQuery).toEqual([ + 'ALERTS{alertname="test", severity="critical", namespace="test"}', + 'ALERTS{alertname="test2", severity="warning", namespace="test2"}', + ]); + }); +}); + +describe('fetchDataForIncidentsAndAlerts', () => { + it('should fetch data for incidents and alerts', async () => { + (buildPrometheusUrl as jest.Mock).mockReturnValue('/mock/url'); + const now = Date.now(); + + const result1 = { + metric: { + alertname: 'test', + severity: 'critical', + namespace: 'test', + }, + values: [ + [now - 1000, '1'], + [now - 500, '2'], + ] as [number, string][], + }; + + const result2 = { + metric: { + alertname: 'test2', + severity: 'warning', + namespace: 'test2', + }, + values: [ + [now - 2000, '3'], + [now - 1500, '4'], + ] as [number, string][], + }; + + const mockPrometheusResponse1: PrometheusResponse = { + status: 'success', + data: { + resultType: 'matrix', + result: [result1], + }, + }; + + const mockPrometheusResponse2: PrometheusResponse = { + status: 'success', + data: { + resultType: 'matrix', + result: [result2], + }, + }; + + const mockConsoleFetchJSON = consoleFetchJSON as jest.MockedFunction; + mockConsoleFetchJSON + .mockResolvedValueOnce(mockPrometheusResponse1) + .mockResolvedValueOnce(mockPrometheusResponse2); + + const range = { endTime: now, duration: 86400000 }; + const customQuery = [ + 'ALERTS{alertname="test", severity="critical", namespace="test"}', + 'ALERTS{alertname="test2", severity="warning", namespace="test2"}', + ]; + const result = await fetchDataForIncidentsAndAlerts(mockConsoleFetchJSON, range, customQuery); + expect(result).toEqual({ + status: 'success', + data: { + resultType: 'matrix', + result: [result1, result2], + }, + }); + expect(mockConsoleFetchJSON).toHaveBeenCalledTimes(2); + }); +}); diff --git a/web/src/components/Incidents/api.ts b/web/src/components/Incidents/api.ts index 4fcc141b8..0b8c71841 100644 --- a/web/src/components/Incidents/api.ts +++ b/web/src/components/Incidents/api.ts @@ -1,22 +1,49 @@ /* eslint-disable max-len */ -import { PrometheusEndpoint, PrometheusResponse } from '@openshift-console/dynamic-plugin-sdk'; +import { + consoleFetchJSON, + PrometheusEndpoint, + PrometheusResponse, +} from '@openshift-console/dynamic-plugin-sdk'; import { getPrometheusBasePath, buildPrometheusUrl } from '../utils'; import { PROMETHEUS_QUERY_INTERVAL_SECONDS } from './utils'; + +const MAX_URL_LENGTH = 2048; + +/** + * Creates a single Prometheus alert query string from a grouped alert value. + * @param {Object} query - Single grouped alert object with src_ prefixed properties and layer/component. + * @returns {string} - A string representing a single Prometheus alert query. + */ +const createSingleAlertQuery = (query) => { + // Dynamically get all keys starting with "src_" + const srcKeys = Object.keys(query).filter( + (key) => key.startsWith('src_') && key != 'src_silenced', + ); + + // Create the alertParts array using the dynamically discovered src_ keys, + // but remove the "src_" prefix from the keys in the final query string. + const alertParts = srcKeys + .filter((key) => query[key]) // Only include keys that are present in the query object + .map((key) => `${key.replace('src_', '')}="${query[key]}"`) // Remove "src_" prefix from keys + .join(', '); + + // Construct the query string for each grouped alert + return `ALERTS{${alertParts}}`; +}; + /** * Creates a Prometheus alerts query string from grouped alert values. * The function dynamically includes any properties in the input objects that have the "src_" prefix, * but the prefix is removed from the keys in the final query string. * * @param {Object[]} groupedAlertsValues - Array of grouped alert objects. - * Each alert object should contain various properties, including "src_" prefixed properties, - * as well as "layer" and "component" for constructing the meta fields in the query. + * Each alert object should contain various properties, including "src_" prefixed properties * * @param {string} groupedAlertsValues[].layer - The layer of the alert, used in the absent condition. * @param {string} groupedAlertsValues[].component - The component of the alert, used in the absent condition. - * @returns {string} - A string representing the combined Prometheus alerts query. - * Each alert query is formatted as `(ALERTS{key="value", ...} + on () group_left (component, layer) (absent(meta{layer="value", component="value"})))` - * and multiple queries are joined by "or". + * @returns {string[]} - An array of strings representing the combined Prometheus alerts query. + * Each alert query is formatted as `(ALERTS{key="value", ...} and multiple queries are joined by "or". * * @example * const alerts = [ @@ -38,63 +65,91 @@ import { PROMETHEUS_QUERY_INTERVAL_SECONDS } from './utils'; * * const query = createAlertsQuery(alerts); * // Returns: - * // '(ALERTS{alertname="AlertmanagerReceiversNotConfigured", namespace="openshift-monitoring", severity="warning"} + on () group_left (component, layer) (absent(meta{layer="core", component="monitoring"}))) or - * // (ALERTS{alertname="AnotherAlert", namespace="default", severity="critical"} + on () group_left (component, layer) (absent(meta{layer="app", component="frontend"})))' + * // ['ALERTS{alertname="AlertmanagerReceiversNotConfigured", namespace="openshift-monitoring", severity="warning"} or + * // ALERTS{alertname="AnotherAlert", namespace="default", severity="critical"}'] */ -export const createAlertsQuery = (groupedAlertsValues) => { - const alertsQuery = groupedAlertsValues - .map((query) => { - // Dynamically get all keys starting with "src_" - const srcKeys = Object.keys(query).filter((key) => key.startsWith('src_')); - - // Create the alertParts array using the dynamically discovered src_ keys, - // but remove the "src_" prefix from the keys in the final query string. - const alertParts = srcKeys - .filter((key) => query[key]) // Only include keys that are present in the query object - .map((key) => `${key.replace('src_', '')}="${query[key]}"`) // Remove "src_" prefix from keys - .join(', '); - - // Construct the query string for each grouped alert - return `(ALERTS{${alertParts}} + on () group_left (component, layer) (absent(meta{layer="${query.layer}", component="${query.component}"})))`; - }) - .join(' or '); // Join all individual alert queries with "or" - - // TODO: remove duplicated conditions, optimize query - - return alertsQuery; +export const createAlertsQuery = (groupedAlertsValues, max_url_length = MAX_URL_LENGTH) => { + const queries = []; + const alertsMap = new Map(); + + let currentQueryParts = []; + let currentQueryLength = 0; + + for (const alertValue of groupedAlertsValues) { + const singleAlertQuery = createSingleAlertQuery(alertValue); + if (alertsMap.has(singleAlertQuery)) { + continue; + } + alertsMap.set(singleAlertQuery, true); + const newQueryLength = currentQueryLength + singleAlertQuery.length + 4; // 4 for ' or ' + + if (newQueryLength <= max_url_length) { + currentQueryParts.push(singleAlertQuery); + currentQueryLength = newQueryLength; + continue; + } + queries.push(currentQueryParts.join(' or ')); + currentQueryParts = [singleAlertQuery]; + currentQueryLength = singleAlertQuery.length; + } + + if (currentQueryParts.length > 0) { + queries.push(currentQueryParts.join(' or ')); + } + + return queries; }; -export const fetchDataForIncidentsAndAlerts = ( +export const fetchDataForIncidentsAndAlerts = async ( fetch: (url: string) => Promise, range: { endTime: number; duration: number }, - customQuery: string, + customQuery: string | string[], ) => { // Calculate samples to ensure step=PROMETHEUS_QUERY_INTERVAL_SECONDS (300s / 5 minutes) // For 24h duration: Math.ceil(86400000 / 288 / 1000) = 300 seconds const samples = Math.floor(range.duration / (PROMETHEUS_QUERY_INTERVAL_SECONDS * 1000)); + const queries = Array.isArray(customQuery) ? customQuery : [customQuery]; - const url = buildPrometheusUrl({ - prometheusUrlProps: { - endpoint: PrometheusEndpoint.QUERY_RANGE, - endTime: range.endTime, - namespace: '', - query: customQuery, - samples, - timespan: range.duration, - }, - basePath: getPrometheusBasePath({ - prometheus: 'cmo', - }), - }); - - if (!url) { - // Return empty result when query is empty to avoid making invalid API calls - return Promise.resolve({ - data: { - result: [], + const promises = queries.map((query) => { + const url = buildPrometheusUrl({ + prometheusUrlProps: { + endpoint: PrometheusEndpoint.QUERY_RANGE, + endTime: range.endTime, + query, + samples, + timespan: range.duration, }, + basePath: getPrometheusBasePath({ + prometheus: 'cmo', + useTenancyPath: false, + }), }); - } - return fetch(url); + if (!url) { + // Return empty result when query is empty to avoid making invalid API calls + return Promise.resolve({ + status: 'success', + data: { + resultType: 'matrix', + result: [], + }, + } as PrometheusResponse); + } + + return consoleFetchJSON(url); + }); + + const responses = await Promise.all(promises); + + // Merge responses + const combinedResult = responses.flatMap((r) => r.data?.result || []); + + // Construct a synthetic response + return { + status: 'success', + data: { + resultType: responses[0]?.data?.resultType || 'matrix', + result: combinedResult, + }, + } as PrometheusResponse; }; diff --git a/web/src/components/Incidents/processAlerts.spec.ts b/web/src/components/Incidents/processAlerts.spec.ts index c662fe0fa..8c424a72d 100644 --- a/web/src/components/Incidents/processAlerts.spec.ts +++ b/web/src/components/Incidents/processAlerts.spec.ts @@ -319,8 +319,6 @@ describe('convertToAlerts', () => { alertname: 'Alert2', namespace: 'ns2', severity: 'warning', - component: 'comp2', - layer: 'layer2', name: 'name2', alertstate: 'firing', }, @@ -331,8 +329,6 @@ describe('convertToAlerts', () => { alertname: 'Alert1', namespace: 'ns1', severity: 'critical', - component: 'comp1', - layer: 'layer1', name: 'name1', alertstate: 'firing', }, @@ -343,6 +339,8 @@ describe('convertToAlerts', () => { const incidents: Array> = [ { group_id: 'incident1', + component: 'comp1', + layer: 'layer1', src_alertname: 'Alert1', src_namespace: 'ns1', src_severity: 'critical', @@ -350,6 +348,8 @@ describe('convertToAlerts', () => { }, { group_id: 'incident2', + component: 'comp2', + layer: 'layer2', src_alertname: 'Alert2', src_namespace: 'ns2', src_severity: 'warning', @@ -370,8 +370,6 @@ describe('convertToAlerts', () => { alertname: 'Alert1', namespace: 'ns1', severity: 'critical', - component: 'comp1', - layer: 'layer1', name: 'name1', alertstate: 'firing', }, @@ -382,8 +380,6 @@ describe('convertToAlerts', () => { alertname: 'Alert2', namespace: 'ns2', severity: 'warning', - component: 'comp2', - layer: 'layer2', name: 'name2', alertstate: 'firing', }, @@ -393,10 +389,22 @@ describe('convertToAlerts', () => { const incidents: Array> = [ { - values: [ - [nowSeconds - 3600, '2'], - [nowSeconds - 1800, '1'], - ], + group_id: 'incident1', + src_alertname: 'Alert1', + src_namespace: 'ns1', + src_severity: 'critical', + component: 'comp1', + layer: 'layer1', + values: [[nowSeconds - 3600, '2']], + }, + { + group_id: 'incident2', + src_alertname: 'Alert2', + src_namespace: 'ns2', + src_severity: 'warning', + component: 'comp2', + layer: 'layer2', + values: [[nowSeconds - 1800, '1']], }, ]; @@ -415,8 +423,6 @@ describe('convertToAlerts', () => { alertname: 'TestAlert', namespace: 'test-namespace', severity: 'critical', - component: 'test-component', - layer: 'test-layer', name: 'test', alertstate: 'firing', }, @@ -430,6 +436,8 @@ describe('convertToAlerts', () => { src_alertname: 'TestAlert', src_namespace: 'test-namespace', src_severity: 'critical', + component: 'test-component', + layer: 'test-layer', silenced: true, values: [[nowSeconds, '2']], }, @@ -439,37 +447,6 @@ describe('convertToAlerts', () => { expect(result).toHaveLength(1); expect(result[0].silenced).toBe(true); }); - - it('should default silenced to false when no matching incident found', () => { - const prometheusResults: PrometheusResult[] = [ - { - metric: { - alertname: 'TestAlert', - namespace: 'test-namespace', - severity: 'critical', - component: 'test-component', - layer: 'test-layer', - name: 'test', - alertstate: 'firing', - }, - values: [[nowSeconds, '2']], - }, - ]; - - const incidents: Array> = [ - { - group_id: 'incident1', - src_alertname: 'DifferentAlert', - src_namespace: 'different-namespace', - src_severity: 'warning', - values: [[nowSeconds, '1']], - }, - ]; - - const result = convertToAlerts(prometheusResults, incidents, now); - expect(result).toHaveLength(1); - expect(result[0].silenced).toBe(false); - }); }); describe('incident merging', () => { @@ -480,8 +457,6 @@ describe('convertToAlerts', () => { alertname: 'TestAlert', namespace: 'test-namespace', severity: 'critical', - component: 'test-component', - layer: 'test-layer', name: 'test', alertstate: 'firing', }, @@ -499,6 +474,8 @@ describe('convertToAlerts', () => { src_alertname: 'TestAlert', src_namespace: 'test-namespace', src_severity: 'critical', + component: 'test-component', + layer: 'test-layer', silenced: false, values: [[nowSeconds - 600, '2']], }, @@ -527,8 +504,6 @@ describe('convertToAlerts', () => { alertname: 'MyAlert', namespace: 'my-namespace', severity: 'warning', - component: 'my-component', - layer: 'my-layer', name: 'my-name', alertstate: 'firing', }, @@ -542,6 +517,8 @@ describe('convertToAlerts', () => { src_alertname: 'MyAlert', src_namespace: 'my-namespace', src_severity: 'warning', + component: 'my-component', + layer: 'my-layer', values: [[nowSeconds, '1']], }, ]; @@ -566,7 +543,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert1', namespace: 'ns1', - component: 'comp1', severity: 'critical', alertstate: 'resolved', }, @@ -576,7 +552,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert2', namespace: 'ns2', - component: 'comp2', severity: 'warning', alertstate: 'firing', }, @@ -597,7 +572,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert1', namespace: 'ns1', - component: 'comp1', severity: 'critical', alertstate: 'firing', }, @@ -610,7 +584,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert1', namespace: 'ns1', - component: 'comp1', severity: 'critical', alertstate: 'firing', }, @@ -632,7 +605,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert1', namespace: 'ns1', - component: 'comp1', severity: 'critical', alertstate: 'firing', }, @@ -642,7 +614,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert2', namespace: 'ns1', - component: 'comp1', severity: 'critical', alertstate: 'firing', }, @@ -660,7 +631,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert1', namespace: 'ns1', - component: 'comp1', severity: 'critical', alertstate: 'firing', }, @@ -670,7 +640,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert1', namespace: 'ns1', - component: 'comp1', severity: 'warning', alertstate: 'firing', }, @@ -690,7 +659,6 @@ describe('deduplicateAlerts', () => { metric: { alertname: 'Alert1', namespace: 'ns1', - component: 'comp1', severity: 'critical', alertstate: 'firing', }, diff --git a/web/src/components/Incidents/processAlerts.ts b/web/src/components/Incidents/processAlerts.ts index 921a4a49f..b216aa2a8 100644 --- a/web/src/components/Incidents/processAlerts.ts +++ b/web/src/components/Incidents/processAlerts.ts @@ -252,9 +252,10 @@ export function convertToAlerts( incident.src_severity === alert.metric.severity, ); - // Use silenced value from incident data (cluster_health_components_map) - // Default to false if no matching incident found - const silenced = matchingIncident?.silenced ?? false; + // If no matching incident found, skip the alert + if (!matchingIncident) { + return null; + } // Add padding points for chart rendering const paddedValues = insertPaddingPointsForChart(sortedValues, currentTime); @@ -265,8 +266,8 @@ export function convertToAlerts( alertname: alert.metric.alertname, namespace: alert.metric.namespace, severity: alert.metric.severity as Severity, - component: alert.metric.component, - layer: alert.metric.layer, + component: matchingIncident.component, + layer: matchingIncident.layer, name: alert.metric.name, alertstate: resolved ? 'resolved' : 'firing', values: paddedValues, @@ -274,7 +275,7 @@ export function convertToAlerts( alertsEndFiring: lastTimestamp, resolved, x: 0, // Will be set after sorting - silenced, + silenced: matchingIncident.silenced ?? false, }; }) .filter((alert): alert is Alert => alert !== null) diff --git a/web/src/components/Incidents/utils.spec.ts b/web/src/components/Incidents/utils.spec.ts index 7bef07021..97c55df8f 100644 --- a/web/src/components/Incidents/utils.spec.ts +++ b/web/src/components/Incidents/utils.spec.ts @@ -1,4 +1,4 @@ -import { insertPaddingPointsForChart } from './utils'; +import { insertPaddingPointsForChart, roundDateToInterval } from './utils'; describe('insertPaddingPointsForChart', () => { describe('edge cases', () => { @@ -287,3 +287,29 @@ describe('insertPaddingPointsForChart', () => { }); }); }); + +describe('roundDateToInterval', () => { + describe('exact 5-minute boundaries', () => { + it('should return unchanged date for 23:55:00', () => { + const date = new Date('2026-01-26T23:55:00.000Z'); + const rounded = roundDateToInterval(date); + expect(rounded.getTime()).toBe(date.getTime()); + }); + }); + + describe('rounding to nearest 5-minute boundary', () => { + it('should round 22:57:00 down to 22:55:00', () => { + const date = new Date('2026-01-26T22:57:00.000Z'); + const rounded = roundDateToInterval(date); + const expected = new Date('2026-01-26T22:55:00.000Z'); + expect(rounded.getTime()).toBe(expected.getTime()); + }); + + it('should round 22:59:00 up to 23:00:00', () => { + const date = new Date('2026-01-26T22:59:00.000Z'); + const rounded = roundDateToInterval(date); + const expected = new Date('2026-01-26T23:00:00.000Z'); + expect(rounded.getTime()).toBe(expected.getTime()); + }); + }); +}); diff --git a/web/src/components/Incidents/utils.ts b/web/src/components/Incidents/utils.ts index a778c0ffa..6f60fc9a8 100644 --- a/web/src/components/Incidents/utils.ts +++ b/web/src/components/Incidents/utils.ts @@ -54,6 +54,26 @@ export const getCurrentTime = (): number => { return Math.floor(now / intervalMs) * intervalMs; }; +/** + * Rounds a Date to the nearest 5-minute boundary for display purposes. + * This is used in tooltips to show cleaner, rounded timestamps instead of precise + * interval boundaries that may differ by seconds. + * + * For example: + * - 22:57:00 -> 22:55:00 (rounds down) + * - 22:59:00 -> 23:00:00 (rounds up) + * - 23:30:00 -> 23:30:00 (already at boundary) + * - 23:29:59 -> 23:30:00 (rounds up) + * + * @param date - The Date object to round + * @returns A new Date object rounded to the nearest 5-minute boundary + */ +export const roundDateToInterval = (date: Date): Date => { + const intervalMs = PROMETHEUS_QUERY_INTERVAL_SECONDS * 1000; + const roundedMs = Math.round(date.getTime() / intervalMs) * intervalMs; + return new Date(roundedMs); +}; + /** * Determines if an incident or alert is resolved based on the time elapsed since the last data point. * diff --git a/web/src/components/MetricsPage.tsx b/web/src/components/MetricsPage.tsx index e65d50872..397dea5ea 100644 --- a/web/src/components/MetricsPage.tsx +++ b/web/src/components/MetricsPage.tsx @@ -114,8 +114,7 @@ import { t_global_spacer_sm, t_global_font_family_mono, } from '@patternfly/react-tokens'; -import { QueryParamProvider, StringParam, useQueryParam } from 'use-query-params'; -import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; +import { StringParam, useQueryParam } from 'use-query-params'; import { GraphUnits, isGraphUnit } from './metrics/units'; import { SimpleSelect, SimpleSelectOption } from '@patternfly/react-templates'; import { valueFormatter } from './console/console-shared/src/components/query-browser/QueryBrowserTooltip'; @@ -123,6 +122,7 @@ import { ALL_NAMESPACES_KEY } from './utils'; import { MonitoringProvider } from '../contexts/MonitoringContext'; import { DataTestIDs } from './data-test'; import { useMonitoring } from '../hooks/useMonitoring'; +import { useQueryNamespace } from './hooks/useQueryNamespace'; // Stores information about the currently focused query input let focusedQuery; @@ -304,7 +304,7 @@ const MetricsActionsMenu: FC = () => { isExpanded={isOpen} data-test={DataTestIDs.MetricsPageActionsDropdownButton} > - Actions + {t('Actions')} )} popperProps={{ position: 'right' }} @@ -613,7 +613,7 @@ const QueryKebab: FC<{ index: number }> = ({ index }) => { export const QueryTable: FC = ({ index, namespace, customDatasource, units }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); - const { plugin } = useMonitoring(); + const { plugin, accessCheckLoading, useMetricsTenancy } = useMonitoring(); const [data, setData] = useState(); const [error, setError] = useState(); @@ -630,9 +630,8 @@ export const QueryTable: FC = ({ index, namespace, customDataso (state: MonitoringState) => getObserveState(plugin, state).queryBrowser.queries[index]?.isExpanded, ); - const pollInterval = useSelector( - (state: MonitoringState) => - Number(getObserveState(plugin, state).queryBrowser.pollInterval) * 15 * 1000, + const pollInterval = useSelector((state: MonitoringState) => + Number(getObserveState(plugin, state).queryBrowser.pollInterval), ); const query = useSelector( (state: MonitoringState) => getObserveState(plugin, state).queryBrowser.queries[index]?.query, @@ -665,7 +664,7 @@ export const QueryTable: FC = ({ index, namespace, customDataso // If the namespace is defined getPrometheusURL will use // the PROMETHEUS_TENANCY_BASE_PATH for requests in the developer view const tick = () => { - if (isEnabled && isExpanded && query) { + if (isEnabled && isExpanded && !accessCheckLoading && query) { safeFetch( buildPrometheusUrl({ prometheusUrlProps: { @@ -675,7 +674,7 @@ export const QueryTable: FC = ({ index, namespace, customDataso }, basePath: getPrometheusBasePath({ prometheus: 'cmo', - namespace, + useTenancyPath: useMetricsTenancy, basePathOverride: customDatasource?.basePath, }), }), @@ -693,7 +692,16 @@ export const QueryTable: FC = ({ index, namespace, customDataso } }; - usePoll(tick, pollInterval, namespace, query, span, lastRequestTime); + usePoll( + tick, + pollInterval, + namespace, + query, + span, + lastRequestTime, + useMetricsTenancy, + accessCheckLoading, + ); useEffect(() => { setData(undefined); @@ -702,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 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, + ]); - const onSort = (e, i, direction) => setSortBy({ index: i, direction }); + useEffect(() => { + if (tableData.columns && tableData.rows) { + dispatch( + queryBrowserPatchQuery(index, { + queryTableData: { columns: tableData.columns, rows: tableData.rows }, + }), + ); + } + }, [dispatch, index, tableData?.columns, tableData?.rows]); - const tableRows = rows.slice((page - 1) * perPage, page * perPage).map((cells) => ({ cells })); + if (isUnused) { + return null; + } else if (isError) { + return ; + } else if (isLoading) { + return ; + } else if (isEmptyGraph) { + return ( +
+ {t('No datapoints found.')} +
+ ); + } return ( <> @@ -846,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, }, } @@ -872,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' @@ -894,7 +937,7 @@ export const QueryTable: FC = ({ index, namespace, customDataso
= ({ customDataSourceName, customDataSource, customDatasourceError, units }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); - const [activeNamespace] = useActiveNamespace(); const { plugin } = useMonitoring(); + const [activeNamespace] = useActiveNamespace(); const dispatch = useDispatch(); @@ -1106,7 +1149,11 @@ const QueryBrowserWrapper: FC<{ const insertExampleQuery = () => { const focusedIndex = focusedQuery?.index ?? 0; const index = queries[focusedIndex] ? focusedIndex : 0; - const text = 'sort_desc(sum(sum_over_time(ALERTS{alertstate="firing"}[24h])) by (alertname))'; + const labelMatchers = + activeNamespace === ALL_NAMESPACES_KEY + ? '{alertstate="firing"}' + : `{alertstate="firing", namespace="${activeNamespace}"}`; + const text = `sort_desc(sum(sum_over_time(ALERTS${labelMatchers}[24h])) by (alertname))`; dispatch(queryBrowserPatchQuery(index, { isEnabled: true, query: text, text })); }; @@ -1162,7 +1209,6 @@ const QueryBrowserWrapper: FC<{ units={units} showStackedControl showDisconnectedControl - useTenancy={activeNamespace !== ALL_NAMESPACES_KEY} /> ); }; @@ -1236,9 +1282,8 @@ const IntervalDropdown = () => { (v: number) => dispatch(queryBrowserSetPollInterval(v)), [dispatch], ); - const pollInterval = useSelector( - (state: MonitoringState) => - Number(getObserveState(plugin, state).queryBrowser.pollInterval) * 15 * 1000, + const pollInterval = useSelector((state: MonitoringState) => + Number(getObserveState(plugin, state).queryBrowser.pollInterval), ); return ; }; @@ -1278,14 +1323,7 @@ const GraphUnitsDropDown: FC = () => { const MetricsPage_: FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const [units, setUnits] = useQueryParam(QueryParams.Units, StringParam); - const [queryNamespace, setQueryNamespace] = useQueryParam(QueryParams.Namespace, StringParam); - const [activeNamespace, setActiveNamespace] = useActiveNamespace(); - - useEffect(() => { - if (queryNamespace && activeNamespace !== queryNamespace) { - setActiveNamespace(queryNamespace); - } - }, [queryNamespace, activeNamespace, setActiveNamespace]); + const { setNamespace } = useQueryNamespace(); const dispatch = useDispatch(); @@ -1368,7 +1406,7 @@ const MetricsPage_: FC = () => { { dispatch(queryBrowserDeleteAllQueries()); - setQueryNamespace(namespace); + setNamespace(namespace); }} /> @@ -1427,9 +1465,7 @@ const MetricsPage = withFallback(MetricsPage_); export const MpCmoMetricsPage: React.FC = () => { return ( - - - + ); }; diff --git a/web/src/components/alerting/AlertDetail/SilencedByTable.tsx b/web/src/components/alerting/AlertDetail/SilencedByTable.tsx index 7263eb374..87b347024 100644 --- a/web/src/components/alerting/AlertDetail/SilencedByTable.tsx +++ b/web/src/components/alerting/AlertDetail/SilencedByTable.tsx @@ -1,11 +1,6 @@ import type { FC, MouseEvent } from 'react'; import { useState, useMemo } from 'react'; -import { - ResourceIcon, - Silence, - SilenceStates, - useActiveNamespace, -} from '@openshift-console/dynamic-plugin-sdk'; +import { ResourceIcon, Silence, SilenceStates } from '@openshift-console/dynamic-plugin-sdk'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom-v5-compat'; import { @@ -30,13 +25,12 @@ import { t_global_spacer_xs } from '@patternfly/react-tokens'; export const SilencedByList: FC<{ silences: Silence[] }> = ({ silences }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); - const [namespace] = useActiveNamespace(); const navigate = useNavigate(); const [isModalOpen, , setModalOpen, setModalClosed] = useBoolean(false); const [silence, setSilence] = useState(null); const editSilence = (event: MouseEvent, rowIndex: number) => { - navigate(getEditSilenceAlertUrl(perspective, silences.at(rowIndex)?.id, namespace)); + navigate(getEditSilenceAlertUrl(perspective, silences.at(rowIndex)?.id)); }; const rowActions = (silence: Silence): IAction[] => { @@ -79,7 +73,7 @@ export const SilencedByList: FC<{ silences: Silence[] }> = ({ silences }) => { {silence.name} diff --git a/web/src/components/alerting/AlertList/AggregateAlertTableRow.tsx b/web/src/components/alerting/AlertList/AggregateAlertTableRow.tsx index 5ac374549..f20958bb4 100644 --- a/web/src/components/alerting/AlertList/AggregateAlertTableRow.tsx +++ b/web/src/components/alerting/AlertList/AggregateAlertTableRow.tsx @@ -1,9 +1,4 @@ -import { - Alert, - ResourceIcon, - TableColumn, - useActiveNamespace, -} from '@openshift-console/dynamic-plugin-sdk'; +import { Alert, ResourceIcon, TableColumn } from '@openshift-console/dynamic-plugin-sdk'; import { ExpandableRowContent, Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import type { FC } from 'react'; import { useState, useMemo } from 'react'; @@ -33,7 +28,6 @@ const AggregateAlertTableRow: AggregateAlertTableRowProps = ({ const { perspective } = usePerspective(); const title = aggregatedAlert.name; const isACMPerspective = perspective === 'acm'; - const [namespace] = useActiveNamespace(); const filteredAlerts = useMemo( () => filterAlerts(aggregatedAlert.alerts, selectedFilters), @@ -99,11 +93,7 @@ const AggregateAlertTableRow: AggregateAlertTableRowProps = ({ diff --git a/web/src/components/alerting/AlertList/AlertTableRow.tsx b/web/src/components/alerting/AlertList/AlertTableRow.tsx index 30de8322b..a84c52cb1 100644 --- a/web/src/components/alerting/AlertList/AlertTableRow.tsx +++ b/web/src/components/alerting/AlertList/AlertTableRow.tsx @@ -21,7 +21,7 @@ import { DropdownItem, Flex, FlexItem } from '@patternfly/react-core'; import KebabDropdown from '../../../components/kebab-dropdown'; import type { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom-v5-compat'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { AlertResource, alertState } from '../../../components/utils'; import { getAlertUrl, @@ -35,9 +35,6 @@ const AlertTableRow: FC<{ alert: Alert }> = ({ alert }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); const navigate = useNavigate(); - const params = useParams<{ ns: string }>(); - - const namespace = params.ns; const state = alertState(alert); @@ -49,7 +46,7 @@ const AlertTableRow: FC<{ alert: Alert }> = ({ alert }) => { dropdownItems.unshift( navigate(getNewSilenceAlertUrl(perspective, alert, namespace))} + onClick={() => navigate(getNewSilenceAlertUrl(perspective, alert))} data-test={DataTestIDs.SilenceAlertDropdownItem} > {t('Silence alert')} @@ -86,12 +83,7 @@ const AlertTableRow: FC<{ alert: Alert }> = ({ alert }) => { diff --git a/web/src/components/alerting/AlertRulesDetailsPage.tsx b/web/src/components/alerting/AlertRulesDetailsPage.tsx index ade877b24..91993c97e 100644 --- a/web/src/components/alerting/AlertRulesDetailsPage.tsx +++ b/web/src/components/alerting/AlertRulesDetailsPage.tsx @@ -6,7 +6,6 @@ import { PrometheusAlert, ResourceIcon, Timestamp, - useActiveNamespace, useResolvedExtensions, } from '@openshift-console/dynamic-plugin-sdk'; import { @@ -83,11 +82,10 @@ const PrometheusTemplate = ({ text }) => ( type ActiveAlertsProps = { alerts: PrometheusAlert[]; - namespace: string; ruleID: string; }; -export const ActiveAlerts: FC = ({ alerts, namespace, ruleID }) => { +export const ActiveAlerts: FC = ({ alerts, ruleID }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); const navigate = useNavigate(); @@ -110,7 +108,7 @@ export const ActiveAlerts: FC = ({ alerts, namespace, ruleID {alertDescription(a)} @@ -148,7 +146,6 @@ const AlertRulesDetailsPage_: FC = () => { const { rules, rulesAlertLoading } = useAlerts(); const { perspective } = usePerspective(); - const [namespace] = useActiveNamespace(); const rule = _.find(rules, { id: params.id }); @@ -187,10 +184,7 @@ const AlertRulesDetailsPage_: FC = () => { - + {t('Alerting rules')} @@ -316,7 +310,6 @@ const AlertRulesDetailsPage_: FC = () => { to={getQueryBrowserUrl({ perspective: perspective, query: rule?.query, - namespace: namespace, })} > @@ -361,7 +354,6 @@ const AlertRulesDetailsPage_: FC = () => { {!sourceId || sourceId === 'prometheus' ? ( { {_.isEmpty(rule?.alerts) ? (
{t('None found')}
) : ( - + )} diff --git a/web/src/components/alerting/AlertRulesPage.tsx b/web/src/components/alerting/AlertRulesPage.tsx index ec700e204..2e6903d2d 100644 --- a/web/src/components/alerting/AlertRulesPage.tsx +++ b/web/src/components/alerting/AlertRulesPage.tsx @@ -97,7 +97,7 @@ const RuleTableRow: FC> = ({ obj }) => {
@@ -217,7 +217,7 @@ const AlertRulesPage_: FC = () => { unfilteredData={rules} scrollNode={() => document.getElementById('alert-rules-table-scroll')} NoDataEmptyMsg={() => { - return ; + return ; }} /> diff --git a/web/src/components/alerting/AlertUtils.tsx b/web/src/components/alerting/AlertUtils.tsx index 25dc49414..13a213fb0 100644 --- a/web/src/components/alerting/AlertUtils.tsx +++ b/web/src/components/alerting/AlertUtils.tsx @@ -53,7 +53,6 @@ import { t_global_text_color_disabled, t_global_text_color_subtle, } from '@patternfly/react-tokens'; -import { ALL_NAMESPACES_KEY } from '../utils'; export const getAdditionalSources = ( data: Array, @@ -243,7 +242,6 @@ export const PopoverField: FC<{ bodyContent: ReactNode; label: string }> = ({ export const Graph: FC = ({ filterLabels = undefined, formatSeriesTitle, - namespace, query, ruleDuration, }) => { @@ -255,7 +253,7 @@ export const Graph: FC = ({ const GraphLink = () => query && perspective !== 'acm' ? ( - + {t('Inspect')} ) : null; @@ -268,7 +266,6 @@ export const Graph: FC = ({ GraphLink={GraphLink} pollInterval={Math.round(timespan / 120)} queries={[query]} - useTenancy={namespace !== ALL_NAMESPACES_KEY} /> ); }; @@ -276,7 +273,6 @@ export const Graph: FC = ({ type GraphProps = { filterLabels?: PrometheusLabels; formatSeriesTitle?: FormatSeriesTitle; - namespace?: string; query: string; ruleDuration: number; showLegend?: boolean; diff --git a/web/src/components/alerting/AlertingPage.tsx b/web/src/components/alerting/AlertingPage.tsx index 5dba7ad44..a5a68321a 100644 --- a/web/src/components/alerting/AlertingPage.tsx +++ b/web/src/components/alerting/AlertingPage.tsx @@ -13,6 +13,7 @@ import { useLocation } from 'react-router-dom'; import { AlertResource, RuleResource, SilenceResource } from '../utils'; import { useDispatch } from 'react-redux'; import { alertingClearSelectorData } from '../../store/actions'; +import { useQueryNamespace } from '../hooks/useQueryNamespace'; const CmoAlertsPage = lazy(() => import(/* webpackChunkName: "CmoAlertsPage" */ './AlertsPage').then((module) => ({ @@ -57,8 +58,10 @@ const namespacedPages = [ const AlertingPage: FC = () => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const dispatch = useDispatch(); + const { useAlertsTenancy, accessCheckLoading } = useMonitoring(); const [perspective] = useActivePerspective(); + const { setNamespace } = useQueryNamespace(); const { plugin, prometheus } = useMonitoring(); @@ -72,37 +75,34 @@ const AlertingPage: FC = () => { () => [ { href: 'alerts', - // t('Alerts') - nameKey: 'Alerts', + nameKey: `${process.env.I18N_NAMESPACE}~Alerts`, component: plugin === 'monitoring-plugin' ? CmoAlertsPage : CooAlertsPage, - name: 'Alerts', + name: t('Alerts'), }, { href: 'silences', - // t('Silences') - nameKey: 'Silences', + nameKey: `${process.env.I18N_NAMESPACE}~Silences`, component: plugin === 'monitoring-plugin' ? CmoSilencesPage : CooSilencesPage, - name: 'Silences', + name: t('Silences'), }, { href: 'alertrules', - // t('Alerting Rules') -- for console.tab extension - // t('Alerting rules') - nameKey: 'Alerting rules', + nameKey: `${process.env.I18N_NAMESPACE}~Alerting rules`, component: plugin === 'monitoring-plugin' ? CmoAlertRulesPage : CooAlertRulesPage, - name: 'Alerting rules', + name: t('Alerting rules'), }, ], - [plugin], + [plugin, t], ); return ( <> - {namespacedPages.includes(pathname) && ( + {namespacedPages.includes(pathname) && !accessCheckLoading && useAlertsTenancy && ( - dispatch(alertingClearSelectorData(prometheus, namespace)) - } + onNamespaceChange={(namespace) => { + dispatch(alertingClearSelectorData(prometheus, namespace)); + setNamespace(namespace); + }} /> )} diff --git a/web/src/components/alerting/AlertsDetailsPage.tsx b/web/src/components/alerting/AlertsDetailsPage.tsx index 218a23cbb..682fa5d76 100644 --- a/web/src/components/alerting/AlertsDetailsPage.tsx +++ b/web/src/components/alerting/AlertsDetailsPage.tsx @@ -9,7 +9,6 @@ import { ResourceIcon, ResourceLink, Rule, - useActiveNamespace, useResolvedExtensions, } from '@openshift-console/dynamic-plugin-sdk'; import * as _ from 'lodash-es'; @@ -101,8 +100,6 @@ const AlertsDetailsPage_: FC = () => { const { alerts, rulesAlertLoading, silences } = useAlerts(); - const [namespace] = useActiveNamespace(); - const hideGraphs = useSelector( (state: MonitoringState) => !!getObserveState(plugin, state).hideGraphs, ); @@ -159,7 +156,7 @@ const AlertsDetailsPage_: FC = () => { - + {t('Alerts')} @@ -193,7 +190,7 @@ const AlertsDetailsPage_: FC = () => { {state !== AlertStates.Silenced && ( diff --git a/web/src/components/alerting/SilencesUtils.tsx b/web/src/components/alerting/SilencesUtils.tsx index c9e0d0517..b42661b3d 100644 --- a/web/src/components/alerting/SilencesUtils.tsx +++ b/web/src/components/alerting/SilencesUtils.tsx @@ -4,7 +4,6 @@ import { ResourceIcon, Silence, SilenceStates, - useActiveNamespace, } from '@openshift-console/dynamic-plugin-sdk'; import { Button, @@ -57,7 +56,6 @@ import { DataTestIDs } from '../data-test'; export const SilenceTableRow: FC = ({ obj, showCheckbox }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); - const [namespace] = useActiveNamespace(); const { createdBy, endsAt, firingAlerts, id, name, startsAt, matchers } = obj; const state = silenceState(obj); @@ -107,7 +105,7 @@ export const SilenceTableRow: FC = ({ obj, showCheckbox }) {name} @@ -205,14 +203,13 @@ export const SilenceState = ({ silence }) => { export const SilenceDropdown: FC = ({ silence, toggleText }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); - const [namespace] = useActiveNamespace(); const navigate = useNavigate(); const [isOpen, setIsOpen, , setClosed] = useBoolean(false); const [isModalOpen, , setModalOpen, setModalClosed] = useBoolean(false); const editSilence = () => { - navigate(getEditSilenceAlertUrl(perspective, silence.id, namespace)); + navigate(getEditSilenceAlertUrl(perspective, silence.id)); }; const dropdownItems = @@ -281,7 +278,6 @@ export const ExpireSilenceModal: FC = ({ }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { perspective } = usePerspective(); - const [namespace] = useActiveNamespace(); const [isInProgress, , setInProgress, setNotInProgress] = useBoolean(false); const [success, , setSuccess] = useBoolean(false); @@ -289,7 +285,7 @@ export const ExpireSilenceModal: FC = ({ const expireSilence = () => { setInProgress(); - const url = getFetchSilenceUrl(perspective, silenceID, namespace); + const url = getFetchSilenceUrl(perspective, silenceID); consoleFetchJSON .delete(url) .then(() => { diff --git a/web/src/components/console/console-shared/src/components/empty-state/EmptyBox.tsx b/web/src/components/console/console-shared/src/components/empty-state/EmptyBox.tsx index 2063b1dd6..04574bbaa 100644 --- a/web/src/components/console/console-shared/src/components/empty-state/EmptyBox.tsx +++ b/web/src/components/console/console-shared/src/components/empty-state/EmptyBox.tsx @@ -2,12 +2,12 @@ import type { FCC } from 'react'; import { useTranslation } from 'react-i18next'; import { ConsoleEmptyState } from './ConsoleEmptyState'; -export const EmptyBox: FCC = ({ label }) => { +export const EmptyBox: FCC = ({ label, customMessage }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); return ( - {label ? t('No {{label}} found', { label }) : t('Not found')} + {customMessage ? customMessage : label ? t('No {{label}} found', { label }) : t('Not found')} ); }; @@ -15,4 +15,5 @@ EmptyBox.displayName = 'EmptyBox'; type EmptyBoxProps = { label?: string; + customMessage?: string; }; diff --git a/web/src/components/console/graphs/promethues-graph.tsx b/web/src/components/console/graphs/promethues-graph.tsx index c4229c2a1..d97e22ee5 100644 --- a/web/src/components/console/graphs/promethues-graph.tsx +++ b/web/src/components/console/graphs/promethues-graph.tsx @@ -21,7 +21,6 @@ const mapStateToProps = (state: RootState) => ({ const PrometheusGraphLink_: FC = ({ children, query, - namespace, ariaChartLinkLabel, }) => { const { perspective } = usePerspective(); @@ -32,7 +31,7 @@ const PrometheusGraphLink_: FC = ({ const params = new URLSearchParams(); queries.forEach((q, index) => params.set(`query${index}`, q)); - const url = getMutlipleQueryBrowserUrl(perspective, params, namespace); + const url = getMutlipleQueryBrowserUrl(perspective, params); return ( = forwardRef( type PrometheusGraphLinkProps = { canAccessMonitoring: boolean; query: string | string[]; - namespace?: string; ariaChartLinkLabel?: string; }; diff --git a/web/src/components/console/models/index.ts b/web/src/components/console/models/index.ts index 794450159..d71c61f81 100644 --- a/web/src/components/console/models/index.ts +++ b/web/src/components/console/models/index.ts @@ -65,14 +65,12 @@ export const NodeModel = { export const NamespaceModel: K8sModel = { apiVersion: 'v1', label: 'Namespace', - // t('Namespace') - labelKey: 'public~Namespace', + labelKey: `${process.env.I18N_NAMESPACE}~Namespace`, plural: 'namespaces', abbr: 'NS', kind: 'Namespace', id: 'namespace', labelPlural: 'Namespaces', - // t('Namespaces') labelPluralKey: 'public~Namespaces', }; @@ -80,14 +78,12 @@ export const ProjectModel: K8sModel = { apiVersion: 'v1', apiGroup: 'project.openshift.io', label: 'Project', - // t('Project') - labelKey: 'public~Project', + labelKey: `${process.env.I18N_NAMESPACE}~Project`, plural: 'projects', abbr: 'PR', kind: 'Project', id: 'project', labelPlural: 'Projects', - // t('Projects') labelPluralKey: 'public~Projects', }; diff --git a/web/src/components/console/utils/units.ts b/web/src/components/console/utils/units.ts index 4cf0200c6..3625735a2 100644 --- a/web/src/components/console/utils/units.ts +++ b/web/src/components/console/utils/units.ts @@ -7,10 +7,15 @@ const TYPES = { divisor: 1000, }, binaryBytes: { - units: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'], + units: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'], space: true, divisor: 1024, }, + decimalBytes: { + units: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'], + space: true, + divisor: 1000, + }, SI: { units: ['', 'k', 'M', 'G', 'T', 'P', 'E'], space: false, @@ -21,6 +26,11 @@ const TYPES = { space: true, divisor: 1000, }, + binaryBytesPerSec: { + units: ['Bps', 'KiBps', 'MiBps', 'GiBps', 'TiBps', 'PiBps', 'EiBps'], + space: true, + divisor: 1024, + }, packetsPerSec: { units: ['pps', 'kpps'], space: true, diff --git a/web/src/components/dashboards/legacy/graph.tsx b/web/src/components/dashboards/legacy/graph.tsx index 4ff9f6bb9..414a20f9d 100644 --- a/web/src/components/dashboards/legacy/graph.tsx +++ b/web/src/components/dashboards/legacy/graph.tsx @@ -8,7 +8,7 @@ import { MonitoringState } from '../../../store/store'; import { getObserveState } from '../../hooks/usePerspective'; import { DEFAULT_GRAPH_SAMPLES } from './utils'; import { CustomDataSource } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/dashboard-data-source'; -import { GraphUnits } from 'src/components/metrics/units'; +import { GraphUnits } from '../../../components/metrics/units'; import { useMonitoring } from '../../../hooks/useMonitoring'; type Props = { diff --git a/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx b/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx index d5935ae06..c102b1eb5 100644 --- a/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx +++ b/web/src/components/dashboards/legacy/legacy-dashboard-page.tsx @@ -1,9 +1,7 @@ -import { Overview } from '@openshift-console/dynamic-plugin-sdk'; +import { NamespaceBar, Overview } from '@openshift-console/dynamic-plugin-sdk'; import type { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom-v5-compat'; -import { QueryParamProvider } from 'use-query-params'; -import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; import { LoadingInline } from '../../../components/console/console-shared/src/components/loading/LoadingInline'; import withFallback from '../../console/console-shared/error/fallbacks/withFallback'; import { usePerspective } from '../../hooks/usePerspective'; @@ -12,16 +10,14 @@ import ErrorAlert from './error'; import { DashboardSkeletonLegacy } from './dashboard-skeleton-legacy'; import { useLegacyDashboards } from './useLegacyDashboards'; import { MonitoringProvider } from '../../../contexts/MonitoringContext'; +import { useOpenshiftProject } from './useOpenshiftProject'; type LegacyDashboardsPageProps = { urlBoard: string; - namespace?: string; }; -const LegacyDashboardsPage_: FC = ({ - urlBoard, - namespace, // only used in developer perspective -}) => { +const LegacyDashboardsPage_: FC = ({ urlBoard }) => { + const { project, setProject } = useOpenshiftProject(); const { legacyDashboardsError, legacyRows, @@ -29,41 +25,42 @@ const LegacyDashboardsPage_: FC = ({ legacyDashboardsMetadata, changeLegacyDashboard, legacyDashboard, - } = useLegacyDashboards(namespace, urlBoard); + } = useLegacyDashboards(project, urlBoard); const { perspective } = usePerspective(); const { t } = useTranslation(process.env.I18N_NAMESPACE); return ( - - - {legacyDashboardsLoading ? ( - - ) : legacyDashboardsError ? ( - - ) : ( - - )} - - + <> + setProject(namespace)} /> + + + {legacyDashboardsLoading ? ( + + ) : legacyDashboardsError ? ( + + ) : ( + + )} + + + ); }; const LegacyDashboardsPageWithFallback = withFallback(LegacyDashboardsPage_); export const MpCmoLegacyDashboardsPage: FC = () => { - const params = useParams<{ ns?: string; dashboardName: string }>(); + const params = useParams<{ dashboardName: string }>(); return ( - - - + ); }; diff --git a/web/src/components/dashboards/legacy/legacy-dashboard.tsx b/web/src/components/dashboards/legacy/legacy-dashboard.tsx index 69768342d..02ced1e49 100644 --- a/web/src/components/dashboards/legacy/legacy-dashboard.tsx +++ b/web/src/components/dashboards/legacy/legacy-dashboard.tsx @@ -1,7 +1,6 @@ import * as _ from 'lodash-es'; import { RedExclamationCircleIcon, - useActiveNamespace, useResolvedExtensions, } from '@openshift-console/dynamic-plugin-sdk'; import { @@ -39,7 +38,7 @@ import { } from '../../hooks/usePerspective'; import KebabDropdown from '../../kebab-dropdown'; import { MonitoringState } from '../../../store/store'; -import { evaluateVariableTemplate } from './legacy-variable-dropdowns'; +import { evaluateVariableTemplate, Variable } from './legacy-variable-dropdowns'; import { Panel, Row } from './types'; import { QueryParams } from '../../query-params'; import { CustomDataSource } from '@openshift-console/dynamic-plugin-sdk-internal/lib/extensions/dashboard-data-source'; @@ -70,7 +69,6 @@ const QueryBrowserLink = ({ if (units) { params.set(QueryParams.Units, units); } - const [namespace] = useActiveNamespace(); if (customDataSourceName) { params.set('datasource', customDataSourceName); @@ -79,7 +77,7 @@ const QueryBrowserLink = ({ return ( {t('Inspect')} @@ -104,7 +102,6 @@ const Card: FC = memo(({ panel, perspective }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { plugin } = useMonitoring(); - const [namespace] = useActiveNamespace(); const pollInterval = useSelector( (state: MonitoringState) => getObserveState(plugin, state).dashboards.pollInterval, ); @@ -115,6 +112,9 @@ const Card: FC = memo(({ panel, perspective }) => { (state: MonitoringState) => getObserveState(plugin, state).dashboards.variables, ); + // Directly use the namespace variable to prevent desync + const namespace = variables?.['namespace'] as Variable; + const ref = useRef(); const [, wasEverVisible] = useIsVisible(ref); @@ -281,7 +281,9 @@ const Card: FC = memo(({ panel, perspective }) => { if (!rawQueries.length) { return null; } - const queries = rawQueries.map((expr) => evaluateVariableTemplate(expr, variables, timespan)); + const queries = rawQueries.map((expr) => + evaluateVariableTemplate(expr, variables, timespan, namespace?.value ?? ''), + ); const isLoading = (_.some(queries, _.isUndefined) && dataSourceInfoLoading) || customDataSource === undefined; @@ -353,7 +355,7 @@ const Card: FC = memo(({ panel, perspective }) => { panel={panel} pollInterval={pollInterval} query={queries[0]} - namespace={namespace} + namespace={namespace?.value ?? ''} customDataSource={customDataSource} /> )} @@ -362,7 +364,7 @@ const Card: FC = memo(({ panel, perspective }) => { panel={panel} pollInterval={pollInterval} queries={queries} - namespace={namespace} + namespace={namespace?.value ?? ''} customDataSource={customDataSource} /> )} diff --git a/web/src/components/dashboards/legacy/legacy-variable-dropdowns.tsx b/web/src/components/dashboards/legacy/legacy-variable-dropdowns.tsx index 922c4ac54..45a4abc40 100644 --- a/web/src/components/dashboards/legacy/legacy-variable-dropdowns.tsx +++ b/web/src/components/dashboards/legacy/legacy-variable-dropdowns.tsx @@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { SingleTypeaheadDropdown } from '../../console/utils/single-typeahead-dropdown'; -import { getPrometheusBasePath, buildPrometheusUrl } from '../../utils'; +import { getPrometheusBasePath, buildPrometheusUrl, ALL_NAMESPACES_KEY } from '../../utils'; import { getQueryArgument, setQueryArgument } from '../../console/utils/router'; import { useSafeFetch } from '../../console/utils/safe-fetch-hook'; @@ -48,6 +48,7 @@ export const evaluateVariableTemplate = ( template: string, variables: any, timespan: number, + namespace: string, ): string => { if (_.isEmpty(template)) { return undefined; @@ -81,8 +82,11 @@ export const evaluateVariableTemplate = ( result = undefined; return false; } - const replacement = + let replacement = v.value === MONITORING_DASHBOARDS_VARIABLE_ALL_OPTION_KEY ? '.+' : v.value || ''; + if (v.name === 'namespace' && namespace !== ALL_NAMESPACES_KEY) { + replacement = namespace; + } result = result.replace(re, replacement); } }); @@ -103,9 +107,10 @@ const LegacyDashboardsVariableOption = ({ value, isSelected, ...rest }) => ); -const LegacyDashboardsVariableDropdown: FC = ({ id, name, namespace }) => { +const LegacyDashboardsVariableDropdown: FC = ({ id, name }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); - const { plugin } = useMonitoring(); + const { plugin, accessCheckLoading, useMetricsTenancy } = useMonitoring(); + const [namespace] = useActiveNamespace(); const timespan = useSelector( (state: MonitoringState) => getObserveState(plugin, state).dashboards.timespan, @@ -115,11 +120,12 @@ const LegacyDashboardsVariableDropdown: FC = ({ id, name, (state: MonitoringState) => getObserveState(plugin, state).dashboards.variables, ); const variable = variables?.[name] as Variable; + const options = useDeepMemo(() => { return variable?.options; }, [variable?.options]); - const query = evaluateVariableTemplate(variable?.query, variables, timespan); + const query = evaluateVariableTemplate(variable?.query, variables, timespan, namespace); const dispatch = useDispatch(); @@ -138,7 +144,10 @@ const LegacyDashboardsVariableDropdown: FC = ({ id, name, if (!customDataSourceName) { return buildPrometheusUrl({ prometheusUrlProps: prometheusProps, - basePath: getPrometheusBasePath({ prometheus: 'cmo' }), + basePath: getPrometheusBasePath({ + prometheus: 'cmo', + useTenancyPath: useMetricsTenancy, + }), }); } else if (extensionsResolved && hasExtensions) { const extension = extensions.find( @@ -155,6 +164,7 @@ const LegacyDashboardsVariableDropdown: FC = ({ id, name, prometheusUrlProps: prometheusProps, basePath: getPrometheusBasePath({ prometheus: 'cmo', + useTenancyPath: useMetricsTenancy, basePathOverride: dataSource?.basePath, }), }); @@ -165,11 +175,11 @@ const LegacyDashboardsVariableDropdown: FC = ({ id, name, setIsError(true); } }, - [customDataSourceName, extensions, extensionsResolved, hasExtensions], + [customDataSourceName, extensions, extensionsResolved, hasExtensions, useMetricsTenancy], ); useEffect(() => { - if (!query) { + if (!query || accessCheckLoading) { return; } // Convert label_values queries to something Prometheus can handle @@ -240,6 +250,7 @@ const LegacyDashboardsVariableDropdown: FC = ({ id, name, timespan, variable?.includeAll, options, + accessCheckLoading, ]); useEffect(() => { @@ -309,7 +320,6 @@ const LegacyDashboardsVariableDropdown: FC = ({ id, name, // Expects to be inside of a Patternfly Split Component export const LegacyDashboardsAllVariableDropdowns: FC = () => { - const [namespace] = useActiveNamespace(); const { plugin } = useMonitoring(); const variables = useSelector( @@ -323,7 +333,7 @@ export const LegacyDashboardsAllVariableDropdowns: FC = () => { return ( {Object.keys(variables).map((name: string) => ( - + ))} ); @@ -342,5 +352,4 @@ export type Variable = { type VariableDropdownProps = { id: string; name: string; - namespace?: string; }; diff --git a/web/src/components/dashboards/legacy/single-stat.tsx b/web/src/components/dashboards/legacy/single-stat.tsx index e3f6d7795..758993678 100644 --- a/web/src/components/dashboards/legacy/single-stat.tsx +++ b/web/src/components/dashboards/legacy/single-stat.tsx @@ -47,6 +47,7 @@ import { t_chart_color_yellow_500, } from '@patternfly/react-tokens'; import { PatternflyToken } from '../../types'; +import { useMonitoring } from '../../../hooks/useMonitoring'; const colorMap: Record = { 'super-light-blue': t_chart_color_blue_100, @@ -107,6 +108,7 @@ const SingleStat: FC = ({ customDataSource, namespace, panel, pollInterva } = panel; const { t } = useTranslation(process.env.I18N_NAMESPACE); + const { accessCheckLoading, useMetricsTenancy } = useMonitoring(); const [error, setError] = useState(); const [isLoading, setIsLoading] = useState(true); const [value, setValue] = useState(); @@ -118,16 +120,17 @@ const SingleStat: FC = ({ customDataSource, namespace, panel, pollInterva prometheusUrlProps: { endpoint: PrometheusEndpoint.QUERY, query, - namespace: namespace, + namespace, }, basePath: getPrometheusBasePath({ prometheus: 'cmo', + useTenancyPath: useMetricsTenancy, basePathOverride: customDataSource?.basePath, }), }); const tick = () => { - if (!url) { + if (!url || accessCheckLoading) { return; } safeFetch(url) @@ -145,7 +148,7 @@ const SingleStat: FC = ({ customDataSource, namespace, panel, pollInterva }); }; - usePoll(tick, pollInterval, query); + usePoll(tick, pollInterval, query, accessCheckLoading, useMetricsTenancy); const filteredVMs = valueMaps?.filter((vm) => vm.op === '='); const valueMap = diff --git a/web/src/components/dashboards/legacy/table.tsx b/web/src/components/dashboards/legacy/table.tsx index fb3803448..363abd514 100644 --- a/web/src/components/dashboards/legacy/table.tsx +++ b/web/src/components/dashboards/legacy/table.tsx @@ -26,7 +26,8 @@ import { formatNumber } from '../../format'; import TablePagination from '../../table-pagination'; import { ColumnStyle, Panel } from './types'; import { CustomDataSource } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/dashboard-data-source'; -import { GraphEmpty } from '../../../components/console/graphs/graph-empty'; +import { GraphEmpty } from '../../console/graphs/graph-empty'; +import { useMonitoring } from '../../../hooks/useMonitoring'; type AugmentedColumnStyle = ColumnStyle & { className?: string; @@ -66,6 +67,7 @@ const perPageOptions: PerPageOptions[] = [5, 10, 20, 50, 100].map((n) => ({ const Table: FC = ({ customDataSource, panel, pollInterval, queries, namespace }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); + const { accessCheckLoading, useMetricsTenancy } = useMonitoring(); const [error, setError] = useState(); const [isLoading, setLoading] = useState(true); @@ -79,6 +81,9 @@ const Table: FC = ({ customDataSource, panel, pollInterval, queries, name const safeFetch = useCallback(useSafeFetch(), []); const tick = () => { + if (accessCheckLoading) { + return; + } const allPromises = _.map(queries, (query) => _.isEmpty(query) ? Promise.resolve() @@ -91,6 +96,7 @@ const Table: FC = ({ customDataSource, panel, pollInterval, queries, name }, basePath: getPrometheusBasePath({ prometheus: 'cmo', + useTenancyPath: useMetricsTenancy, basePathOverride: customDataSource?.basePath, }), }), @@ -132,7 +138,7 @@ const Table: FC = ({ customDataSource, panel, pollInterval, queries, name }); }; - usePoll(tick, pollInterval, queries); + usePoll(tick, pollInterval, queries, useMetricsTenancy, accessCheckLoading); if (isLoading) { return ; } diff --git a/web/src/components/dashboards/legacy/useLegacyDashboards.ts b/web/src/components/dashboards/legacy/useLegacyDashboards.ts index 62398af6d..6e2ae65d5 100644 --- a/web/src/components/dashboards/legacy/useLegacyDashboards.ts +++ b/web/src/components/dashboards/legacy/useLegacyDashboards.ts @@ -21,7 +21,8 @@ import { import { CombinedDashboardMetadata } from '../perses/hooks/useDashboardsData'; import { useNavigate } from 'react-router-dom-v5-compat'; import { QueryParams } from '../../query-params'; -import { NumberParam, StringParam, useQueryParam } from 'use-query-params'; +import { NumberParam, useQueryParam } from 'use-query-params'; +import { ALL_NAMESPACES_KEY } from '../../utils'; export const useLegacyDashboards = (namespace: string, urlBoard: string) => { const { t } = useTranslation('plugin__monitoring-plugin'); @@ -29,50 +30,20 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { // eslint-disable-next-line react-hooks/exhaustive-deps const safeFetch = useCallback(useSafeFetch(), []); - const [legacyDashboards, setLegacyDashboards] = useState([]); + const [unfilteredLegacyDashboards, setUnfilteredLegacyDashboards] = useState([]); const [legacyDashboardsError, setLegacyDashboardsError] = useState(); - const [dashboardParam] = useQueryParam(QueryParams.Dashboard, StringParam); const [refreshInterval] = useQueryParam(QueryParams.RefreshInterval, NumberParam); const [legacyDashboardsLoading, , , setLegacyDashboardsLoaded] = useBoolean(true); - const [initialLoad, , , setInitialLoaded] = useBoolean(true); + const [initialLoad, , setInitialUnloaded, setInitialLoaded] = useBoolean(true); const dispatch = useDispatch(); const navigate = useNavigate(); - const legacyDashboard = useMemo(() => { - if (perspective === 'dev') { - return dashboardParam; - } - return urlBoard; - }, [perspective, dashboardParam, urlBoard]); useEffect(() => { safeFetch('/api/console/monitoring-dashboard-config') .then((response) => { setLegacyDashboardsLoaded(); setLegacyDashboardsError(undefined); - let items = response.items; - if (namespace) { - items = _.filter( - items, - (item) => item.metadata?.labels['console.openshift.io/odc-dashboard'] === 'true', - ); - } - const getBoardData = (item): Board => { - try { - return { - data: JSON.parse(_.values(item.data)[0]), - name: item.metadata.name, - }; - } catch { - setLegacyDashboardsError( - t('Could not parse JSON data for dashboard "{{dashboard}}"', { - dashboard: item.metadata.name, - }), - ); - return { data: undefined, name: item?.metadata?.name }; - } - }; - const newBoards = _.sortBy(_.map(items, getBoardData), (v) => _.toLower(v?.data?.title)); - setLegacyDashboards(newBoards); + setUnfilteredLegacyDashboards(response.items); }) .catch((err) => { setLegacyDashboardsLoaded(); @@ -80,10 +51,37 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { setLegacyDashboardsError(_.get(err, 'json.error', err.message)); } }); - }, [namespace, safeFetch, setLegacyDashboardsLoaded, t]); + }, [safeFetch, setLegacyDashboardsLoaded]); + + // Move namespace filtering out of the fetch response call to avoid race conditions + const legacyDashboards = useMemo(() => { + let items = unfilteredLegacyDashboards; + if (namespace && namespace !== ALL_NAMESPACES_KEY) { + items = _.filter( + items, + (item) => item.metadata?.labels['console.openshift.io/odc-dashboard'] === 'true', + ); + } + const getBoardData = (item): Board => { + try { + return { + data: JSON.parse(_.values(item.data)[0]), + name: item.metadata.name, + }; + } catch { + setLegacyDashboardsError( + t('Could not parse JSON data for dashboard "{{dashboard}}"', { + dashboard: item.metadata.name, + }), + ); + return { data: undefined, name: item?.metadata?.name }; + } + }; + return _.sortBy(_.map(items, getBoardData), (v) => _.toLower(v?.data?.title)); + }, [namespace, unfilteredLegacyDashboards, setLegacyDashboardsError, t]); const legacyRows = useMemo(() => { - const data = _.find(legacyDashboards, { name: legacyDashboard })?.data; + const data = _.find(legacyDashboards, { name: urlBoard })?.data; return data?.rows?.length ? data.rows @@ -101,17 +99,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { } return acc; }, []) ?? []; - }, [legacyDashboard, legacyDashboards]); - - useEffect(() => { - // Dashboard query argument is only set in dev perspective, so skip for admin - if (perspective !== 'dev') { - return; - } - const allVariables = getAllVariables(legacyDashboards, legacyDashboard, namespace); - dispatch(dashboardsPatchAllVariables(allVariables)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [namespace, legacyDashboard]); + }, [urlBoard, legacyDashboards]); // Homogenize data needed for dashboards dropdown between legacy and perses dashboards // to enable both to use the same component @@ -140,10 +128,9 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { const queryArguments = getAllQueryArguments(); const params = new URLSearchParams(queryArguments); - let url = getLegacyDashboardsUrl(perspective, newBoard, namespace); - url = `${url}${perspective === 'dev' ? '&' : '?'}${params.toString()}`; + const url = `${getLegacyDashboardsUrl(perspective, newBoard)}?${params.toString()}`; - if (newBoard !== legacyDashboard || initialLoad) { + if (newBoard !== urlBoard || initialLoad) { if (params.get(QueryParams.Dashboard) !== newBoard) { navigate(url, { replace: true }); } @@ -165,7 +152,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { }, [ perspective, - legacyDashboard, + urlBoard, dispatch, navigate, namespace, @@ -177,15 +164,24 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { useEffect(() => { if ( - (!legacyDashboard || - !legacyDashboards.some((legacyBoard) => legacyBoard.name === legacyDashboard) || + (!urlBoard || + !legacyDashboards.some((legacyBoard) => legacyBoard.name === urlBoard) || initialLoad) && !_.isEmpty(legacyDashboards) ) { - changeLegacyDashboard(legacyDashboard || legacyDashboards?.[0]?.name); + changeLegacyDashboard(urlBoard || legacyDashboards?.[0]?.name); setInitialLoaded(); } - }, [legacyDashboards, changeLegacyDashboard, initialLoad, setInitialLoaded, legacyDashboard]); + }, [legacyDashboards, changeLegacyDashboard, initialLoad, setInitialLoaded, urlBoard]); + + useEffect(() => { + // Basically perform a full reload when changing a namespace to force the variables and the + // dashboard to reset. This is needed for when we transition between ALL_NS and a normal + // namespace, but is performed quickly and should help insure consistency when transitioning + // between any namespaces + setInitialUnloaded(); + /* eslint-disable react-hooks/exhaustive-deps */ + }, [namespace]); // Clear variables on unmount useEffect(() => { @@ -201,7 +197,7 @@ export const useLegacyDashboards = (namespace: string, urlBoard: string) => { legacyRows, legacyDashboardsMetadata, changeLegacyDashboard, - legacyDashboard, + legacyDashboard: urlBoard, }; }; @@ -227,11 +223,14 @@ const getAllVariables = (boards: Board[], newBoardName: string, namespace: strin allVariables[v.name] = { datasource: v.datasource, includeAll: !!v.includeAll, - isHidden: namespace && v.name === 'namespace' ? true : v.hide !== 0, - isLoading: namespace ? v.type === 'query' && !namespace : v.type === 'query', + isHidden: v.name === 'namespace' && namespace !== ALL_NAMESPACES_KEY ? true : v.hide !== 0, + isLoading: v.name === 'namespace' ? false : v.type === 'query', options: _.map(v.options, 'value'), query: v.type === 'query' ? v.query : undefined, - value: namespace && v.name === 'namespace' ? namespace : value || v.options?.[0]?.value, + value: + v.name === 'namespace' && namespace !== ALL_NAMESPACES_KEY + ? namespace + : value || v.options?.[0]?.value, }; } }); diff --git a/web/src/components/dashboards/legacy/useOpenshiftProject.ts b/web/src/components/dashboards/legacy/useOpenshiftProject.ts new file mode 100644 index 000000000..10a8cfb69 --- /dev/null +++ b/web/src/components/dashboards/legacy/useOpenshiftProject.ts @@ -0,0 +1,74 @@ +import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; +import { useCallback, useEffect } from 'react'; +import { QueryParams } from '../../query-params'; +import { StringParam, useQueryParam } from 'use-query-params'; +import { useDispatch, useSelector } from 'react-redux'; +import { dashboardsPatchVariable } from '../../../store/actions'; +import { MonitoringState } from '../../../store/store'; +import { getObserveState } from '../../hooks/usePerspective'; +import { useMonitoring } from '../../../hooks/useMonitoring'; +import { ALL_NAMESPACES_KEY } from '../../utils'; + +export const useOpenshiftProject = () => { + const [activeNamespace, setActiveNamespace] = useActiveNamespace(); + const [openshiftProject, setOpenshiftProject] = useQueryParam( + QueryParams.OpenshiftProject, + StringParam, + ); + const { plugin } = useMonitoring(); + const variableNamespace = useSelector( + (state: MonitoringState) => + getObserveState(plugin, state).dashboards.variables['namespace']?.value ?? '', + ); + const dispatch = useDispatch(); + + useEffect(() => { + // If the URL parameter is set, but the activeNamespace doesn't match it, then + // set the activeNamespace to match the URL parameter + if (openshiftProject && openshiftProject !== activeNamespace) { + setActiveNamespace(openshiftProject); + if (variableNamespace !== openshiftProject && openshiftProject !== ALL_NAMESPACES_KEY) { + dispatch( + dashboardsPatchVariable('namespace', { + // Dashboards space variable shouldn't use the ALL_NAMESPACES_KEY + value: openshiftProject, + }), + ); + } + return; + } + if (!openshiftProject) { + setOpenshiftProject(activeNamespace); + if (variableNamespace !== activeNamespace && openshiftProject !== ALL_NAMESPACES_KEY) { + // Dashboards space variable shouldn't use the ALL_NAMESPACES_KEY + dispatch( + dashboardsPatchVariable('namespace', { + value: activeNamespace, + }), + ); + } + return; + } + }, [ + activeNamespace, + setActiveNamespace, + openshiftProject, + setOpenshiftProject, + dispatch, + variableNamespace, + ]); + + const setProject = useCallback( + (namespace: string) => { + setActiveNamespace(namespace); + setOpenshiftProject(namespace); + dispatch(dashboardsPatchVariable('namespace', { value: namespace })); + }, + [setActiveNamespace, setOpenshiftProject, dispatch], + ); + + return { + project: openshiftProject, + setProject, + }; +}; diff --git a/web/src/components/dashboards/perses/PersesWrapper.tsx b/web/src/components/dashboards/perses/PersesWrapper.tsx index 043fc1efd..3e622d11e 100644 --- a/web/src/components/dashboards/perses/PersesWrapper.tsx +++ b/web/src/components/dashboards/perses/PersesWrapper.tsx @@ -1,3 +1,4 @@ +import '../../../perses-config'; import { ThemeOptions, ThemeProvider } from '@mui/material'; import { ChartThemeColor, getThemeColors } from '@patternfly/react-charts/victory'; import { @@ -22,35 +23,41 @@ import { } from '@perses-dev/dashboards'; import { DataQueriesProvider, + PluginLoader, PluginRegistry, TimeRangeProviderWithQueryParams, useInitialRefreshInterval, useInitialTimeRange, usePluginBuiltinVariableDefinitions, + ValidationProvider, } from '@perses-dev/plugin-system'; import React, { useMemo } from 'react'; import { usePatternFlyTheme } from '../../hooks/usePatternflyTheme'; import { OcpDatasourceApi } from './datasource-api'; import { PERSES_PROXY_BASE_PATH, useFetchPersesDashboard } from './perses-client'; -import { CachedDatasourceAPI } from './perses/datasource-api'; +import { CachedDatasourceAPI } from './perses/datasource-cache-api'; import { chart_color_blue_100, - chart_color_blue_200, chart_color_blue_300, + chart_color_blue_400, + chart_color_blue_500, t_color_gray_95, t_color_white, + t_global_background_color_100, + t_global_background_color_400, } from '@patternfly/react-tokens'; import { QueryParams } from '../../query-params'; import { StringParam, useQueryParam } from 'use-query-params'; import { useTranslation } from 'react-i18next'; import { LoadingBox } from '../../../components/console/console-shared/src/components/loading/LoadingBox'; -import { pluginLoader } from './persesPluginsLoader'; +import { remotePluginLoader } from '@perses-dev/plugin-system'; // Override eChart defaults with PatternFly colors. -const patternflyBlue300 = '#2b9af3'; -const patternflyBlue400 = '#0066cc'; -const patternflyBlue500 = '#004080'; -const patternflyBlue600 = '#002952'; +const patternflyBlue100 = chart_color_blue_100.value; +const patternflyBlue300 = chart_color_blue_300.value; +const patternflyBlue400 = chart_color_blue_400.value; +const patternflyBlue500 = chart_color_blue_500.value; +const patternflyBlue600 = chart_color_blue_100.value; const defaultPaletteColors = [patternflyBlue400, patternflyBlue500, patternflyBlue600]; const chartColorScale = getThemeColors(ChartThemeColor.multiUnordered).chart.colorScale; @@ -71,7 +78,9 @@ interface PersesWrapperProps { const mapPatterflyThemeToMUI = (theme: 'light' | 'dark'): ThemeOptions => { const isDark = theme === 'dark'; const primaryTextColor = isDark ? t_color_white.value : t_color_gray_95.value; - const primaryBackgroundColor = 'var(--pf-t--global--background--color--primary--default)'; + const primaryBackgroundColor = isDark + ? t_global_background_color_400.value + : t_global_background_color_100.value; return { typography: { @@ -86,17 +95,17 @@ const mapPatterflyThemeToMUI = (theme: 'light' | 'dark'): ThemeOptions => { }, h2: { // Panel Group Heading - color: 'var(--pf-t--global--text--color--brand--default)', fontWeight: 'var(--pf-t--global--font--weight--body--default)', fontSize: 'var(--pf-t--global--font--size--600)', }, }, palette: { + mode: isDark ? 'dark' : 'light', // Help CodeMirror detect theme mode primary: { light: chart_color_blue_100.value, - main: chart_color_blue_200.value, - dark: chart_color_blue_300.value, - contrastText: primaryTextColor, + main: patternflyBlue300, + dark: patternflyBlue500, + contrastText: t_color_white.value, }, secondary: { main: primaryTextColor, @@ -133,13 +142,6 @@ const mapPatterflyThemeToMUI = (theme: 'light' | 'dark'): ThemeOptions => { }, }, }, - MuiSvgIcon: { - styleOverrides: { - root: { - color: theme === 'dark' ? t_color_white.value : t_color_gray_95.value, - }, - }, - }, MuiCard: { styleOverrides: { root: { @@ -154,9 +156,9 @@ const mapPatterflyThemeToMUI = (theme: 'light' | 'dark'): ThemeOptions => { '&.MuiCardHeader-root': { borderBottom: 'none', paddingBlockEnd: 'var(--pf-t--global--spacer--md)', - paddingBlockStart: 'var(--pf-t--global--spacer--lg)', - paddingLeft: 'var(--pf-t--global--spacer--lg)', - paddingRight: 'var(--pf-t--global--spacer--lg)', + paddingBlockStart: 'var(--pf-t--global--spacer--md)', + paddingLeft: 'var(--pf-t--global--spacer--md)', + paddingRight: 'var(--pf-t--global--spacer--md)', }, }, }, @@ -167,9 +169,9 @@ const mapPatterflyThemeToMUI = (theme: 'light' | 'dark'): ThemeOptions => { '&.MuiCardContent-root': { borderTop: 'none', '&:last-child': { - paddingBottom: 'var(--pf-t--global--spacer--lg)', - paddingLeft: 'var(--pf-t--global--spacer--lg)', - paddingRight: 'var(--pf-t--global--spacer--lg)', + paddingBottom: 'var(--pf-t--global--spacer--md)', + paddingLeft: 'var(--pf-t--global--spacer--sm)', + paddingRight: 'var(--pf-t--global--spacer--md)', }, }, }, @@ -201,10 +203,166 @@ const mapPatterflyThemeToMUI = (theme: 'light' | 'dark'): ThemeOptions => { }, }, }, + MuiButton: { + styleOverrides: { + root: { + '&.MuiButton-colorPrimary': { + borderRadius: 'var(--pf-t--global--border--radius--pill)', + borderColor: 'var(--pf-t--global--border--color--default)', + color: isDark ? patternflyBlue100 : patternflyBlue300, + }, + // Buttons with colored backgrounds should have white text + '&.MuiButton-contained.MuiButton-colorPrimary': { + color: t_color_white.value, + }, + }, + }, + }, + MuiButtonGroup: { + styleOverrides: { + root: { + // Remove border-radius from button groups to prevent pill shape + '& .MuiButton-root': { + borderRadius: 'var(--pf-t--global--border--radius--tiny) !important', + }, + }, + grouped: { + borderRadius: 'var(--pf-t--global--border--radius--tiny) !important', + }, + firstButton: { + borderRadius: 'var(--pf-t--global--border--radius--tiny) !important', + }, + lastButton: { + borderRadius: 'var(--pf-t--global--border--radius--tiny) !important', + }, + middleButton: { + borderRadius: 'var(--pf-t--global--border--radius--tiny) !important', + }, + }, + }, + MuiFormLabel: { + styleOverrides: { + root: { + // Align placeholder text in Editing Panel + '&.MuiFormLabel-root.MuiInputLabel-root.MuiInputLabel-formControl.MuiInputLabel-animated.MuiInputLabel-sizeMedium.MuiInputLabel-outlined.MuiFormLabel-colorPrimary[data-shrink="false"]': + { + top: '-7px', + }, + }, + }, + }, + MuiTab: { + styleOverrides: { + root: { + // Selected tab color + '&.MuiButtonBase-root.MuiTab-root.Mui-selected': { + color: isDark ? patternflyBlue100 : patternflyBlue300, + }, + }, + }, + }, + MuiTabs: { + styleOverrides: { + indicator: { + // Tab indicator should match color of selected MuiTab + '&.MuiTabs-indicator': { + backgroundColor: isDark ? patternflyBlue100 : patternflyBlue300, + }, + }, + }, + }, + MuiDrawer: { + styleOverrides: { + paper: { + // Editing Variables Panel + '&.MuiDrawer-paper.MuiDrawer-paperAnchorRight': { + borderTopLeftRadius: 'var(--pf-t--global--border--radius--medium) !important', + borderBottomLeftRadius: 'var(--pf-t--global--border--radius--medium) !important', + borderTopRightRadius: '0 !important', + borderBottomRightRadius: '0 !important', + }, + '&.MuiDrawer-paper.MuiDrawer-paperAnchorLeft': { + borderTopRightRadius: 'var(--pf-t--global--border--radius--medium) !important', + borderBottomRightRadius: 'var(--pf-t--global--border--radius--medium) !important', + borderTopLeftRadius: '0 !important', + borderBottomLeftRadius: '0 !important', + }, + // Editing Variable Panel - drawer cancel button + '& .MuiButton-colorSecondary': { + borderRadius: 'var(--pf-t--global--border--radius--pill) !important', + }, + }, + }, + }, + MuiAccordion: { + styleOverrides: { + root: { + // Editing Variables Panel + borderRadius: 'var(--pf-t--global--border--radius--medium) !important', + '&.MuiAccordion-root': { + borderRadius: 'var(--pf-t--global--border--radius--medium) !important', + }, + // Hide the separator line above accordion + '&::before': { + opacity: '0 !important', + }, + backgroundColor: + 'var(--pf-t--global--background--color--action--plain--default) !important', + }, + }, + }, + MuiAccordionSummary: { + styleOverrides: { + root: { + // Editing Variables Panel - accordion header + borderRadius: 'var(--pf-t--global--border--radius--medium) !important', + backgroundColor: 'var(--pf-t--global--background--color--floating--default) !important', + '&.Mui-expanded': { + borderBottomLeftRadius: '0 !important', + borderBottomRightRadius: '0 !important', + borderTopLeftRadius: 'var(--pf-t--global--border--radius--medium) !important', + borderTopRightRadius: 'var(--pf-t--global--border--radius--medium) !important', + }, + }, + }, + }, + MuiAccordionDetails: { + styleOverrides: { + root: { + // Editing Variables Panel - accordion contents + backgroundColor: 'var(--pf-t--global--background--color--floating--default) !important', + borderBottomLeftRadius: 'var(--pf-t--global--border--radius--medium) !important', + borderBottomRightRadius: 'var(--pf-t--global--border--radius--medium) !important', + borderTopLeftRadius: '0 !important', + borderTopRightRadius: '0 !important', + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + // Uniform font weight in all table cells + fontWeight: 'var(--pf-t--global--font--weight--body--default) !important', + }, + }, + }, }, }; }; +export function useRemotePluginLoader(): PluginLoader { + const pluginLoader = useMemo( + () => + remotePluginLoader({ + baseURL: window.PERSES_PLUGIN_ASSETS_PATH, + apiPrefix: window.PERSES_PLUGIN_ASSETS_PATH, + }), + [], + ); + + return pluginLoader; +} + export function PersesWrapper({ children, project }: PersesWrapperProps) { const { theme } = usePatternFlyTheme(); const [dashboardName] = useQueryParam(QueryParams.Dashboard, StringParam); @@ -225,13 +383,14 @@ export function PersesWrapper({ children, project }: PersesWrapperProps) { }, }); + const pluginLoader = useRemotePluginLoader(); + return ( {!project ? ( @@ -257,8 +416,14 @@ function InnerWrapper({ children, project, dashboardName }) { const DEFAULT_DASHBOARD_DURATION = '30m'; const DEFAULT_REFRESH_INTERVAL = '0s'; - const initialTimeRange = useInitialTimeRange(DEFAULT_DASHBOARD_DURATION); - const initialRefreshInterval = useInitialRefreshInterval(DEFAULT_REFRESH_INTERVAL); + const dashboardDuration = persesDashboard?.spec?.duration; + const dashboardTimeInterval = persesDashboard?.spec?.refreshInterval; + + const effectiveDuration = dashboardDuration || DEFAULT_DASHBOARD_DURATION; + const effectiveRefreshInterval = dashboardTimeInterval || DEFAULT_REFRESH_INTERVAL; + + const initialTimeRange = useInitialTimeRange(effectiveDuration); + const initialRefreshInterval = useInitialRefreshInterval(effectiveRefreshInterval); const builtinVariables = useMemo(() => { const result = [ @@ -299,17 +464,6 @@ function InnerWrapper({ children, project, dashboardName }) { return ; } - let clearedDashboardResource: DashboardResource | undefined; - if (Array.isArray(persesDashboard)) { - if (persesDashboard.length === 0) { - clearedDashboardResource = undefined; - } else { - clearedDashboardResource = persesDashboard[0]; - } - } else { - clearedDashboardResource = persesDashboard; - } - return ( - - {clearedDashboardResource ? ( + + {persesDashboard ? ( - {children} + {children} ) : ( <>{children} diff --git a/web/src/components/dashboards/perses/ToastProvider.tsx b/web/src/components/dashboards/perses/ToastProvider.tsx new file mode 100644 index 000000000..024ebce7c --- /dev/null +++ b/web/src/components/dashboards/perses/ToastProvider.tsx @@ -0,0 +1,67 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Alert, + AlertProps, + AlertGroup, + AlertActionCloseButton, + AlertVariant, +} from '@patternfly/react-core'; + +interface ToastItem { + key: string; + title: string; + variant: AlertProps['variant']; +} + +interface ToastContextType { + addAlert: (title: string, variant: AlertProps['variant']) => void; + removeAlert: (key: string) => void; + alerts: ToastItem[]; +} + +const ToastContext = createContext(undefined); + +export const useToast = () => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const context = useContext(ToastContext); + if (!context) { + throw new Error(t('useToast must be used within ToastProvider')); + } + return context; +}; + +export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [alerts, setAlerts] = useState([]); + + const addAlert = (title: string, variant: AlertProps['variant']) => { + const key = new Date().getTime().toString(); + setAlerts((prevAlerts) => [{ title, variant, key }, ...prevAlerts]); + }; + + const removeAlert = (key: string) => { + setAlerts((prevAlerts) => prevAlerts.filter((alert) => alert.key !== key)); + }; + + return ( + + {children} + + {alerts.map(({ key, variant, title }) => ( + removeAlert(key)} + /> + } + key={key} + /> + ))} + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-action-modals.tsx b/web/src/components/dashboards/perses/dashboard-action-modals.tsx new file mode 100644 index 000000000..c3b56fbef --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-action-modals.tsx @@ -0,0 +1,557 @@ +import { + Button, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + FormGroup, + TextInput, + FormHelperText, + HelperText, + HelperTextItem, + ValidatedOptions, + HelperTextItemVariant, + ModalVariant, + AlertVariant, + Stack, + StackItem, + Spinner, +} from '@patternfly/react-core'; +import { TypeaheadSelect, TypeaheadSelectOption } from '@patternfly/react-templates'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useUpdateDashboardMutation, + useCreateDashboardMutation, + useDeleteDashboardMutation, + useCreateProjectMutation, +} from './dashboard-api'; +import { + renameDashboardDialogValidationSchema, + RenameDashboardValidationType, + createDashboardDialogValidationSchema, + CreateDashboardValidationType, + useDashboardValidationSchema, +} from './dashboard-action-validations'; + +import { Controller, FormProvider, SubmitHandler, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { DashboardResource, getResourceExtendedDisplayName } from '@perses-dev/core'; +import { useToast } from './ToastProvider'; +import { generateMetadataName } from './dashboard-utils'; +import { useEditableProjects } from './hooks/useEditableProjects'; +import { usePerses } from './hooks/usePerses'; +import { t_global_spacer_200, t_global_font_weight_200 } from '@patternfly/react-tokens'; +import { useNavigate } from 'react-router-dom-v5-compat'; +import { usePerspective, getDashboardUrl } from '../../hooks/usePerspective'; + +const formGroupStyle = { + fontWeight: t_global_font_weight_200.value, +} as React.CSSProperties; + +const LabelSpacer = () => { + return
; +}; + +interface ActionModalProps { + dashboard: DashboardResource; + isOpen: boolean; + onClose: () => void; + handleModalClose: () => void; +} + +export const RenameActionModal = ({ dashboard, isOpen, onClose }: ActionModalProps) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const { addAlert } = useToast(); + + const form = useForm({ + resolver: zodResolver(renameDashboardDialogValidationSchema(t)), + mode: 'onBlur', + defaultValues: { dashboardName: '' }, + }); + + const updateDashboardMutation = useUpdateDashboardMutation(); + + if (!dashboard) { + return null; + } + + const processForm: SubmitHandler = (data) => { + if (dashboard.spec?.display) { + dashboard.spec.display.name = data.dashboardName; + } else { + dashboard.spec.display = { name: data.dashboardName }; + } + + updateDashboardMutation.mutate(dashboard, { + onSuccess: (updatedDashboard: DashboardResource) => { + const msg = t( + `Dashboard ${getResourceExtendedDisplayName( + updatedDashboard, + )} has been successfully updated`, + ); + addAlert(msg, AlertVariant.success); + handleClose(); + }, + onError: (err) => { + const msg = t(`Could not rename dashboard. ${err}`); + addAlert(msg, AlertVariant.danger); + throw err; + }, + }); + }; + + const handleClose = () => { + onClose(); + form.reset({ dashboardName: '' }); + }; + + return ( + + + +
+ + ( + + + + {fieldState.error && ( + + + } + variant={HelperTextItemVariant.error} + > + {fieldState.error.message} + + + + )} + + )} + /> + + + + + +
+
+
+ ); +}; + +export const DuplicateActionModal = ({ dashboard, isOpen, onClose }: ActionModalProps) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const { addAlert } = useToast(); + + const navigate = useNavigate(); + const { perspective } = usePerspective(); + + const { + editableProjects, + allProjects, + hasEditableProject, + permissionsLoading, + permissionsError, + } = useEditableProjects(); + + const { persesProjects } = usePerses(); + const createProjectMutation = useCreateProjectMutation(); + + const defaultProject = useMemo(() => { + if (!dashboard) return ''; + + if (dashboard.metadata.project && editableProjects.includes(dashboard.metadata.project)) { + return dashboard.metadata.project; + } + + return allProjects[0] || ''; + }, [dashboard, editableProjects, allProjects]); + + const form = useForm({ + resolver: zodResolver(createDashboardDialogValidationSchema(t)), + mode: 'onBlur', + defaultValues: { + projectName: defaultProject, + dashboardName: '', + }, + }); + + const selectedProjectName = form.watch('projectName'); + const dashboardName = form.watch('dashboardName'); + + const { schema: dynamicValidationSchema, isSchemaLoading } = useDashboardValidationSchema( + selectedProjectName, + t, + ); + + const projectOptions = useMemo(() => { + if (!editableProjects) { + return []; + } + return editableProjects.map((project) => ({ + content: project, + value: project, + selected: project === selectedProjectName, + })); + }, [editableProjects, selectedProjectName]); + + const createDashboardMutation = useCreateDashboardMutation(); + + React.useEffect(() => { + const isPerseProject = persesProjects?.some( + (project) => project.metadata?.name === selectedProjectName, + ); + + if (dynamicValidationSchema && selectedProjectName && !isSchemaLoading && isPerseProject) { + const currentValues = form.getValues(); + const result = dynamicValidationSchema.safeParse(currentValues); + + if (!result.success) { + const hasDashboardIssue = result.error.issues.some( + (issue) => issue.path[0] === 'dashboardName', + ); + + if (hasDashboardIssue) { + result.error.issues.forEach((issue) => { + if (issue.path[0] === 'dashboardName') { + form.setError('dashboardName', { + type: 'validate', + message: issue.message, + }); + } + }); + } else { + form.clearErrors('dashboardName'); + } + } else { + form.clearErrors('dashboardName'); + } + } else if (!isPerseProject && selectedProjectName) { + // Clear any existing validation errors for non-Perses projects + form.clearErrors('dashboardName'); + } + }, [ + selectedProjectName, + dynamicValidationSchema, + form, + dashboardName, + isSchemaLoading, + persesProjects, + ]); + + React.useEffect(() => { + if (isOpen && dashboard && editableProjects?.length > 0 && defaultProject) { + form.reset({ + projectName: defaultProject, + dashboardName: '', + }); + } + }, [isOpen, dashboard, defaultProject, editableProjects?.length, form]); + + if (!dashboard) { + return null; + } + + const processForm: SubmitHandler = async (data) => { + // Check if project exists, create it if it doesn't + const projectExists = persesProjects?.some( + (project) => project.metadata.name === data.projectName, + ); + + if (!projectExists) { + try { + await createProjectMutation.mutateAsync(data.projectName); + addAlert( + t('Project "{{project}}" created successfully', { project: data.projectName }), + 'success', + ); + } catch (projectError) { + const errorMessage = + projectError?.message || + t('Failed to create project "{{project}}". Please try again.', { + project: data.projectName, + }); + addAlert(t('Error creating project: {{error}}', { error: errorMessage }), 'danger'); + return; + } + } + + const newDashboard: DashboardResource = { + ...dashboard, + metadata: { + ...dashboard.metadata, + name: generateMetadataName(data.dashboardName), + project: data.projectName, + }, + spec: { + ...dashboard.spec, + display: { + ...dashboard.spec.display, + name: data.dashboardName, + }, + }, + }; + + createDashboardMutation.mutate(newDashboard, { + onSuccess: (createdDashboard: DashboardResource) => { + const msg = t( + `Dashboard ${getResourceExtendedDisplayName( + createdDashboard, + )} has been successfully created`, + ); + addAlert(msg, AlertVariant.success); + + handleClose(); + + const dashboardUrl = getDashboardUrl(perspective); + const dashboardParam = `dashboard=${createdDashboard.metadata.name}`; + const projectParam = `project=${createdDashboard.metadata.project}`; + const editModeParam = `edit=true`; + navigate(`${dashboardUrl}?${dashboardParam}&${projectParam}&${editModeParam}`); + }, + onError: (err) => { + const msg = t(`Could not duplicate dashboard. ${err}`); + addAlert(msg, AlertVariant.danger); + }, + }); + }; + + const handleClose = () => { + onClose(); + form.reset(); + }; + + const onProjectSelect = (_event: any, selection: string) => { + form.setValue('projectName', selection); + }; + + return ( + + + {permissionsLoading ? ( + + {t('Loading...')} + + ) : permissionsError ? ( + + + {t('Failed to load project permissions. Please refresh the page and try again.')} + + ) : ( + +
+ + + + ( + + + + {fieldState.error && ( + + + } + variant={HelperTextItemVariant.error} + > + {fieldState.error.message} + + + + )} + + )} + /> + + + ( + + + + t('No namespace found for "{{filter}}"', { filter }) + } + onClearSelection={() => { + form.setValue('projectName', ''); + }} + onSelect={onProjectSelect} + isCreatable={false} + maxMenuHeight="200px" + /> + {fieldState.error && ( + + + } + variant={HelperTextItemVariant.error} + > + {fieldState.error.message} + + + + )} + + )} + /> + + + + + + + +
+
+ )} +
+ ); +}; + +export const DeleteActionModal = ({ dashboard, isOpen, onClose }: ActionModalProps) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const { addAlert } = useToast(); + + const deleteDashboardMutation = useDeleteDashboardMutation(); + const dashboardName = dashboard?.spec?.display?.name ?? t('this dashboard'); + + const handleDeleteConfirm = async () => { + if (!dashboard) return; + + deleteDashboardMutation.mutate(dashboard, { + onSuccess: (deletedDashboard: DashboardResource) => { + const msg = t( + `Dashboard ${getResourceExtendedDisplayName( + deletedDashboard, + )} has been successfully deleted`, + ); + addAlert(msg, AlertVariant.success); + onClose(); + }, + onError: (err) => { + const msg = t(`Could not delete dashboard. ${err}`); + addAlert(msg, AlertVariant.danger); + throw err; + }, + }); + }; + + return ( + + + + {t('Are you sure you want to delete ')} + {dashboardName} + {t('? This action can not be undone.')} + + + + + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-action-validations.ts b/web/src/components/dashboards/perses/dashboard-action-validations.ts new file mode 100644 index 000000000..97d73d409 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-action-validations.ts @@ -0,0 +1,92 @@ +import { z } from 'zod'; +import { useMemo } from 'react'; +import { nameSchema } from '@perses-dev/core'; +import { useDashboardList } from './dashboard-api'; +import { generateMetadataName } from './dashboard-utils'; + +export const createDashboardDisplayNameValidationSchema = (t: (key: string) => string) => + z.string().min(1, t('Required')).max(75, t('Must be 75 or fewer characters long')); + +export const createDashboardDialogValidationSchema = (t: (key: string) => string) => + z.object({ + projectName: nameSchema, + dashboardName: createDashboardDisplayNameValidationSchema(t), + }); + +export const renameDashboardDialogValidationSchema = (t: (key: string) => string) => + z.object({ + dashboardName: createDashboardDisplayNameValidationSchema(t), + }); + +export type CreateDashboardValidationType = z.infer< + ReturnType +>; +export type RenameDashboardValidationType = z.infer< + ReturnType +>; + +export interface DashboardValidationSchema { + schema?: z.ZodSchema; + isSchemaLoading: boolean; + hasSchemaError: boolean; +} + +// Validate dashboard name and check if it doesn't already exist +export function useDashboardValidationSchema( + projectName?: string, + t?: (key: string, options?: any) => string, +): DashboardValidationSchema { + const { + data: dashboards, + isLoading: isDashboardsLoading, + isError, + } = useDashboardList({ project: projectName }); + return useMemo((): DashboardValidationSchema => { + if (isDashboardsLoading) + return { + schema: undefined, + isSchemaLoading: true, + hasSchemaError: false, + }; + + if (isError) { + return { + hasSchemaError: true, + isSchemaLoading: false, + schema: undefined, + }; + } + + if (!dashboards?.length) + return { + schema: createDashboardDialogValidationSchema(t), + isSchemaLoading: false, + hasSchemaError: false, + }; + + const refinedSchema = createDashboardDialogValidationSchema(t).refine( + (schema) => { + return !(dashboards ?? []).some((dashboard) => { + return ( + dashboard.metadata.project.toLowerCase() === schema.projectName.toLowerCase() && + dashboard.metadata.name.toLowerCase() === + generateMetadataName(schema.dashboardName).toLowerCase() + ); + }); + }, + (schema) => ({ + // eslint-disable-next-line max-len + message: t + ? t(`Dashboard name '{{dashboardName}}' already exists in '{{projectName}}' project!`, { + dashboardName: schema.dashboardName, + projectName: schema.projectName, + }) + : // eslint-disable-next-line max-len + `Dashboard name '${schema.dashboardName}' already exists in '${schema.projectName}' project!`, + path: ['dashboardName'], + }), + ); + + return { schema: refinedSchema, isSchemaLoading: false, hasSchemaError: false }; + }, [dashboards, isDashboardsLoading, isError, t]); +} diff --git a/web/src/components/dashboards/perses/dashboard-actions-menu.tsx b/web/src/components/dashboards/perses/dashboard-actions-menu.tsx new file mode 100644 index 000000000..e4e83fdac --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-actions-menu.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import { + Dropdown, + DropdownList, + DropdownItem, + MenuToggle, + MenuToggleElement, + MenuToggleAction, + Tooltip, +} from '@patternfly/react-core'; +import { useTranslation } from 'react-i18next'; +import { useEditableProjects } from './hooks/useEditableProjects'; +import { DashboardCreateDialog } from './dashboard-create-dialog'; +import { DashboardImportDialog } from './dashboard-import-dialog'; +import { persesDashboardDataTestIDs } from '../../data-test'; + +export const DashboardActionsMenu: React.FunctionComponent = () => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const { hasEditableProject, permissionsLoading } = useEditableProjects(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [isImportModalOpen, setIsImportModalOpen] = useState(false); + + const disabled = permissionsLoading || !hasEditableProject; + + const handleCreateClick = () => { + setIsCreateModalOpen(true); + setIsDropdownOpen(false); + }; + + const handleImportClick = () => { + setIsImportModalOpen(true); + setIsDropdownOpen(false); + }; + + const onToggleClick = () => { + setIsDropdownOpen(!isDropdownOpen); + }; + + const onSelect = () => { + setIsDropdownOpen(false); + }; + + const splitButton = ( + setIsDropdownOpen(open)} + toggle={(toggleRef: React.Ref) => ( + + {permissionsLoading ? t('Checking permissions...') : t('Create')} + , + ]} + onClick={onToggleClick} + isExpanded={isDropdownOpen} + isDisabled={disabled} + aria-label={t('Dashboard actions')} + /> + )} + > + + + {t('Import')} + + + + ); + + return ( + <> + {!permissionsLoading && !hasEditableProject ? ( + + {splitButton} + + ) : ( + splitButton + )} + setIsCreateModalOpen(false)} + /> + setIsImportModalOpen(false)} + /> + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-api.ts b/web/src/components/dashboards/perses/dashboard-api.ts new file mode 100644 index 000000000..69cf97e8b --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-api.ts @@ -0,0 +1,157 @@ +import { DashboardResource, ProjectResource } from '@perses-dev/core'; +import buildURL from './perses/url-builder'; +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; +import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { StatusError } from '@perses-dev/core'; +import { PERSES_PROXY_BASE_PATH } from './perses-client'; + +const resource = 'dashboards'; + +const updateDashboard = async (entity: DashboardResource): Promise => { + const url = buildURL({ + resource: resource, + project: entity.metadata.project, + name: entity.metadata.name, + }); + + return consoleFetchJSON.put(url, entity); +}; + +export const useUpdateDashboardMutation = (): UseMutationResult< + DashboardResource, + Error, + DashboardResource +> => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: [resource], + mutationFn: updateDashboard, + onSuccess: () => { + return queryClient.invalidateQueries({ queryKey: [resource] }); + }, + }); +}; + +const createDashboard = async (entity: DashboardResource): Promise => { + const url = buildURL({ + resource: resource, + project: entity.metadata.project, + }); + + return consoleFetchJSON.post(url, entity); +}; + +export const useCreateDashboardMutation = ( + onSuccess?: (data: DashboardResource, variables: DashboardResource) => Promise | unknown, +): UseMutationResult => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: [resource], + mutationFn: (dashboard) => createDashboard(dashboard), + onSuccess: onSuccess, + onSettled: () => { + return queryClient.invalidateQueries({ queryKey: [resource] }); + }, + }); +}; + +const deleteDashboard = async (entity: DashboardResource): Promise => { + const url = buildURL({ + resource: resource, + project: entity.metadata.project, + name: entity.metadata.name, + }); + + await consoleFetchJSON.delete(url); +}; + +export function useDeleteDashboardMutation(): UseMutationResult< + DashboardResource, + Error, + DashboardResource +> { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: [resource], + mutationFn: (entity: DashboardResource) => { + return deleteDashboard(entity).then(() => { + return entity; + }); + }, + onSuccess: (dashboard) => { + queryClient.removeQueries({ + queryKey: [resource, dashboard.metadata.project, dashboard.metadata.name], + }); + return queryClient.invalidateQueries({ queryKey: [resource] }); + }, + }); +} + +export const getDashboards = async ( + project?: string, + metadataOnly: boolean = false, +): Promise => { + const queryParams = new URLSearchParams(); + if (metadataOnly) { + queryParams.set('metadata_only', 'true'); + } + const url = buildURL({ resource: resource, project: project, queryParams: queryParams }); + + return consoleFetchJSON(url); +}; + +type DashboardListOptions = Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' +> & { + project?: string; + metadataOnly?: boolean; +}; + +export function useDashboardList( + options: DashboardListOptions, +): UseQueryResult { + return useQuery({ + queryKey: [resource, options.project, options.metadataOnly], + queryFn: () => { + return getDashboards(options.project, options.metadataOnly); + }, + ...options, + }); +} + +export const createPersesProject = async (projectName: string): Promise => { + const createProjectURL = '/api/v1/projects'; + const persesURL = `${PERSES_PROXY_BASE_PATH}${createProjectURL}`; + + const newProject: Partial = { + kind: 'Project', + metadata: { + name: projectName, + version: 0, + }, + spec: { + display: { + name: projectName, + }, + }, + }; + + return consoleFetchJSON.post(persesURL, newProject); +}; + +export const useCreateProjectMutation = (): UseMutationResult => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['projects'], + mutationFn: createPersesProject, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['projects'] }); + queryClient.invalidateQueries({ queryKey: [resource] }); + }, + }); +}; diff --git a/web/src/components/dashboards/perses/dashboard-app.tsx b/web/src/components/dashboards/perses/dashboard-app.tsx new file mode 100644 index 000000000..62b9e4616 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-app.tsx @@ -0,0 +1,205 @@ +import { ReactElement, ReactNode, useState, useCallback, useEffect } from 'react'; +import { Box } from '@mui/material'; +import { ChartsProvider, ErrorAlert, ErrorBoundary, useChartsTheme } from '@perses-dev/components'; +import { + DashboardResource, + EphemeralDashboardResource, + getResourceExtendedDisplayName, +} from '@perses-dev/core'; +import { useDatasourceStore } from '@perses-dev/plugin-system'; +import { + PanelDrawer, + Dashboard, + PanelGroupDialog, + DeletePanelGroupDialog, + DashboardDiscardChangesConfirmationDialog, + DeletePanelDialog, + EmptyDashboardProps, + EditJsonDialog, + SaveChangesConfirmationDialog, + LeaveDialog, +} from '@perses-dev/dashboards'; +import { + useDashboard, + useDiscardChangesConfirmationDialog, + useEditMode, +} from '@perses-dev/dashboards'; +import { OCPDashboardToolbar } from './dashboard-toolbar'; +import { useUpdateDashboardMutation } from './dashboard-api'; +import { useTranslation } from 'react-i18next'; +import { useToast } from './ToastProvider'; +import { useSearchParams } from 'react-router-dom-v5-compat'; + +export interface DashboardAppProps { + dashboardResource: DashboardResource | EphemeralDashboardResource; + emptyDashboardProps?: Partial; + isReadonly: boolean; + isVariableEnabled: boolean; + isDatasourceEnabled: boolean; + isCreating?: boolean; + isInitialVariableSticky?: boolean; + // If true, browser confirmation dialog will be shown + // when navigating away with unsaved changes (closing tab, ...). + isLeavingConfirmDialogEnabled?: boolean; + dashboardTitleComponent?: ReactNode; + onDiscard?: (entity: DashboardResource) => void; +} + +export const OCPDashboardApp = (props: DashboardAppProps): ReactElement => { + const { + dashboardResource, + emptyDashboardProps, + isReadonly, + isVariableEnabled, + isDatasourceEnabled, + isCreating, + isInitialVariableSticky, + isLeavingConfirmDialogEnabled, + onDiscard, + } = props; + + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + const chartsTheme = useChartsTheme(); + const { addAlert } = useToast(); + + const { isEditMode, setEditMode } = useEditMode(); + const { dashboard, setDashboard } = useDashboard(); + + const [originalDashboard, setOriginalDashboard] = useState< + DashboardResource | EphemeralDashboardResource | undefined + >(undefined); + const [saveErrorOccurred, setSaveErrorOccurred] = useState(false); + + useEffect(() => { + if (saveErrorOccurred && !isEditMode) { + setEditMode(true); + setSaveErrorOccurred(false); + } + }, [isEditMode, saveErrorOccurred, setEditMode]); + const { setSavedDatasources } = useDatasourceStore(); + + const { openDiscardChangesConfirmationDialog, closeDiscardChangesConfirmationDialog } = + useDiscardChangesConfirmationDialog(); + + const [searchParams] = useSearchParams(); + const isEdit = searchParams.get('edit'); + useEffect(() => { + if (isEdit === 'true') { + setEditMode(true); + } + }, [isEdit, setEditMode]); + + const handleDiscardChanges = (): void => { + // Reset to the original spec and exit edit mode + if (originalDashboard) { + setDashboard(originalDashboard); + } + setEditMode(false); + closeDiscardChangesConfirmationDialog(); + if (onDiscard) { + onDiscard(dashboard as unknown as DashboardResource); + } + }; + + const onEditButtonClick = (): void => { + setEditMode(true); + setOriginalDashboard(dashboard); + setSavedDatasources(dashboard.spec.datasources ?? {}); + }; + + const onCancelButtonClick = (): void => { + // check if dashboard has been modified + if (JSON.stringify(dashboard) === JSON.stringify(originalDashboard)) { + setEditMode(false); + } else { + openDiscardChangesConfirmationDialog({ + onDiscardChanges: () => { + handleDiscardChanges(); + }, + onCancel: () => { + closeDiscardChangesConfirmationDialog(); + }, + }); + } + }; + + const updateDashboardMutation = useUpdateDashboardMutation(); + + const onSave = useCallback( + async (data: DashboardResource | EphemeralDashboardResource) => { + if (data.kind !== 'Dashboard') { + throw new Error('Invalid kind'); + } + + try { + const result = await updateDashboardMutation.mutateAsync(data, { + onSuccess: (updatedDashboard: DashboardResource) => { + addAlert( + t( + `Dashboard ${getResourceExtendedDisplayName( + updatedDashboard, + )} has been successfully updated`, + ), + 'success', + ); + + setSaveErrorOccurred(false); + return updatedDashboard; + }, + }); + return result; + } catch (error) { + addAlert(`${error}`, 'danger'); + setSaveErrorOccurred(true); + return null; + } + }, + [updateDashboardMutation, addAlert, t], + ); + + return ( + + + + + + + + + + + + + + + + {isLeavingConfirmDialogEnabled && + isEditMode && + (LeaveDialog({ original: originalDashboard, current: dashboard }) as ReactElement)} + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-create-dialog.tsx b/web/src/components/dashboards/perses/dashboard-create-dialog.tsx new file mode 100644 index 000000000..2175f20b3 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-create-dialog.tsx @@ -0,0 +1,264 @@ +import { useMemo, useState } from 'react'; +import { + Alert, + Button, + Modal, + ModalBody, + ModalHeader, + ModalFooter, + ModalVariant, + FormGroup, + Form, + TextInput, + FormHelperText, + HelperText, + HelperTextItem, + HelperTextItemVariant, + ValidatedOptions, +} from '@patternfly/react-core'; +import { TypeaheadSelect, TypeaheadSelectOption } from '@patternfly/react-templates'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { usePerses } from './hooks/usePerses'; +import { useEditableProjects } from './hooks/useEditableProjects'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { DashboardResource } from '@perses-dev/core'; +import { useCreateDashboardMutation, useCreateProjectMutation } from './dashboard-api'; +import { createNewDashboard } from './dashboard-utils'; +import { useToast } from './ToastProvider'; +import { usePerspective, getDashboardUrl } from '../../hooks/usePerspective'; + +interface DashboardCreateDialogProps { + isOpen: boolean; + onClose: () => void; +} + +export const DashboardCreateDialog: React.FunctionComponent = ({ + isOpen, + onClose, +}) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const navigate = useNavigate(); + const { perspective } = usePerspective(); + const { addAlert } = useToast(); + const { editableProjects, permissionsError } = useEditableProjects(); + const [selectedProject, setSelectedProject] = useState(null); + const [dashboardName, setDashboardName] = useState(''); + const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({}); + const createDashboardMutation = useCreateDashboardMutation(); + const createProjectMutation = useCreateProjectMutation(); + const { persesProjects } = usePerses(); + + const projectOptions = useMemo(() => { + if (!editableProjects) { + return []; + } + + return editableProjects?.map((project) => ({ + content: project, + value: project, + selected: project === selectedProject, + })); + }, [editableProjects, selectedProject]); + + const { persesProjectDashboards: dashboards } = usePerses( + isOpen && selectedProject ? selectedProject : undefined, + ); + + const handleSetDashboardName = (_event, dashboardName: string) => { + setDashboardName(dashboardName); + if (formErrors.dashboardName) { + setFormErrors((prev) => ({ ...prev, dashboardName: '' })); + } + }; + + const handleAdd = async () => { + setFormErrors({}); + + if (!selectedProject || !dashboardName.trim()) { + const errors: { [key: string]: string } = {}; + if (!selectedProject) errors.project = t('Project is required'); + if (!dashboardName.trim()) errors.dashboardName = t('Dashboard name is required'); + setFormErrors(errors); + return; + } + + try { + if ( + dashboards && + dashboards.some( + (d) => + d.metadata.project === selectedProject && + d.metadata.name.toLowerCase() === dashboardName.trim().toLowerCase(), + ) + ) { + setFormErrors({ + dashboardName: `Dashboard name "${dashboardName}" already exists in this project`, + }); + return; + } + + const projectExists = persesProjects?.some( + (project) => project.metadata.name === selectedProject, + ); + + if (!projectExists) { + try { + await createProjectMutation.mutateAsync(selectedProject as string); + addAlert( + t('Project "{{project}}" created successfully', { project: selectedProject }), + 'success', + ); + } catch (projectError) { + const errorMessage = + projectError?.message || + t('Failed to create project "{{project}}". Please try again.', { + project: selectedProject, + }); + addAlert(t('Error creating project: {{error}}', { error: errorMessage }), 'danger'); + setFormErrors({ general: errorMessage }); + return; + } + } + + const newDashboard: DashboardResource = createNewDashboard( + dashboardName.trim(), + selectedProject as string, + ); + + const createdDashboard = await createDashboardMutation.mutateAsync(newDashboard); + + addAlert(`Dashboard "${dashboardName}" created successfully`, 'success'); + + const dashboardUrl = getDashboardUrl(perspective); + const dashboardParam = `dashboard=${createdDashboard.metadata.name}`; + const projectParam = `project=${createdDashboard.metadata.project}`; + const editModeParam = `edit=true`; + navigate(`${dashboardUrl}?${dashboardParam}&${projectParam}&${editModeParam}`); + + handleClose(); + } catch (error) { + const errorMessage = error?.message || t('Failed to create dashboard. Please try again.'); + addAlert(`Error creating dashboard: ${errorMessage}`, 'danger'); + setFormErrors({ general: errorMessage }); + } + }; + + const handleClose = () => { + setDashboardName(''); + setFormErrors({}); + setSelectedProject(null); + onClose(); + }; + + const onSelect = (_event: any, selection: string) => { + setSelectedProject(selection); + }; + + return ( + + + + {permissionsError && ( + + )} + {formErrors.general && ( + + )} +
{ + e.preventDefault(); + handleAdd(); + }} + > + + t('No project found for "{{filter}}"', { filter })} + onClearSelection={() => { + setSelectedProject(null); + }} + onSelect={onSelect} + isCreatable={false} + maxMenuHeight="200px" + /> + + + + {formErrors.dashboardName && ( + + + } + variant={HelperTextItemVariant.error} + > + {formErrors.dashboardName} + + + + )} + +
+
+ + + + +
+ ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-frame.tsx b/web/src/components/dashboards/perses/dashboard-frame.tsx new file mode 100644 index 000000000..f454345c7 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-frame.tsx @@ -0,0 +1,50 @@ +import React, { ReactNode } from 'react'; +import { DashboardEmptyState } from './emptystates/DashboardEmptyState'; +import { DashboardHeader } from './dashboard-header'; +import { CombinedDashboardMetadata } from './hooks/useDashboardsData'; +import { ProjectBar } from './project/ProjectBar'; +import { PersesWrapper } from './PersesWrapper'; +import { ToastProvider } from './ToastProvider'; +import { PagePadding } from './dashboard-page-padding'; + +interface DashboardFrameProps { + activeProject: string | null; + setActiveProject: (project: string | null) => void; + activeProjectDashboardsMetadata: CombinedDashboardMetadata[]; + changeBoard: (boardName: string) => void; + dashboardDisplayName: string; + children: ReactNode; +} + +export const DashboardFrame: React.FC = ({ + activeProject, + setActiveProject, + activeProjectDashboardsMetadata, + changeBoard, + dashboardDisplayName, + children, +}) => { + return ( + <> + + + + {activeProjectDashboardsMetadata?.length === 0 ? ( + + ) : ( + <> + + {children} + + + )} + + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-header.tsx b/web/src/components/dashboards/perses/dashboard-header.tsx new file mode 100644 index 000000000..5e417adc3 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-header.tsx @@ -0,0 +1,151 @@ +import type { FC, PropsWithChildren } from 'react'; +import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Divider, Stack, StackItem } from '@patternfly/react-core'; + +import { DocumentTitle, ListPageHeader } from '@openshift-console/dynamic-plugin-sdk'; +import { CombinedDashboardMetadata } from './hooks/useDashboardsData'; + +import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; +import { useNavigate } from 'react-router-dom-v5-compat'; +import { getDashboardsListUrl, usePerspective } from '../../hooks/usePerspective'; + +import { + chart_color_blue_100, + chart_color_blue_300, + t_global_spacer_md, + t_global_spacer_xl, +} from '@patternfly/react-tokens'; +import { listPersesDashboardsDataTestIDs } from '../../data-test'; +import { usePatternFlyTheme } from '../../hooks/usePatternflyTheme'; +import { DashboardActionsMenu } from './dashboard-actions-menu'; +import { PagePadding } from './dashboard-page-padding'; + +const DASHBOARD_VIEW_PATH = 'v2/dashboards/view'; + +const shouldHideFavoriteButton = (): boolean => { + const currentUrl = window.location.href; + return currentUrl.includes(DASHBOARD_VIEW_PATH); +}; + +const DashboardBreadCrumb: React.FunctionComponent<{ dashboardDisplayName?: string }> = ({ + dashboardDisplayName, +}) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + const { perspective } = usePerspective(); + const { theme } = usePatternFlyTheme(); + const navigate = useNavigate(); + + const handleDashboardsClick = () => { + navigate(getDashboardsListUrl(perspective)); + }; + + const lightThemeColor = chart_color_blue_100.value; + + const darkThemeColor = chart_color_blue_300.value; + + const linkColor = theme == 'dark' ? lightThemeColor : darkThemeColor; + + return ( + + + {t('Dashboards')} + + {dashboardDisplayName && ( + + {dashboardDisplayName} + + )} + + ); +}; + +const DashboardPageHeader: React.FunctionComponent<{ dashboardDisplayName?: string }> = ({ + dashboardDisplayName, +}) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const hideFavBtn = shouldHideFavoriteButton(); + + return ( + + + + + + + + + + ); +}; + +const DashboardListPageHeader: React.FunctionComponent = () => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const hideFavBtn = shouldHideFavoriteButton(); + + return ( + + + + ); +}; + +type MonitoringDashboardsPageProps = PropsWithChildren<{ + boardItems: CombinedDashboardMetadata[]; + changeBoard: (dashboardName: string) => void; + dashboardDisplayName: string; + activeProject?: string; +}>; + +export const DashboardHeader: FC = memo( + ({ children, dashboardDisplayName }) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + return ( + <> + {t('Metrics dashboards')} + + + + {children} + + ); + }, +); + +export const DashboardListHeader: FC = memo(({ children }) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + return ( + <> + {t('Metrics dashboards')} + + + + + {children} + + ); +}); diff --git a/web/src/components/dashboards/perses/dashboard-import-dialog.tsx b/web/src/components/dashboards/perses/dashboard-import-dialog.tsx new file mode 100644 index 000000000..d21e22f29 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-import-dialog.tsx @@ -0,0 +1,445 @@ +import { CodeEditor } from '@patternfly/react-code-editor'; +import { + Alert, + Button, + FileUpload, + Form, + FormGroup, + FormHelperText, + HelperText, + HelperTextItem, + HelperTextItemVariant, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalVariant, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { TypeaheadSelect, TypeaheadSelectOption } from '@patternfly/react-templates'; +import yaml from 'js-yaml'; +import { ChangeEvent, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; +import { useEditableProjects } from './hooks/useEditableProjects'; + +import { DashboardResource } from '@perses-dev/core'; +import { usePatternFlyTheme } from '../../hooks/usePatternflyTheme'; +import { getDashboardUrl, usePerspective } from '../../hooks/usePerspective'; +import { useCreateDashboardMutation } from './dashboard-api'; +import { useMigrateDashboard } from './migrate-api'; +import { useToast } from './ToastProvider'; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_MIME_TYPES = ['application/json', 'text/yaml', 'application/x-yaml', 'text/x-yaml']; + +const getErrorMessage = (error: unknown): string | undefined => { + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error !== null && 'message' in error) { + return String((error as { message: unknown }).message); + } + return typeof error === 'string' ? error : undefined; +}; + +// Sanitize dashboard name to prevent XSS when displaying in alerts/UI +const sanitizeDashboardName = (name: string | undefined): string => { + if (!name) return 'Untitled'; + // Remove potentially dangerous characters and limit length + return name.replace(/[<>"'&]/g, '').substring(0, 100); +}; + +type DashboardType = 'grafana' | 'perses' | undefined; + +interface ParsedDashboard { + kind: DashboardType; + data: Record | DashboardResource; +} + +interface DashboardImportDialogProps { + isOpen: boolean; + onClose: () => void; +} + +export const DashboardImportDialog: React.FunctionComponent = ({ + isOpen, + onClose, +}) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const navigate = useNavigate(); + const { perspective } = usePerspective(); + const { addAlert } = useToast(); + const { editableProjects, permissionsError } = useEditableProjects(); + const { theme } = usePatternFlyTheme(); + + const [selectedProject, setSelectedProject] = useState(null); + const [dashboardInput, setDashboardInput] = useState(''); + const [parsedDashboard, setParsedDashboard] = useState(); + const [parseError, setParseError] = useState(''); + const [filename, setFilename] = useState(''); + const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({}); + const [isUploadingFile, setIsUploadingFile] = useState(false); + + const createDashboardMutation = useCreateDashboardMutation(); + const migrateMutation = useMigrateDashboard(); + + const projectOptions = useMemo(() => { + if (!editableProjects) { + return []; + } + + return editableProjects.map((project) => ({ + content: project, + value: project, + selected: project === selectedProject, + })); + }, [editableProjects, selectedProject]); + + const getDashboardType = (dashboard: Record): DashboardType => { + if ('kind' in dashboard && dashboard.kind === 'Dashboard') { + return 'perses'; + } + if ('panels' in dashboard || 'templating' in dashboard || 'annotations' in dashboard) { + return 'grafana'; + } + return undefined; + }; + + const detectInputFormat = (input: string): 'json' | 'yaml' => { + const trimmed = input.trim(); + if (trimmed.startsWith('{')) { + return 'json'; + } + + return 'yaml'; + }; + + const parseDashboardInput = (input: string): void => { + if (!input.trim()) { + setParsedDashboard(undefined); + setParseError(''); + return; + } + + const detectedFormat = detectInputFormat(input); + + try { + let parsed: Record; + + if (detectedFormat === 'json') { + parsed = JSON.parse(input); + } else { + const loaded = yaml.load(input); + if (typeof loaded !== 'object' || loaded === null || Array.isArray(loaded)) { + throw new Error('Dashboard must be a valid object'); + } + parsed = loaded as Record; + } + + const type = getDashboardType(parsed); + if (type) { + setParsedDashboard({ kind: type, data: parsed }); + setParseError(''); + } else { + setParsedDashboard(undefined); + setParseError( + t( + 'Unable to detect dashboard format. Please provide a valid Perses or Grafana dashboard.', + ), + ); + } + } catch (error) { + setParsedDashboard(undefined); + const errorMessage = error instanceof Error ? error.message : String(error); + setParseError( + t('Invalid {{format}}: {{error}}', { + format: detectedFormat.toUpperCase(), + error: errorMessage, + }), + ); + } + }; + + const handleDashboardInputChange = (value: string) => { + setDashboardInput(value); + parseDashboardInput(value); + }; + + const handleFileUpload = async ( + _event: ChangeEvent, + file: File, + ): Promise => { + if (file) { + if (file.type && !ALLOWED_MIME_TYPES.includes(file.type)) { + setParseError( + t('Invalid file type. Please upload a JSON or YAML file (.json, .yaml, .yml)'), + ); + return; + } + + if (file.size > MAX_FILE_SIZE) { + setParseError(t('File size exceeds maximum allowed size of 5MB')); + return; + } + setIsUploadingFile(true); + try { + setFilename(file.name); + const text = await file.text(); + setDashboardInput(text); + parseDashboardInput(text); + } finally { + setIsUploadingFile(false); + } + } + }; + + const handleClearFile = (): void => { + setFilename(''); + setDashboardInput(''); + setParsedDashboard(undefined); + setParseError(''); + }; + + const isImporting = createDashboardMutation.isPending || migrateMutation.isPending; + + const importDashboard = async (dashboard: DashboardResource, projectName: string) => { + dashboard.metadata.project = projectName; + + const createdDashboard = await createDashboardMutation.mutateAsync(dashboard); + + const displayName = sanitizeDashboardName( + createdDashboard.spec?.display?.name || createdDashboard.metadata.name, + ); + addAlert(t('Dashboard "{{name}}" imported successfully', { name: displayName }), 'success'); + + const dashboardUrl = getDashboardUrl(perspective); + const dashboardParam = `dashboard=${createdDashboard.metadata.name}`; + const projectParam = `project=${createdDashboard.metadata.project}`; + const editModeParam = `edit=true`; + navigate(`${dashboardUrl}?${dashboardParam}&${projectParam}&${editModeParam}`); + + handleClose(); + }; + + const handleImport = async () => { + if (isImporting) { + return; + } + + setFormErrors({}); + + if (!selectedProject) { + setFormErrors({ project: t('Project is required') }); + return; + } + + if (!parsedDashboard) { + setFormErrors({ dashboard: t('A valid dashboard is required') }); + return; + } + + // Capture current values before async operations to prevent race conditions + const currentProject = selectedProject; + const currentParsedDashboard = parsedDashboard; + + try { + if (currentParsedDashboard.kind === 'grafana') { + // Migrate Grafana dashboard first, then import + migrateMutation.mutate( + { + grafanaDashboard: currentParsedDashboard.data as Record, + useDefaultDatasource: true, + }, + { + onSuccess: async (migratedDashboard) => { + try { + await importDashboard(migratedDashboard, currentProject); + } catch (error) { + const errorMessage = + getErrorMessage(error) || t('Failed to import dashboard. Please try again.'); + addAlert( + t('Error importing dashboard: {{error}}', { error: errorMessage }), + 'danger', + ); + setFormErrors({ general: errorMessage }); + } + }, + onError: (error) => { + const errorMessage = + getErrorMessage(error) || t('Migration failed. Please try again.'); + setFormErrors({ general: errorMessage }); + }, + }, + ); + } else { + // Direct import for Perses dashboard + await importDashboard(currentParsedDashboard.data as DashboardResource, currentProject); + } + } catch (error) { + const errorMessage = + getErrorMessage(error) || t('Failed to import dashboard. Please try again.'); + setFormErrors({ general: errorMessage }); + } + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + const resetForm = () => { + setDashboardInput(''); + setParsedDashboard(undefined); + setParseError(''); + setSelectedProject(null); + setFilename(''); + setFormErrors({}); + }; + + const onProjectSelect = (_event: unknown, selection: string) => { + setSelectedProject(selection); + }; + + const canImport = parsedDashboard && selectedProject && !isImporting && !parseError; + + return ( + + + + {permissionsError && ( + + )} + {formErrors.general && ( + + )} + +
+ + + + + + {t( + 'Upload a dashboard file or paste the dashboard definition directly in the editor below.', + )} + + + + + + + + + + + {(parseError || formErrors.dashboard) && ( + + + } + variant={HelperTextItemVariant.error} + > + {parseError || formErrors.dashboard} + + + + )} + {parsedDashboard && ( + + + + {parsedDashboard.kind === 'grafana' + ? t( + 'Grafana dashboard detected. It will be automatically migrated to Perses format. Note: migration may be partial as not all Grafana features are supported.', + ) + : t('Perses dashboard detected.')} + + + + )} + + + + {parsedDashboard && ( + + + + t('No project found for "{{filter}}"', { filter }) + } + onClearSelection={() => { + setSelectedProject(null); + }} + onSelect={onProjectSelect} + isCreatable={false} + maxMenuHeight="200px" + /> + + + )} + +
+
+ + + + +
+ ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-list-frame.tsx b/web/src/components/dashboards/perses/dashboard-list-frame.tsx new file mode 100644 index 000000000..6bce52cc6 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-list-frame.tsx @@ -0,0 +1,36 @@ +import React, { ReactNode } from 'react'; +import { DashboardListHeader } from './dashboard-header'; +import { CombinedDashboardMetadata } from './hooks/useDashboardsData'; +import { ProjectBar } from './project/ProjectBar'; + +interface DashboardListFrameProps { + activeProject: string | null; + setActiveProject: (project: string | null) => void; + activeProjectDashboardsMetadata: CombinedDashboardMetadata[]; + changeBoard: (boardName: string) => void; + dashboardName: string; + children: ReactNode; +} + +export const DashboardListFrame: React.FC = ({ + activeProject, + setActiveProject, + activeProjectDashboardsMetadata, + changeBoard, + dashboardName, + children, +}) => { + return ( + <> + + + {children} + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-list-page.tsx b/web/src/components/dashboards/perses/dashboard-list-page.tsx new file mode 100644 index 000000000..92c3391e9 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-list-page.tsx @@ -0,0 +1,29 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { type FC } from 'react'; +import { QueryParamProvider } from 'use-query-params'; +import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; +import { DashboardList } from './dashboard-list'; +import { ToastProvider } from './ToastProvider'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: false, + }, + }, +}); + +const DashboardListPage: FC = () => { + return ( + + + + + + + + ); +}; + +export default DashboardListPage; diff --git a/web/src/components/dashboards/perses/dashboard-list.tsx b/web/src/components/dashboards/perses/dashboard-list.tsx new file mode 100644 index 000000000..89e177012 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-list.tsx @@ -0,0 +1,466 @@ +import React, { ReactNode, useCallback, useMemo, useState, type FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDashboardsData } from './hooks/useDashboardsData'; + +import { + Button, + EmptyState, + EmptyStateBody, + EmptyStateVariant, + Pagination, + Title, + Tooltip, +} from '@patternfly/react-core'; +import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView'; +import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters'; +import { + DataViewTable, + DataViewTh, + DataViewTr, +} from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter'; +import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar'; +import { useDataViewFilters, useDataViewSort } from '@patternfly/react-data-view'; +import { useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks'; +import { ActionsColumn, ThProps } from '@patternfly/react-table'; +import { Link, useSearchParams } from 'react-router-dom-v5-compat'; + +import { getDashboardUrl, usePerspective } from '../../hooks/usePerspective'; +import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { listPersesDashboardsDataTestIDs } from '../../../components/data-test'; +import { DashboardListFrame } from './dashboard-list-frame'; +import { usePersesEditPermissions } from './dashboard-toolbar'; +import { DashboardResource } from '@perses-dev/core'; +import { + DeleteActionModal, + DuplicateActionModal, + RenameActionModal, +} from './dashboard-action-modals'; +import { useEditableProjects } from './hooks/useEditableProjects'; +const perPageOptions = [ + { title: '10', value: 10 }, + { title: '20', value: 20 }, +]; + +const DashboardActionsCell = React.memo( + ({ + project, + dashboard, + onRename, + onDuplicate, + onDelete, + emptyActions, + }: { + project: string; + dashboard: DashboardResource; + onRename: (dashboard: DashboardResource) => void; + onDuplicate: (dashboard: DashboardResource) => void; + onDelete: (dashboard: DashboardResource) => void; + emptyActions: any[]; + }) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + const { permissionsLoading } = useEditableProjects(); + const { canEdit } = usePersesEditPermissions(project); + const disabled = !canEdit; + + const rowSpecificActions = useMemo( + () => [ + { + title: t('Rename dashboard'), + onClick: () => onRename(dashboard), + }, + { + title: t('Duplicate dashboard'), + onClick: () => onDuplicate(dashboard), + }, + { + title: t('Delete dashboard'), + onClick: () => onDelete(dashboard), + }, + ], + [dashboard, onRename, onDuplicate, onDelete, t], + ); + + if (disabled) { + return ( + +
+ +
+
+ ); + } + if (permissionsLoading) { + return ( + +
+ +
+
+ ); + } + + return ; + }, +); + +interface DashboardRowNameLink { + link: ReactNode; + label: string; +} + +interface DashboardRow { + name: DashboardRowNameLink; + project: string; + created: ReactNode; + modified: ReactNode; + // Raw values for sorting + createdAt?: string; + updatedAt?: string; + // Reference to original dashboard data + dashboard: DashboardResource; +} + +interface DashboardRowFilters { + name?: string; + 'project-filter'?: string; +} + +const sortDashboardData = ( + data: DashboardRow[], + sortBy: keyof DashboardRow | undefined, + direction: 'asc' | 'desc' | undefined, +): DashboardRow[] => { + if (!sortBy || !direction) return data; + + return [...data].sort((a, b) => { + let aValue: any; + let bValue: any; + + if (sortBy === 'name') { + aValue = a.name.label; + bValue = b.name.label; + } else if (sortBy === 'created') { + aValue = a.createdAt; + bValue = b.createdAt; + } else if (sortBy === 'modified') { + aValue = a.updatedAt; + bValue = b.updatedAt; + } else { + aValue = a[sortBy]; + bValue = b[sortBy]; + } + + if (direction === 'asc') { + return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + } else { + return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; + } + }); +}; + +interface DashboardsTableProps { + persesDashboards: DashboardResource[]; + persesDashboardsLoading: boolean; + activeProject: string | null; +} + +const DashboardsTable: React.FunctionComponent = ({ + persesDashboards, + persesDashboardsLoading, + activeProject, +}) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + const { perspective } = usePerspective(); + const dashboardBaseURL = getDashboardUrl(perspective); + + const [searchParams, setSearchParams] = useSearchParams(); + const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams }); + + const { filters, onSetFilters, clearAllFilters } = useDataViewFilters({ + initialFilters: { name: '', 'project-filter': '' }, + searchParams, + setSearchParams, + }); + const pagination = useDataViewPagination({ perPage: perPageOptions[0].value }); + const { page, perPage } = pagination; + + const DASHBOARD_COLUMNS = useMemo( + () => [ + { label: t('Dashboard'), key: 'name' as keyof DashboardRow, index: 0 }, + { label: t('Project'), key: 'project' as keyof DashboardRow, index: 1 }, + { label: t('Created on'), key: 'created' as keyof DashboardRow, index: 2 }, + { label: t('Last Modified'), key: 'modified' as keyof DashboardRow, index: 3 }, + ], + [t], + ); + const sortByIndex = useMemo(() => { + return DASHBOARD_COLUMNS.findIndex((item) => item.key === sortBy); + }, [DASHBOARD_COLUMNS, sortBy]); + + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ + sortBy: { + index: sortByIndex, + direction, + defaultDirection: 'asc', + }, + onSort: (_event, index, direction) => onSort(_event, DASHBOARD_COLUMNS[index].key, direction), + columnIndex, + }); + + const tableColumns: DataViewTh[] = DASHBOARD_COLUMNS.map((column, index) => ({ + cell: t(column.label), + props: { sort: getSortParams(index) }, + })); + + const tableRows: DashboardRow[] = useMemo(() => { + if (persesDashboardsLoading) { + return []; + } + return persesDashboards.map((board) => { + const metadata = board?.metadata; + const displayName = board?.spec?.display?.name; + const dashboardsParams = `?dashboard=${metadata?.name}&project=${metadata?.project}`; + const dashboardName: DashboardRowNameLink = { + link: ( + + {displayName} + + ), + label: displayName || '', + }; + + return { + name: dashboardName, + project: board?.metadata?.project || '', + created: , + modified: , + createdAt: metadata?.createdAt, + updatedAt: metadata?.updatedAt, + dashboard: board, + }; + }); + }, [dashboardBaseURL, persesDashboards, persesDashboardsLoading]); + + const filteredData = useMemo( + () => + tableRows.filter( + (item) => + (!filters.name || + item.name?.label?.toLocaleLowerCase().includes(filters.name?.toLocaleLowerCase())) && + (!filters['project-filter'] || + item.project + ?.toLocaleLowerCase() + .includes(filters['project-filter']?.toLocaleLowerCase())) && + (!activeProject || item.project === activeProject), + ), + [filters, tableRows, activeProject], + ); + + const sortedAndFilteredData = useMemo( + () => sortDashboardData(filteredData, sortBy as keyof DashboardRow, direction), + [filteredData, sortBy, direction], + ); + + const [targetedDashboard, setTargetedDashboard] = useState(); + const [isRenameModalOpen, setIsRenameModalOpen] = useState(false); + const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + const handleRenameModalOpen = useCallback((dashboard: DashboardResource) => { + setTargetedDashboard(dashboard); + setIsRenameModalOpen(true); + }, []); + + const handleRenameModalClose = useCallback(() => { + setIsRenameModalOpen(false); + setTargetedDashboard(undefined); + }, []); + + const handleDuplicateModalOpen = useCallback((dashboard: DashboardResource) => { + setTargetedDashboard(dashboard); + setIsDuplicateModalOpen(true); + }, []); + + const handleDuplicateModalClose = useCallback(() => { + setIsDuplicateModalOpen(false); + setTargetedDashboard(undefined); + }, []); + + const handleDeleteModalOpen = useCallback((dashboard: DashboardResource) => { + setTargetedDashboard(dashboard); + setIsDeleteModalOpen(true); + }, []); + + const handleDeleteModalClose = useCallback(() => { + setIsDeleteModalOpen(false); + setTargetedDashboard(undefined); + }, []); + + const emptyRowActions = useMemo( + () => [ + { + title: t("You don't have permissions for dashboard actions"), + onClick: () => {}, + }, + ], + [t], + ); + + const pageRows: DataViewTr[] = useMemo(() => { + return sortedAndFilteredData + .slice((page - 1) * perPage, (page - 1) * perPage + perPage) + .map(({ name, project, created, modified, dashboard }) => [ + name.link, + project, + created, + modified, + { + cell: ( + + ), + props: { isActionCell: true }, + }, + ]); + }, [ + sortedAndFilteredData, + page, + perPage, + emptyRowActions, + handleRenameModalOpen, + handleDuplicateModalOpen, + handleDeleteModalOpen, + ]); + + const PaginationTool = () => { + return ( + + ); + }; + + const hasFiltersApplied = filters.name || filters['project-filter']; + const hasData = sortedAndFilteredData.length > 0; + + return ( + + } + filters={ + onSetFilters(values)} values={filters}> + + + + } + /> + {hasData ? ( + <> + + + + + + ) : ( + + + {hasFiltersApplied ? t('No results found') : t('No dashboards found')} + + + {hasFiltersApplied + ? t('No results match the filter criteria. Clear filters to show results.') + : t('No Perses dashboards are currently available in this project.')} + + {hasFiltersApplied && ( + + )} + + )} + } /> + + ); +}; + +export const DashboardList: FC = () => { + const { + activeProjectDashboardsMetadata, + changeBoard, + dashboardName, + setActiveProject, + activeProject, + persesDashboards, + combinedInitialLoad, + } = useDashboardsData(); + + return ( + + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-page-padding.tsx b/web/src/components/dashboards/perses/dashboard-page-padding.tsx new file mode 100644 index 000000000..c0647b980 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-page-padding.tsx @@ -0,0 +1,27 @@ +import { t_global_spacer_sm } from '@patternfly/react-tokens'; +import { FC, ReactNode } from 'react'; + +interface PagePaddingProps { + children: ReactNode; + top?: string; + bottom?: string; + left?: string; + right?: string; +} + +export const PagePadding: FC = ({ + children, + top = t_global_spacer_sm.value, + bottom = t_global_spacer_sm.value, + left = t_global_spacer_sm.value, + right = t_global_spacer_sm.value, +}) => { + const style = { + paddingTop: top, + paddingBottom: bottom, + paddingLeft: left, + paddingRight: right, + }; + + return
{children}
; +}; diff --git a/web/src/components/dashboards/perses/dashboard-page.tsx b/web/src/components/dashboards/perses/dashboard-page.tsx index d8e3be2d7..979482475 100644 --- a/web/src/components/dashboards/perses/dashboard-page.tsx +++ b/web/src/components/dashboards/perses/dashboard-page.tsx @@ -1,76 +1,113 @@ -import { Overview } from '@openshift-console/dynamic-plugin-sdk'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import type { FC } from 'react'; +import { useEffect, type FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom-v5-compat'; import { QueryParamProvider } from 'use-query-params'; import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; import { LoadingInline } from '../../console/console-shared/src/components/loading/LoadingInline'; -import { PersesWrapper } from './PersesWrapper'; -import { DashboardSkeleton } from './dashboard-skeleton'; -import { DashboardEmptyState } from './emptystates/DashboardEmptyState'; +import { OCPDashboardApp } from './dashboard-app'; +import { DashboardFrame } from './dashboard-frame'; import { ProjectEmptyState } from './emptystates/ProjectEmptyState'; import { useDashboardsData } from './hooks/useDashboardsData'; -import PersesBoard from './perses-dashboards'; -import { ProjectBar } from './project/ProjectBar'; +import { ToastProvider } from './ToastProvider'; const queryClient = new QueryClient({ defaultOptions: { queries: { + retry: false, refetchOnWindowFocus: false, - retry: 0, }, }, }); -const MonitoringDashboardsPage_: FC = () => { +const DashboardPage_: FC = () => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const [searchParams] = useSearchParams(); + const { - changeBoard, activeProjectDashboardsMetadata, - combinedInitialLoad, - activeProject, - setActiveProject, + changeBoard, dashboardName, + setActiveProject, + activeProject, + combinedInitialLoad, } = useDashboardsData(); + // Get dashboard and project from URL parameters + const urlDashboard = searchParams.get('dashboard'); + const urlProject = searchParams.get('project'); + + // Set active project if provided in URL + if (urlProject && urlProject !== activeProject) { + setActiveProject(urlProject); + } + + useEffect(() => { + if (urlDashboard && urlDashboard !== dashboardName) { + changeBoard(urlDashboard); + } + }, [urlDashboard, dashboardName, changeBoard]); + if (combinedInitialLoad) { return ; } - if (!activeProject) { - // If we have loaded all of the requests fully and there are no projects, then - return ; // empty state + if (activeProjectDashboardsMetadata?.length === 0) { + return ; + } + + // Find the dashboard that matches either the URL parameter or the current dashboardName + const targetDashboardName = urlDashboard || dashboardName; + const currentDashboard = activeProjectDashboardsMetadata.find( + (d) => d.name === targetDashboardName, + ); + + if (!currentDashboard) { + return ( +
+

{t('Dashboard not found')}

+

+ {t('The dashboard "{{name}}" was not found in project "{{project}}".', { + name: targetDashboardName, + project: activeProject || urlProject, + })} +

+
+ ); } return ( - <> - - - {activeProjectDashboardsMetadata.length === 0 ? ( - - ) : ( - - - - - - )} - - + + + ); }; -const MonitoringDashboardsPageWrapper: FC = () => { +const DashboardPage: React.FC = () => { return ( - + + + ); }; -export default MonitoringDashboardsPageWrapper; +export default DashboardPage; diff --git a/web/src/components/dashboards/perses/dashboard-skeleton.tsx b/web/src/components/dashboards/perses/dashboard-skeleton.tsx deleted file mode 100644 index 9f4853669..000000000 --- a/web/src/components/dashboards/perses/dashboard-skeleton.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import * as _ from 'lodash-es'; -import type { FC, PropsWithChildren } from 'react'; -import { memo, useCallback, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Divider, PageSection, Split, SplitItem, Stack, StackItem } from '@patternfly/react-core'; -import { - DashboardStickyToolbar, - useDashboardActions, - useVariableDefinitions, -} from '@perses-dev/dashboards'; -import { TimeRangeControls } from '@perses-dev/plugin-system'; -import { DashboardDropdown } from '../shared/dashboard-dropdown'; -import { CombinedDashboardMetadata } from './hooks/useDashboardsData'; -import { DocumentTitle, ListPageHeader } from '@openshift-console/dynamic-plugin-sdk'; - -const HeaderTop: FC = memo(() => { - const { t } = useTranslation(process.env.I18N_NAMESPACE); - - return ( - - - - - - - - ); -}); - -type MonitoringDashboardsPageProps = PropsWithChildren<{ - boardItems: CombinedDashboardMetadata[]; - changeBoard: (dashboardName: string) => void; - dashboardName: string; - activeProject?: string; -}>; - -export const DashboardSkeleton: FC = memo( - ({ children, boardItems, changeBoard, dashboardName, activeProject }) => { - const { t } = useTranslation(process.env.I18N_NAMESPACE); - const { setDashboard } = useDashboardActions(); - const variables = useVariableDefinitions(); - - const onChangeBoard = useCallback( - (selectedDashboard: string) => { - changeBoard(selectedDashboard); - - const selectedBoard = boardItems.find( - (item) => - item.name.toLowerCase() === selectedDashboard.toLowerCase() && - item.project?.toLowerCase() === activeProject?.toLowerCase(), - ); - - if (selectedBoard) { - setDashboard(selectedBoard.persesDashboard); - } - }, - [changeBoard, boardItems, activeProject, setDashboard], - ); - - useEffect(() => { - onChangeBoard(dashboardName); - }, [dashboardName, onChangeBoard]); - - return ( - <> - {t('Metrics dashboards')} - - - - {!_.isEmpty(boardItems) && ( - - - - )} - {variables.length > 0 ? ( - - {t('Dashboard Variables')} - - - ) : null} - - - - - - - - - {children} - - ); - }, -); diff --git a/web/src/components/dashboards/perses/dashboard-toolbar.tsx b/web/src/components/dashboards/perses/dashboard-toolbar.tsx new file mode 100644 index 000000000..2791c6e66 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-toolbar.tsx @@ -0,0 +1,268 @@ +import { Alert, Box, Button, Stack, Tooltip, useMediaQuery, useTheme } from '@mui/material'; +import { ErrorAlert, ErrorBoundary } from '@perses-dev/components'; +import { + AddGroupButton, + AddPanelButton, + DashboardStickyToolbar, + DownloadButton, + EditDatasourcesButton, + EditJsonButton, + EditVariablesButton, + OnSaveDashboard, + SaveDashboardButton, + useDashboardActions, + useEditMode, +} from '@perses-dev/dashboards'; +import { TimeRangeControls, useTimeZoneParams } from '@perses-dev/plugin-system'; +import { ReactElement, ReactNode, useCallback, useEffect } from 'react'; + +import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; +import { StackItem } from '@patternfly/react-core'; +import * as _ from 'lodash-es'; +import PencilIcon from 'mdi-material-ui/PencilOutline'; +import { useTranslation } from 'react-i18next'; +import { DashboardDropdown } from '../shared/dashboard-dropdown'; +import { useDashboardsData } from './hooks/useDashboardsData'; + +import { persesDashboardDataTestIDs } from '../../data-test'; + +export interface DashboardToolbarProps { + dashboardName: string; + dashboardTitleComponent?: ReactNode; + initialVariableIsSticky?: boolean; + isReadonly: boolean; + isVariableEnabled: boolean; + isDatasourceEnabled: boolean; + onEditButtonClick: () => void; + onCancelButtonClick: () => void; + onSave?: OnSaveDashboard; +} + +export interface EditButtonProps { + /** + * The label used inside the button. + */ + label?: string; + + /** + * Handler that puts the dashboard into editing mode. + */ + onClick: () => void; + + /** + * Whether the button is disabled. + */ + disabled?: boolean; + + /** + * Tooltip text to show when button is disabled. + */ + disabledTooltip?: string; + + /** + * Whether permissions are still loading. + */ + loading?: boolean; + + /** + * The active project/namespace for permissions check. + */ + activeProject?: string | null; +} + +export const EditButton = ({ onClick, activeProject }: EditButtonProps): ReactElement => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const { canEdit, loading } = usePersesEditPermissions(activeProject); + const disabled = !canEdit; + + const button = ( + + ); + + if (disabled && !loading) { + return ( + + {button} + + ); + } + + return button; +}; + +export const usePersesEditPermissions = (namespace: string | null = null) => { + const [canCreate, createLoading] = useAccessReview({ + group: 'perses.dev', + resource: 'persesdashboards', + verb: 'create', + namespace, + }); + + const [canUpdate, updateLoading] = useAccessReview({ + group: 'perses.dev', + resource: 'persesdashboards', + verb: 'update', + namespace, + }); + + const [canDelete, deleteLoading] = useAccessReview({ + group: 'perses.dev', + resource: 'persesdashboards', + verb: 'delete', + namespace, + }); + + const loading = createLoading || updateLoading || deleteLoading; + const canEdit = canUpdate && canCreate && canDelete; + + return { canEdit, loading }; +}; + +export const OCPDashboardToolbar = (props: DashboardToolbarProps): ReactElement => { + const { + initialVariableIsSticky, + isReadonly, + isVariableEnabled, + isDatasourceEnabled, + onEditButtonClick, + onCancelButtonClick, + onSave, + } = props; + + const { isEditMode } = useEditMode(); + const { timeZone, setTimeZone } = useTimeZoneParams('local'); + + const isBiggerThanSm = useMediaQuery(useTheme().breakpoints.up('sm')); + const isBiggerThanMd = useMediaQuery(useTheme().breakpoints.up('md')); + + const testId = 'dashboard-toolbar'; + + const { + changeBoard, + activeProjectDashboardsMetadata: boardItems, + activeProject, + dashboardName, + } = useDashboardsData(); + + const { setDashboard } = useDashboardActions(); + + const onChangeBoard = useCallback( + (selectedDashboard: string) => { + changeBoard(selectedDashboard); + + const selectedBoard = boardItems.find( + (item) => + item.name.toLowerCase() === selectedDashboard.toLowerCase() && + item.project?.toLowerCase() === activeProject?.toLowerCase(), + ); + + if (selectedBoard) { + setDashboard(selectedBoard.persesDashboard); + } + }, + [activeProject, boardItems, changeBoard, setDashboard], + ); + + useEffect(() => { + onChangeBoard(dashboardName); + }, [dashboardName, onChangeBoard]); + + return ( + <> + + theme.palette.primary.main + (isEditMode ? '30' : '0'), + alignItems: 'center', + }} + > + {!_.isEmpty(boardItems) && ( + + + + )} + {isEditMode ? ( + + {isReadonly && ( + + Dashboard managed via code only. Download JSON and commit changes to save. + + )} + + {isVariableEnabled && } + {isDatasourceEnabled && } + + + + + + + ) : ( + <> + {isBiggerThanSm && ( + + + + )} + + )} + + theme.spacing(1, 2, 0, 2), + flexDirection: isBiggerThanMd ? 'row' : 'column', + flexWrap: 'nowrap', + gap: 1, + }} + > + + + palette.background.default, + }} + /> + + + + + setTimeZone(tz.value)} + /> + + + + + + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-utils.ts b/web/src/components/dashboards/perses/dashboard-utils.ts new file mode 100644 index 000000000..2a8101aec --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-utils.ts @@ -0,0 +1,37 @@ +import { DashboardResource } from '@perses-dev/core'; + +/** + * Generated a resource name valid for the API. + * By removing accents from alpha characters and replace specials character by underscores. + */ +export const generateMetadataName = (name: string): string => { + return name + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + .replace(/[^a-zA-Z0-9_.-]/g, '_'); +}; + +export const createNewDashboard = ( + dashboardName: string, + projectName: string, +): DashboardResource => { + return { + kind: 'Dashboard', + metadata: { + name: generateMetadataName(dashboardName), + project: projectName, + version: 0, + }, + spec: { + display: { + name: dashboardName, + }, + datasources: {}, + panels: {}, + layouts: [], + variables: [], + duration: '1h', + refreshInterval: '30s', + }, + }; +}; diff --git a/web/src/components/dashboards/perses/datasource-api.ts b/web/src/components/dashboards/perses/datasource-api.ts index f3adb79cd..5100c34f5 100644 --- a/web/src/components/dashboards/perses/datasource-api.ts +++ b/web/src/components/dashboards/perses/datasource-api.ts @@ -1,5 +1,9 @@ -import { DatasourceResource, DatasourceSelector, GlobalDatasourceResource } from '@perses-dev/core'; -import { DatasourceApi } from '@perses-dev/dashboards'; +import { + DatasourceResource, + DatasourceSelector, + GlobalDatasourceResource, + DatasourceApi, +} from '@perses-dev/core'; import { fetchDatasourceList } from './perses/datasource-client'; import { fetchGlobalDatasourceList } from './perses/global-datasource-client'; import { TFunction } from 'i18next'; diff --git a/web/src/components/dashboards/perses/hooks/useDashboardsData.ts b/web/src/components/dashboards/perses/hooks/useDashboardsData.ts index ca700d6a5..1d5f0651e 100644 --- a/web/src/components/dashboards/perses/hooks/useDashboardsData.ts +++ b/web/src/components/dashboards/perses/hooks/useDashboardsData.ts @@ -1,11 +1,11 @@ -import { useMemo, useCallback, useEffect } from 'react'; +import { useMemo, useCallback, useRef } from 'react'; import { DashboardResource } from '@perses-dev/core'; import { useNavigate } from 'react-router-dom-v5-compat'; import { StringParam, useQueryParam } from 'use-query-params'; import { getAllQueryArguments } from '../../../console/utils/router'; import { useBoolean } from '../../../hooks/useBoolean'; -import { getDashboardsUrl, usePerspective } from '../../../hooks/usePerspective'; +import { getDashboardUrl, usePerspective } from '../../../hooks/usePerspective'; import { QueryParams } from '../../../query-params'; import { useActiveProject } from '../project/useActiveProject'; import { usePerses } from './usePerses'; @@ -39,13 +39,33 @@ export const useDashboardsData = () => { return true; }, [persesProjectsLoading, persesDashboardsLoading, initialPageLoad, setInitialPageLoadFalse]); + const prevDashboardsRef = useRef([]); + const prevMetadataRef = useRef([]); + // Homogenize data needed for dashboards dropdown between legacy and perses dashboards // to enable both to use the same component const combinedDashboardsMetadata = useMemo(() => { if (combinedInitialLoad) { return []; } - return persesDashboards.map((persesDashboard) => { + + // Check if dashboards data has actually changed to avoid recreation + const dashboardsChanged = + persesDashboards.length !== prevDashboardsRef.current.length || + persesDashboards.some((dashboard, i) => { + const prevDashboard = prevDashboardsRef.current[i]; + return ( + dashboard?.metadata?.name !== prevDashboard?.metadata?.name || + dashboard?.spec?.display?.name !== prevDashboard?.spec?.display?.name || + dashboard?.metadata?.project !== prevDashboard?.metadata?.project + ); + }); + + if (!dashboardsChanged && prevMetadataRef.current.length > 0) { + return prevMetadataRef.current; + } + + const newMetadata = persesDashboards.map((persesDashboard) => { const name = persesDashboard?.metadata?.name; const displayName = persesDashboard?.spec?.display?.name || name; @@ -57,10 +77,17 @@ export const useDashboardsData = () => { persesDashboard, }; }); + + prevDashboardsRef.current = persesDashboards; + prevMetadataRef.current = newMetadata; + return newMetadata; }, [persesDashboards, combinedInitialLoad]); // Retrieve dashboard metadata for the currently selected project const activeProjectDashboardsMetadata = useMemo(() => { + if (!activeProject) { + return combinedDashboardsMetadata; + } return combinedDashboardsMetadata.filter((combinedDashboardMetadata) => { return combinedDashboardMetadata.project === activeProject; }); @@ -74,35 +101,35 @@ export const useDashboardsData = () => { } const queryArguments = getAllQueryArguments(); + delete queryArguments.edit; + const params = new URLSearchParams(queryArguments); - params.set(QueryParams.Project, activeProject); + + const dashboard = combinedDashboardsMetadata.find((item) => item.name === newBoard); + + const projectToUse = activeProject || dashboard?.project; + + if (projectToUse) { + params.set(QueryParams.Project, projectToUse); + } params.set(QueryParams.Dashboard, newBoard); + if (dashboard?.persesDashboard?.spec?.duration) { + params.set(QueryParams.Start, dashboard.persesDashboard.spec.duration); + } + if (dashboard?.persesDashboard?.spec?.refreshInterval) { + params.set(QueryParams.Refresh, dashboard.persesDashboard.spec.refreshInterval); + } - let url = getDashboardsUrl(perspective); + let url = getDashboardUrl(perspective); url = `${url}?${params.toString()}`; if (newBoard !== dashboardName) { navigate(url, { replace: true }); } }, - [perspective, dashboardName, navigate, activeProject], + [perspective, dashboardName, navigate, activeProject, combinedDashboardsMetadata], ); - // If a dashboard hasn't been selected yet, or if the current project doesn't have a - // matching board name then display the board present in the URL parameters or the first - // board in the dropdown list - useEffect(() => { - const metadataMatch = activeProjectDashboardsMetadata.find((activeProjectDashboardMetadata) => { - return ( - activeProjectDashboardMetadata.project === activeProject && - activeProjectDashboardMetadata.name === dashboardName - ); - }); - if (!dashboardName || !metadataMatch) { - changeBoard(activeProjectDashboardsMetadata?.[0]?.name); - } - }, [dashboardName, changeBoard, activeProject, activeProjectDashboardsMetadata]); - return { persesAvailable, persesProjectsLoading, diff --git a/web/src/components/dashboards/perses/hooks/useEditableProjects.ts b/web/src/components/dashboards/perses/hooks/useEditableProjects.ts new file mode 100644 index 000000000..9e9640980 --- /dev/null +++ b/web/src/components/dashboards/perses/hooks/useEditableProjects.ts @@ -0,0 +1,111 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { PersesUserPermissions, useFetchPersesPermissions } from '../perses-client'; +import { useOcpProjects } from './useOcpProjects'; +import { K8sResourceKind } from '@openshift-console/dynamic-plugin-sdk'; + +interface Projects { + editableProjects: string[] | undefined; + allProjects: string[] | undefined; +} + +const useUsername = (): string => { + const getUser = (state: any) => state.sdkCore?.user; + const user = useSelector(getUser); + return user?.metadata?.name || user?.username; +}; + +const combinePersesAndOcpProjects = ( + persesUserPermissions: PersesUserPermissions, + ocpProjects: K8sResourceKind[], +): string[] => { + const persesProjectNames = Object.keys(persesUserPermissions).filter((name) => name !== '*'); + const allAvailableProjects = new Set([...persesProjectNames]); + ocpProjects.forEach((project) => { + if (project.metadata?.name) { + allAvailableProjects.add(project.metadata.name); + } + }); + return Array.from(allAvailableProjects); +}; + +const getEditableProjects = ( + persesUserPermissions: PersesUserPermissions, + allAvailableProjects: string[], +): string[] => { + const editableProjectNames = new Set(); + Object.entries(persesUserPermissions).forEach(([projectName, permissions]) => { + const hasDashboardPermissions = permissions.some((permission) => { + const allActions = permission.actions.includes('*'); + const individualActions = + permission.actions.includes('create') && + permission.actions.includes('update') && + permission.actions.includes('delete'); + const hasPermission = + permission.scopes.includes('Dashboard') && (individualActions || allActions); + return hasPermission; + }); + + if (hasDashboardPermissions) { + if (projectName === '*') { + allAvailableProjects.forEach((p) => editableProjectNames.add(p)); + } else { + editableProjectNames.add(projectName); + } + } + }); + return Array.from(editableProjectNames); +}; + +export const useEditableProjects = () => { + const username = useUsername(); + const { ocpProjects } = useOcpProjects(); + + const { persesUserPermissions, persesPermissionsLoading, persesPermissionsError } = + useFetchPersesPermissions(username); + + const { editableProjects, allProjects }: Projects = useMemo(() => { + if (persesPermissionsLoading) { + return { + editableProjects: undefined, + allProjects: undefined, + }; + } + if (!persesUserPermissions) { + return { + editableProjects: undefined, + allProjects: undefined, + }; + } + if (persesPermissionsError) { + return { + editableProjects: undefined, + allProjects: undefined, + }; + } + + const allAvailableProjects = combinePersesAndOcpProjects(persesUserPermissions, ocpProjects); + const editableProjectNames = getEditableProjects(persesUserPermissions, allAvailableProjects); + + // Sort projects alphabetically + const sortedEditableProjects = editableProjectNames.sort((a, b) => a.localeCompare(b)); + const sortedProjects = allAvailableProjects.sort((a, b) => a.localeCompare(b)); + + return { + editableProjects: sortedEditableProjects, + allProjects: sortedProjects, + }; + }, [persesPermissionsLoading, persesUserPermissions, persesPermissionsError, ocpProjects]); + + const hasEditableProject = useMemo(() => { + return editableProjects ? editableProjects.length > 0 : false; + }, [editableProjects]); + + return { + editableProjects, + allProjects, + hasEditableProject, + permissionsLoading: persesPermissionsLoading, + permissionsError: persesPermissionsError, + }; +}; diff --git a/web/src/components/dashboards/perses/hooks/useOcpProjects.ts b/web/src/components/dashboards/perses/hooks/useOcpProjects.ts new file mode 100644 index 000000000..1038eae4c --- /dev/null +++ b/web/src/components/dashboards/perses/hooks/useOcpProjects.ts @@ -0,0 +1,25 @@ +import { useMemo } from 'react'; +import { K8sResourceKind, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { ProjectModel } from '../../../console/models'; + +export const useOcpProjects = () => { + const [ocpProjects, ocpProjectsLoaded] = useK8sWatchResource({ + isList: true, + kind: ProjectModel.kind, + optional: true, + }); + + const memoizedOcpProjects = useMemo(() => { + return ocpProjects || []; + }, [ocpProjects]); + + const result = useMemo( + () => ({ + ocpProjects: memoizedOcpProjects, + ocpProjectsLoaded, + }), + [memoizedOcpProjects, ocpProjectsLoaded], + ); + + return result; +}; diff --git a/web/src/components/dashboards/perses/hooks/usePerses.ts b/web/src/components/dashboards/perses/hooks/usePerses.ts index 643432418..10e6cacf3 100644 --- a/web/src/components/dashboards/perses/hooks/usePerses.ts +++ b/web/src/components/dashboards/perses/hooks/usePerses.ts @@ -1,9 +1,15 @@ -import { fetchPersesProjects, fetchPersesDashboardsMetadata } from '../perses-client'; +import { + fetchPersesProjects, + fetchPersesDashboardsMetadata, + fetchPersesDashboardsByProject, +} from '../perses-client'; import { useQuery } from '@tanstack/react-query'; import { NumberParam, useQueryParam } from 'use-query-params'; import { QueryParams } from '../../../query-params'; +import { useTranslation } from 'react-i18next'; -export const usePerses = () => { +export const usePerses = (project?: string | number) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); const [refreshInterval] = useQueryParam(QueryParams.RefreshInterval, NumberParam); const { @@ -24,16 +30,39 @@ export const usePerses = () => { } = useQuery({ queryKey: ['dashboards'], queryFn: fetchPersesDashboardsMetadata, - enabled: true, + enabled: !project, // Only fetch all dashboards when no specific project is requested + refetchInterval: refreshInterval, + }); + + const { + isLoading: persesProjectDashboardsLoading, + error: persesProjectDashboardsError, + data: persesProjectDashboards, + } = useQuery({ + queryKey: ['dashboards', 'project', project], + queryFn: () => { + if (project === undefined || project === null) { + throw new Error(t('Project is required for fetching project dashboards')); + } + return fetchPersesDashboardsByProject(String(project)); + }, + enabled: !!project, refetchInterval: refreshInterval, }); return { - persesDashboards: persesDashboards ?? [], - persesDashboardsError, - persesDashboardsLoading, + // All Dashboards - fallback to project dashboards when all dashboards query is disabled + persesDashboards: persesDashboards ?? persesProjectDashboards ?? [], + persesDashboardsError: persesDashboardsError ?? persesProjectDashboardsError, + persesDashboardsLoading: + persesDashboardsLoading || (!!project && persesProjectDashboardsLoading), + // All Projects persesProjectsLoading, persesProjects: persesProjects ?? [], persesProjectsError, + // Dashboards of a given project + persesProjectDashboards: persesProjectDashboards ?? [], + persesProjectDashboardsError, + persesProjectDashboardsLoading, }; }; diff --git a/web/src/components/dashboards/perses/migrate-api.ts b/web/src/components/dashboards/perses/migrate-api.ts new file mode 100644 index 000000000..50bc7bdaa --- /dev/null +++ b/web/src/components/dashboards/perses/migrate-api.ts @@ -0,0 +1,39 @@ +import { DashboardResource } from '@perses-dev/core'; +import { useMutation, UseMutationResult } from '@tanstack/react-query'; +import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; +import { PERSES_PROXY_BASE_PATH } from './perses-client'; + +const MIGRATE_ENDPOINT = `${PERSES_PROXY_BASE_PATH}/api/migrate`; + +export interface MigrateBodyRequest { + input?: Record; + grafanaDashboard: Record; + useDefaultDatasource?: boolean; +} + +const migrateDashboard = async (body: MigrateBodyRequest): Promise => { + const requestBody = { + input: body.input || {}, + grafanaDashboard: body.grafanaDashboard, + useDefaultDatasource: !!body.useDefaultDatasource, + }; + + try { + const result = await consoleFetchJSON.post(MIGRATE_ENDPOINT, requestBody); + return result as DashboardResource; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(`Failed to migrate dashboard: ${message}`); + } +}; + +export function useMigrateDashboard(): UseMutationResult< + DashboardResource, + Error, + MigrateBodyRequest +> { + return useMutation({ + mutationKey: ['migrate'], + mutationFn: migrateDashboard, + }); +} diff --git a/web/src/components/dashboards/perses/perses-client.ts b/web/src/components/dashboards/perses/perses-client.ts index f1fae0b44..9720ab763 100644 --- a/web/src/components/dashboards/perses/perses-client.ts +++ b/web/src/components/dashboards/perses/perses-client.ts @@ -13,6 +13,13 @@ export const fetchPersesDashboardsMetadata = (): Promise => return ocpPersesFetchJson(persesURL); }; +export const fetchPersesDashboardsByProject = (project: string): Promise => { + const dashboardsEndpoint = `${PERSES_PROXY_BASE_PATH}/api/v1/dashboards`; + const persesURL = `${dashboardsEndpoint}?project=${encodeURIComponent(project)}`; + + return ocpPersesFetchJson(persesURL); +}; + export const fetchPersesProjects = (): Promise => { const listProjectURL = '/api/v1/projects'; const persesURL = `${PERSES_PROXY_BASE_PATH}${listProjectURL}`; @@ -20,6 +27,22 @@ export const fetchPersesProjects = (): Promise => { return ocpPersesFetchJson(persesURL); }; +export interface PersesPermission { + scopes: string[]; + actions: string[]; +} + +export type PersesUserPermissions = { + [projectName: string]: PersesPermission[]; +}; + +export const fetchPersesUserPermissions = (username: string): Promise => { + const userPermissionsURL = `/api/v1/users/${encodeURIComponent(username)}/permissions`; + const persesURL = `${PERSES_PROXY_BASE_PATH}${userPermissionsURL}`; + + return ocpPersesFetchJson(persesURL); +}; + export async function ocpPersesFetchJson(url: string): Promise { // Use perses fetch as base fetch call as it handles refresh tokens return fetchJson(url, { @@ -59,3 +82,28 @@ export const useFetchPersesDashboard = (project: string, dashboardName: string) persesDashboardLoading, }; }; + +export const useFetchPersesPermissions = (username: string) => { + const { + isLoading: persesPermissionsLoading, + error: persesPermissionsError, + data: persesUserPermissions, + } = useQuery({ + queryKey: ['perses-user-permissions', username], + queryFn: () => fetchPersesUserPermissions(username), + enabled: !!username, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + refetchOnWindowFocus: true, + retry: 2, + onError: (error) => { + // eslint-disable-next-line no-console + console.warn('Failed to fetch Perses user permissions:', error); + }, + }); + + return { + persesUserPermissions, + persesPermissionsError, + persesPermissionsLoading, + }; +}; diff --git a/web/src/components/dashboards/perses/perses-dashboards.tsx b/web/src/components/dashboards/perses/perses-dashboards.tsx deleted file mode 100644 index c0ec617d2..000000000 --- a/web/src/components/dashboards/perses/perses-dashboards.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Dashboard } from '@perses-dev/dashboards'; -import { useTranslation } from 'react-i18next'; - -/** - * This component is what we use to integrate perses into the openshift console - * It is a combinatoin of the ViewDashboard & DashboardApp components in the perses - * codebase. We can't use those components directly as they come bundled with the - * Dashboard toolbar, and are overall not needed for the non-editable dashboards. - * As we look to implement customizable dashboards we should look to remove the - * required DashboardsToolbar (as we will be providing our own) - */ - -function PersesBoard() { - const { t } = useTranslation(process.env.I18N_NAMESPACE); - - return ( - - ); -} - -export default PersesBoard; diff --git a/web/src/components/dashboards/perses/perses/datasource-api.ts b/web/src/components/dashboards/perses/perses/datasource-cache-api.ts similarity index 97% rename from web/src/components/dashboards/perses/perses/datasource-api.ts rename to web/src/components/dashboards/perses/perses/datasource-cache-api.ts index 5f7fff949..d0cf2110a 100644 --- a/web/src/components/dashboards/perses/perses/datasource-api.ts +++ b/web/src/components/dashboards/perses/perses/datasource-cache-api.ts @@ -12,8 +12,13 @@ // limitations under the License. import { getCSRFToken } from '@openshift-console/dynamic-plugin-sdk/lib/utils/fetch/console-fetch-utils'; -import { DatasourceResource, DatasourceSelector, GlobalDatasourceResource } from '@perses-dev/core'; -import { BuildDatasourceProxyUrlFunc, DatasourceApi } from '@perses-dev/dashboards'; +import { + DatasourceResource, + DatasourceSelector, + GlobalDatasourceResource, + BuildDatasourceProxyUrlFunc, + DatasourceApi, +} from '@perses-dev/core'; import LRUCache from 'lru-cache'; class Cache { diff --git a/web/src/components/dashboards/perses/persesPluginsLoader.tsx b/web/src/components/dashboards/perses/persesPluginsLoader.tsx deleted file mode 100644 index 2ede40b40..000000000 --- a/web/src/components/dashboards/perses/persesPluginsLoader.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { dynamicImportPluginLoader } from '@perses-dev/plugin-system'; - -import * as barchartPlugin from '@perses-dev/bar-chart-plugin'; -import * as datasourceVariablePlugin from '@perses-dev/datasource-variable-plugin'; -import * as flameChartPlugin from '@perses-dev/flame-chart-plugin'; -import * as gaugeChartPlugin from '@perses-dev/gauge-chart-plugin'; -import * as heatmapChartPlugin from '@perses-dev/heatmap-chart-plugin'; -import * as histogramChartPlugin from '@perses-dev/histogram-chart-plugin'; -import * as lokiPlugin from '@perses-dev/loki-plugin'; -import * as markdownPlugin from '@perses-dev/markdown-plugin'; -import * as pieChartPlugin from '@perses-dev/pie-chart-plugin'; -import * as prometheusPlugin from '@perses-dev/prometheus-plugin'; -import * as pyroscopePlugin from '@perses-dev/pyroscope-plugin'; -import * as scatterChartPlugin from '@perses-dev/scatter-chart-plugin'; -import * as statChartPlugin from '@perses-dev/stat-chart-plugin'; -import * as staticListVariablePlugin from '@perses-dev/static-list-variable-plugin'; -import * as statusHistoryChartPlugin from '@perses-dev/status-history-chart-plugin'; -import * as tablePlugin from '@perses-dev/table-plugin'; -import * as tempoPlugin from '@perses-dev/tempo-plugin'; -import * as timeseriesChartPlugin from '@perses-dev/timeseries-chart-plugin'; -import * as timeSeriesTablePlugin from '@perses-dev/timeseries-table-plugin'; -import * as traceTablePlugin from '@perses-dev/trace-table-plugin'; -import * as tracingGanttChartPlugin from '@perses-dev/tracing-gantt-chart-plugin'; - -export const pluginLoader = dynamicImportPluginLoader([ - { - resource: barchartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(barchartPlugin), - }, - { - resource: datasourceVariablePlugin.getPluginModule(), - importPlugin: () => Promise.resolve(datasourceVariablePlugin), - }, - { - resource: flameChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(flameChartPlugin), - }, - { - resource: gaugeChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(gaugeChartPlugin), - }, - { - resource: heatmapChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(heatmapChartPlugin), - }, - { - resource: histogramChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(histogramChartPlugin), - }, - { - resource: lokiPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(lokiPlugin), - }, - { - resource: markdownPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(markdownPlugin), - }, - { - resource: pieChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(pieChartPlugin), - }, - { - resource: prometheusPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(prometheusPlugin), - }, - { - resource: pyroscopePlugin.getPluginModule(), - importPlugin: () => Promise.resolve(pyroscopePlugin), - }, - { - resource: scatterChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(scatterChartPlugin), - }, - { - resource: statChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(statChartPlugin), - }, - { - resource: staticListVariablePlugin.getPluginModule(), - importPlugin: () => Promise.resolve(staticListVariablePlugin), - }, - { - resource: statusHistoryChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(statusHistoryChartPlugin), - }, - { - resource: tablePlugin.getPluginModule(), - importPlugin: () => Promise.resolve(tablePlugin), - }, - { - resource: tempoPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(tempoPlugin), - }, - { - resource: timeseriesChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(timeseriesChartPlugin), - }, - { - resource: timeSeriesTablePlugin.getPluginModule(), - importPlugin: () => Promise.resolve(timeSeriesTablePlugin), - }, - { - resource: traceTablePlugin.getPluginModule(), - importPlugin: () => Promise.resolve(traceTablePlugin), - }, - { - resource: tracingGanttChartPlugin.getPluginModule(), - importPlugin: () => Promise.resolve(tracingGanttChartPlugin), - }, -]); diff --git a/web/src/components/dashboards/perses/project/ProjectBar.tsx b/web/src/components/dashboards/perses/project/ProjectBar.tsx index 1e0465979..1d785a7db 100644 --- a/web/src/components/dashboards/perses/project/ProjectBar.tsx +++ b/web/src/components/dashboards/perses/project/ProjectBar.tsx @@ -1,21 +1,34 @@ import type { SetStateAction, Dispatch, FC } from 'react'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { KEYBOARD_SHORTCUTS } from './utils'; +import { getDashboardsListUrl, usePerspective } from '../../../hooks/usePerspective'; import ProjectDropdown from './ProjectDropdown'; export type ProjectBarProps = { - setActiveProject: Dispatch>; - activeProject: string; + setActiveProject: Dispatch>; + activeProject: string | null; }; export const ProjectBar: FC = ({ setActiveProject, activeProject }) => { + const navigate = useNavigate(); + const { perspective } = usePerspective(); + return (
{ - setActiveProject(newProject); + const params = new URLSearchParams(); + if (newProject === '') { + setActiveProject(null); + } else { + params.set('project', newProject); + setActiveProject(newProject); + } + const url = `${getDashboardsListUrl(perspective)}?${params.toString()}`; + navigate(url); }} - selected={activeProject} + selected={activeProject || ''} shortCut={KEYBOARD_SHORTCUTS.focusNamespaceDropdown} />
diff --git a/web/src/components/dashboards/perses/project/ProjectDropdown.tsx b/web/src/components/dashboards/perses/project/ProjectDropdown.tsx index 490bc2913..e30fca273 100644 --- a/web/src/components/dashboards/perses/project/ProjectDropdown.tsx +++ b/web/src/components/dashboards/perses/project/ProjectDropdown.tsx @@ -18,7 +18,7 @@ import fuzzysearch from 'fuzzysearch'; import { useTranslation } from 'react-i18next'; import ProjectMenuToggle from './ProjectMenuToggle'; import { alphanumericCompare } from './utils'; -import { usePerses } from '../hooks/usePerses'; +import { useEditableProjects } from '../hooks/useEditableProjects'; import { useCallback, useMemo, useRef, useState } from 'react'; export const NoResults: React.FC<{ @@ -108,24 +108,26 @@ const ProjectMenu: React.FC<{ menuRef: React.MutableRefObject; }> = ({ setOpen, onSelect, selected, menuRef }) => { const filterRef = useRef(null); + const { t } = useTranslation(process.env.I18N_NAMESPACE); const [filterText, setFilterText] = useState(''); - const { persesProjects } = usePerses(); + const { allProjects } = useEditableProjects(); const optionItems = useMemo(() => { - const items = persesProjects.map((item) => { - const { name } = item.metadata; - return { title: item?.spec?.display?.name ?? name, key: name }; - }); + const items = + allProjects?.map((projectName) => { + return { title: projectName, key: projectName }; + }) || []; - if (!items.some((option) => option.key === selected)) { + if (selected && !items.some((option) => option.key === selected)) { items.push({ title: selected, key: selected }); // Add current project if it isn't included } items.sort((a, b) => alphanumericCompare(a.title, b.title)); + items.unshift({ title: t('All Projects'), key: '' }); return items; - }, [persesProjects, selected]); + }, [allProjects, selected, t]); const isOptionShown = useCallback( (option) => { @@ -189,7 +191,7 @@ const ProjectDropdown: React.FC = ({ const { t } = useTranslation(process.env.I18N_NAMESPACE); const menuRef = useRef(null); const [isOpen, setOpen] = useState(false); - const { persesProjectsError, persesProjectsLoading, persesProjects } = usePerses(); + const { allProjects, permissionsLoading, permissionsError } = useEditableProjects(); // const title = selected === LEGACY_DASHBOARDS_KEY ? legacyDashboardsTitle : selected; @@ -200,14 +202,11 @@ const ProjectDropdown: React.FC = ({ menuRef, }; - if (persesProjectsLoading || persesProjectsError || persesProjects.length === 0) { + if (permissionsLoading || permissionsError || !allProjects || allProjects.length === 0) { return null; } - const selectedProject = persesProjects.find( - (persesProject) => persesProject.metadata.name === selected, - ); - const title = selectedProject?.spec?.display?.name ?? t('Dashboards'); + const title = selected && allProjects.includes(selected) ? selected : t('All Projects'); return (
diff --git a/web/src/components/dashboards/perses/project/useActiveProject.tsx b/web/src/components/dashboards/perses/project/useActiveProject.tsx index ddaf13cc3..5643ada83 100644 --- a/web/src/components/dashboards/perses/project/useActiveProject.tsx +++ b/web/src/components/dashboards/perses/project/useActiveProject.tsx @@ -11,7 +11,7 @@ import { QueryParams } from '../../../query-params'; import { StringParam, useQueryParam } from 'use-query-params'; export const useActiveProject = () => { - const [activeProject, setActiveProject] = useState(''); + const [activeProject, setActiveProject] = useState(null); const [activeNamespace, setActiveNamespace] = useActiveNamespace(); const { perspective } = usePerspective(); const { persesProjects, persesProjectsLoading } = usePerses(); @@ -24,7 +24,6 @@ export const useActiveProject = () => { // Sync the state and the URL param useEffect(() => { - // If data and url hasn't been set yet, default to legacy dashboard (for now) if (!activeProject && projectFromUrl) { setActiveProject(projectFromUrl); return; @@ -32,14 +31,10 @@ export const useActiveProject = () => { if (persesProjectsLoading) { return; } - if (!activeProject && !projectFromUrl) { - // set to first project - setActiveProject(persesProjects[0]?.metadata?.name); - return; - // If activeProject isn't set yet, but the url is, then load from url - } // If the url and the data is out of sync, follow the data - setProject(activeProject); + if (activeProject) { + setProject(activeProject); + } }, [ projectFromUrl, activeProject, diff --git a/web/src/components/dashboards/perses/project/utils.ts b/web/src/components/dashboards/perses/project/utils.ts index 16e3817ce..4a0352ea7 100644 --- a/web/src/components/dashboards/perses/project/utils.ts +++ b/web/src/components/dashboards/perses/project/utils.ts @@ -1,5 +1,8 @@ export const alphanumericCompare = (a: string, b: string): number => { - return a.localeCompare(b, undefined, { + const safeA = a || ''; + const safeB = b || ''; + + return safeA.localeCompare(safeB, undefined, { numeric: true, sensitivity: 'base', }); diff --git a/web/src/components/dashboards/shared/dashboard-dropdown.tsx b/web/src/components/dashboards/shared/dashboard-dropdown.tsx index d00ca5ba3..41f51bf92 100644 --- a/web/src/components/dashboards/shared/dashboard-dropdown.tsx +++ b/web/src/components/dashboards/shared/dashboard-dropdown.tsx @@ -9,7 +9,7 @@ import { StackItem, } from '@patternfly/react-core'; import type { FC } from 'react'; -import { memo } from 'react'; +import { memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { SingleTypeaheadDropdown } from '../../console/utils/single-typeahead-dropdown'; @@ -61,8 +61,14 @@ export const DashboardDropdown: FC = ({ items, onChange, children: item.title, })); + useEffect(() => { + if (items.filter((item) => item.name === selectedKey).length === 0) { + onChange(items.at(0)?.name); + } + }, [items, selectedKey, onChange]); + return ( - + diff --git a/web/src/components/data-test.ts b/web/src/components/data-test.ts index f540a4535..b77e4508e 100644 --- a/web/src/components/data-test.ts +++ b/web/src/components/data-test.ts @@ -19,6 +19,7 @@ export const DataTestIDs = { ExpireSilenceButton: 'expire-silence-button', ExpireXSilencesButton: 'expire-x-silences-button', Expression: 'expression', + FavoriteStarButton: 'favorite-button', KebabDropdownButton: 'kebab-dropdown-button', MastHeadHelpIcon: 'help-dropdown-toggle', MastHeadApplicationItem: 'application-launcher-item', @@ -59,6 +60,7 @@ export const DataTestIDs = { NamespaceDropdownShowSwitch: 'showSystemSwitch', NamespaceDropdownTextFilter: 'dropdown-text-filter', PersesDashboardDropdown: 'dashboard-dropdown', + PersesCreateDashboardButton: 'create-dashboard-button-list-page', SeverityBadgeHeader: 'severity-badge-header', SeverityBadge: 'severity-badge', SilenceAlertDropdownItem: 'silence-alert-dropdown-item', @@ -169,11 +171,24 @@ export const LegacyTestIDs = { SelectAllSilencesCheckbox: 'select-all-silences-checkbox', PersesDashboardSection: 'dashboard', NamespaceBarDropdown: 'namespace-bar-dropdown', + ApplicationLauncher: 'application-launcher', }; export const IDs = { ChartAxis0ChartLabel: 'chart-axis-0-ChartLabel', //id^=IDs.ChartAxis0ChartLabel AxisX 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', + persesDashboardCreateDashboardName: 'text-input-create-dashboard-dialog-name', + persesDashboardRenameDashboardName: 'rename-modal-text-input', + persesDashboardDuplicateDashboardName: 'duplicate-modal-dashboard-name-form-group-text-input', + persesDashboardImportDashboardUploadFileInput: 'import-dashboard-file-filename', + persesDashboardRefreshIntervalDropdown: 'refreshInterval', }; export const Classes = { @@ -201,6 +216,11 @@ export const Classes = { MetricsPageQueryAutocomplete: '.cm-tooltip-autocomplete.cm-tooltip.cm-tooltip-below', MoreLessTag: '.pf-v6-c-label-group__label, .pf-v5-c-chip-group__label', NamespaceDropdown: '.pf-v6-c-menu-toggle.co-namespace-dropdown__menu-toggle', + NamespaceDropdownExpanded: + '.pf-v6-c-menu-toggle.pf-m-expanded.co-namespace-dropdown__menu-toggle', + PersesCreateDashboardProjectDropdown: '.pf-v6-c-menu-toggle.pf-m-full-width.pf-m-typeahead', + PersesCreateDashboardDashboardNameError: '.pf-v6-c-helper-text__item-text', + PersesListDashboardCount: '.pf-v6-c-menu-toggle__text', SectionHeader: '.pf-v6-c-title.pf-m-h2, .co-section-heading', TableHeaderColumn: '.pf-v6-c-table__button, .pf-c-table__button', SilenceAlertTitle: '.pf-v6-c-alert__title, .pf-v5-c-alert__title', @@ -212,18 +232,121 @@ export const Classes = { SilenceKebabDropdown: '.pf-v6-c-menu-toggle.pf-m-plain, .pf-v5-c-dropdown__toggle.pf-m-plain', SilenceLabelRow: '.pf-v6-l-grid.pf-m-all-12-col-on-sm.pf-m-all-4-col-on-md.pf-m-gutter, .row', SilenceState: '.pf-v6-l-stack__item, .co-break-word', + ImportDashboardTextArea: '.view-lines.monaco-mouse-cursor-text', }; export const persesAriaLabels = { TimeRangeDropdown: 'Select time range. Currently set to [object Object]', RefreshButton: 'Refresh', - RefreshIntervalDropdown: 'Select refresh interval. Currently set to 0s', ZoomInButton: 'Zoom in', ZoomOutButton: 'Zoom out', + ViewJSONButton: 'View JSON', + EditJSONButton: 'Edit 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 ', + PanelExportTimeSeriesDataAsCSV: 'Export time series data as CSV', + //Add Panel tabs + AddPanelTabs: 'Panel configuration tabs', + //List Page + persesDashboardKebabIcon: 'Kebab toggle', + //dialogProjectDropdown + dialogProjectInput: 'Type to filter', + //Import Dashboard + dashboardActionsMenu: 'Dashboard actions', + importDashboardProjectInputButton: 'Typeahead menu toggle', + importDashboardDuplicatedDashboardError: 'Danger alert:document already exists', }; -export const persesDataTestIDs = { +//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 = { + createDashboardButtonToolbar: 'create-dashboard-button-list-page', + importDashboardButtonToolbar: 'import-dashboard-button-list-page', + editDashboardButtonToolbar: 'edit-dashboard-button-toolbar', + cancelButtonToolbar: 'cancel-button-toolbar', +}; + +export const listPersesDashboardsDataTestIDs = { + PersesBreadcrumbDashboardItem: 'perses-dashboards-breadcrumb-dashboard-item', + PersesBreadcrumbDashboardNameItem: 'perses-dashboards-breadcrumb-dashboard-name-item', + NameFilter: 'name-filter', + ProjectFilter: 'project-filter', + EmptyStateTitle: 'empty-state-title', + EmptyStateBody: 'empty-state-body', + ClearAllFiltersButton: 'clear-all-filters-button', + DashboardLinkPrefix: 'perseslistpage-', +}; + +export const listPersesDashboardsOUIAIDs = { + PageHeaderSubtitle: 'PageHeader-subtitle', + PersesBreadcrumb: 'perses-dashboards-breadcrumb', + PersesDashListDataViewTable: 'PersesDashList-DataViewTable', + persesListDataViewHeaderClearAllFiltersButton: 'PersesDashList-DataViewHeader-clear-all-filters', + persesListDataViewFilters: 'DataViewFilters', + 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', }; diff --git a/web/src/components/dropdown-poll-interval.tsx b/web/src/components/dropdown-poll-interval.tsx index 9c20c56e4..fb467705f 100644 --- a/web/src/components/dropdown-poll-interval.tsx +++ b/web/src/components/dropdown-poll-interval.tsx @@ -6,6 +6,7 @@ import { parsePrometheusDuration, } from './console/console-shared/src/datetime/prometheus'; import { SimpleSelect, SimpleSelectOption } from '@patternfly/react-templates'; +import { LegacyDashboardPageTestIDs } from './data-test'; type DropDownPollIntervalProps = { setInterval: (v: number) => void; @@ -49,6 +50,7 @@ export const DropDownPollInterval: FunctionComponent initialOptions={initialOptions} onSelect={(_ev, selection) => onSelect(_ev, selection)} toggleWidth="150px" + data-test={LegacyDashboardPageTestIDs.PollIntervalDropdownOptions} /> ); }; diff --git a/web/src/components/hooks/usePerspective.tsx b/web/src/components/hooks/usePerspective.tsx index e609b4cfe..b2794782d 100644 --- a/web/src/components/hooks/usePerspective.tsx +++ b/web/src/components/hooks/usePerspective.tsx @@ -5,7 +5,6 @@ import * as _ from 'lodash-es'; import { ALERTMANAGER_BASE_PATH, ALERTMANAGER_PROXY_PATH, - ALERTMANAGER_TENANCY_BASE_PATH, AlertResource, labelsToParams, MonitoringPlugins, @@ -14,7 +13,7 @@ import { } from '../utils'; import { GraphUnits } from '../metrics/units'; import { QueryParams } from '../query-params'; -import { MonitoringState } from 'src/store/store'; +import { MonitoringState } from '../../store/store'; export type UrlRoot = 'monitoring' | 'dev-monitoring' | 'multicloud/monitoring' | 'virt-monitoring'; @@ -62,161 +61,124 @@ export const usePerspective = (): usePerspectiveReturn => { } }; -export const getAlertsUrl = (perspective: Perspective, namespace?: string) => { +export const getAlertsUrl = (perspective: Perspective) => { switch (perspective) { case 'acm': return `/multicloud${AlertResource.url}`; - case 'admin': - return AlertResource.url; case 'virtualization-perspective': return `/virt-monitoring/alerts`; - case 'dev': + case 'admin': default: - return `/dev-monitoring/ns/${namespace}/alerts`; + return AlertResource.url; } }; // There is no equivalent rules list page in the developer perspective -export const getAlertRulesUrl = (perspective: Perspective, namespace?: string) => { +export const getAlertRulesUrl = (perspective: Perspective) => { switch (perspective) { case 'acm': return `/multicloud${RuleResource.url}`; case 'virtualization-perspective': return `/virt-monitoring/alertrules`; - case 'dev': - return `/dev-monitoring/ns/${namespace}/alertrules`; case 'admin': default: return RuleResource.url; } }; -export const getSilencesUrl = (perspective: Perspective, namespace?: string) => { +export const getSilencesUrl = (perspective: Perspective) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}`; - case 'admin': - return SilenceResource.url; case 'virtualization-perspective': return `/virt-monitoring/silences`; - case 'dev': + case 'admin': default: - return `/dev-monitoring/ns/${namespace}/silences`; + return SilenceResource.url; } }; -export const getNewSilenceAlertUrl = ( - perspective: Perspective, - alert: PrometheusAlert, - namespace?: string, -) => { +export const getNewSilenceAlertUrl = (perspective: Perspective, alert: PrometheusAlert) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}/~new?${labelsToParams(alert.labels)}`; - case 'admin': - return `${SilenceResource.url}/~new?${labelsToParams(alert.labels)}`; case 'virtualization-perspective': return `/virt-monitoring/silences/~new?${labelsToParams(alert.labels)}`; - case 'dev': + case 'admin': default: - return `/dev-monitoring/ns/${namespace}/silences/~new?${labelsToParams(alert.labels)}`; + return `${SilenceResource.url}/~new?${labelsToParams(alert.labels)}`; } }; -export const getNewSilenceUrl = (perspective: Perspective, namespace?: string) => { +export const getNewSilenceUrl = (perspective: Perspective) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}/~new`; case 'virtualization-perspective': return `/virt-monitoring/silences/~new`; case 'admin': - return `${SilenceResource.url}/~new`; - case 'dev': default: - return `/dev-monitoring/ns/${namespace}/silences/~new`; + return `${SilenceResource.url}/~new`; } }; -export const getRuleUrl = (perspective: Perspective, rule: Rule, namespace?: string) => { +export const getRuleUrl = (perspective: Perspective, rule: Rule) => { switch (perspective) { case 'acm': return `/multicloud${RuleResource.url}/${_.get(rule, 'id')}`; case 'virtualization-perspective': return `/virt-monitoring/alertrules/${rule?.id}`; case 'admin': - return `${RuleResource.url}/${_.get(rule, 'id')}`; - case 'dev': default: - return `/dev-monitoring/ns/${namespace}/rules/${rule?.id}`; + return `${RuleResource.url}/${_.get(rule, 'id')}`; } }; -export const getSilenceAlertUrl = (perspective: Perspective, id: string, namespace?: string) => { +export const getSilenceAlertUrl = (perspective: Perspective, id: string) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}/${id}`; case 'virtualization-perspective': return `/virt-monitoring/silences/${id}`; case 'admin': - return `${SilenceResource.url}/${id}`; - case 'dev': default: - return `/dev-monitoring/ns/${namespace}/silences/${id}`; + return `${SilenceResource.url}/${id}`; } }; -export const getEditSilenceAlertUrl = ( - perspective: Perspective, - id: string, - namespace?: string, -) => { +export const getEditSilenceAlertUrl = (perspective: Perspective, id: string) => { switch (perspective) { case 'acm': return `/multicloud${SilenceResource.url}/${id}/edit`; - case 'admin': - return `${SilenceResource.url}/${id}/edit`; case 'virtualization-perspective': return `/virt-monitoring/silences/${id}/edit`; - case 'dev': + case 'admin': default: - return `/dev-monitoring/ns/${namespace}/silences/${id}/edit`; + return `${SilenceResource.url}/${id}/edit`; } }; -export const getAlertUrl = ( - perspective: Perspective, - alert: PrometheusAlert, - ruleID: string, - namespace?: string, -) => { +export const getAlertUrl = (perspective: Perspective, alert: PrometheusAlert, ruleID: string) => { switch (perspective) { case 'acm': return `/multicloud${AlertResource.url}/${ruleID}?${labelsToParams(alert.labels)}`; case 'virtualization-perspective': return `/virt-monitoring/alerts/${ruleID}?${labelsToParams(alert.labels)}`; case 'admin': - return `${AlertResource.url}/${ruleID}?${labelsToParams(alert.labels)}`; - case 'dev': default: - return `/dev-monitoring/ns/${namespace}/alerts/${ruleID}?${labelsToParams(alert.labels)}`; + return `${AlertResource.url}/${ruleID}?${labelsToParams(alert.labels)}`; } }; -export const getFetchSilenceUrl = ( - perspective: Perspective, - silenceID: string, - namespace?: string, -) => { +export const getFetchSilenceUrl = (perspective: Perspective, silenceID: string) => { switch (perspective) { case 'acm': return `${ALERTMANAGER_PROXY_PATH}/api/v2/silence/${silenceID}`; - case 'admin': - return `${ALERTMANAGER_BASE_PATH}/api/v2/silence/${silenceID}`; case 'virtualization-perspective': return `${ALERTMANAGER_BASE_PATH}/api/v2/silence/${silenceID}`; - case 'dev': default: - return `${ALERTMANAGER_TENANCY_BASE_PATH}/api/v2/silence/${silenceID}?namespace=${namespace}`; + case 'admin': + return `${ALERTMANAGER_BASE_PATH}/api/v2/silence/${silenceID}`; } }; @@ -234,67 +196,62 @@ export const getObserveState = (plugin: MonitoringPlugins, state: MonitoringStat export const getQueryBrowserUrl = ({ perspective, query, - namespace, units, }: { perspective: Perspective; query: string; - namespace?: string; units?: GraphUnits; }) => { const unitsQueryParam = units ? `&${QueryParams.Units}=${units}` : ''; switch (perspective) { - case 'admin': - return `/monitoring/query-browser?query0=${encodeURIComponent(query)}${unitsQueryParam}`; - case 'dev': - return `/dev-monitoring/ns/${namespace}/metrics?query0=${encodeURIComponent( - query, - )}${unitsQueryParam}`; case 'virtualization-perspective': return `/virt-monitoring/query-browser?query0=${encodeURIComponent(query)}${unitsQueryParam}`; case 'acm': - default: return ''; + case 'admin': + default: + return `/monitoring/query-browser?query0=${encodeURIComponent(query)}${unitsQueryParam}`; } }; -export const getMutlipleQueryBrowserUrl = ( - perspective: Perspective, - params: URLSearchParams, - namespace?: string, -) => { +export const getMutlipleQueryBrowserUrl = (perspective: Perspective, params: URLSearchParams) => { switch (perspective) { - case 'admin': - return `/monitoring/query-browser?${params.toString()}`; - case 'dev': - return `/dev-monitoring/ns/${namespace}/metrics?${params.toString()}`; case 'virtualization-perspective': return `/virt-monitoring/query-browser?${params.toString()}`; case 'acm': - default: return ''; + case 'admin': + default: + return `/monitoring/query-browser?${params.toString()}`; } }; -export const getLegacyDashboardsUrl = ( - perspective: Perspective, - boardName: string, - namespace?: string, -) => { +export const getLegacyDashboardsUrl = (perspective: Perspective, boardName: string) => { switch (perspective) { case 'virtualization-perspective': return `/virt-monitoring/dashboards/${boardName}`; + case 'acm': + return ''; case 'admin': + default: return `/monitoring/dashboards/${boardName}`; - case 'dev': - return `/dev-monitoring/ns/${namespace}?dashboard=${boardName}`; + } +}; + +export const getDashboardUrl = (perspective: Perspective) => { + switch (perspective) { + case 'virtualization-perspective': + return `/virt-monitoring/v2/dashboards/view`; + case 'admin': + return `/monitoring/v2/dashboards/view`; case 'acm': + return `/multicloud/monitoring/v2/dashboards/view`; default: return ''; } }; -export const getDashboardsUrl = (perspective: Perspective) => { +export const getDashboardsListUrl = (perspective: Perspective) => { switch (perspective) { case 'virtualization-perspective': return `/virt-monitoring/v2/dashboards`; diff --git a/web/src/components/hooks/useQueryNamespace.ts b/web/src/components/hooks/useQueryNamespace.ts new file mode 100644 index 000000000..8151c2901 --- /dev/null +++ b/web/src/components/hooks/useQueryNamespace.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { StringParam, useQueryParam } from 'use-query-params'; +import { QueryParams } from '../query-params'; +import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; + +// Utility hook to syncronize the namespace parameter in the URL with the activeNamespace +// the console uses. It will return the namespace parameter if set or the activeNamespace if +// it isn't set. +export const useQueryNamespace = () => { + const [queryNamespace, setQueryNamespace] = useQueryParam(QueryParams.Namespace, StringParam); + const [activeNamespace, setActiveNamespace] = useActiveNamespace(); + + useEffect(() => { + if (queryNamespace && activeNamespace !== queryNamespace) { + setActiveNamespace(queryNamespace); + } + }, [queryNamespace, activeNamespace, setActiveNamespace, setQueryNamespace]); + + return { + namespace: queryNamespace || activeNamespace, + setNamespace: setQueryNamespace, + }; +}; diff --git a/web/src/components/metrics/promql-expression-input.tsx b/web/src/components/metrics/promql-expression-input.tsx index 659749627..4f2900464 100644 --- a/web/src/components/metrics/promql-expression-input.tsx +++ b/web/src/components/metrics/promql-expression-input.tsx @@ -9,28 +9,28 @@ import { } from '@codemirror/autocomplete'; import { defaultKeymap, - historyKeymap, history, + historyKeymap, insertNewlineAndIndent, } from '@codemirror/commands'; import { - indentOnInput, - HighlightStyle, bracketMatching, + HighlightStyle, + indentOnInput, syntaxHighlighting, } from '@codemirror/language'; -import { tags } from '@lezer/highlight'; import { lintKeymap } from '@codemirror/lint'; import { highlightSelectionMatches } from '@codemirror/search'; import { EditorState, Prec } from '@codemirror/state'; import { + placeholder as codeMirrorPlaceholder, EditorView, highlightSpecialChars, keymap, - placeholder as codeMirrorPlaceholder, ViewPlugin, ViewUpdate, } from '@codemirror/view'; +import { tags } from '@lezer/highlight'; import { PrometheusEndpoint, useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; import { Button, @@ -46,31 +46,30 @@ import { import { CloseIcon, ExclamationCircleIcon } from '@patternfly/react-icons'; import { PromQLExtension } from '@prometheus-io/codemirror-promql'; import type { FC } from 'react'; -import { useRef, useState, useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSafeFetch } from '../console/utils/safe-fetch-hook'; -import { ALL_NAMESPACES_KEY, getPrometheusBasePath, PROMETHEUS_BASE_PATH } from '../utils'; -import { LabelNamesResponse } from '@perses-dev/prometheus-plugin'; import { + t_global_color_brand_default, + t_global_color_nonstatus_purple_default, + t_global_color_nonstatus_yellow_default, t_global_color_status_custom_default, t_global_color_status_danger_default, t_global_color_status_success_default, t_global_color_status_warning_default, + t_global_font_family_mono, + t_global_font_size_sm, t_global_font_weight_body_bold, + t_global_spacer_xs, t_global_text_color_disabled, t_global_text_color_regular, - t_global_spacer_xs, t_global_text_color_subtle, - t_global_font_family_mono, - t_global_font_size_sm, - t_global_color_brand_default, - t_global_color_nonstatus_yellow_default, - t_global_color_nonstatus_purple_default, } from '@patternfly/react-tokens'; -import { usePatternFlyTheme } from '../hooks/usePatternflyTheme'; import { useMonitoring } from '../../hooks/useMonitoring'; +import { usePatternFlyTheme } from '../hooks/usePatternflyTheme'; +import { getPrometheusBasePath, PROMETHEUS_BASE_PATH } from '../utils'; const box_shadow = ` var(--pf-t--global--box-shadow--X--md--default) @@ -327,9 +326,11 @@ export const PromQLExpressionInput: FC = ({ onValueChange, onSelectionChange, }) => { + type LabelNamesResponse = { data?: string[] }; + const { t } = useTranslation(process.env.I18N_NAMESPACE); const [namespace] = useActiveNamespace(); - const { prometheus } = useMonitoring(); + const { prometheus, accessCheckLoading, useMetricsTenancy } = useMonitoring(); const { theme: pfTheme } = usePatternFlyTheme(); const containerRef = useRef(null); @@ -343,12 +344,16 @@ export const PromQLExpressionInput: FC = ({ const safeFetch = useCallback(useSafeFetch(), []); useEffect(() => { + if (accessCheckLoading) { + return; + } // If we are using the tenancy path, then add the namespace as a query parameter at the end of // the url - const namespaceQueryParam = namespace !== ALL_NAMESPACES_KEY ? `?namespace=${namespace}` : ''; - const url = `${getPrometheusBasePath({ namespace, prometheus })}/${ - PrometheusEndpoint.LABEL - }/__name__/values${namespaceQueryParam}`; + const namespaceQueryParam = useMetricsTenancy ? `?namespace=${namespace}` : ''; + const url = `${getPrometheusBasePath({ + useTenancyPath: useMetricsTenancy, + prometheus, + })}/${PrometheusEndpoint.LABEL}/__name__/values${namespaceQueryParam}`; safeFetch(url) .then((response) => { const metrics = response?.data; @@ -363,7 +368,7 @@ export const PromQLExpressionInput: FC = ({ setErrorMessage(message); } }); - }, [safeFetch, t, namespace, prometheus]); + }, [safeFetch, t, namespace, prometheus, accessCheckLoading, useMetricsTenancy]); const onClear = () => { if (viewRef.current !== null) { diff --git a/web/src/components/query-browser.tsx b/web/src/components/query-browser.tsx index 5cad8d827..0ab30202f 100644 --- a/web/src/components/query-browser.tsx +++ b/web/src/components/query-browser.tsx @@ -606,10 +606,9 @@ const QueryBrowser_: FC = ({ units, onDataChange, isPlain = false, - useTenancy = false, }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); - const { plugin, prometheus } = useMonitoring(); + const { plugin, prometheus, accessCheckLoading, useMetricsTenancy } = useMonitoring(); const hideGraphs = useSelector( (state: MonitoringState) => !!getObserveState(plugin, state).hideGraphs, @@ -652,7 +651,7 @@ const QueryBrowser_: FC = ({ const canStack = _.sumBy(graphData, 'length') <= maxStacks; - const [activeNamespace] = useActiveNamespace(); + const [namespace] = useActiveNamespace(); // If provided, `timespan` overrides any existing span setting useEffect(() => { @@ -678,10 +677,10 @@ const QueryBrowser_: FC = ({ // Clear any existing series data when the namespace is changed useEffect(() => { dispatch(queryBrowserDeleteAllSeries()); - }, [dispatch, activeNamespace]); + }, [dispatch, namespace]); const tick = () => { - if (hideGraphs) { + if (hideGraphs || accessCheckLoading) { return undefined; } @@ -701,7 +700,7 @@ const QueryBrowser_: FC = ({ prometheusUrlProps: { endpoint: PrometheusEndpoint.QUERY_RANGE, endTime: timeRange.endTime, - namespace: activeNamespace, + namespace, query, samples: Math.ceil(samples / timeRanges.length), timeout: '60s', @@ -709,7 +708,7 @@ const QueryBrowser_: FC = ({ }, basePath: getPrometheusBasePath({ prometheus, - namespace: useTenancy ? activeNamespace : '', + useTenancyPath: useMetricsTenancy, basePathOverride: customDataSource?.basePath, }), }), @@ -850,15 +849,17 @@ const QueryBrowser_: FC = ({ delay, endTime, filterLabels, - activeNamespace, + namespace, queriesKey, samples, span, lastRequestTime, showDisconnectedValues, + accessCheckLoading, + useMetricsTenancy, ); - useLayoutEffect(() => setUpdating(true), [endTime, activeNamespace, queriesKey, samples, span]); + useLayoutEffect(() => setUpdating(true), [endTime, namespace, queriesKey, samples, span]); const onSpanChange = useCallback( (newSpan: number) => { @@ -1111,7 +1112,6 @@ export type QueryBrowserProps = { units?: GraphUnits; onDataChange?: (data: any) => void; isPlain?: boolean; - useTenancy?: boolean; }; type SpanControlsProps = { diff --git a/web/src/components/query-params.ts b/web/src/components/query-params.ts index 0255bf2ce..bfc725d79 100644 --- a/web/src/components/query-params.ts +++ b/web/src/components/query-params.ts @@ -7,4 +7,9 @@ export enum QueryParams { Project = 'project', Namespace = 'namespace', Units = 'units', + // Use openshift-namespace query parameter for dashboards page since grafana variables cannot have + // a `-` character in their name + OpenshiftProject = 'project-dropdown-value', + Refresh = 'refresh', + Start = 'start', } diff --git a/web/src/components/redirects/dev-redirects.tsx b/web/src/components/redirects/dev-redirects.tsx new file mode 100644 index 000000000..db2c8bd19 --- /dev/null +++ b/web/src/components/redirects/dev-redirects.tsx @@ -0,0 +1,85 @@ +import type { FC } from 'react'; +import { Navigate, useParams } from 'react-router-dom-v5-compat'; +import { + getAlertRulesUrl, + getAlertsUrl, + getEditSilenceAlertUrl, + getLegacyDashboardsUrl, + getSilenceAlertUrl, +} from '../hooks/usePerspective'; +import { QueryParams } from '../query-params'; +import { SilenceResource } from '../utils'; + +export const DashboardRedirect: FC = () => { + const pathParams = useParams<{ ns: string }>(); + + const queryParams = new URLSearchParams(window.location.search); + queryParams.append(QueryParams.OpenshiftProject, pathParams.ns); + + const dashboardName = queryParams.get(QueryParams.Dashboard); + queryParams.delete(QueryParams.Dashboard); + + return ; +}; + +export const AlertRedirect: FC = () => { + const pathParams = useParams<{ ns: string; ruleID: string }>(); + + const queryParams = new URLSearchParams(window.location.search); + queryParams.append(QueryParams.Namespace, pathParams.ns); + + return ( + + ); +}; + +export const RulesRedirect: FC = () => { + const pathParams = useParams<{ ns: string; id: string }>(); + + const queryParams = new URLSearchParams(window.location.search); + queryParams.append(QueryParams.Namespace, pathParams.ns); + + return ( + + ); +}; + +export const SilenceRedirect: FC = () => { + const pathParams = useParams<{ ns: string; id: string }>(); + + const queryParams = new URLSearchParams(window.location.search); + queryParams.append(QueryParams.Namespace, pathParams.ns); + + return ( + + ); +}; + +export const SilenceEditRedirect: FC = () => { + const pathParams = useParams<{ ns: string; id: string }>(); + + const queryParams = new URLSearchParams(window.location.search); + queryParams.append(QueryParams.Namespace, pathParams.ns); + + return ( + + ); +}; + +export const SilenceNewRedirect: FC = () => { + const pathParams = useParams<{ ns: string }>(); + + const queryParams = new URLSearchParams(window.location.search); + queryParams.append(QueryParams.Namespace, pathParams.ns); + + return ; +}; + +export const MetricsRedirect: FC = () => { + const pathParams = useParams<{ ns: string }>(); + + const queryParams = new URLSearchParams(window.location.search); + queryParams.append(QueryParams.Namespace, pathParams.ns); + + return ; +}; diff --git a/web/src/components/prometheus-redirect-page.tsx b/web/src/components/redirects/prometheus-redirect-page.tsx similarity index 84% rename from web/src/components/prometheus-redirect-page.tsx rename to web/src/components/redirects/prometheus-redirect-page.tsx index d423acfe0..b09d8c11e 100644 --- a/web/src/components/prometheus-redirect-page.tsx +++ b/web/src/components/redirects/prometheus-redirect-page.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import { Navigate } from 'react-router-dom-v5-compat'; -import { getAllQueryArguments } from './console/utils/router'; -import { usePerspective } from './hooks/usePerspective'; +import { getAllQueryArguments } from '../console/utils/router'; +import { usePerspective } from '../hooks/usePerspective'; // Handles links that have the Prometheus UI's URL format (expected for links in alerts sent by // Alertmanager). The Prometheus UI specifies the PromQL query with the GET param `g0.expr`, so we diff --git a/web/src/components/targets-page.tsx b/web/src/components/targets-page.tsx index 3757dd68f..95967db16 100644 --- a/web/src/components/targets-page.tsx +++ b/web/src/components/targets-page.tsx @@ -457,7 +457,7 @@ const List: FC = ({ data, loaded, loadError, unfilteredData }) => { Row={Row} unfilteredData={unfilteredData} NoDataEmptyMsg={() => { - return ; + return ; }} /> ); diff --git a/web/src/components/utils.ts b/web/src/components/utils.ts index 3fdc316bb..f0e6c679f 100644 --- a/web/src/components/utils.ts +++ b/web/src/components/utils.ts @@ -219,11 +219,11 @@ const getSearchParams = ({ export const getPrometheusBasePath = ({ prometheus, - namespace, + useTenancyPath, basePathOverride, }: { prometheus?: Prometheus; - namespace?: string; + useTenancyPath?: boolean; basePathOverride?: string; }) => { if (basePathOverride) { @@ -232,7 +232,7 @@ export const getPrometheusBasePath = ({ if (prometheus === 'acm') { return PROMETHEUS_PROXY_PATH; - } else if (namespace && namespace !== ALL_NAMESPACES_KEY) { + } else if (useTenancyPath) { return PROMETHEUS_TENANCY_BASE_PATH; } else { return PROMETHEUS_BASE_PATH; @@ -246,13 +246,21 @@ export const buildPrometheusUrl = ({ prometheusUrlProps: PrometheusURLProps; basePath: string; }): string | null => { + if ( + basePath !== PROMETHEUS_TENANCY_BASE_PATH || + prometheusUrlProps.namespace === ALL_NAMESPACES_KEY + ) { + prometheusUrlProps.namespace = undefined; + } if (prometheusUrlProps.endpoint !== PrometheusEndpoint.RULES && !prometheusUrlProps.query) { // Empty query provided, skipping API call return null; } const params = getSearchParams(prometheusUrlProps); - return `${basePath}/${prometheusUrlProps.endpoint}?${params.toString()}`; + return `${basePath}/${prometheusUrlProps.endpoint}${ + params.size > 0 ? '?' + params.toString() : '' + }`; }; type PrometheusURLProps = { @@ -267,14 +275,16 @@ type PrometheusURLProps = { export const getAlertmanagerSilencesUrl = ({ prometheus, + useTenancyPath, namespace, }: { prometheus: Prometheus; + useTenancyPath: boolean; namespace?: string; }) => { if (prometheus === 'acm') { return `${ALERTMANAGER_PROXY_PATH}/api/v2/silences`; - } else if (namespace && namespace !== ALL_NAMESPACES_KEY) { + } else if (useTenancyPath && namespace && namespace !== ALL_NAMESPACES_KEY) { return `${ALERTMANAGER_TENANCY_BASE_PATH}/api/v2/silences?namespace=${namespace}`; } else { return `${ALERTMANAGER_BASE_PATH}/api/v2/silences`; diff --git a/web/src/contexts/MonitoringContext.tsx b/web/src/contexts/MonitoringContext.tsx index 668587570..fb48a4316 100644 --- a/web/src/contexts/MonitoringContext.tsx +++ b/web/src/contexts/MonitoringContext.tsx @@ -1,19 +1,73 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { MonitoringPlugins, Prometheus } from '../components/utils'; +import { QueryParamProvider } from 'use-query-params'; +import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; +import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; type MonitoringContextType = { /** Dictates which plugin this code is being run in */ plugin: MonitoringPlugins; /** Dictates which prometheus instance this code should contact */ prometheus: Prometheus; + /** Dictates if the user has accesss to alerts in ALL namespaces + * If so, then don't show the namespace bar on alerting pages + */ + useAlertsTenancy: boolean; + /** Dictates if the user has accesss to alerts in ALL namespaces + * If so, then don't show the namespace bar on alerting pages + */ + useMetricsTenancy: boolean; + /** Dictates if the users access is being loaded. */ + accessCheckLoading: boolean; }; export const MonitoringContext = React.createContext({ plugin: 'monitoring-plugin', prometheus: 'cmo', + useAlertsTenancy: false, + useMetricsTenancy: false, + accessCheckLoading: true, }); -export const MonitoringProvider: React.FC<{ monitoringContext: MonitoringContextType }> = ({ - children, - monitoringContext, -}) => {children}; +export const MonitoringProvider: React.FC<{ + monitoringContext: { + plugin: MonitoringPlugins; + prometheus: Prometheus; + }; +}> = ({ children, monitoringContext }) => { + const [allNamespaceAlertsTenancy, alertAccessCheckLoading] = useAccessReview({ + group: 'monitoring.coreos.com', + resource: 'prometheusrules', + verb: 'get', + namespace: '*', + }); + const [allNamespaceMeticsTenancy, metricsAccessCheckLoading] = useAccessReview({ + group: 'monitoring.coreos.com', + resource: 'prometheuses/api', + verb: 'get', + name: 'k8s', + namespace: '*', + }); + + const monContext = useMemo(() => { + return { + ...monitoringContext, + // We only need to use the tenancy path when we are querying the in cluster monitoring + useAlertsTenancy: monitoringContext.prometheus === 'cmo' && !allNamespaceAlertsTenancy, + useMetricsTenancy: monitoringContext.prometheus === 'cmo' && !allNamespaceMeticsTenancy, + accessCheckLoading: alertAccessCheckLoading || metricsAccessCheckLoading, + }; + }, [ + monitoringContext, + allNamespaceAlertsTenancy, + alertAccessCheckLoading, + allNamespaceMeticsTenancy, + metricsAccessCheckLoading, + ]); + + return ( + + {children} + + ); +}; diff --git a/web/src/e2e-tests-app.tsx b/web/src/e2e-tests-app.tsx index c5a06ae33..ddb60590c 100644 --- a/web/src/e2e-tests-app.tsx +++ b/web/src/e2e-tests-app.tsx @@ -3,8 +3,6 @@ import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter, Link, Route, Routes } from 'react-router-dom-v5-compat'; import { combineReducers, createStore } from 'redux'; -import { QueryParamProvider } from 'use-query-params'; -import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; import { MpCmoAlertingPage } from './components/alerting/AlertingPage'; import { MpCmoAlertRulesDetailsPage } from './components/alerting/AlertRulesDetailsPage'; import { MpCmoAlertRulesPage } from './components/alerting/AlertRulesPage'; @@ -16,7 +14,7 @@ import { MpCmoSilencesDetailsPage } from './components/alerting/SilencesDetailsP import { MpCmoSilencesPage } from './components/alerting/SilencesPage'; import { MpCmoLegacyDashboardsPage } from './components/dashboards/legacy/legacy-dashboard-page'; import { MpCmoMetricsPage } from './components/MetricsPage'; -import PrometheusRedirectPage from './components/prometheus-redirect-page'; +import PrometheusRedirectPage from './components/redirects/prometheus-redirect-page'; import { MpCmoTargetsPage } from './components/targets-page'; import i18n from './i18n'; import ObserveReducers from './store/reducers'; @@ -37,31 +35,29 @@ const App = () => ( Dashboards Targets
- - - } /> + + } /> - } /> - } /> + } /> + } /> - } /> - } /> + } /> + } /> - } /> - } /> + } /> + } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> - }> - } /> - } /> - } /> - - - + }> + } /> + } /> + } /> + + ); diff --git a/web/src/hooks/useAlerts.ts b/web/src/hooks/useAlerts.ts index f48f8e156..34cd2080d 100644 --- a/web/src/hooks/useAlerts.ts +++ b/web/src/hooks/useAlerts.ts @@ -6,7 +6,6 @@ import { isAlertingRulesSource, PrometheusEndpoint, Rule, - useActiveNamespace, useResolvedExtensions, } from '@openshift-console/dynamic-plugin-sdk'; import { @@ -26,22 +25,25 @@ import { getAdditionalSources, } from '../components/alerting/AlertUtils'; import { MonitoringState } from '../store/store'; -import { getObserveState, usePerspective } from '../components/hooks/usePerspective'; +import { getObserveState } from '../components/hooks/usePerspective'; +import { useQueryNamespace } from '../components/hooks/useQueryNamespace'; const POLLING_INTERVAL_MS = 15 * 1000; // 15 seconds -export const useAlerts = (props?: { overrideNamespace?: string }) => { +export const useAlerts = (props?: { dontUseTenancy?: boolean }) => { // Retrieve external information which dictates which alerts to load and use const { plugin } = useMonitoring(); - const [namespace] = useActiveNamespace(); - const { prometheus } = useMonitoring(); - const { perspective } = usePerspective(); - const overriddenNamespace = props?.overrideNamespace ? props.overrideNamespace : namespace; + const { namespace } = useQueryNamespace(); + const { prometheus, useAlertsTenancy, accessCheckLoading } = useMonitoring(); + const overriddenNamespace = + props?.dontUseTenancy || !useAlertsTenancy ? ALL_NAMESPACES_KEY : namespace; // Start polling for alerts, rules, and silences const { trigger } = useAlertsPoller({ namespace: overriddenNamespace, prometheus, + useAlertsTenancy, + accessCheckLoading, }); // Retrieve alerts, rules and silences from the store, which is populated in the poller @@ -96,16 +98,6 @@ export const useAlerts = (props?: { overrideNamespace?: string }) => { return clusterArray.sort(); }, [silences]); - // When a user with cluster scoped alerts retrieves alerts from the tenancy API endpoint - // the API will still retrun ALL alerts, not just the ones which are available at that tenant - // As such we manually filter down the alerts on the frontend - const namespacedAlerts = useMemo(() => { - if (perspective === 'acm' || namespace === ALL_NAMESPACES_KEY) { - return alerts; - } - return alerts?.filter((alert) => alert.labels?.namespace === namespace); - }, [alerts, perspective, namespace]); - return { trigger, additionalAlertSourceLabels, @@ -115,16 +107,20 @@ export const useAlerts = (props?: { overrideNamespace?: string }) => { rulesAlertLoading, rules, silences, - alerts: namespacedAlerts, + alerts, }; }; const useAlertsPoller = ({ namespace, prometheus, + useAlertsTenancy, + accessCheckLoading, }: { namespace?: string; prometheus: Prometheus; + useAlertsTenancy: boolean; + accessCheckLoading: boolean; }) => { const dispatch = useDispatch(); const [customExtensions] = @@ -143,14 +139,32 @@ const useAlertsPoller = ({ const rulesUrl = buildPrometheusUrl({ prometheusUrlProps: { endpoint: PrometheusEndpoint.RULES, namespace }, - basePath: getPrometheusBasePath({ prometheus, namespace }), + basePath: getPrometheusBasePath({ prometheus, useTenancyPath: useAlertsTenancy }), + }); + const silencesUrl = getAlertmanagerSilencesUrl({ + prometheus, + namespace, + useTenancyPath: useAlertsTenancy, }); - const silencesUrl = getAlertmanagerSilencesUrl({ prometheus, namespace }); const fetchDispatch = () => - dispatch(fetchAlertingData(prometheus, namespace, rulesUrl, alertsSource, silencesUrl)); + dispatch( + fetchAlertingData( + prometheus, + namespace, + rulesUrl, + alertsSource, + silencesUrl, + !accessCheckLoading, // Wait to poll until we know which endpoint to use + ), + ); + + const dependencies = useMemo( + () => [namespace, rulesUrl, silencesUrl, useAlertsTenancy, accessCheckLoading], + [namespace, rulesUrl, silencesUrl, useAlertsTenancy, accessCheckLoading], + ); - usePoll(fetchDispatch, POLLING_INTERVAL_MS); + usePoll(fetchDispatch, POLLING_INTERVAL_MS, dependencies); return { trigger: fetchDispatch }; }; diff --git a/web/src/hooks/useMonitoring.ts b/web/src/hooks/useMonitoring.ts index 731152e41..a21002ac1 100644 --- a/web/src/hooks/useMonitoring.ts +++ b/web/src/hooks/useMonitoring.ts @@ -2,9 +2,13 @@ import { useContext } from 'react'; import { MonitoringContext } from '../contexts/MonitoringContext'; export const useMonitoring = () => { - const { prometheus, plugin } = useContext(MonitoringContext); + const { prometheus, plugin, useAlertsTenancy, useMetricsTenancy, accessCheckLoading } = + useContext(MonitoringContext); return { prometheus, plugin, + useAlertsTenancy, + useMetricsTenancy, + accessCheckLoading, }; }; diff --git a/web/src/index.d.ts b/web/src/index.d.ts index 953c07ef5..d77b674a9 100644 --- a/web/src/index.d.ts +++ b/web/src/index.d.ts @@ -10,6 +10,17 @@ declare interface Window { prometheusBaseURL: string; prometheusTenancyBaseURL: string; }; + /** + * Perses app configuration made available globally for plugin compatibility + */ + PERSES_APP_CONFIG: { + api_prefix: string; + }; + /** + * Plugin assets path used by module federation for loading plugin assets + * Set to the same value as the proxy base URL + */ + PERSES_PLUGIN_ASSETS_PATH: string; } // TODO: Remove when upgrading to TypeScript 4.1.2+, which has a type for ListFormat and diff --git a/web/src/perses-config.ts b/web/src/perses-config.ts new file mode 100644 index 000000000..276590f56 --- /dev/null +++ b/web/src/perses-config.ts @@ -0,0 +1,20 @@ +/** + * Perses Plugin Configuration + * + * This module configures global variables needed for Perses plugins to load assets + * through the OpenShift Console monitoring plugin proxy. The proxy path is injected + * at build time via webpack DefinePlugin. + */ + +// Build-time injected proxy URL for Perses plugins +declare const PERSES_PROXY_BASE_URL: string; + +// Configuration object for Perses app compatibility +const PERSES_APP_CONFIG = { + api_prefix: PERSES_PROXY_BASE_URL, +}; + +// Set up window globals for plugin system compatibility +// These are needed for plugins that use getPublicPath() in their Module Federation configs +window.PERSES_APP_CONFIG = PERSES_APP_CONFIG; +window.PERSES_PLUGIN_ASSETS_PATH = PERSES_PROXY_BASE_URL; diff --git a/web/src/store/thunks.ts b/web/src/store/thunks.ts index b7824f552..5c3b5fa6d 100644 --- a/web/src/store/thunks.ts +++ b/web/src/store/thunks.ts @@ -22,9 +22,13 @@ export const fetchAlertingData = rulesUrl: string, alertsSource: any, silencesUrl: string, + active: boolean, ): ThunkAction> => // eslint-disable-next-line @typescript-eslint/no-unused-vars async (dispatch, getState) => { + if (!active) { + return; + } dispatch(alertingSetLoading(prometheus, namespace)); const [rulesResponse, silencesResponse] = await Promise.allSettled([ diff --git a/web/webpack.config.ts b/web/webpack.config.ts index adf2f5bbf..7515729c8 100644 --- a/web/webpack.config.ts +++ b/web/webpack.config.ts @@ -25,18 +25,6 @@ const config: Configuration = { }, module: { rules: [ - { - test: /\.(jsx?|tsx?)$/, - exclude: /node_modules/, - use: [ - { - loader: 'ts-loader', - options: { - configFile: path.resolve(__dirname, 'tsconfig.json'), - }, - }, - ], - }, { test: /\.scss$/, exclude: /node_modules\/(?!(@patternfly|@openshift-console\/plugin-shared)\/).*/, @@ -100,6 +88,8 @@ const config: Configuration = { patterns: [{ from: path.resolve(__dirname, 'locales'), to: 'locales' }], }), new DefinePlugin({ + // Build-time injection of proxy path for config module + PERSES_PROXY_BASE_URL: JSON.stringify('/api/proxy/plugin/monitoring-console-plugin/perses'), 'process.env.I18N_NAMESPACE': process.env.I18N_NAMESPACE ? JSON.stringify(process.env.I18N_NAMESPACE) : JSON.stringify('plugin__monitoring-plugin'), @@ -119,6 +109,23 @@ if (process.env.NODE_ENV === 'production') { config.optimization.chunkIds = 'deterministic'; config.optimization.minimize = true; config.devtool = false; + + // Use default esbuild-loader for prod + config.module.rules?.unshift({ + test: /\.[jt]sx?$/, + loader: 'esbuild-loader', + options: { + target: 'es2021', + }, + }); +} else { + config.module.rules?.unshift({ + test: /\.(jsx?|tsx?)$/, + exclude: /node_modules/, + use: { + loader: 'swc-loader', + }, + }); } export default config; diff --git a/web/webpack.standalone.config.ts b/web/webpack.standalone.config.ts index 86ffa26c7..2b61b435a 100644 --- a/web/webpack.standalone.config.ts +++ b/web/webpack.standalone.config.ts @@ -31,9 +31,10 @@ const config: Configuration = { exclude: /node_modules/, use: [ { - loader: 'ts-loader', + loader: 'esbuild-loader', options: { - configFile: path.resolve(__dirname, 'tsconfig.json'), + target: 'es2021', + tsconfig: path.resolve(__dirname, 'tsconfig.json'), }, }, ], @@ -90,7 +91,7 @@ const config: Configuration = { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', - 'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type, Authorization' + 'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type, Authorization', }, devMiddleware: { writeToDisk: true,