diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 1238dbbd9a..bebbb402dc 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -253,10 +253,14 @@ jobs: done echo "All backend checks completed successfully" - ui_unit_tests: + ui_unit_test_shards: needs: [check_changes] if: needs.check_changes.outputs.has_ui_changes == 'true' timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] runs-on: labels: ubicloud-standard-8 permissions: @@ -265,8 +269,6 @@ jobs: steps: - name: Clone the current repo uses: actions/checkout@v5 - with: - fetch-depth: 0 - uses: ./.github/actions/github-auth - name: Setup Node.js @@ -276,14 +278,92 @@ jobs: cache: 'npm' cache-dependency-path: web/package-lock.json + - name: Restore node_modules + id: nm + uses: actions/cache/restore@v4 + with: + path: web/node_modules + key: node-modules-${{ runner.os }}-node24-${{ hashFiles('web/package-lock.json', 'web/package.json', 'web/patches/**') }} + - name: Install dependencies + if: steps.nm.outputs.cache-hit != 'true' working-directory: web timeout-minutes: 5 run: npm ci - - name: Run frontend unit tests with coverage and check thresholds + # Only main saves it: a PR-saved cache is visible to that PR alone and fills the 10 GB repo cache. + - name: Save node_modules + if: github.ref == 'refs/heads/main' && steps.nm.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: web/node_modules + key: node-modules-${{ runner.os }}-node24-${{ hashFiles('web/package-lock.json', 'web/package.json', 'web/patches/**') }} + + - name: Run this shard with coverage working-directory: web - run: ./coverage.sh check + env: + UI_SHARD_BALANCE: '1' + run: | + npx vitest run --coverage --shard=${{ matrix.shard }}/4 --reporter=default --reporter=blob \ + --coverage.reportOnFailure --coverage.reporter=json \ + --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 \ + --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: ui-unit-blob-${{ matrix.shard }} + path: web/.vitest-reports + include-hidden-files: true + retention-days: 1 + compression-level: 1 + + # Keeps the required-check name `ui_unit_tests` while the work runs in shards. + ui_unit_tests: + needs: [check_changes, ui_unit_test_shards] + if: always() && needs.check_changes.outputs.has_ui_changes == 'true' + timeout-minutes: 20 + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Require every shard to pass + run: | + [ "${{ needs.ui_unit_test_shards.result }}" = "success" ] || { echo "shards: ${{ needs.ui_unit_test_shards.result }}"; exit 1; } + + - name: Clone the current repo + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + cache: 'npm' + cache-dependency-path: web/package-lock.json + + - name: Restore node_modules + id: nm + uses: actions/cache/restore@v4 + with: + path: web/node_modules + key: node-modules-${{ runner.os }}-node24-${{ hashFiles('web/package-lock.json', 'web/package.json', 'web/patches/**') }} + + - name: Install dependencies + if: steps.nm.outputs.cache-hit != 'true' + working-directory: web + timeout-minutes: 5 + run: npm ci + + - uses: actions/download-artifact@v4 + with: + pattern: ui-unit-blob-* + path: web/.vitest-reports + merge-multiple: true + + - name: Merge shard coverage and check thresholds + working-directory: web + run: ./coverage.sh merge ui_code_quality: needs: [check_changes] diff --git a/AGENTS.md b/AGENTS.md index c42a70d489..8fcd9114cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,16 @@ before considering the task done. If it flags a file, fix it with `npx prettier --write ` (not a full-repo `--write`) and re-run `format:check` to confirm it's clean. +- New UI features fetch search results over the HTTP streaming endpoints, not + the one-shot ones. Logs and other SQL searches use + `POST /api/{org}/_search_stream`, or `_search_multi_stream` for several + streams. Traces use `GET /api/{org}/{stream}/traces/latest_stream`. Call them + through `useHttpStreaming` in `web/src/composables/useStreamingSearch.ts`, + which handles chunking, cancellation and the `trace_id`. Do not add new UI + callers of `_search`, `_search_multi` or `traces/latest`. Wherever a + streaming endpoint is supported for a call, the UI uses it, in existing code + as well as new code. The one-shot endpoints are for the public API and for + the few UI calls that have no streaming equivalent yet. ## Rust code organization @@ -69,7 +79,13 @@ reworded: ## Database schema (migrations / SeaORM entities) -Applies to every NEW table and column (sqlite, MySQL and PostgreSQL alike): +Applies to every NEW table and column (SQLite and PostgreSQL alike): + +- MySQL is not a supported meta store. Support was removed in #10522 + (`src/infra/src/db/mysql.rs` is gone). Do not add MySQL branches, MySQL-only + migrations, MySQL feature flags or MySQL test matrices. Remaining `mysql` + mentions in the tree belong to the database monitoring feature, which watches + customers' MySQL servers, not to the meta store. - No `json` / `jsonb` columns (sea-orm `.json()` / `.json_binary()`). JSON is stored in a `.text()` column holding the serialized string; Rust diff --git a/scripts/translations/.translation_state.json b/scripts/translations/.translation_state.json index 4542b910d9..8a894e8c5e 100644 --- a/scripts/translations/.translation_state.json +++ b/scripts/translations/.translation_state.json @@ -20201,6 +20201,8 @@ "collapseFields": "166bd3007d204cc4", "createAlert": "0d45260035d06818", "custom": "494ca78f7374e46f", + "dataViewCapped": "7325c4012766274e", + "dataViewEmpty": "492230ebfdb52619", "deletePanel": "94ef8739c0021cf6", "deletePanelMessage": "7a2ebb2ec0d8f080", "deletePanelTitle": "94ef8739c0021cf6", @@ -20299,6 +20301,9 @@ "panelQueryMustHaveStream": "38e4b015c08d5ed2", "pivotField": "b1fb54fe642eea35", "pivotFieldTooltip": "2899dc3dad2edcdf", + "previewChart": "3e5b90ae053ff3e7", + "previewData": "cec3a9b89b2e3913", + "previewView": "bba9db4d6467fb35", "promQL": "b480c1c74e687b1a", "queryInspector": "137ea027e182fe16", "refresh": "0e91610117029a62", @@ -20311,6 +20316,8 @@ "rowFields": "849ea764f1d166cc", "save": "1509f561f2416598", "saveAndStay": "36cde213a1f9122d", + "showChart": "6d64a1a5dcbf43c4", + "showRawData": "84b93a056a49f909", "source": "0e570ca6fabe24f9", "sunburstLevelsHint": "d1c3b001a0b680e2", "target": "978354db0c00fc78", @@ -20760,7 +20767,7 @@ "search": "e0e8a54761ea47d2", "searchHistory": "98bb2ed79442e418", "selectColumn": "91db086469f6b553", - "selectExternalDestination": "bf60152355f4409e", + "selectExternalDestination": "3a0217058ee6517f", "selectStream": "b7a2b0a66ccc9073", "selectValidTimeRange": "1caf1805174e58d7", "separatorRequiredStringseparated": "9be4ec7cb09e1530", diff --git a/src/api/http/src/handler/http/router/openapi.rs b/src/api/http/src/handler/http/router/openapi.rs index ebddd51b43..d65be43f11 100644 --- a/src/api/http/src/handler/http/router/openapi.rs +++ b/src/api/http/src/handler/http/router/openapi.rs @@ -280,17 +280,21 @@ const MCP_OPT_OUT_CATEGORIES: [&str; 2] = ["authorization", "users"]; openobserve_api_management::request::dashboards::reports_history::get_report_history, openobserve_api_management::request::authz::fga::create_role, openobserve_api_management::request::authz::fga::delete_role, + openobserve_api_management::request::authz::fga::delete_role_bulk, openobserve_api_management::request::authz::fga::get_roles, openobserve_api_management::request::authz::fga::update_role, openobserve_api_management::request::authz::fga::get_role_permissions, openobserve_api_management::request::authz::fga::get_all_role_permissions, + openobserve_api_management::request::authz::fga::get_roles_for_user, openobserve_api_management::request::authz::fga::get_roles_for_all_users, openobserve_api_management::request::authz::fga::get_users_with_role, openobserve_api_management::request::authz::fga::create_group, openobserve_api_management::request::authz::fga::update_group, + openobserve_api_management::request::authz::fga::get_groups_for_user, openobserve_api_management::request::authz::fga::get_groups, openobserve_api_management::request::authz::fga::get_group_details, openobserve_api_management::request::authz::fga::delete_group, + openobserve_api_management::request::authz::fga::delete_group_bulk, keys::save, keys::get, keys::list, @@ -2031,6 +2035,26 @@ mod tests { ); } + #[test] + fn fga_bulk_deletes_and_per_user_lookups_are_published() { + let spec = serde_json::to_value(ApiDoc::openapi()).unwrap(); + for (path, method) in [ + ("/api/{org_id}/roles/bulk", "delete"), + ("/api/{org_id}/groups/bulk", "delete"), + ("/api/{org_id}/users/{user_email}/roles", "get"), + ("/api/{org_id}/users/{user_email}/groups", "get"), + ] { + let op = &spec["paths"][path][method]; + assert!(op.is_object(), "{method} {path} missing"); + // OSS stubs answer 403 before any validation, so only enterprise can return 400. + assert_eq!( + op["responses"]["400"].is_object(), + cfg!(feature = "enterprise"), + "{method} {path}" + ); + } + } + #[test] fn fleet_endpoints_are_documented_and_the_removed_ones_are_not() { let spec = serde_json::to_value(ApiDoc::openapi()).unwrap(); diff --git a/src/api/management/src/request/authz/fga.rs b/src/api/management/src/request/authz/fga.rs index 9a573d9eac..a4f5a38a34 100644 --- a/src/api/management/src/request/authz/fga.rs +++ b/src/api/management/src/request/authz/fga.rs @@ -48,6 +48,7 @@ use crate::{ request_body(content = inline(UserRoleRequest), description = "UserRoleRequest", content_type = "application/json"), responses( (status = 200, description = "Success", content_type = "application/json", body = Object), + (status = 400, description = "Invalid, standard or already existing role name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ), extensions( @@ -68,6 +69,14 @@ pub async fn create_role( if role_name.is_empty() || is_standard_role(&role_name) { return MetaHttpResponse::bad_request("Custom role name cannot be empty or standard role"); } + // A name the role object accepts can still overflow the `#has` role user. + if o2_openfga::authorizer::org_scoped_object("role", &org_id, &role_name).is_none() + || !o2_openfga::authorizer::is_valid_tuple_user( + &o2_openfga::authorizer::roles::format_role_user(&org_id, &role_name), + ) + { + return MetaHttpResponse::bad_request("Invalid role name"); + } match o2_openfga::authorizer::roles::create_role(&role_name, &org_id).await { Ok(_) => MetaHttpResponse::ok("Role created successfully"), @@ -132,6 +141,7 @@ pub async fn create_role( ), responses( (status = 200, description = "Success", content_type = "application/json", body = Object), + (status = 400, description = "Invalid role name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ), extensions( @@ -140,9 +150,12 @@ pub async fn create_role( ) )] pub async fn delete_role(Path((org_id, role_name)): Path<(String, String)>) -> Response { + if !is_deletable_role(&org_id, &role_name) { + return MetaHttpResponse::bad_request("Invalid role name"); + } match o2_openfga::authorizer::roles::delete_role(&org_id, &role_name).await { Ok(_) => MetaHttpResponse::ok(serde_json::json!({"successful": "true"})), - Err(err) => MetaHttpResponse::internal_error(err), + Err(e) => failure_response(&e, "Failed to delete role"), } } @@ -165,6 +178,7 @@ pub async fn delete_role(Path((org_id, role_name)): Path<(String, String)>) -> R request_body(content = BulkDeleteRequest, description = "names of role to be deleted", content_type = "application/json"), responses( (status = 200, description = "Success", content_type = "application/json", body = BulkDeleteResponse), + (status = 400, description = "Invalid role name", content_type = "application/json", body = ()), ), extensions( ("x-o2-ratelimit" = json!({"module": "Roles", "operation": "delete"})), @@ -176,6 +190,13 @@ pub async fn delete_role_bulk( Headers(user_email): Headers, Json(req): Json, ) -> Response { + if let Some(name) = req + .ids + .iter() + .find(|name| !is_deletable_role(&org_id, name)) + { + return MetaHttpResponse::bad_request(format!("Invalid role name: {name}")); + } let user_id = user_email.user_id; for name in &req.ids { @@ -196,27 +217,15 @@ pub async fn delete_role_bulk( } } - let mut successful = Vec::with_capacity(req.ids.len()); - let mut unsuccessful = Vec::with_capacity(req.ids.len()); - let mut err = None; - - for name in req.ids { - match o2_openfga::authorizer::roles::delete_role(&org_id, &name).await { - Ok(_) => { - successful.push(name); - } - Err(e) => { - log::error!("error in deleting role {org_id}/{name} : {e}"); - unsuccessful.push(name); - err = Some(e.to_string()); - } - } - } - MetaHttpResponse::json(BulkDeleteResponse { - successful, - unsuccessful, - err, - }) + let org = org_id.as_str(); + MetaHttpResponse::json( + bulk_delete( + req.ids, + |name| async move { o2_openfga::authorizer::roles::delete_role(org, &name).await }, + "Failed to delete role", + ) + .await, + ) } #[cfg(not(feature = "enterprise"))] @@ -343,10 +352,10 @@ pub async fn get_roles( } } - match o2_openfga::authorizer::roles::get_all_roles(&org_id, permitted).await { - Ok(res) => Json(res).into_response(), - Err(err) => MetaHttpResponse::internal_error(err), - } + lookup_response( + o2_openfga::authorizer::roles::get_all_roles(&org_id, permitted).await, + "Failed to list roles", + ) } #[cfg(not(feature = "enterprise"))] @@ -429,7 +438,7 @@ pub async fn update_role( .await { Ok(_) => MetaHttpResponse::ok("Role updated successfully"), - Err(err) => MetaHttpResponse::internal_error(err), + Err(e) => failure_response(&e, "Failed to update role"), } } @@ -481,16 +490,20 @@ pub async fn update_role( ), responses( (status = 200, description = "Success", content_type = "application/json", body = inline(Vec)), + (status = 400, description = "Invalid role name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ) )] pub async fn get_role_permissions( Path((org_id, role_id, resource)): Path<(String, String, String)>, ) -> Response { - match o2_openfga::authorizer::roles::get_role_permissions(&org_id, &role_id, &resource).await { - Ok(res) => Json(res).into_response(), - Err(err) => MetaHttpResponse::internal_error(err), + if !is_storable_name("role", &org_id, &role_id) { + return MetaHttpResponse::bad_request("Invalid role name"); } + lookup_response( + o2_openfga::authorizer::roles::get_role_permissions(&org_id, &role_id, &resource).await, + "Failed to get role permissions", + ) } #[cfg(feature = "enterprise")] @@ -512,14 +525,18 @@ pub async fn get_role_permissions( ), responses( (status = 200, description = "Success", content_type = "application/json", body = inline(Vec)), + (status = 400, description = "Invalid role name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ) )] pub async fn get_all_role_permissions(Path((org_id, role_id)): Path<(String, String)>) -> Response { - match o2_openfga::authorizer::roles::get_all_role_permissions(&org_id, &role_id).await { - Ok(res) => Json(res).into_response(), - Err(err) => MetaHttpResponse::internal_error(err), + if !is_storable_name("role", &org_id, &role_id) { + return MetaHttpResponse::bad_request("Invalid role name"); } + lookup_response( + o2_openfga::authorizer::roles::get_all_role_permissions(&org_id, &role_id).await, + "Failed to get role permissions", + ) } #[cfg(not(feature = "enterprise"))] @@ -592,11 +609,15 @@ pub async fn get_role_permissions(Path(_path): Path<(String, String, String)>) - ), responses( (status = 200, description = "Success", content_type = "application/json", body = inline(Vec)), + (status = 400, description = "Invalid role name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ) )] pub async fn get_users_with_role(Path((org_id, role_id)): Path<(String, String)>) -> Response { - role_lookup_response( + if !is_storable_name("role", &org_id, &role_id) { + return MetaHttpResponse::bad_request("Invalid role name"); + } + lookup_response( o2_openfga::authorizer::roles::get_users_with_role(&org_id, &role_id).await, "Failed to get users with role", ) @@ -646,11 +667,15 @@ pub async fn get_users_with_role(Path(_path): Path<(String, String)>) -> Respons ), responses( (status = 200, description = "Success", content_type = "application/json", body = inline(Vec)), + (status = 400, description = "Invalid user id", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ) )] pub async fn get_roles_for_user(Path((org_id, user_email)): Path<(String, String)>) -> Response { - role_lookup_response( + if !o2_openfga::authorizer::is_valid_user_id(&user_email) { + return MetaHttpResponse::bad_request("Invalid user id"); + } + lookup_response( o2_openfga::authorizer::roles::get_roles_for_org_user(&org_id, &user_email).await, "Failed to get roles for user", ) @@ -703,10 +728,10 @@ pub async fn get_roles_for_user(Path(_path): Path<(String, String)>) -> Response ) )] pub async fn get_roles_for_all_users(Path(org_id): Path) -> Response { - match o2_openfga::authorizer::roles::get_roles_for_all_org_users(&org_id).await { - Ok(res) => Json(res).into_response(), - Err(err) => MetaHttpResponse::internal_error(err), - } + lookup_response( + o2_openfga::authorizer::roles::get_roles_for_all_org_users(&org_id).await, + "Failed to get roles for users", + ) } #[cfg(not(feature = "enterprise"))] @@ -752,14 +777,18 @@ pub async fn get_roles_for_all_users(Path(_path): Path) -> Response { ), responses( (status = 200, description = "Success", content_type = "application/json", body = inline(Vec)), + (status = 400, description = "Invalid user id", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ) )] pub async fn get_groups_for_user(Path((org_id, user_email)): Path<(String, String)>) -> Response { - match o2_openfga::authorizer::groups::get_groups_for_org_user(&org_id, &user_email).await { - Ok(res) => Json(res).into_response(), - Err(err) => MetaHttpResponse::internal_error(err), + if !o2_openfga::authorizer::is_valid_user_id(&user_email) { + return MetaHttpResponse::bad_request("Invalid user id"); } + lookup_response( + o2_openfga::authorizer::groups::get_groups_for_org_user(&org_id, &user_email).await, + "Failed to get groups for user", + ) } #[cfg(not(feature = "enterprise"))] @@ -832,8 +861,7 @@ pub async fn create_group( match o2_openfga::authorizer::groups::create_group(&org_id, &user_grp.name, users).await { Ok(_) => MetaHttpResponse::ok("Group created successfully"), - // The authorizer's error carries raw OpenFGA text, which stays in the server log. - Err(_) => MetaHttpResponse::internal_error("Failed to create group"), + Err(e) => failure_response(&e, "Failed to create group"), } } @@ -885,7 +913,7 @@ pub async fn create_group( request_body(content = inline(UserGroupRequest), description = "UserGroupRequest", content_type = "application/json"), responses( (status = 200, description = "Success", content_type = "application/json", body = Object), - (status = 400, description = "Invalid group name or user id", content_type = "application/json", body = ()), + (status = 400, description = "Invalid group name, user id or role name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ) )] @@ -907,6 +935,15 @@ pub async fn update_group( { return MetaHttpResponse::bad_request("Invalid user id in group users"); } + if !user_grp + .add_roles + .iter() + .chain(user_grp.remove_roles.iter()) + .flatten() + .all(|role| o2_openfga::authorizer::org_scoped_object("role", &org_id, role).is_some()) + { + return MetaHttpResponse::bad_request("Invalid role name in group roles"); + } match o2_openfga::authorizer::groups::update_group( &org_id, @@ -919,8 +956,7 @@ pub async fn update_group( .await { Ok(_) => MetaHttpResponse::ok("Group updated successfully"), - // The authorizer's error carries raw OpenFGA text, which stays in the server log. - Err(_) => MetaHttpResponse::internal_error("Failed to update group"), + Err(e) => failure_response(&e, "Failed to update group"), } } @@ -1012,10 +1048,10 @@ pub async fn get_groups( permitted = Some(local_permitted); } - match o2_openfga::authorizer::groups::get_all_groups(&org_id, permitted).await { - Ok(res) => Json(res).into_response(), - Err(err) => MetaHttpResponse::internal_error(err), - } + lookup_response( + o2_openfga::authorizer::groups::get_all_groups(&org_id, permitted).await, + "Failed to list groups", + ) } #[cfg(not(feature = "enterprise"))] @@ -1061,14 +1097,18 @@ pub async fn get_groups(Path(_path): Path) -> Response { ), responses( (status = 200, description = "Success", content_type = "application/json", body = inline(UserGroup)), + (status = 400, description = "Invalid group name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ) )] pub async fn get_group_details(Path((org_id, group_name)): Path<(String, String)>) -> Response { - match o2_openfga::authorizer::groups::get_group_details(&org_id, &group_name).await { - Ok(res) => Json(res).into_response(), - Err(err) => MetaHttpResponse::internal_error(err), + if !is_storable_name("group", &org_id, &group_name) { + return MetaHttpResponse::bad_request("Invalid group name"); } + lookup_response( + o2_openfga::authorizer::groups::get_group_details(&org_id, &group_name).await, + "Failed to get group", + ) } #[cfg(not(feature = "enterprise"))] @@ -1185,13 +1225,17 @@ pub async fn get_resources(Path(_org_id): Path) -> Response { ), responses( (status = 200, description = "Success", content_type = "application/json", body = Object), + (status = 400, description = "Invalid group name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ) )] pub async fn delete_group(Path((org_id, group_name)): Path<(String, String)>) -> Response { + if !is_storable_name("group", &org_id, &group_name) { + return MetaHttpResponse::bad_request("Invalid group name"); + } match o2_openfga::authorizer::groups::delete_group(&org_id, &group_name).await { Ok(_) => MetaHttpResponse::ok(serde_json::json!({"successful": "true"})), - Err(err) => MetaHttpResponse::internal_error(err), + Err(e) => failure_response(&e, "Failed to delete group"), } } @@ -1214,6 +1258,7 @@ pub async fn delete_group(Path((org_id, group_name)): Path<(String, String)>) -> request_body(content = BulkDeleteRequest, description = "user group names", content_type = "application/json"), responses( (status = 200, description = "Success", content_type = "application/json", body = BulkDeleteResponse), + (status = 400, description = "Invalid group name", content_type = "application/json", body = ()), (status = 500, description = "Failure", content_type = "application/json", body = ()), ), extensions( @@ -1225,6 +1270,13 @@ pub async fn delete_group_bulk( Headers(user_email): Headers, Json(req): Json, ) -> Response { + if let Some(name) = req + .ids + .iter() + .find(|name| !is_storable_name("group", &org_id, name)) + { + return MetaHttpResponse::bad_request(format!("Invalid group name: {name}")); + } let user_id = user_email.user_id; for name in &req.ids { @@ -1245,28 +1297,15 @@ pub async fn delete_group_bulk( } } - let mut successful = Vec::with_capacity(req.ids.len()); - let mut unsuccessful = Vec::with_capacity(req.ids.len()); - let mut err = None; - - for name in req.ids { - match o2_openfga::authorizer::groups::delete_group(&org_id, &name).await { - Ok(_) => { - successful.push(name); - } - Err(e) => { - log::error!("error in deleting group {org_id}/{name} : {e}"); - unsuccessful.push(name); - err = Some(e.to_string()); - } - } - } - - MetaHttpResponse::json(BulkDeleteResponse { - successful, - unsuccessful, - err, - }) + let org = org_id.as_str(); + MetaHttpResponse::json( + bulk_delete( + req.ids, + |name| async move { o2_openfga::authorizer::groups::delete_group(org, &name).await }, + "Failed to delete group", + ) + .await, + ) } #[cfg(not(feature = "enterprise"))] @@ -1343,16 +1382,63 @@ fn hide_slack_unless_enabled<'a, 'b>( .collect() } -/// The lookup's list, or a fixed 500 that keeps the OpenFGA error in the server log. +/// The lookup's result, or a fixed 500 that keeps the OpenFGA error in the server log. #[cfg(feature = "enterprise")] -fn role_lookup_response(result: Result, anyhow::Error>, failure: &str) -> Response { +fn lookup_response( + result: Result, + failure: &str, +) -> Response { match result { - Ok(list) => Json(list).into_response(), - Err(e) => { - log::error!("{failure}: {e}"); - MetaHttpResponse::internal_error(failure) + Ok(found) => Json(found).into_response(), + Err(e) => failure_response(&e, failure), + } +} + +#[cfg(feature = "enterprise")] +fn failure_response(e: &anyhow::Error, failure: &str) -> Response { + // The authorizer's error carries raw OpenFGA text, which stays in the server log. + log::error!("{failure}: {e}"); + MetaHttpResponse::internal_error(failure) +} + +#[cfg(feature = "enterprise")] +fn is_storable_name(object_type: &str, org_id: &str, name: &str) -> bool { + // Shape only: an empty name was storable before, so such a role or group must stay deletable. + o2_openfga::authorizer::is_valid_tuple_object(&format!("{object_type}:{org_id}/{name}")) +} + +#[cfg(feature = "enterprise")] +fn is_deletable_role(org_id: &str, name: &str) -> bool { + // Deleting reads the `#has` role user back; an overlong one fails every read. + is_storable_name("role", org_id, name) + && o2_openfga::authorizer::is_valid_tuple_user( + &o2_openfga::authorizer::roles::format_role_user(org_id, name), + ) +} + +#[cfg(feature = "enterprise")] +async fn bulk_delete(ids: Vec, delete: F, failure: &str) -> BulkDeleteResponse +where + F: Fn(String) -> Fut, + Fut: std::future::Future>, +{ + let mut successful = Vec::with_capacity(ids.len()); + let mut unsuccessful = Vec::with_capacity(ids.len()); + for name in ids { + match delete(name.clone()).await { + Ok(_) => successful.push(name), + Err(e) => { + log::error!("{failure} {name}: {e}"); + unsuccessful.push(name); + } } } + let err = (!unsuccessful.is_empty()).then(|| format!("{failure}: {}", unsuccessful.join(", "))); + BulkDeleteResponse { + successful, + unsuccessful, + err, + } } #[cfg(test)] @@ -1456,6 +1542,10 @@ mod tests { extract_status_code(&resp), StatusCode::INTERNAL_SERVER_ERROR ); + assert_eq!( + response_json(resp).await["message"], + "Failed to delete role" + ); } #[cfg(not(feature = "enterprise"))] @@ -1475,6 +1565,7 @@ mod tests { extract_status_code(&resp), StatusCode::INTERNAL_SERVER_ERROR ); + assert_eq!(response_json(resp).await["message"], "Failed to list roles"); } #[cfg(not(feature = "enterprise"))] @@ -1509,6 +1600,10 @@ mod tests { extract_status_code(&resp), StatusCode::INTERNAL_SERVER_ERROR ); + assert_eq!( + response_json(resp).await["message"], + "Failed to update role" + ); // A type the model lacks is the client's error, not OpenFGA's 400 surfaced as a 500. let resp = update_role( @@ -1576,7 +1671,7 @@ mod tests { init_ofga_test(); let resp = get_users_with_role(Path(("test_org".to_string(), "test_role".to_string()))).await; - // The failing-read 500 is covered by `role_lookup_response_hides_the_openfga_error`. + // The failing-read 500 is covered by `lookup_response_hides_the_openfga_error`. assert!( !o2_openfga::config::get_config().enabled, "unit tests run without OpenFGA" @@ -1603,7 +1698,7 @@ mod tests { "test@example.com".to_string(), ))) .await; - // The failing-read 500 is covered by `role_lookup_response_hides_the_openfga_error`. + // The failing-read 500 is covered by `lookup_response_hides_the_openfga_error`. assert!( !o2_openfga::config::get_config().enabled, "unit tests run without OpenFGA" @@ -1625,9 +1720,9 @@ mod tests { #[cfg(feature = "enterprise")] #[tokio::test] - async fn role_lookup_response_hides_the_openfga_error() { - let failed = role_lookup_response( - Err(anyhow::anyhow!("raw OpenFGA text")), + async fn lookup_response_hides_the_openfga_error() { + let failed = lookup_response( + Err::, _>(anyhow::anyhow!("raw OpenFGA text")), "Failed to get roles for user", ); assert_eq!( @@ -1638,11 +1733,167 @@ mod tests { response_json(failed).await["message"], "Failed to get roles for user" ); - let found = role_lookup_response(Ok(vec!["editor".to_string()]), "unused"); + let found = lookup_response(Ok(vec!["editor".to_string()]), "unused"); assert_eq!(extract_status_code(&found), StatusCode::OK); assert_eq!(response_json(found).await, serde_json::json!(["editor"])); } + #[cfg(feature = "enterprise")] + #[tokio::test] + async fn roles_and_groups_reject_unstorable_input_with_400() { + init_ofga_test(); + let path = |name: &str| Path(("test_org".to_string(), name.to_string())); + let bulk = |name: &str| BulkDeleteRequest { + ids: vec!["fine".to_string(), name.to_string()], + }; + // Fits the `role:` object but not the `#has` role user a delete reads back. + let too_long = "r".repeat(240); + assert!(is_storable_name("role", "test_org", &too_long)); + for resp in [ + delete_role(path(&too_long)).await, + delete_role_bulk( + Path("test_org".to_string()), + mock_user_email(), + Json(bulk(&too_long)), + ) + .await, + ] { + assert_eq!(extract_status_code(&resp), StatusCode::BAD_REQUEST); + } + for name in ["a#b", "a b", "a:b"] { + let responses = [ + delete_role(path(name)).await, + delete_group(path(name)).await, + get_group_details(path(name)).await, + get_users_with_role(path(name)).await, + get_all_role_permissions(path(name)).await, + get_role_permissions(Path(( + "test_org".to_string(), + name.to_string(), + "stream".to_string(), + ))) + .await, + delete_role_bulk( + Path("test_org".to_string()), + mock_user_email(), + Json(bulk(name)), + ) + .await, + delete_group_bulk( + Path("test_org".to_string()), + mock_user_email(), + Json(bulk(name)), + ) + .await, + ]; + for resp in responses { + assert_eq!( + extract_status_code(&resp), + StatusCode::BAD_REQUEST, + "{name:?}" + ); + } + let roles = delete_role_bulk( + Path("test_org".to_string()), + mock_user_email(), + Json(bulk(name)), + ) + .await; + assert_eq!( + response_json(roles).await["message"], + format!("Invalid role name: {name}") + ); + let groups = delete_group_bulk( + Path("test_org".to_string()), + mock_user_email(), + Json(bulk(name)), + ) + .await; + assert_eq!( + response_json(groups).await["message"], + format!("Invalid group name: {name}") + ); + } + for user in ["*", "a#b", ""] { + for resp in [ + get_groups_for_user(path(user)).await, + get_roles_for_user(path(user)).await, + ] { + assert_eq!( + extract_status_code(&resp), + StatusCode::BAD_REQUEST, + "{user:?}" + ); + } + } + } + + #[cfg(feature = "enterprise")] + #[tokio::test] + async fn update_role_rejects_invalid_input_with_400() { + init_ofga_test(); + let members = |user: &str| Some(HashSet::from([user.to_string()])); + // Fits the 256-char `role:` object but not the `#has` role-user form. + let too_long_for_role_user = "r".repeat(240); + assert!( + o2_openfga::authorizer::org_scoped_object("role", "test_org", &too_long_for_role_user) + .is_some() + ); + for (name, add_users, remove_users) in [ + ("test_role", members("*"), None), + ("test_role", None, members("a#b")), + ("a#b", None, None), + ("", members("user1"), None), + (too_long_for_role_user.as_str(), None, None), + ] { + let role_request = o2_dex::meta::auth::RoleRequest { + add: vec![], + remove: vec![], + add_users, + remove_users, + }; + let resp = update_role( + Path(("test_org".to_string(), name.to_string())), + Json(role_request), + ) + .await; + assert_eq!( + extract_status_code(&resp), + StatusCode::BAD_REQUEST, + "{name:?}" + ); + } + let resp = create_role( + Path("test_org".to_string()), + Json(UserRoleRequest { + role: too_long_for_role_user, + custom: None, + }), + ) + .await; + assert_eq!(response_json(resp).await["message"], "Invalid role name"); + } + + #[cfg(feature = "enterprise")] + #[tokio::test] + async fn bulk_delete_hides_the_openfga_error() { + let resp = bulk_delete( + vec!["kept".to_string(), "broken".to_string()], + |name| async move { + if name == "broken" { + Err(anyhow::anyhow!("raw OpenFGA text")) + } else { + Ok(()) + } + }, + "Failed to delete role", + ) + .await; + assert_eq!(resp.successful, vec!["kept".to_string()]); + assert_eq!(resp.unsuccessful, vec!["broken".to_string()]); + assert_eq!(resp.err.as_deref(), Some("Failed to delete role: broken")); + } + #[tokio::test] async fn test_get_roles_for_all_users_enterprise() { #[cfg(feature = "enterprise")] @@ -1653,6 +1904,10 @@ mod tests { extract_status_code(&resp), StatusCode::INTERNAL_SERVER_ERROR ); + assert_eq!( + response_json(resp).await["message"], + "Failed to get roles for users" + ); } #[cfg(not(feature = "enterprise"))] @@ -1676,6 +1931,10 @@ mod tests { extract_status_code(&resp), StatusCode::INTERNAL_SERVER_ERROR ); + assert_eq!( + response_json(resp).await["message"], + "Failed to get groups for user" + ); } #[cfg(not(feature = "enterprise"))] @@ -1828,6 +2087,23 @@ mod tests { "{name:?}" ); } + for (add_roles, remove_roles) in [(members("a#b"), None), (None, members(""))] { + let group_request = UserGroupRequest { + add_users: None, + remove_users: None, + add_roles, + remove_roles, + }; + let resp = update_group( + Path(("test_org".to_string(), "test_group".to_string())), + Json(group_request), + ) + .await; + assert_eq!( + response_json(resp).await["message"], + "Invalid role name in group roles" + ); + } } #[tokio::test] @@ -1840,6 +2116,10 @@ mod tests { extract_status_code(&resp), StatusCode::INTERNAL_SERVER_ERROR ); + assert_eq!( + response_json(resp).await["message"], + "Failed to list groups" + ); } #[cfg(not(feature = "enterprise"))] @@ -1860,6 +2140,7 @@ mod tests { extract_status_code(&resp), StatusCode::INTERNAL_SERVER_ERROR ); + assert_eq!(response_json(resp).await["message"], "Failed to get group"); } #[cfg(not(feature = "enterprise"))] @@ -1880,6 +2161,10 @@ mod tests { extract_status_code(&resp), StatusCode::INTERNAL_SERVER_ERROR ); + assert_eq!( + response_json(resp).await["message"], + "Failed to delete group" + ); } #[cfg(not(feature = "enterprise"))] diff --git a/src/core/src/onboarding.rs b/src/core/src/onboarding.rs index e033154751..58c88cca53 100644 --- a/src/core/src/onboarding.rs +++ b/src/core/src/onboarding.rs @@ -119,7 +119,12 @@ impl FirstDataIo for LiveIo { stream_name: &str, ) -> Vec> { let mut created = Vec::new(); - for kind in [StreamType::Logs, StreamType::Metrics, StreamType::Traces] { + for kind in [ + StreamType::Logs, + StreamType::Metrics, + StreamType::Traces, + StreamType::Profiles, + ] { for name in db::schema::list_streams_from_cache(org_id, kind).await { if !is_user_data_stream(kind, &name) || (kind == stream_type && name == stream_name) { diff --git a/src/core/src/profiles/mod.rs b/src/core/src/profiles/mod.rs index 7acb155694..26f7cd1a48 100644 --- a/src/core/src/profiles/mod.rs +++ b/src/core/src/profiles/mod.rs @@ -430,6 +430,7 @@ async fn buffer_records( Authz::new(stream_name), ) .await; + crate::onboarding::on_user_stream_created(org_id, StreamType::Profiles, stream_name); } stream_schema_map.insert(stream_name.to_string(), SchemaCache::new(schema)); } diff --git a/src/core/src/workflows/mod.rs b/src/core/src/workflows/mod.rs index 8c4a69f10d..6787f246ce 100644 --- a/src/core/src/workflows/mod.rs +++ b/src/core/src/workflows/mod.rs @@ -410,7 +410,10 @@ pub async fn update_workflow(workflow: Workflow) -> Result<(), anyhow::Error> { pub async fn update_draft(workflow: Workflow) -> Result<(), anyhow::Error> { reject_unsupported_nodes(&workflow)?; - db::workflows::update_draft_record(workflow.clone()).await?; + // a 0-row update must not be replayed: peers would insert it as a new draft + if db::workflows::update_draft_record(&workflow.org_id, workflow.clone()).await? == 0 { + return Ok(()); + } db::workflows::notify_draft_upsert(&workflow).await?; Ok(()) } @@ -634,7 +637,10 @@ pub async fn delete_workflow(org_id: &str, id: &str) -> Result<(), anyhow::Error } pub async fn delete_draft(org_id: &str, id: &str) -> Result<(), anyhow::Error> { - db::workflows::delete_draft_record(id).await?; + // a 0-row delete must not drop the owner's tuple or replay unscoped to peers + if db::workflows::delete_draft_record(org_id, id).await? == 0 { + return Ok(()); + } remove_ownership(org_id, "workflows", Authz::new(id)).await; db::workflows::notify_draft_delete(id).await?; Ok(()) diff --git a/src/db/src/workflows.rs b/src/db/src/workflows.rs index 32908c4c89..fa1b5cd685 100644 --- a/src/db/src/workflows.rs +++ b/src/db/src/workflows.rs @@ -252,9 +252,8 @@ pub async fn update_workflow_record(workflow: Workflow) -> Result<(), anyhow::Er Ok(()) } -pub async fn update_draft_record(workflow: Workflow) -> Result<(), anyhow::Error> { - infra::table::workflows::update_draft(workflow).await?; - Ok(()) +pub async fn update_draft_record(org_id: &str, workflow: Workflow) -> Result { + infra::table::workflows::update_draft(org_id, workflow).await } pub async fn promote_draft(org_id: &str, workflow: Workflow) -> Result<(), anyhow::Error> { @@ -267,9 +266,8 @@ pub async fn delete_workflow_record(id: &str) -> Result<(), anyhow::Error> { Ok(()) } -pub async fn delete_draft_record(id: &str) -> Result<(), anyhow::Error> { - infra::table::workflows::delete_draft(id).await?; - Ok(()) +pub async fn delete_draft_record(org_id: &str, id: &str) -> Result { + infra::table::workflows::delete_draft(org_id, id).await } pub async fn notify_workflow_upsert(workflow: &Workflow) -> Result<(), anyhow::Error> { diff --git a/src/infra/src/schema/mod.rs b/src/infra/src/schema/mod.rs index f57ba04f6f..81c670b5d9 100644 --- a/src/infra/src/schema/mod.rs +++ b/src/infra/src/schema/mod.rs @@ -198,7 +198,7 @@ pub fn remove_latest(map: &mut hashbrown::HashMap, key: &st remove_counted(map, &ORG_USER_STREAMS, key); } -/// Whether the org has a logs, metrics or traces stream outside the internal ones; O(1). +/// Whether the org has a logs, metrics, traces or profiles stream outside the internal ones; O(1). pub fn org_has_user_stream(org_id: &str) -> bool { ORG_USER_STREAMS.contains_key(org_id) } @@ -206,7 +206,7 @@ pub fn org_has_user_stream(org_id: &str) -> bool { pub fn is_user_data_stream(stream_type: StreamType, stream_name: &str) -> bool { matches!( stream_type, - StreamType::Logs | StreamType::Metrics | StreamType::Traces + StreamType::Logs | StreamType::Metrics | StreamType::Traces | StreamType::Profiles ) && !INTERNAL_STREAM_NAMES.contains(&stream_name) && !stream_name.starts_with("_o2_") } @@ -1140,6 +1140,7 @@ fn user_data_org(key: &str) -> Option<&str> { "logs" => StreamType::Logs, "metrics" => StreamType::Metrics, "traces" => StreamType::Traces, + "profiles" => StreamType::Profiles, _ => return None, }; is_user_data_stream(stream_type, stream_name).then_some(org_id) @@ -1946,16 +1947,18 @@ mod tests { } assert!(index.is_empty()); assert!(is_user_data_stream(StreamType::Metrics, "cpu_usage")); + assert!(is_user_data_stream(StreamType::Profiles, "default")); assert!(!is_user_data_stream(StreamType::Logs, "usage")); assert!(!is_user_data_stream(StreamType::EnrichmentTables, "app")); assert_eq!(user_data_org("acme/metrics/up/with/slash"), Some("acme")); + assert_eq!(user_data_org("acme/profiles/default"), Some("acme")); } #[test] fn test_user_stream_index_equals_a_recount_after_10k_random_inserts_and_removes() { let mut map = hashbrown::HashMap::new(); let index = RwHashMap::default(); - let types = ["logs", "metrics", "traces", "metadata"]; + let types = ["logs", "metrics", "traces", "profiles", "metadata"]; let names = ["app", "usage", "_o2_x", "default", "cpu"]; let mut seed: u64 = 0x5eed; let mut next = || { diff --git a/src/infra/src/table/workflows.rs b/src/infra/src/table/workflows.rs index cd87f6a7ec..562115325c 100644 --- a/src/infra/src/table/workflows.rs +++ b/src/infra/src/table/workflows.rs @@ -15,8 +15,8 @@ use config::meta::pipeline::components::{Edge, Node}; use sea_orm::{ - ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, Set, - TransactionTrait, prelude::Expr, sea_query::Func, + ColumnTrait, Condition, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, + Set, TransactionTrait, prelude::Expr, sea_query::Func, }; use serde::{Deserialize, Serialize}; @@ -794,34 +794,19 @@ pub async fn save_draft(draft: Workflow) -> Result<(), anyhow::Error> { Ok(()) } -pub async fn update_draft(draft: Workflow) -> Result<(), anyhow::Error> { - let client = get_orm_client_rw().await; - - let now = chrono::Utc::now().timestamp_millis(); - let model = workflow_drafts::ActiveModel { - updated_at: Set(now), - name: Set(draft.name), - description: Set(draft.description), - nodes: Set(serde_json::to_string(&draft.nodes)?), - edges: Set(serde_json::to_string(&draft.edges)?), - ..Default::default() - }; - workflow_drafts::Entity::update_many() - .filter(workflow_drafts::Column::Id.eq(draft.id)) - .set(model) - .exec(client) - .await?; - Ok(()) +/// Returns the number of drafts updated, 0 when `org_id` does not own the draft. +pub async fn update_draft(org_id: &str, draft: Workflow) -> Result { + update_draft_in(get_orm_client_rw().await, org_id, draft).await } -pub async fn delete_draft(id: &str) -> Result<(), anyhow::Error> { - let client = get_orm_client_rw().await; +/// Returns the number of drafts deleted, 0 when `org_id` does not own the draft. +pub async fn delete_draft(org_id: &str, id: &str) -> Result { + delete_draft_in(get_orm_client_rw().await, org_id, id).await +} - workflow_drafts::Entity::delete_many() - .filter(workflow_drafts::Column::Id.eq(id)) - .exec(client) - .await?; - Ok(()) +/// Org that owns draft `id`, for the super-cluster replay whose delete message carries no org. +pub async fn get_draft_org_id(id: &str) -> Result, anyhow::Error> { + get_draft_org_id_in(get_orm_client_ro().await, id).await } pub async fn delete_drafts_by_org(org_id: &str) -> Result<(), anyhow::Error> { @@ -889,3 +874,176 @@ pub async fn promote_draft_to_workflow(org_id: &str, draft: Workflow) -> Result< Ok(()) } + +async fn update_draft_in( + conn: &C, + org_id: &str, + draft: Workflow, +) -> Result { + let now = chrono::Utc::now().timestamp_millis(); + let model = workflow_drafts::ActiveModel { + updated_at: Set(now), + name: Set(draft.name), + description: Set(draft.description), + nodes: Set(serde_json::to_string(&draft.nodes)?), + edges: Set(serde_json::to_string(&draft.edges)?), + ..Default::default() + }; + let res = workflow_drafts::Entity::update_many() + .filter(workflow_drafts::Column::OrgId.eq(org_id)) + .filter(workflow_drafts::Column::Id.eq(draft.id)) + .set(model) + .exec(conn) + .await?; + Ok(res.rows_affected) +} + +async fn delete_draft_in( + conn: &C, + org_id: &str, + id: &str, +) -> Result { + let res = workflow_drafts::Entity::delete_many() + .filter(workflow_drafts::Column::OrgId.eq(org_id)) + .filter(workflow_drafts::Column::Id.eq(id)) + .exec(conn) + .await?; + Ok(res.rows_affected) +} + +async fn get_draft_org_id_in( + conn: &C, + id: &str, +) -> Result, anyhow::Error> { + let res = workflow_drafts::Entity::find() + .select_only() + .column(workflow_drafts::Column::OrgId) + .filter(workflow_drafts::Column::Id.eq(id)) + .into_tuple::() + .one(conn) + .await?; + Ok(res) +} + +#[cfg(test)] +mod tests { + use sea_orm::{ConnectOptions, Database, DatabaseConnection, Schema}; + + use super::*; + + const DRAFT_ID: &str = "draft-1"; + const OWNER_ORG: &str = "owner"; + const OTHER_ORG: &str = "other"; + + async fn db_with_draft() -> DatabaseConnection { + let mut opts = ConnectOptions::new("sqlite::memory:".to_string()); + opts.max_connections(1); + let db = Database::connect(opts).await.unwrap(); + let backend = db.get_database_backend(); + let schema = Schema::new(backend); + db.execute(backend.build(&schema.create_table_from_entity(workflow_drafts::Entity))) + .await + .unwrap(); + workflow_drafts::Entity::insert(workflow_drafts::ActiveModel { + id: Set(DRAFT_ID.to_string()), + org_id: Set(OWNER_ORG.to_string()), + folder_id: Set("folder".to_string()), + created_at: Set(1), + updated_at: Set(1), + created_by: Set("user".to_string()), + name: Set("original".to_string()), + description: Set("original".to_string()), + nodes: Set("[]".to_string()), + edges: Set("[]".to_string()), + }) + .exec(&db) + .await + .unwrap(); + db + } + + async fn stored_draft(db: &DatabaseConnection) -> Option { + workflow_drafts::Entity::find_by_id(DRAFT_ID) + .one(db) + .await + .unwrap() + } + + fn renamed_draft(org_id: &str) -> Workflow { + Workflow { + id: DRAFT_ID.to_string(), + org_id: org_id.to_string(), + folder_id: "folder".to_string(), + created_at: 1, + updated_at: 1, + created_by: "user".to_string(), + enabled: true, + name: "renamed".to_string(), + description: "renamed".to_string(), + nodes: vec![], + edges: vec![], + } + } + + #[tokio::test] + async fn update_draft_from_another_org_changes_nothing() { + let db = db_with_draft().await; + + let updated = update_draft_in(&db, OTHER_ORG, renamed_draft(OTHER_ORG)) + .await + .unwrap(); + + assert_eq!(updated, 0); + assert_eq!(stored_draft(&db).await.unwrap().name, "original"); + } + + #[tokio::test] + async fn update_draft_from_owning_org_applies() { + let db = db_with_draft().await; + + let updated = update_draft_in(&db, OWNER_ORG, renamed_draft(OWNER_ORG)) + .await + .unwrap(); + + assert_eq!(updated, 1); + assert_eq!(stored_draft(&db).await.unwrap().name, "renamed"); + } + + #[tokio::test] + async fn delete_draft_from_another_org_removes_nothing() { + let db = db_with_draft().await; + + let deleted = delete_draft_in(&db, OTHER_ORG, DRAFT_ID).await.unwrap(); + + assert_eq!(deleted, 0); + assert!(stored_draft(&db).await.is_some()); + } + + #[tokio::test] + async fn delete_draft_from_owning_org_removes_it() { + let db = db_with_draft().await; + + let deleted = delete_draft_in(&db, OWNER_ORG, DRAFT_ID).await.unwrap(); + + assert_eq!(deleted, 1); + assert!(stored_draft(&db).await.is_none()); + } + + #[tokio::test] + async fn get_draft_org_id_returns_the_owning_org() { + let db = db_with_draft().await; + + let org = get_draft_org_id_in(&db, DRAFT_ID).await.unwrap(); + + assert_eq!(org.as_deref(), Some(OWNER_ORG)); + } + + #[tokio::test] + async fn get_draft_org_id_returns_none_for_an_unknown_draft() { + let db = db_with_draft().await; + + let org = get_draft_org_id_in(&db, "missing").await.unwrap(); + + assert_eq!(org, None); + } +} diff --git a/src/super_cluster_queue/src/search_job/mod.rs b/src/super_cluster_queue/src/search_job/mod.rs index 9802070ba3..af5881e1f0 100644 --- a/src/super_cluster_queue/src/search_job/mod.rs +++ b/src/super_cluster_queue/src/search_job/mod.rs @@ -193,7 +193,7 @@ pub(crate) async fn process(msg: Message) -> Result<()> { match existing { Some(_) => { log::info!("updating existing workflow draft for {org_id}/{id}"); - match infra::table::workflows::update_draft(draft.clone()).await { + match infra::table::workflows::update_draft(&org_id, draft.clone()).await { Ok(_) => {} Err(e) => { log::error!( @@ -220,10 +220,19 @@ pub(crate) async fn process(msg: Message) -> Result<()> { MessageType::WorkflowDraftDelete => { let id: String = json::from_slice(&msg.value.unwrap())?; log::info!("received workflow draft delete for {id}"); - match infra::table::workflows::delete_draft(&id).await { - Ok(_) => {} + match infra::table::workflows::get_draft_org_id(&id).await { + Ok(Some(org_id)) => { + if let Err(e) = infra::table::workflows::delete_draft(&org_id, &id).await { + log::error!( + "error while deleting workflow draft from db {org_id}/{id} : {e}" + ); + } + } + Ok(None) => { + log::debug!("workflow draft {id} not found, nothing to delete"); + } Err(e) => { - log::error!("error while deleting workflow draft from db {id} : {e}"); + log::error!("error while looking up workflow draft {id} for delete : {e}"); } } log::info!("successfully handled workflow draft delete for {id}"); diff --git a/tests/api-testing/README.md b/tests/api-testing/README.md index 06d52f06fa..f285b9b9e6 100644 --- a/tests/api-testing/README.md +++ b/tests/api-testing/README.md @@ -119,6 +119,19 @@ file should pass strict ruff. | `serial` | Reads or changes server-wide state (root passcode, shared seed stream, org-wide counts). Jobs that run the default set with `-n` (OSS `api_integration_tests`, ENT `oss_in_ent`) run `-m "not serial" -n 4 --dist loadfile`, then `-m serial` alone; mark any new test of that kind. | | `order(N)` | Run order pin (legacy — avoid in new tests). | +## Enterprise suites with their own setup + +CI copies `o2-enterprise/tests/*` over `tests/` and runs these suites in their own shards, never in the full run. Locally, deselect them from `rye run pytest` with `--ignore=tests/fleet/ --ignore=tests/fleet_flag_off/ --ignore=tests/fleet_collector/ --ignore=tests/rbac/fleet/`. + +| Suite | Needs | Run | +|---|---|---| +| `tests/fleet/` | An enterprise server with `O2_FLEET_ENABLED=true` (the default) | `rye run pytest tests/fleet/ -n 4 --dist loadfile` | +| `tests/fleet_flag_off/` | Two starts over the same `./data`: first with `O2_FLEET_ENABLED=true` to seed a real token, then with `O2_FLEET_ENABLED=false` (fails otherwise) | `FLEET_FLAG_OFF_SEED=/tmp/seed.json PYTHONPATH=.:tests rye run python -m fleet_flag_off.seed_token`, restart the server with the flag off, then `FLEET_FLAG_OFF_SEED=/tmp/seed.json rye run pytest tests/fleet_flag_off/` | +| `tests/fleet_collector/` | An enterprise server with `O2_FLEET_ENABLED=true`, a running Docker daemon, and the collector image pinned as `IMAGE_REF` in `tests/fleet_collector/collector_docker.py` (pulled on first use). Containers reach the server through `host.docker.internal`. | `rye run pytest tests/fleet_collector/` (no `-n`) | +| `tests/rbac/fleet/` | An enterprise server with `O2_OPENFGA_ENABLED=true`, `O2_OPENFGA_LIST_ONLY_PERMITTED=true` (the filtered-list checks need it) and a running OpenFGA server; CI runs it in the `ent_rbac` shard | `rye run pytest tests/rbac/fleet/` | + +Fleet runs leave `fl_auto_*` orgs behind, because org delete is cloud-only. Most are empty, but the flag-off seed org keeps its fleet and live token, and the fleet suite's orgs keep a disabled ingestion token. Remove them by hand before reusing a local server. + ## Troubleshooting - **`KeyError: 'ZO_BASE_URL'` on collection** — `conftest.py` reads env vars at import. Export them before running pytest (even `--collect-only`). diff --git a/tests/api-testing/tests/edition/__init__.py b/tests/api-testing/tests/edition/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/api-testing/tests/edition/test_fleet_edition_gate.py b/tests/api-testing/tests/edition/test_fleet_edition_gate.py new file mode 100644 index 0000000000..c4fdc1421f --- /dev/null +++ b/tests/api-testing/tests/edition/test_fleet_edition_gate.py @@ -0,0 +1,24 @@ +"""Fleet management is served by the enterprise build only; the edition comes from `/config`, so both branches run.""" + +from __future__ import annotations + +from support.client import OpenObserveClient + +ENTERPRISE_BUILDS = ("enterprise", "cloud") + + +def test_fl_gate_02_04_fleet_routes_follow_the_edition(client: OpenObserveClient): + config = client.get("config") + assert config.status_code == 200, config.text + build_type = config.json()["build_type"] + fleets = client.get("fleet/fleets") + + if build_type in ENTERPRISE_BUILDS: + assert config.json()["fleet_enabled"] is True, config.json() + assert fleets.status_code == 200, fleets.text + assert "default" in {f["id"] for f in fleets.json()["items"]}, fleets.text + else: + assert build_type == "opensource", build_type + assert config.json()["fleet_enabled"] is False, config.json() + # The OSS router never registers the fleet routes (enterprise-only `fleet_routes`), so there is no 403 stub. + assert fleets.status_code == 404, fleets.text diff --git a/tests/ui-testing/pages/apiCleanup.js b/tests/ui-testing/pages/apiCleanup.js index 0da1f27f20..9fc1bd2999 100644 --- a/tests/ui-testing/pages/apiCleanup.js +++ b/tests/ui-testing/pages/apiCleanup.js @@ -3712,6 +3712,50 @@ class APICleanup { return removed; } + /** Crashed-run fleet leftovers; quiet on OSS, where the fleet routes are not registered and answer 404. */ + async cleanupFleetArtifacts(fleetPrefixes = ['fl_auto_'], configPrefixes = ['fl-auto-']) { + testLogger.info('Starting fleet artifact cleanup', { fleetPrefixes, configPrefixes }); + const headers = { 'Authorization': this.authHeader, 'Content-Type': 'application/json' }; + const api = (path) => `${this.baseUrl}/api/${this.org}${path}`; + const json = async (res) => { try { return await res.json(); } catch { return null; } }; + const starts = (value, prefixes) => typeof value === 'string' && prefixes.some((p) => value.startsWith(p)); + const removed = { tokens: 0, fleets: 0, configs: 0 }; + // The fl_auto_ roles and users do not depend on the fleet API, so sweep them even when it is unavailable. + for (const prefix of fleetPrefixes) await this.cleanupIamArtifacts(prefix); + try { + const fleetsRes = await this._fetch(api('/fleet/fleets?offset=0&limit=1000'), { method: 'GET', headers }); + if (!fleetsRes.ok) { + testLogger.info('Fleet cleanup skipped — fleet API unavailable (OSS build or fleet off)', { status: fleetsRes.status }); + return removed; + } + const fleets = ((await json(fleetsRes))?.items ?? []).filter((f) => starts(f?.name, fleetPrefixes)); + const ids = new Set(fleets.map((f) => f.id)); + const tokensRes = await this._fetch(api('/fleet/tokens?offset=0&limit=1000'), { method: 'GET', headers }); + for (const tok of (tokensRes.ok ? (await json(tokensRes))?.items : null) ?? []) { + if (ids.has(tok?.fleet?.id) && !tok.revoked) { + await this._fetch(api(`/fleet/tokens/${tok.id}/revoke`), { method: 'POST', headers }); + removed.tokens += 1; + } + } + // Deleting a fleet also removes its revoked tokens and rollout history, which unpins its configurations. + for (const f of fleets) { + await this._fetch(api(`/fleet/fleets/${f.id}`), { method: 'DELETE', headers }); + removed.fleets += 1; + } + const configsRes = await this._fetch(api('/fleet/configs?offset=0&limit=1000'), { method: 'GET', headers }); + for (const c of (configsRes.ok ? (await json(configsRes))?.items : null) ?? []) { + if (starts(c?.id, configPrefixes)) { + await this._fetch(api(`/fleet/configs/${c.id}`), { method: 'DELETE', headers }); + removed.configs += 1; + } + } + testLogger.info('Fleet artifact cleanup complete', removed); + } catch (e) { + testLogger.warn('Fleet artifact cleanup hit an error — continuing', { error: e?.message }); + } + return removed; + } + /** * Verify stream exists via API * @param {string} streamName - Name of the stream to verify diff --git a/tests/ui-testing/pages/fleetPages/fleetConfigPage.js b/tests/ui-testing/pages/fleetPages/fleetConfigPage.js new file mode 100644 index 0000000000..d4de4a4b65 --- /dev/null +++ b/tests/ui-testing/pages/fleetPages/fleetConfigPage.js @@ -0,0 +1,203 @@ +const { expect } = require('@playwright/test'); +const testLogger = require('../../playwright-tests/utils/test-logger.js'); +const { getOrgIdentifier } = require('../../playwright-tests/utils/cloud-auth.js'); + +const PAGE_TIMEOUT_MS = 30000; + +class FleetConfigPage { + constructor(page) { + this.page = page; + this.editorPage = '[data-test="fleet-config-editor-page"]'; + this.backLink = '[data-test="app-page-header-back"]'; + this.modeChip = '[data-test="fleet-config-editor-mode-chip"]'; + this.visualMode = '[data-test="fleet-config-editor-mode-visual"]'; + // The YAML toggle item has no data-test of its own; reka renders its value on the button. + this.yamlMode = '[data-test="fleet-config-editor-mode"] button[value="yaml"]'; + this.yamlEditor = '[data-test="fleet-config-editor-yaml"]'; + this.canvas = '[data-test="fleet-editor-canvas"]'; + this.validateBtn = '[data-test="fleet-config-editor-validate"]'; + this.validationChip = '[data-test="fleet-config-editor-validation"]'; + this.publishBtn = '[data-test="fleet-config-editor-publish"]'; + this.publishDialog = '[data-test="fleet-config-editor-publish-dialog"]'; + this.publishMessageField = '[data-test="fleet-config-editor-publish-message-field"]'; + this.dialogPrimaryBtn = '[data-test="o-dialog-primary-btn"]'; + this.startRolloutBtn = '[data-test="fleet-config-editor-start-rollout"]'; + this.historyBtn = '[data-test="fleet-config-editor-history"]'; + this.history = '[data-test="fleet-config-history"]'; + this.diff = '[data-test="fleet-config-diff"]'; + this.properties = '[data-test="fleet-component-properties"]'; + this.newSettingKeyField = '[data-test="fleet-component-properties-new-key-field"]'; + this.newSettingValueField = '[data-test="fleet-component-properties-new-value-field"]'; + this.addSettingSubmit = '[data-test="fleet-component-properties-add-submit"]'; + this.successToast = '[data-test^="o-toast-"][data-test-variant="success"]'; + } + + paletteItem(kind, key) { + return this.page.locator(`[data-test="fleet-component-palette-item-${kind}-${key}"]`); + } + + node(nodeId) { + return this.page.locator(`[data-test=${JSON.stringify(`fleet-editor-node-${nodeId}`)}]`); + } + + // Dotted keys carry quotes and dots, so the attribute value is a quoted CSS string. + propertyField(key) { + return this.page.locator(`[data-test=${JSON.stringify(`fleet-component-properties-field-${key}-field`)}]`); + } + + historyCompare(version) { + return this.page.locator(`[data-test="fleet-config-history-compare-${version}"]`); + } + + async goto(configId) { + const url = `${process.env.ZO_BASE_URL}/web/fleet/configurations/${encodeURIComponent(configId)}?org_identifier=${getOrgIdentifier()}`; + await this.page.goto(url, { waitUntil: 'domcontentloaded' }); + await expect(this.page.locator(this.editorPage)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.validateBtn)).toBeEnabled({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectTitle(configId) { + await expect(this.page.locator(this.editorPage).getByRole('heading', { name: configId })).toBeVisible(); + } + + async clickBack() { + await this.page.locator(this.backLink).click(); + } + + async expectVisualMode() { + await expect(this.page.locator(this.visualMode)).toHaveAttribute('data-state', 'on', { timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.canvas)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async switchToYaml() { + if ((await this.page.locator(this.yamlMode).getAttribute('data-state')) !== 'on') await this.page.locator(this.yamlMode).click(); + await expect(this.page.locator(this.yamlMode)).toHaveAttribute('data-state', 'on'); + await expect(this.page.locator(this.yamlEditor)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async switchToVisual() { + await expect(this.page.locator(this.visualMode)).toBeEnabled({ timeout: PAGE_TIMEOUT_MS }); + await this.page.locator(this.visualMode).click(); + await this.expectVisualMode(); + } + + async expectVisualUnavailable() { + await expect(this.page.locator(this.visualMode)).toBeDisabled({ timeout: PAGE_TIMEOUT_MS }); + } + + async monaco(action, text) { + return await this.page.locator(this.yamlEditor).evaluate( + (node, { action, text }) => { + const editor = window.monaco?.editor?.getEditors?.().find((e) => node.contains(e.getDomNode())); + if (!editor) return null; + if (action === 'set') { + editor.setValue(text); + // The editor emits after a 500ms debounce that only a text blur flushes; unflushed, a quick switch to Visual drops the edit. + editor.focus(); + if (!node.contains(document.activeElement)) throw new Error('YAML editor did not take focus'); + document.activeElement.blur(); + } + return editor.getValue(); + }, + { action, text }, + ); + } + + async readYaml() { + await expect.poll(() => this.monaco('read'), { timeout: PAGE_TIMEOUT_MS }).not.toBeNull(); + return await this.monaco('read'); + } + + async expectYaml(expected) { + await expect.poll(async () => ((await this.monaco('read')) ?? '').trimEnd(), { timeout: PAGE_TIMEOUT_MS }).toBe( + expected.trimEnd(), + ); + } + + async setYaml(text) { + await expect.poll(() => this.monaco('read'), { timeout: PAGE_TIMEOUT_MS }).not.toBeNull(); + await this.monaco('set', text); + testLogger.info('YAML editor set', { lines: text.split('\n').length }); + } + + async addFromPalette(kind, key, nodeId) { + await this.paletteItem(kind, key).click(); + await expect(this.node(nodeId)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async selectNode(nodeId) { + await expect(this.node(nodeId)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await this.node(nodeId).click(); + await expect(this.page.locator(this.properties)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectPropertyValue(key, value) { + await expect(this.propertyField(key)).toHaveValue(value, { timeout: PAGE_TIMEOUT_MS }); + } + + async expectNoProperty(key) { + await expect(this.propertyField(key)).toHaveCount(0); + } + + async setProperty(key, value) { + await this.propertyField(key).fill(value); + await expect(this.propertyField(key)).toHaveValue(value); + } + + async addSetting(key, value) { + await this.page.locator(this.newSettingKeyField).fill(key); + await this.page.locator(this.newSettingValueField).fill(value); + await this.page.locator(this.addSettingSubmit).click(); + await this.expectPropertyValue(key, value); + } + + async validate() { + await this.page.locator(this.validateBtn).click(); + } + + async expectValidationChip(text) { + await expect(this.page.locator(this.validationChip)).toHaveText(text, { timeout: PAGE_TIMEOUT_MS }); + } + + async publish(message) { + await this.page.locator(this.publishBtn).click(); + const dialog = this.page.locator(this.publishDialog); + await expect(dialog).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await dialog.locator(this.publishMessageField).fill(message); + await dialog.locator(this.dialogPrimaryBtn).click(); + } + + async expectModeChip(text) { + await expect(this.page.locator(this.modeChip)).toHaveText(text, { timeout: PAGE_TIMEOUT_MS }); + } + + async expectSuccessToast(text) { + await expect(this.page.locator(this.successToast).filter({ hasText: text }).first()).toBeVisible({ + timeout: PAGE_TIMEOUT_MS, + }); + } + + async openHistory() { + await this.page.locator(this.historyBtn).click(); + await expect(this.page.locator(this.history)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectCompareDisabled(version) { + await expect(this.historyCompare(version)).toBeDisabled({ timeout: PAGE_TIMEOUT_MS }); + } + + async compare(version) { + await this.historyCompare(version).click(); + await expect(this.page.locator(this.diff)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectDiffContains(text) { + await expect(this.page.locator(this.diff)).toContainText(text); + } + + async openDeployFromEditor() { + await this.page.locator(this.startRolloutBtn).click(); + } +} + +module.exports = FleetConfigPage; diff --git a/tests/ui-testing/pages/fleetPages/fleetFleetsPage.js b/tests/ui-testing/pages/fleetPages/fleetFleetsPage.js new file mode 100644 index 0000000000..5cf4a45ba6 --- /dev/null +++ b/tests/ui-testing/pages/fleetPages/fleetFleetsPage.js @@ -0,0 +1,65 @@ +const { expect } = require('@playwright/test'); + +const PAGE_TIMEOUT_MS = 30000; +const DASH = '—'; + +class FleetFleetsPage { + constructor(page) { + this.page = page; + this.rail = '[data-test="fleet-rail"]'; + this.panel = '[data-test="fleet-fleets-panel"]'; + this.panelName = '[data-test="fleet-fleets-name"]'; + this.addCollectorBtn = '[data-test="fleet-fleets-add-collector"]'; + this.deployBtn = '[data-test="fleet-fleets-change-version"]'; + this.nameDialog = '[data-test="fleet-rail-name-dialog"]'; + this.nameDialogField = '[data-test="fleet-rail-name-dialog-name-field"]'; + this.dialogPrimaryBtn = '[data-test="o-dialog-primary-btn"]'; + this.rolloutsDrawer = '[data-test="fleet-rollouts-drawer"]'; + this.rolloutStatus = '[data-test="fleet-rollouts-status"]'; + } + + railItem(fleetId) { + return this.page.locator(`[data-test="fleet-rail-item-${fleetId}"]`); + } + + async expectPanelFor(name) { + await expect(this.page.locator(this.panel)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.panelName)).toHaveText(name); + } + + async expectRailItemActive(fleetId) { + await expect(this.railItem(fleetId)).toHaveAttribute('data-state', 'active', { timeout: PAGE_TIMEOUT_MS }); + } + + // The rail shows a collector count only for a fleet the caller may read, and a dash otherwise. + async expectRailItemReadable(fleetId, readable) { + const item = this.railItem(fleetId); + await expect(item).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + if (readable) await expect(item).not.toContainText(DASH); + else await expect(item).toContainText(DASH); + } + + async openAddCollector() { + await expect(this.page.locator(this.panel)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await this.page.locator(this.addCollectorBtn).click(); + } + + async openDeploy() { + await expect(this.page.locator(this.panel)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await this.page.locator(this.deployBtn).click(); + } + + async submitNewFleet(name) { + const dialog = this.page.locator(this.nameDialog); + await expect(dialog).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await dialog.locator(this.nameDialogField).fill(name); + await dialog.locator(this.dialogPrimaryBtn).click(); + } + + async expectRolloutDrawerStatus(text) { + await expect(this.page.locator(this.rolloutsDrawer)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.rolloutStatus)).toHaveText(text, { timeout: PAGE_TIMEOUT_MS }); + } +} + +module.exports = FleetFleetsPage; diff --git a/tests/ui-testing/pages/fleetPages/fleetInstallPage.js b/tests/ui-testing/pages/fleetPages/fleetInstallPage.js new file mode 100644 index 0000000000..ec63bc1cde --- /dev/null +++ b/tests/ui-testing/pages/fleetPages/fleetInstallPage.js @@ -0,0 +1,301 @@ +const { expect } = require('@playwright/test'); +const testLogger = require('../../playwright-tests/utils/test-logger.js'); + +const PAGE_TIMEOUT_MS = 30000; +const SCRIPT_BASE = 'https://raw.githubusercontent.com/openobserve/agents/main'; +const IMAGE = 'o2cr.ai/openobserve/otel-fleet-collector'; +const HELM_NAMESPACE = 'openobserve-collector'; +const AGENT_RELEASE = 'otel-fleet-collector'; +const GATEWAY_RELEASE = 'otel-fleet-collector-gateway'; +const MASK = '•'.repeat(12); + +class FleetInstallPage { + constructor(page) { + this.page = page; + this.dialog = '[data-test="fleet-install-dialog"]'; + this.noToken = '[data-test="fleet-install-dialog-no-token"]'; + this.tokenTrigger = '[data-test="fleet-install-dialog-token-trigger"]'; + this.tokenOption = '[data-test="fleet-install-dialog-token-option"]'; + this.joinsText = '[data-test="fleet-install-dialog-fleet"]'; + this.copyOnce = '[data-test="fleet-install-dialog-copy-once"]'; + this.placeholderHint = '[data-test="fleet-install-dialog-placeholder-hint"]'; + this.waiting = '[data-test="fleet-install-dialog-waiting"]'; + this.tokenFormName = '[data-test="fleet-token-form-name-field"]'; + this.tokenFormSubmit = '[data-test="fleet-token-form-submit"]'; + this.command = '[data-test="fleet-install-command"]'; + this.commandCode = '[data-test="fleet-install-command-code-pre"] code'; + this.revealBtn = '[data-test="fleet-install-command-code-reveal-btn"]'; + this.copyBtn = '[data-test="fleet-install-command-code-copy-btn"]'; + this.websocket = '[data-test="fleet-install-command-websocket"] button[role="checkbox"]'; + this.clusterNameField = '[data-test="fleet-install-command-cluster-name-field"]'; + this.uninstallTrigger = '[data-test="fleet-install-command-uninstall"] button[aria-expanded]'; + this.uninstallCode = '[data-test="fleet-uninstall-command-code-pre"] code'; + } + + osButton(platform) { + return this.page.locator(`[data-test="fleet-install-command-os-${platform}"]`); + } + + roleButton(role) { + return this.page.locator(`[data-test="fleet-install-command-role-${role}"]`); + } + + static resolveBaseUrl(endpoint, origin) { + let url; + try { + url = new URL(endpoint || '/', origin); + } catch { + url = new URL(origin); + } + return `${url.origin}${url.pathname}`.replace(/\/+$/, ''); + } + + static opampEndpoint(t) { + const url = new URL(t.baseUrl); + const scheme = t.http ? url.protocol.replace(':', '') : url.protocol === 'https:' ? 'wss' : 'ws'; + return `${scheme}://${url.host}${url.pathname.replace(/\/+$/, '')}/api/${t.org}/v1/opamp`; + } + + static expectedInstallLines(platform, t) { + const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`; + const join = (args, cont) => args.map((a, i) => ` ${a}${i < args.length - 1 ? cont : ''}`); + if (platform === 'linux' || platform === 'macos') { + const args = [ + `--url ${q(t.baseUrl)}`, + `--org ${q(t.org)}`, + `--token ${q(t.token)}`, + ...(t.version ? [`--version ${q(t.version)}`] : []), + `--transport ${t.http ? 'http' : 'ws'}`, + ]; + return [`curl -fsSL ${SCRIPT_BASE}/fleet/install.sh | sudo bash -s -- \\`, ...join(args, ' \\')]; + } + if (platform === 'windows') { + const ps = (v) => `'${v.replace(/'/g, "''")}'`; + const args = [ + `-Url ${ps(t.baseUrl)}`, + `-Org ${ps(t.org)}`, + `-Token ${ps(t.token)}`, + ...(t.version ? [`-Version ${ps(t.version)}`] : []), + `-Transport ${t.http ? 'http' : 'ws'}`, + ]; + return [`& ([scriptblock]::Create((irm ${SCRIPT_BASE}/fleet/install.ps1))) \``, ...join(args, ' `')]; + } + if (platform === 'docker') { + const baseUrl = t.baseUrl.replace(/\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, '//host.docker.internal'); + const insecure = !baseUrl.startsWith('https:'); + return [ + `AUTH=$(printf 'fleet:%s' ${q(t.token)} | base64 | tr -d '\\n')`, + 'mkdir -p otel-fleet && cat > otel-fleet/supervisor.yaml </dev/null 2>&1 || true', + 'docker run -d --name otel-fleet-collector --restart unless-stopped \\', + ' --hostname "$(hostname)" \\', + ' -v "$PWD/otel-fleet/supervisor.yaml:/etc/opampsupervisor/supervisor.yaml:ro" \\', + ' -v otel-fleet-state:/var/lib/opampsupervisor \\', + ` ${IMAGE}:${t.version ?? 'latest'}`, + ]; + } + const gateway = t.role === 'gateway'; + const cluster = t.clusterName?.trim().replace(/\\/g, '\\\\').replace(/,/g, '\\,'); + const sets = [ + ...(gateway ? ['--set agent.enabled=false', '--set gateway.enabled=true'] : []), + ...(cluster ? [`--set-string clusterName=${q(cluster)}`] : []), + `--set image.repository=${IMAGE}`, + ...(t.version ? [`--set image.tag=${q(t.version)}`] : []), + `--set opamp.endpoint=${q(FleetInstallPage.opampEndpoint(t))}`, + `--set opamp.token=${q(t.token)}`, + ]; + return [ + 'helm repo add openobserve https://charts.openobserve.ai && helm repo update', + `helm upgrade --install ${gateway ? GATEWAY_RELEASE : AGENT_RELEASE} openobserve/otel-fleet-collector \\`, + ` --namespace ${HELM_NAMESPACE} --create-namespace \\`, + ...join(sets, ' \\'), + ]; + } + + static expectedUninstallLines(platform, role = 'agent') { + if (platform === 'windows') return [`& ([scriptblock]::Create((irm ${SCRIPT_BASE}/fleet/install.ps1))) -Uninstall`]; + if (platform === 'docker') { + return ['docker rm -f otel-fleet-collector', 'docker volume rm otel-fleet-state', 'rm -rf otel-fleet']; + } + if (platform === 'kubernetes') { + return [`helm uninstall ${role === 'gateway' ? GATEWAY_RELEASE : AGENT_RELEASE} --namespace ${HELM_NAMESPACE}`]; + } + return [`curl -fsSL ${SCRIPT_BASE}/fleet/install.sh | sudo bash -s -- --uninstall`]; + } + + static maskToken(token) { + return token.slice(0, 4) + MASK + token.slice(-4); + } + + // The command targets the ingestion URL, or the app's own base when none is configured. + async installTarget(seed, token) { + const ingestion = await seed.ingestionBaseUrl(); + const url = new URL(this.page.url()); + const pos = url.pathname.indexOf('/web/'); + const appBase = pos > -1 ? url.origin + url.pathname.slice(0, pos) : url.origin; + const target = { + baseUrl: FleetInstallPage.resolveBaseUrl(ingestion || appBase, url.origin), + org: seed.orgId(), + token, + version: await seed.targetCollectorVersion(), + }; + testLogger.info('Install target', { baseUrl: target.baseUrl, org: target.org, version: target.version }); + return target; + } + + async expectDialogOpen() { + await expect(this.page.locator(this.dialog)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectCommandShown() { + await expect(this.page.locator(this.commandCode)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + // A single-choice toggle deselects its active item on a second click, so only an inactive item is clicked. + async selectPlatform(platform) { + if ((await this.osButton(platform).getAttribute('data-state')) !== 'on') await this.osButton(platform).click(); + await expect(this.osButton(platform)).toHaveAttribute('data-state', 'on'); + } + + async selectRole(role) { + if ((await this.roleButton(role).getAttribute('data-state')) !== 'on') await this.roleButton(role).click(); + await expect(this.roleButton(role)).toHaveAttribute('data-state', 'on'); + } + + async expectWebsocket(checked) { + await expect(this.page.locator(this.websocket)).toHaveAttribute('aria-checked', String(checked)); + } + + async setWebsocket(checked) { + const box = this.page.locator(this.websocket); + if ((await box.getAttribute('aria-checked')) !== String(checked)) await box.click(); + await this.expectWebsocket(checked); + } + + async revealToken() { + await this.page.locator(this.revealBtn).click(); + } + + async setClusterName(name) { + await this.page.locator(this.clusterNameField).fill(name); + } + + async commandText() { + return (await this.page.locator(this.commandCode).textContent()) ?? ''; + } + + async expectCommandLines(lines) { + await expect.poll(async () => (await this.commandText()).split('\n'), { timeout: PAGE_TIMEOUT_MS }).toEqual(lines); + } + + async expectCommandMasked(token) { + await expect.poll(() => this.commandText(), { timeout: PAGE_TIMEOUT_MS }).toContain(FleetInstallPage.maskToken(token)); + expect(await this.commandText()).not.toContain(token); + } + + async expandUninstall() { + const trigger = this.page.locator(this.uninstallTrigger).first(); + if ((await trigger.getAttribute('aria-expanded')) !== 'true') await trigger.click(); + await expect(this.page.locator(this.uninstallCode)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectUninstallLines(lines, token) { + await expect + .poll(async () => ((await this.page.locator(this.uninstallCode).textContent()) ?? '').split('\n'), { + timeout: PAGE_TIMEOUT_MS, + }) + .toEqual(lines); + expect(await this.page.locator(this.uninstallCode).textContent()).not.toContain(token); + } + + async copyCommand() { + await this.page.locator(this.copyBtn).click(); + return await this.page.evaluate(() => navigator.clipboard.readText()); + } + + async expectNoTokenState() { + await expect(this.page.locator(this.noToken)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + // The dialog must read GET /fleet/collectors again once a token exists, or it never notices the collector. + async createInlineTokenAndAwaitCollectorsPoll(name) { + let created = false; + const onResponse = (resp) => { + if (resp.request().method() === 'POST' && /\/fleet\/tokens$/.test(new URL(resp.url()).pathname) && resp.ok()) { + created = true; + } + }; + this.page.on('response', onResponse); + try { + const createdResp = this.page.waitForResponse( + (r) => r.request().method() === 'POST' && /\/fleet\/tokens$/.test(new URL(r.url()).pathname), + ); + const polled = this.page.waitForResponse( + (r) => created && r.request().method() === 'GET' && /\/fleet\/collectors$/.test(new URL(r.url()).pathname), + { timeout: PAGE_TIMEOUT_MS }, + ); + await this.page.locator(this.noToken).locator(this.tokenFormName).fill(name); + await this.page.locator(this.noToken).locator(this.tokenFormSubmit).click(); + const resp = await createdResp; + expect(resp.status(), 'token create from the dialog failed').toBe(201); + const pollResp = await polled; + expect(pollResp.status()).toBe(200); + return await resp.json(); + } finally { + this.page.off('response', onResponse); + } + } + + async expectCopyOnceAndWaiting() { + await expect(this.page.locator(this.copyOnce)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.waiting)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.placeholderHint)).toHaveCount(0); + } + + async expectSelectedToken(label) { + await expect(this.page.locator(this.tokenTrigger)).toHaveAttribute('data-test-selected-label', label, { + timeout: PAGE_TIMEOUT_MS, + }); + } + + async tokenOptionLabels() { + const trigger = this.page.locator(this.tokenTrigger); + await expect(async () => { + if ((await trigger.getAttribute('aria-expanded')) !== 'true') await trigger.click(); + await expect(trigger).toHaveAttribute('aria-expanded', 'true', { timeout: 2000 }); + }).toPass({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.tokenOption).first()).toBeVisible(); + const labels = await this.page.locator(this.tokenOption).evaluateAll((els) => + els.map((e) => e.getAttribute('data-test-label')), + ); + await this.page.keyboard.press('Escape'); + return labels; + } + + async expectJoinsText(text) { + await expect(this.page.locator(this.joinsText)).toHaveText(text, { timeout: PAGE_TIMEOUT_MS }); + } +} + +module.exports = FleetInstallPage; diff --git a/tests/ui-testing/pages/fleetPages/fleetManagerPage.js b/tests/ui-testing/pages/fleetPages/fleetManagerPage.js new file mode 100644 index 0000000000..7e2fec5ec2 --- /dev/null +++ b/tests/ui-testing/pages/fleetPages/fleetManagerPage.js @@ -0,0 +1,219 @@ +const { expect } = require('@playwright/test'); +const testLogger = require('../../playwright-tests/utils/test-logger.js'); +const { getAuthHeaders, getOrgIdentifier } = require('../../playwright-tests/utils/cloud-auth.js'); + +const PAGE_TIMEOUT_MS = 30000; + +class FleetManagerPage { + constructor(page) { + this.page = page; + this.navDataTile = '[data-test="menu-link-\\/streams-item"]'; + this.navFleetItem = '[data-test="nav-group-item-fleet"]'; + this.managerPage = '[data-test="fleet-manager-page"]'; + this.flowOverview = '[data-test="fleet-flow-overview"]'; + this.flowError = '[data-test="fleet-flow-overview-error"]'; + this.flowNoFleets = '[data-test="fleet-flow-overview-no-fleets"]'; + this.fleetsArea = '[data-test="fleet-fleets"]'; + this.fleetsNoAccess = '[data-test="fleet-fleets-no-access"]'; + this.fleetsNotFound = '[data-test="fleet-fleets-not-found"]'; + this.fleetsPanel = '[data-test="fleet-fleets-panel"]'; + this.tableForbidden = '[data-test="o2-table-forbidden"]'; + this.collectorsScopeAll = '[data-test="fleet-collectors-scope-all"]'; + this.columnToggleBtn = '[data-test="o2-table-column-toggle-btn"]'; + this.columnTogglePanel = '[data-test="o2-table-column-toggle-panel"]'; + this.columnResetBtn = '[data-test="o2-table-column-toggle-reset-btn"]'; + this.settingsRoot = '[data-test="fleet-settings"]'; + this.settingsTargetVersion = '[data-test="fleet-settings-target-version"]'; + this.settingsAlerts = '[data-test="fleet-settings-alerts"]'; + this.settingsAlertLibrary = '[data-test="fleet-settings-alert-library"]'; + this.errorToast = '[data-test^="o-toast-"][data-test-variant="error"]'; + } + + tab(name) { + return this.page.locator(`[data-test="fleet-manager-tab-${name}"]`); + } + + createButton(tab) { + return this.page.locator(`[data-test="fleet-manager-create-${tab}"]`); + } + + collectorStat(key) { + return this.page.locator(`[data-test="fleet-collectors-stat-${key}"]`); + } + + tableHeader(columnId) { + return this.page.locator(`th[data-test="o2-table-th-${columnId}"]`); + } + + columnToggleCheckbox(columnId) { + return this.page.locator(`[data-test="o2-table-column-toggle-item-${columnId}"] button[role="checkbox"]`); + } + + columnToggleItem(columnId) { + return this.page.locator(`[data-test="o2-table-column-toggle-item-${columnId}"]`); + } + + // Fail loudly, never skip: these specs run only on an enterprise build with fleet management on. + async assertEnabled() { + const base = process.env.ZO_BASE_URL.replace(/\/$/, ''); + // The unauthenticated /config is the login bootstrap and has no fleet_enabled; the org config does. + const config = await this.page.request.get(`${base}/api/${getOrgIdentifier()}/config`, { headers: getAuthHeaders() }); + const fleetEnabled = config.ok() ? (await config.json())?.fleet_enabled : undefined; + const list = await this.page.request.get(`${base}/api/${getOrgIdentifier()}/fleet/fleets?offset=0&limit=1`, { + headers: getAuthHeaders(), + }); + if (fleetEnabled !== true || list.status() !== 200) { + throw new Error( + `Fleet management is NOT available on ${base} (/config fleet_enabled=${fleetEnabled}, GET /fleet/fleets -> ${list.status()}). ` + + 'These specs are enterprise-only and must never be skipped: run them on an enterprise build with O2_FLEET_ENABLED=true.', + ); + } + } + + async gotoFleet(query = {}) { + const params = new URLSearchParams({ org_identifier: getOrgIdentifier(), ...query }); + await this.page.goto(`${process.env.ZO_BASE_URL}/web/fleet?${params}`, { waitUntil: 'domcontentloaded' }); + await expect(this.page.locator(this.managerPage)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async openFromNav() { + const tile = this.page.locator(this.navDataTile); + const item = this.page.locator(this.navFleetItem); + await expect(tile).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + // The flyout opens on hover after a delay and closes on scroll, so open-and-reveal retries as a unit. + await expect(async () => { + await this.page.mouse.move(0, 0); + await tile.hover(); + await expect(item).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: PAGE_TIMEOUT_MS }); + await item.click({ force: true }); + await expect(this.page.locator(this.managerPage)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async clickCreate(tab) { + await this.createButton(tab).click(); + } + + async clickTab(name) { + await this.tab(name).click(); + await this.expectTabActive(name); + } + + async expectTabActive(name) { + await expect(this.tab(name)).toHaveAttribute('data-state', 'active', { timeout: PAGE_TIMEOUT_MS }); + } + + async expectOnFleetRoute(query = {}) { + await expect + .poll(() => { + const url = new URL(this.page.url()); + if (!url.pathname.replace(/\/+$/, '').endsWith('/fleet')) return `path ${url.pathname}`; + for (const [k, v] of Object.entries(query)) { + if (url.searchParams.get(k) !== v) return `${k}=${url.searchParams.get(k)}`; + } + return 'ok'; + }, { timeout: PAGE_TIMEOUT_MS, message: `URL never matched /web/fleet ${JSON.stringify(query)}` }) + .toBe('ok'); + } + + async expectQueryParam(key, value) { + await expect.poll(() => new URL(this.page.url()).searchParams.get(key), { timeout: PAGE_TIMEOUT_MS }).toBe(value); + } + + async goBackAndExpectLeftFleet() { + await this.page.goBack({ waitUntil: 'domcontentloaded' }); + await expect + .poll(() => new URL(this.page.url()).pathname, { timeout: PAGE_TIMEOUT_MS }) + .not.toMatch(/\/fleet\/?$/); + } + + async expectOverviewVisible() { + await this.expectTabActive('overview'); + await expect(this.page.locator(this.flowOverview)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectOverviewNoFleetsWithoutErrorBanner() { + await expect(this.page.locator(this.flowNoFleets)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.flowError)).toHaveCount(0); + } + + async expectFleetNoAccess(text) { + const panel = this.page.locator(this.fleetsNoAccess); + await expect(panel).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + if (text) await expect(panel).toContainText(text); + } + + async expectCollectorsForbidden() { + await expect(this.page.locator(this.fleetsArea).locator(this.tableForbidden)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectCollectorsNotForbidden() { + await expect(this.page.locator(this.fleetsArea).locator(this.tableForbidden)).toHaveCount(0); + } + + async expectFleetNotFound(text) { + const panel = this.page.locator(this.fleetsNotFound); + await expect(panel).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(panel).toContainText(text); + } + + async expectFleetPanelHidden() { + await expect(this.page.locator(this.fleetsPanel)).toHaveCount(0); + } + + async expectAllFleetsScope() { + await expect(this.page.locator(this.collectorsScopeAll)).toHaveAttribute('data-state', 'on', { timeout: PAGE_TIMEOUT_MS }); + await expect(this.tableHeader('fleet')).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectStatPressed(key) { + await expect(this.collectorStat(key)).toHaveAttribute('aria-pressed', 'true', { timeout: PAGE_TIMEOUT_MS }); + } + + async openColumnMenu() { + const panel = this.page.locator(this.columnTogglePanel); + await expect(async () => { + if (!(await panel.isVisible())) await this.page.locator(this.columnToggleBtn).first().click(); + await expect(panel).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectColumnChecked(columnId, checked) { + await this.openColumnMenu(); + await expect(this.columnToggleCheckbox(columnId)).toHaveAttribute('data-state', checked ? 'checked' : 'unchecked'); + } + + async toggleColumn(columnId) { + await this.openColumnMenu(); + await this.columnToggleItem(columnId).click(); + } + + async resetColumns() { + await this.openColumnMenu(); + await this.page.locator(this.columnResetBtn).click(); + } + + async expectColumnShown(columnId, shown) { + if (shown) await expect(this.tableHeader(columnId)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + else await expect(this.tableHeader(columnId)).toHaveCount(0); + } + + async expectSettingsWithoutFleetAlerts(removedTexts) { + await expect(this.page.locator(this.settingsTargetVersion)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.settingsAlerts)).toHaveCount(0); + await expect(this.page.locator(this.settingsAlertLibrary)).toHaveCount(0); + const settings = this.page.locator(this.settingsRoot); + for (const text of removedTexts) await expect(settings).not.toContainText(text); + } + + async expectErrorToast(text) { + await expect(this.page.locator(this.errorToast).filter({ hasText: text }).first()).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + testLogger.info('Error toast shown', { text }); + } + + async expectAnyErrorToast() { + await expect(this.page.locator(this.errorToast).first()).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } +} + +module.exports = FleetManagerPage; diff --git a/tests/ui-testing/pages/fleetPages/fleetRolloutPage.js b/tests/ui-testing/pages/fleetPages/fleetRolloutPage.js new file mode 100644 index 0000000000..a533721291 --- /dev/null +++ b/tests/ui-testing/pages/fleetPages/fleetRolloutPage.js @@ -0,0 +1,71 @@ +const { expect } = require('@playwright/test'); + +const PAGE_TIMEOUT_MS = 30000; + +class FleetRolloutPage { + constructor(page) { + this.page = page; + this.dialog = '[data-test="fleet-new-rollout-dialog"]'; + this.fleetTrigger = '[data-test="fleet-new-rollout-dialog-fleet-trigger"]'; + this.fleetError = '[data-test="fleet-new-rollout-dialog-fleet-error"]'; + this.start = '[data-test="fleet-new-rollout-dialog-start"]'; + // The start and batching items have no data-test; reka renders each item's value on its button. + this.startAt = '[data-test="fleet-new-rollout-dialog-start"] button[value="at"]'; + this.firstField = '[data-test="fleet-new-rollout-dialog-first-field"]'; + this.firstError = '[data-test="fleet-new-rollout-dialog-first-error"]'; + this.initialField = '[data-test="fleet-new-rollout-dialog-initial-field"]'; + this.maxField = '[data-test="fleet-new-rollout-dialog-max-field"]'; + this.maxError = '[data-test="fleet-new-rollout-dialog-max-error"]'; + this.primaryBtn = '[data-test="o-dialog-primary-btn"]'; + } + + batchingItem(kind) { + return this.page.locator(`[data-test="fleet-new-rollout-dialog-batching"] button[value="${kind}"]`); + } + + async expectReady() { + await expect(this.page.locator(this.dialog)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.start)).toBeVisible({ timeout: PAGE_TIMEOUT_MS }); + } + + async expectNoFleetPicked() { + await expect(this.page.locator(this.fleetTrigger)).toHaveAttribute('data-test-selected-value', ''); + } + + async submit() { + await this.page.locator(this.dialog).locator(this.primaryBtn).click(); + } + + async expectFleetError(text) { + await expect(this.page.locator(this.fleetError)).toHaveText(text, { timeout: PAGE_TIMEOUT_MS }); + await expect(this.page.locator(this.dialog)).toBeVisible(); + } + + async expectScheduledDisabled() { + await expect(this.page.locator(this.startAt)).toBeDisabled({ timeout: PAGE_TIMEOUT_MS }); + } + + async chooseBatching(kind) { + if ((await this.batchingItem(kind).getAttribute('data-state')) !== 'on') await this.batchingItem(kind).click(); + await expect(this.batchingItem(kind)).toHaveAttribute('data-state', 'on'); + } + + async setCanaryFirst(value) { + await this.page.locator(this.firstField).fill(String(value)); + } + + async setProgressive(initial, max) { + await this.page.locator(this.initialField).fill(String(initial)); + await this.page.locator(this.maxField).fill(String(max)); + } + + async expectFirstError(text) { + await expect(this.page.locator(this.firstError)).toHaveText(text, { timeout: PAGE_TIMEOUT_MS }); + } + + async expectMaxError(text) { + await expect(this.page.locator(this.maxError)).toHaveText(text, { timeout: PAGE_TIMEOUT_MS }); + } +} + +module.exports = FleetRolloutPage; diff --git a/tests/ui-testing/pages/fleetPages/fleetSeed.js b/tests/ui-testing/pages/fleetPages/fleetSeed.js new file mode 100644 index 0000000000..554f6700ce --- /dev/null +++ b/tests/ui-testing/pages/fleetPages/fleetSeed.js @@ -0,0 +1,392 @@ +const crypto = require('crypto'); +const { expect } = require('@playwright/test'); +const testLogger = require('../../playwright-tests/utils/test-logger.js'); +const { getAuthHeaders, getOrgIdentifier } = require('../../playwright-tests/utils/cloud-auth.js'); + +const FLEET_PREFIX = 'fl_auto_'; +const CONFIG_PREFIX = 'fl-auto-'; +const LIST_PAGE = 'offset=0&limit=1000'; + +const FLEET_YAML = { + seed: 'service:\n pipelines:\n logs: {}\n', + publishable: [ + 'receivers:', + ' otlp:', + ' protocols:', + ' http:', + ' endpoint: "0.0.0.0:4318"', + 'exporters:', + ' otlphttp:', + ' endpoint: "https://example.com"', + 'service:', + ' pipelines:', + ' logs:', + ' receivers: [otlp]', + ' exporters: [otlphttp]', + '', + ].join('\n'), + broken: 'receivers:\n otlp: [unclosed\n', + dottedKey: [ + 'processors:', + ' resourcedetection:', + ' detectors:', + ' - system', + ' system:', + ' resource_attributes:', + ' host.id:', + ' enabled: true', + 'exporters:', + ' debug:', + ' verbosity: basic', + 'service:', + ' pipelines:', + ' logs:', + ' processors: [resourcedetection]', + ' exporters: [debug]', + '', + ].join('\n'), + quotedNumber: [ + 'exporters:', + ' debug:', + ' verbosity: "123"', + 'service:', + ' pipelines:', + ' logs:', + ' exporters: [debug]', + '', + ].join('\n'), + paletteResult: [ + 'receivers:', + ' otlp:', + ' protocols:', + ' grpc:', + ' endpoint: "0.0.0.0:4317"', + ' http:', + ' endpoint: "0.0.0.0:4318"', + 'processors:', + ' batch:', + ' timeout: 5s', + ' send_batch_size: 8192', + ' memory_limiter:', + ' check_interval: 1s', + ' limit_mib: 400', + ' spike_limit_mib: 100', + 'exporters:', + ' otlphttp/openobserve:', + ' endpoint: "${env:O2_ENDPOINT}"', + ' headers:', + ' Authorization: "${env:O2_TOKEN}"', + ' stream-name: default', + 'service:', + ' pipelines:', + ' logs:', + ' receivers: [otlp]', + ' processors: [memory_limiter, batch]', + ' exporters: [otlphttp/openobserve]', + '', + ].join('\n'), +}; + +class FleetSeed { + constructor(page) { + this.page = page; + this.run = crypto.randomUUID().replace(/-/g, '').slice(0, 8); + this.fleetPrefix = `${FLEET_PREFIX}${this.run}_`; + this.configPrefix = `${CONFIG_PREFIX}${this.run}-`; + this.loginUserField = '[data-test="login-user-id-field"]'; + this.loginPasswordField = '[data-test="login-password-field"]'; + this.loginSubmit = '[data-test="login-sign-in"]'; + this.loginInternalUser = '[data-test="login-as-internal-user"]'; + this.appShell = '[data-test="navbar-main-nav"]'; + } + + // beforeAll and afterAll run on different raw pages, so the run prefix must outlive the first one. + setPage(page) { + this.page = page; + } + + fleetName(suffix) { + return `${this.fleetPrefix}${suffix}`; + } + + configId(suffix) { + return `${this.configPrefix}${suffix}`; + } + + orgId() { + return getOrgIdentifier(); + } + + apiUrl(path) { + return `${process.env.ZO_BASE_URL.replace(/\/$/, '')}/api/${getOrgIdentifier()}${path}`; + } + + async call(method, path, data) { + const resp = await this.page.request.fetch(this.apiUrl(path), { + method, + headers: getAuthHeaders(), + ...(data === undefined ? {} : { data }), + }); + const text = await resp.text(); + let body = null; + try { + body = text ? JSON.parse(text) : null; + } catch { + body = text; + } + return { status: resp.status(), body }; + } + + async ok(method, path, data, what) { + const res = await this.call(method, path, data); + if (res.status >= 400) { + throw new Error(`${what} failed: ${method} ${path} -> ${res.status} ${JSON.stringify(res.body).slice(0, 300)}`); + } + return res.body; + } + + async createFleet(suffix) { + const name = this.fleetName(suffix); + const body = await this.ok('POST', '/fleet/fleets', { name }, `create fleet ${name}`); + if (!body?.id) throw new Error(`create fleet ${name}: no id in ${JSON.stringify(body)}`); + testLogger.info('Seeded fleet', { id: body.id, name }); + return { id: body.id, name }; + } + + async createToken(fleetId, suffix) { + const name = this.fleetName(suffix); + const body = await this.ok( + 'POST', + '/fleet/tokens', + { name, expires_in_days: null, max_uses: null, fleet: fleetId }, + `create token ${name}`, + ); + if (!/^o2ft_/.test(body?.token ?? '')) throw new Error(`create token ${name}: no o2ft_ value returned`); + return { id: body.id, name, token: body.token }; + } + + async createConfig(suffix, yaml) { + const id = this.configId(suffix); + await this.ok('POST', '/fleet/configs', { id, description: 'fleet automation', yaml }, `create config ${id}`); + return id; + } + + async publishConfig(id, yaml, message = 'fleet automation') { + const body = await this.ok('POST', `/fleet/configs/${id}/versions`, { yaml, message }, `publish ${id}`); + return body.version; + } + + async getConfig(id) { + return await this.ok('GET', `/fleet/configs/${id}`, undefined, `get config ${id}`); + } + + async setDesired(fleetId, config, version) { + const body = await this.ok( + 'PUT', + `/fleet/fleets/${fleetId}/desired`, + { + config, + version, + start: { kind: 'now' }, + batching: { kind: 'one' }, + verify: false, + auto_rollback: false, + }, + `deploy ${config} v${version} to ${fleetId}`, + ); + return { id: body.id }; + } + + async waitForRolloutStatus(rolloutId, status) { + await expect + .poll(async () => (await this.call('GET', `/fleet/rollouts/${rolloutId}`)).body?.status, { + timeout: 30000, + message: `rollout ${rolloutId} never reached ${status}`, + }) + .toBe(status); + } + + async rolloutsForConfig(configId) { + const body = await this.ok('GET', `/fleet/rollouts?${LIST_PAGE}`, undefined, 'list rollouts'); + return (body?.items ?? []).filter((r) => r.config === configId); + } + + async listFleets() { + const body = await this.ok('GET', `/fleet/fleets?${LIST_PAGE}`, undefined, 'list fleets'); + return body?.items ?? []; + } + + async expectNoFleetNamed(name) { + const names = (await this.listFleets()).map((f) => f.name); + expect(names, `fleet ${name} exists although its create was refused`).not.toContain(name); + } + + async targetCollectorVersion() { + const body = await this.ok('GET', '/fleet/settings', undefined, 'get fleet settings'); + return body?.target_collector_version || null; + } + + async ingestionBaseUrl() { + const config = await this.ok('GET', '/config', undefined, 'get org config'); + return config?.ingestion_url || null; + } + + async assertRbacEnabled() { + const { status } = await this.call('GET', '/roles'); + if (status >= 400) { + throw new Error(`RBAC is off on ${process.env.ZO_BASE_URL} (GET /roles -> ${status}); the Fleet-RBAC shard must start OpenFGA.`); + } + } + + async createRole(suffix, grants) { + const role = this.fleetName(suffix); + await this.ok('POST', '/roles', { role }, `create role ${role}`); + if (grants.length) { + await this.ok( + 'PUT', + `/roles/${role}`, + { add: grants, remove: [], add_users: [], remove_users: [] }, + `grant ${role}`, + ); + } + return role; + } + + async createMember(suffix, role = null) { + const email = `${this.fleetName(suffix)}_${crypto.randomUUID().slice(0, 8)}@example.com`; + const password = `Fl#9${crypto.randomBytes(12).toString('base64url')}aZ`; + await this.ok( + 'POST', + '/users', + { email, password, first_name: 'Automation', last_name: 'TestUser', role: 'user' }, + `create member ${email}`, + ); + if (role) { + await this.ok( + 'PUT', + `/roles/${role}`, + { add: [], remove: [], add_users: [email], remove_users: [] }, + `assign ${role} to ${email}`, + ); + } + return { email, password }; + } + + fleetGrant(object, permission) { + return { object: `fleets:${object}`, permission }; + } + + orgGrant(type, permission) { + return { object: `${type}:_all_${getOrgIdentifier()}`, permission }; + } + + async loginAsMember(browser, member) { + const context = await browser.newContext({ + viewport: { width: 1500, height: 1024 }, + permissions: ['clipboard-read', 'clipboard-write'], + }); + const page = await context.newPage(); + await page.goto(`${process.env.ZO_BASE_URL}/web/?org_identifier=${getOrgIdentifier()}`, { + waitUntil: 'domcontentloaded', + }); + const userField = page.locator(this.loginUserField); + const internal = page.locator(this.loginInternalUser).first(); + await expect(userField.or(internal)).toBeVisible({ timeout: 30000 }); + if (await internal.isVisible()) await internal.click(); + await userField.fill(member.email); + await page.locator(this.loginPasswordField).fill(member.password); + await page.locator(this.loginSubmit).click(); + await expect(page.locator(this.appShell)).toBeVisible({ timeout: 30000 }); + testLogger.info('Signed in as fleet member', { email: member.email }); + return { context, page }; + } + + // Grants reach OpenFGA asynchronously, so a member's first read must see them before the UI is judged. + async waitForMemberStatus(memberPage, path, status) { + await expect + .poll( + async () => + ( + await memberPage.request.fetch(this.apiUrl(path), { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }) + ).status(), + { timeout: 30000, message: `member GET ${path} never answered ${status}` }, + ) + .toBe(status); + } + + async cleanup() { + const removed = { tokens: 0, fleets: 0, configs: 0, roles: 0, users: 0 }; + const mine = async (path, key) => { + const res = await this.call('GET', path); + if (res.status >= 400) return []; + const items = Array.isArray(res.body) ? res.body : (res.body?.items ?? res.body?.data ?? []); + return items.filter((x) => typeof key(x) === 'string'); + }; + const fleets = (await mine(`/fleet/fleets?${LIST_PAGE}`, (f) => f?.name)).filter((f) => + f.name.startsWith(this.fleetPrefix), + ); + const fleetIds = new Set(fleets.map((f) => f.id)); + for (const tok of await mine(`/fleet/tokens?${LIST_PAGE}`, (t) => t?.id)) { + if (fleetIds.has(tok.fleet?.id) && !tok.revoked) { + await this.call('POST', `/fleet/tokens/${tok.id}/revoke`); + removed.tokens += 1; + } + } + for (const f of fleets) { + await this.call('DELETE', `/fleet/fleets/${f.id}`); + removed.fleets += 1; + } + for (const c of await mine(`/fleet/configs?${LIST_PAGE}`, (c) => c?.id)) { + if (c.id.startsWith(this.configPrefix)) { + await this.call('DELETE', `/fleet/configs/${c.id}`); + removed.configs += 1; + } + } + for (const u of await mine('/users', (u) => u?.email)) { + if (u.email.startsWith(this.fleetPrefix)) { + await this.call('DELETE', `/users/${u.email}`); + removed.users += 1; + } + } + const roles = await this.call('GET', '/roles'); + for (const r of roles.status < 400 && Array.isArray(roles.body) ? roles.body : []) { + if (typeof r === 'string' && r.startsWith(this.fleetPrefix)) { + await this.call('DELETE', `/roles/${r}`); + removed.roles += 1; + } + } + testLogger.info('Fleet teardown', { run: this.run, ...removed }); + return removed; + } + + // Anything still carrying this run's prefix after cleanup() is a leak; a failed list read fails loudly. + async survivors() { + const left = []; + for (const f of await this.listFleets()) { + if (f.name?.startsWith(this.fleetPrefix)) left.push(`fleet ${f.name}`); + } + const configs = await this.ok('GET', `/fleet/configs?${LIST_PAGE}`, undefined, 'list configs'); + for (const c of configs?.items ?? []) { + if (c.id?.startsWith(this.configPrefix)) left.push(`config ${c.id}`); + } + const roles = await this.call('GET', '/roles'); + if (roles.status < 400) { + for (const r of Array.isArray(roles.body) ? roles.body : []) { + if (typeof r === 'string' && r.startsWith(this.fleetPrefix)) left.push(`role ${r}`); + } + } + const users = await this.ok('GET', '/users', undefined, 'list users'); + for (const u of users?.data ?? []) { + if (u?.email?.startsWith(this.fleetPrefix)) left.push(`user ${u.email}`); + } + return left; + } +} + +module.exports = FleetSeed; +module.exports.FLEET_YAML = FLEET_YAML; +module.exports.FLEET_PREFIX = FLEET_PREFIX; +module.exports.CONFIG_PREFIX = CONFIG_PREFIX; diff --git a/tests/ui-testing/pages/iamPages/rolesPage.js b/tests/ui-testing/pages/iamPages/rolesPage.js index b7c3496554..f6ef415aaf 100644 --- a/tests/ui-testing/pages/iamPages/rolesPage.js +++ b/tests/ui-testing/pages/iamPages/rolesPage.js @@ -193,6 +193,17 @@ export class RolesPage { return [...new Set(slugs.map((s) => s.replace(RAIL_ITEM, '')))]; } + /** Rail module key → its label, read without the granted-count badge. */ + async railModuleLabels() { + const rows = await this.page + .locator(`[data-test^="${RAIL_ITEM}"]`) + .evaluateAll((els) => els.map((e) => [ + e.getAttribute('data-test'), + (e.querySelector('.o-tab__label')?.textContent ?? '').trim(), + ])); + return Object.fromEntries(rows.map(([slug, label]) => [slug.replace(RAIL_ITEM, ''), label])); + } + /** * Resource keys of the rows the pane is CURRENTLY showing — after any search or * filter. On the All Modules view a row is a whole module, so this is the set one diff --git a/tests/ui-testing/pages/page-manager.js b/tests/ui-testing/pages/page-manager.js index f349553a97..19ab633e88 100644 --- a/tests/ui-testing/pages/page-manager.js +++ b/tests/ui-testing/pages/page-manager.js @@ -145,6 +145,12 @@ const SchemaLoadPage = require("./generalPages/schemaLoadPage.js"); const APICleanup = require("./apiCleanup.js"); const WorkflowsPage = require("./workflowsPages/workflowsPage.js"); const WorkflowFoldersPage = require("./workflowsPages/workflowFoldersPage.js"); +const FleetManagerPage = require("./fleetPages/fleetManagerPage.js"); +const FleetFleetsPage = require("./fleetPages/fleetFleetsPage.js"); +const FleetInstallPage = require("./fleetPages/fleetInstallPage.js"); +const FleetConfigPage = require("./fleetPages/fleetConfigPage.js"); +const FleetRolloutPage = require("./fleetPages/fleetRolloutPage.js"); +const FleetSeed = require("./fleetPages/fleetSeed.js"); // ===== LOGS, REPORTS, STREAMS, PIPELINES ADDITIONAL PAGE OBJECTS ===== import { LogsQueryPage } from "./logsPages/logsQueryPage.js"; @@ -261,6 +267,14 @@ class PageManager { this.workflowsPage = new WorkflowsPage(page); this.workflowFoldersPage = new WorkflowFoldersPage(page); + // ===== FLEET MANAGEMENT PAGE OBJECTS ===== + this.fleetManagerPage = new FleetManagerPage(page); + this.fleetFleetsPage = new FleetFleetsPage(page); + this.fleetInstallPage = new FleetInstallPage(page); + this.fleetConfigPage = new FleetConfigPage(page); + this.fleetRolloutPage = new FleetRolloutPage(page); + this.fleetSeed = new FleetSeed(page); + // ===== SANITY SPEC ADDITIONAL PAGE OBJECTS ===== this.logsPage = new LogsPage(page); this.logsToolbarPinsPage = new LogsToolbarPinsPage(page); diff --git a/tests/ui-testing/playwright-tests/Fleet/fleet-access.spec.js b/tests/ui-testing/playwright-tests/Fleet/fleet-access.spec.js new file mode 100644 index 0000000000..fcfe500122 --- /dev/null +++ b/tests/ui-testing/playwright-tests/Fleet/fleet-access.spec.js @@ -0,0 +1,161 @@ +const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js'); +const testLogger = require('../utils/test-logger.js'); +const PageManager = require('../../pages/page-manager.js'); + +test.describe('Fleet management access', { tag: ['@fleet', '@enterprise', '@all'] }, () => { + // Serial: the roles, users and fleets are made once in beforeAll and shared, as in IAM/iam-roles-enforcement.spec.js. + test.describe.configure({ mode: 'serial' }); + + let pm; + let seed; + let fleetA; + let fleetB; + const members = {}; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + try { + const setup = new PageManager(page); + await setup.fleetManagerPage.assertEnabled(); + seed = setup.fleetSeed; + await seed.assertRbacEnabled(); + fleetA = await seed.createFleet('a'); + fleetB = await seed.createFleet('b'); + const reader = await seed.createRole('reader', [ + seed.orgGrant('fleets', 'AllowList'), + seed.orgGrant('fleets', 'AllowGet'), + seed.orgGrant('fleet_configs', 'AllowGet'), + seed.orgGrant('fleet_management', 'AllowGet'), + ]); + const aWriter = await seed.createRole('a_writer', [seed.fleetGrant(fleetA.id, 'AllowAll')]); + const listOnly = await seed.createRole('list_only', [seed.orgGrant('fleets', 'AllowList')]); + members.none = await seed.createMember('none'); + members.reader = await seed.createMember('reader', reader); + members.aWriter = await seed.createMember('a_writer', aWriter); + members.listOnly = await seed.createMember('list_only', listOnly); + testLogger.info('Fleet access fixtures ready', { fleetA: fleetA.id, fleetB: fleetB.id }); + } finally { + await page.close(); + } + }); + + test.afterAll(async ({ browser }) => { + if (!seed) return; + const page = await browser.newPage(); + try { + seed.setPage(page); + await seed.cleanup(); + expect(await seed.survivors()).toEqual([]); + } finally { + await page.close(); + } + }); + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + await navigateToBase(page); + pm = new PageManager(page); + // The beforeAll page that seeded the fixtures is closed, so seed API calls need this test's live page. + seed.setPage(page); + }); + + test('FL-RBAC-02 + R-16105a · a member without fleet grants gets the no-access states and no pipelines error banner', { + tag: ['@fleetAccess', '@regression', '@P0'], + }, async ({ browser }) => { + const member = await seed.loginAsMember(browser, members.none); + try { + const mpm = new PageManager(member.page); + await mpm.fleetManagerPage.gotoFleet({ tab: 'fleets' }); + await mpm.fleetManagerPage.expectFleetNoAccess(); + await mpm.fleetManagerPage.expectCollectorsForbidden(); + + await mpm.fleetManagerPage.clickTab('overview'); + await mpm.fleetManagerPage.expectOverviewNoFleetsWithoutErrorBanner(); + } finally { + await member.context.close(); + } + }); + + test('FL-RBAC-03 + R-16105b · a reader clicking New fleet gets "You don\'t have permission to do this" and nothing is created', { + tag: ['@fleetAccess', '@regression', '@P0'], + }, async ({ browser }) => { + const member = await seed.loginAsMember(browser, members.reader); + try { + await seed.waitForMemberStatus(member.page, `/fleet/fleets/${fleetA.id}`, 200); + const mpm = new PageManager(member.page); + const refused = seed.fleetName('refused'); + await mpm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleetA.id }); + await mpm.fleetFleetsPage.expectPanelFor(fleetA.name); + await mpm.fleetManagerPage.clickCreate('fleets'); + await mpm.fleetFleetsPage.submitNewFleet(refused); + await mpm.fleetManagerPage.expectErrorToast("You don't have permission to do this"); + await seed.expectNoFleetNamed(refused); + } finally { + await member.context.close(); + } + }); + + test('R-3232-ui · a writer of fleet A only can open the Fleets tab and read fleet A, not fleet B', { + tag: ['@fleetAccess', '@regression', '@P0'], + }, async ({ browser }) => { + const member = await seed.loginAsMember(browser, members.aWriter); + try { + await seed.waitForMemberStatus(member.page, `/fleet/fleets/${fleetA.id}`, 200); + const mpm = new PageManager(member.page); + await mpm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleetA.id }); + await mpm.fleetFleetsPage.expectPanelFor(fleetA.name); + await mpm.fleetManagerPage.expectCollectorsNotForbidden(); + await mpm.fleetFleetsPage.expectRailItemReadable(fleetA.id, true); + await mpm.fleetFleetsPage.expectRailItemReadable(fleetB.id, false); + + await mpm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleetB.id }); + await mpm.fleetManagerPage.expectFleetNoAccess(); + } finally { + await member.context.close(); + } + }); + + test('FL-RBAC-08 · a member without GET on a fleet sees "You can see this fleet, but not its collectors or versions"', { + tag: ['@fleetAccess', '@P2'], + }, async ({ browser }) => { + // With O2_OPENFGA_LIST_ONLY_PERMITTED off (this shard), LIST adds nothing over no grant; this proves the no-GET panel only. + const member = await seed.loginAsMember(browser, members.listOnly); + try { + const mpm = new PageManager(member.page); + await mpm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleetA.id }); + await mpm.fleetManagerPage.expectFleetNoAccess('You can see this fleet, but not its collectors or versions'); + await mpm.fleetFleetsPage.expectRailItemReadable(fleetA.id, false); + } finally { + await member.context.close(); + } + }); + + test('R-16104c · the Settings tab has no "Alerts for fleets" section', { + tag: ['@fleetAccess', '@regression', '@P2'], + }, async () => { + await pm.fleetManagerPage.gotoFleet({ tab: 'settings' }); + await pm.fleetManagerPage.expectTabActive('settings'); + await pm.fleetManagerPage.expectSettingsWithoutFleetAlerts(['Alerts for fleets', 'Open the alert library']); + }); + + test('R-16105c · collectors "Reset to default" keeps Architecture and Last connected from hidden', { + tag: ['@fleetAccess', '@regression', '@P3'], + }, async () => { + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleetA.id }); + await pm.fleetFleetsPage.expectPanelFor(fleetA.name); + await pm.fleetManagerPage.expectColumnChecked('arch', false); + await pm.fleetManagerPage.expectColumnChecked('lastConnectedFrom', false); + + await pm.fleetManagerPage.toggleColumn('os'); + await pm.fleetManagerPage.expectColumnChecked('os', false); + await pm.fleetManagerPage.expectColumnShown('os', false); + + await pm.fleetManagerPage.resetColumns(); + await pm.fleetManagerPage.expectColumnChecked('os', true); + await pm.fleetManagerPage.expectColumnChecked('arch', false); + await pm.fleetManagerPage.expectColumnChecked('lastConnectedFrom', false); + await pm.fleetManagerPage.expectColumnShown('os', true); + await pm.fleetManagerPage.expectColumnShown('arch', false); + await pm.fleetManagerPage.expectColumnShown('lastConnectedFrom', false); + }); +}); diff --git a/tests/ui-testing/playwright-tests/Fleet/fleet-config-deploy.spec.js b/tests/ui-testing/playwright-tests/Fleet/fleet-config-deploy.spec.js new file mode 100644 index 0000000000..163b9ba2dc --- /dev/null +++ b/tests/ui-testing/playwright-tests/Fleet/fleet-config-deploy.spec.js @@ -0,0 +1,174 @@ +const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js'); +const testLogger = require('../utils/test-logger.js'); +const PageManager = require('../../pages/page-manager.js'); +const { FLEET_YAML } = require('../../pages/fleetPages/fleetSeed.js'); + +const DOTTED_KEY = 'system.resource_attributes."host.id".enabled'; + +test.describe('Fleet management configurations and deploy', { tag: ['@fleet', '@enterprise', '@all'] }, () => { + test.describe.configure({ mode: 'parallel' }); + + let pm; + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + // Cleared first so a failed navigation never leaves afterEach the previous test's closed page. + pm = undefined; + await navigateToBase(page); + pm = new PageManager(page); + await pm.fleetManagerPage.assertEnabled(); + }); + + test.afterEach(async () => { + if (!pm) return; + await pm.fleetSeed.cleanup(); + expect(await pm.fleetSeed.survivors()).toEqual([]); + }); + + test('FL-CFG-03 + FL-CFG-08 · palette OTLP, Batch, OpenObserve and Memory limiter wire a pipeline with memory_limiter first and env references', { + tag: ['@fleetConfig', '@smoke', '@P0'], + }, async () => { + const config = await pm.fleetSeed.createConfig('palette', FLEET_YAML.seed); + await pm.fleetConfigPage.goto(config); + await pm.fleetConfigPage.expectVisualMode(); + + await pm.fleetConfigPage.addFromPalette('receiver', 'otlp', 'logs|receiver:otlp'); + await pm.fleetConfigPage.addFromPalette('processor', 'batch', 'logs|processor:batch'); + await pm.fleetConfigPage.addFromPalette('exporter', 'openobserve', 'logs|exporter:otlphttp/openobserve'); + await pm.fleetConfigPage.addFromPalette('processor', 'memory_limiter', 'logs|processor:memory_limiter'); + + await pm.fleetConfigPage.switchToYaml(); + await pm.fleetConfigPage.expectYaml(FLEET_YAML.paletteResult); + const yaml = await pm.fleetConfigPage.readYaml(); + expect(yaml).toContain('processors: [memory_limiter, batch]'); + expect(yaml).toContain('endpoint: "${env:O2_ENDPOINT}"'); + expect(yaml).toContain('Authorization: "${env:O2_TOKEN}"'); + }); + + test('FL-CFG-04 + FL-CFG-05 · a YAML edit round-trips through Visual byte for byte and a dotted key stays one key', { + tag: ['@fleetConfig', '@regression', '@P1'], + }, async () => { + const config = await pm.fleetSeed.createConfig('roundtrip', FLEET_YAML.seed); + await pm.fleetConfigPage.goto(config); + await pm.fleetConfigPage.switchToYaml(); + await pm.fleetConfigPage.setYaml(FLEET_YAML.dottedKey); + + await pm.fleetConfigPage.switchToVisual(); + await pm.fleetConfigPage.selectNode('logs|processor:resourcedetection'); + await pm.fleetConfigPage.expectPropertyValue(DOTTED_KEY, 'true'); + await pm.fleetConfigPage.expectNoProperty('system.resource_attributes.host.id.enabled'); + await pm.fleetConfigPage.setProperty(DOTTED_KEY, 'false'); + + await pm.fleetConfigPage.switchToYaml(); + await pm.fleetConfigPage.expectYaml(FLEET_YAML.dottedKey.replace('enabled: true', 'enabled: false')); + }); + + test('R-16104a · Visual keeps "true", "123" and "null" as strings, and YAML with a quoted number opens in Visual', { + tag: ['@fleetConfig', '@regression', '@P1'], + }, async () => { + const config = await pm.fleetSeed.createConfig('quoted', FLEET_YAML.seed); + await pm.fleetConfigPage.goto(config); + await pm.fleetConfigPage.switchToYaml(); + await pm.fleetConfigPage.setYaml(FLEET_YAML.quotedNumber); + + await pm.fleetConfigPage.switchToVisual(); + await pm.fleetConfigPage.selectNode('logs|exporter:debug'); + await pm.fleetConfigPage.expectPropertyValue('verbosity', '"123"'); + await pm.fleetConfigPage.addSetting('fl_true', '"true"'); + await pm.fleetConfigPage.addSetting('fl_num', '"123"'); + await pm.fleetConfigPage.addSetting('fl_null', '"null"'); + + await pm.fleetConfigPage.switchToYaml(); + await pm.fleetConfigPage.expectYaml( + FLEET_YAML.quotedNumber.replace( + ' verbosity: "123"\n', + ' verbosity: "123"\n fl_true: "true"\n fl_num: "123"\n fl_null: "null"\n', + ), + ); + }); + + test('FL-CFG-09 · invalid YAML validates as "1 error · publish blocked" and Publish is refused', { + tag: ['@fleetConfig', '@smoke', '@P0'], + }, async () => { + const config = await pm.fleetSeed.createConfig('invalid', FLEET_YAML.seed); + await pm.fleetConfigPage.goto(config); + await pm.fleetConfigPage.switchToYaml(); + await pm.fleetConfigPage.setYaml(FLEET_YAML.broken); + await pm.fleetConfigPage.expectVisualUnavailable(); + + await pm.fleetConfigPage.validate(); + await pm.fleetConfigPage.expectValidationChip('1 error · publish blocked'); + + await pm.fleetConfigPage.publish('fleet automation invalid'); + await pm.fleetManagerPage.expectAnyErrorToast(); + await pm.fleetConfigPage.expectModeChip('Editing draft'); + expect((await pm.fleetSeed.getConfig(config)).latest_version).toBe(0); + }); + + test('FL-VER-01 + FL-VER-02 · publishing reads "published v1 · not rolled out"; after v2 Compare shows the diff and is off for v1', { + tag: ['@fleetConfig', '@P1'], + }, async () => { + const config = await pm.fleetSeed.createConfig('versions', FLEET_YAML.publishable); + await pm.fleetConfigPage.goto(config); + await pm.fleetConfigPage.expectModeChip('Editing draft'); + + await pm.fleetConfigPage.publish('first version'); + await pm.fleetConfigPage.expectModeChip('Viewing published v1 · not rolled out'); + await pm.fleetConfigPage.expectSuccessToast('Published v1'); + + await pm.fleetConfigPage.switchToYaml(); + await pm.fleetConfigPage.setYaml(FLEET_YAML.publishable.replace('https://example.com', 'https://example.org')); + await pm.fleetConfigPage.expectModeChip('Editing draft'); + await pm.fleetConfigPage.publish('second version'); + await pm.fleetConfigPage.expectModeChip('Viewing published v2 · not rolled out'); + + await pm.fleetConfigPage.openHistory(); + await pm.fleetConfigPage.expectCompareDisabled(1); + await pm.fleetConfigPage.compare(2); + await pm.fleetConfigPage.expectDiffContains('https://example.org'); + expect((await pm.fleetSeed.getConfig(config)).latest_version).toBe(2); + }); + + test('R-16104b · Deploy from the editor opens with no fleet picked and is blocked with "Pick a fleet"', { + tag: ['@fleetDeploy', '@regression', '@P1'], + }, async () => { + await pm.fleetSeed.createFleet('pick'); + const config = await pm.fleetSeed.createConfig('deploy', FLEET_YAML.publishable); + await pm.fleetSeed.publishConfig(config, FLEET_YAML.publishable); + await pm.fleetConfigPage.goto(config); + + await pm.fleetConfigPage.openDeployFromEditor(); + await pm.fleetRolloutPage.expectReady(); + await pm.fleetRolloutPage.expectNoFleetPicked(); + await pm.fleetRolloutPage.submit(); + await pm.fleetRolloutPage.expectFleetError('Pick a fleet'); + expect(await pm.fleetSeed.rolloutsForConfig(config)).toEqual([]); + }); + + test('FL-RO-01 · the deploy dialog disables Scheduled for an empty fleet and refuses counts below 1 and a max below the initial batch', { + tag: ['@fleetDeploy', '@P1'], + }, async () => { + const fleet = await pm.fleetSeed.createFleet('guards'); + const config = await pm.fleetSeed.createConfig('guards', FLEET_YAML.publishable); + const v1 = await pm.fleetSeed.publishConfig(config, FLEET_YAML.publishable); + const seeded = await pm.fleetSeed.setDesired(fleet.id, config, v1); + await pm.fleetSeed.waitForRolloutStatus(seeded.id, 'completed'); + // A desired v1 plus a published v2 seeds a valid config and version, so only the batching fields are judged. + await pm.fleetSeed.publishConfig(config, FLEET_YAML.publishable.replace('https://example.com', 'https://example.org')); + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleet.id }); + await pm.fleetFleetsPage.openDeploy(); + await pm.fleetRolloutPage.expectReady(); + await pm.fleetRolloutPage.expectScheduledDisabled(); + + await pm.fleetRolloutPage.chooseBatching('canary'); + await pm.fleetRolloutPage.setCanaryFirst(0); + await pm.fleetRolloutPage.submit(); + await pm.fleetRolloutPage.expectFirstError('Enter a whole number of 1 or more'); + + await pm.fleetRolloutPage.chooseBatching('progressive'); + await pm.fleetRolloutPage.setProgressive(5, 2); + await pm.fleetRolloutPage.submit(); + await pm.fleetRolloutPage.expectMaxError('Use at least the initial batch'); + expect((await pm.fleetSeed.rolloutsForConfig(config)).map((r) => r.id)).toEqual([seeded.id]); + }); +}); diff --git a/tests/ui-testing/playwright-tests/Fleet/fleet-install.spec.js b/tests/ui-testing/playwright-tests/Fleet/fleet-install.spec.js new file mode 100644 index 0000000000..8daabb930d --- /dev/null +++ b/tests/ui-testing/playwright-tests/Fleet/fleet-install.spec.js @@ -0,0 +1,151 @@ +const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js'); +const testLogger = require('../utils/test-logger.js'); +const PageManager = require('../../pages/page-manager.js'); +const FleetInstallPage = require('../../pages/fleetPages/fleetInstallPage.js'); + +const PLATFORMS = [ + { id: 'FL-INST-03', label: 'Linux', platform: 'linux' }, + { id: 'FL-INST-04', label: 'macOS', platform: 'macos' }, + { id: 'FL-INST-05', label: 'Windows', platform: 'windows' }, + { id: 'FL-INST-06', label: 'Docker', platform: 'docker' }, + { id: 'FL-INST-07', label: 'Kubernetes agent', platform: 'kubernetes', role: 'agent' }, + { id: 'FL-INST-08', label: 'Kubernetes gateway', platform: 'kubernetes', role: 'gateway' }, +]; + +test.describe('Fleet management Add collector', { tag: ['@fleet', '@enterprise', '@all'] }, () => { + test.describe.configure({ mode: 'parallel' }); + + let pm; + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + // Cleared first so a failed navigation never leaves afterEach the previous test's closed page. + pm = undefined; + await navigateToBase(page); + pm = new PageManager(page); + await pm.fleetManagerPage.assertEnabled(); + }); + + test.afterEach(async () => { + if (!pm) return; + await pm.fleetSeed.cleanup(); + expect(await pm.fleetSeed.survivors()).toEqual([]); + }); + + for (const p of PLATFORMS) { + test(`${p.id} + FL-INST-09 + FL-UNI-02 · ${p.label}: every install line over HTTP and WebSocket, uninstall without the token`, { + tag: ['@fleetInstall', '@regression', '@P0'], + }, async () => { + const fleet = await pm.fleetSeed.createFleet(`inst_${p.platform}_${p.role ?? 'host'}`); + const token = await pm.fleetSeed.createToken(fleet.id, 'tok'); + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleet.id }); + await pm.fleetFleetsPage.openAddCollector(); + await pm.fleetInstallPage.expectDialogOpen(); + await pm.fleetInstallPage.expectCommandShown(); + + await pm.fleetInstallPage.selectPlatform(p.platform); + if (p.role) await pm.fleetInstallPage.selectRole(p.role); + const target = { ...(await pm.fleetInstallPage.installTarget(pm.fleetSeed, token.token)), role: p.role }; + await pm.fleetInstallPage.revealToken(); + + await pm.fleetInstallPage.expectWebsocket(false); + await pm.fleetInstallPage.expectCommandLines( + FleetInstallPage.expectedInstallLines(p.platform, { ...target, http: true }), + ); + + await pm.fleetInstallPage.setWebsocket(true); + await pm.fleetInstallPage.expectCommandLines( + FleetInstallPage.expectedInstallLines(p.platform, { ...target, http: false }), + ); + + await pm.fleetInstallPage.expandUninstall(); + await pm.fleetInstallPage.expectUninstallLines( + FleetInstallPage.expectedUninstallLines(p.platform, p.role), + token.token, + ); + }); + } + + test('R-16042 · a Kubernetes cluster name becomes --set-string clusterName with commas escaped; empty adds no flag', { + tag: ['@fleetInstall', '@regression', '@P2'], + }, async () => { + const fleet = await pm.fleetSeed.createFleet('cluster'); + const token = await pm.fleetSeed.createToken(fleet.id, 'tok'); + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleet.id }); + await pm.fleetFleetsPage.openAddCollector(); + await pm.fleetInstallPage.expectCommandShown(); + await pm.fleetInstallPage.selectPlatform('kubernetes'); + const target = { ...(await pm.fleetInstallPage.installTarget(pm.fleetSeed, token.token)), http: true, role: 'agent' }; + await pm.fleetInstallPage.revealToken(); + + await pm.fleetInstallPage.setClusterName('prod,us-east'); + const withName = FleetInstallPage.expectedInstallLines('kubernetes', { ...target, clusterName: 'prod,us-east' }); + expect(withName).toContain(" --set-string clusterName='prod\\,us-east' \\"); + await pm.fleetInstallPage.expectCommandLines(withName); + + await pm.fleetInstallPage.setClusterName(''); + const withoutName = FleetInstallPage.expectedInstallLines('kubernetes', target); + expect(withoutName.some((line) => line.includes('clusterName'))).toBe(false); + await pm.fleetInstallPage.expectCommandLines(withoutName); + }); + + test('FL-INST-10 · the token stays masked until Reveal, and Copy gives the full command with the real token', { + tag: ['@fleetInstall', '@P1'], + }, async () => { + const fleet = await pm.fleetSeed.createFleet('mask'); + const token = await pm.fleetSeed.createToken(fleet.id, 'tok'); + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleet.id }); + await pm.fleetFleetsPage.openAddCollector(); + await pm.fleetInstallPage.expectCommandShown(); + const target = { ...(await pm.fleetInstallPage.installTarget(pm.fleetSeed, token.token)), http: true }; + + await pm.fleetInstallPage.expectCommandMasked(token.token); + const copied = await pm.fleetInstallPage.copyCommand(); + expect(copied.split('\n')).toEqual(FleetInstallPage.expectedInstallLines('linux', target)); + + await pm.fleetInstallPage.revealToken(); + await pm.fleetInstallPage.expectCommandLines(FleetInstallPage.expectedInstallLines('linux', target)); + }); + + test('FL-INST-01 + R-16103 · with no token the inline form creates one, shows the command and polls collectors again', { + tag: ['@fleetInstall', '@regression', '@P0'], + }, async () => { + const fleet = await pm.fleetSeed.createFleet('notoken'); + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleet.id }); + await pm.fleetFleetsPage.openAddCollector(); + await pm.fleetInstallPage.expectNoTokenState(); + + const created = await pm.fleetInstallPage.createInlineTokenAndAwaitCollectorsPoll(pm.fleetSeed.fleetName('inline')); + expect(created.token).toMatch(/^o2ft_/); + expect(created.fleet?.id).toBe(fleet.id); + + await pm.fleetInstallPage.expectCommandShown(); + await pm.fleetInstallPage.expectCopyOnceAndWaiting(); + await pm.fleetInstallPage.expectCommandMasked(created.token); + }); + + test('FL-INST-02 + FL-INST-11 · the token select lists "{name} (no collectors)" newest first, says which fleet it joins, and --url is the ingestion URL', { + tag: ['@fleetInstall', '@P1'], + }, async () => { + const fleet = await pm.fleetSeed.createFleet('pick'); + const older = await pm.fleetSeed.createToken(fleet.id, 'older'); + const newer = await pm.fleetSeed.createToken(fleet.id, 'newer'); + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleet.id }); + await pm.fleetFleetsPage.openAddCollector(); + await pm.fleetInstallPage.expectCommandShown(); + + await pm.fleetInstallPage.expectSelectedToken(`${newer.name} (no collectors)`); + expect(await pm.fleetInstallPage.tokenOptionLabels()).toEqual([ + `${newer.name} (no collectors)`, + `${older.name} (no collectors)`, + ]); + await pm.fleetInstallPage.expectJoinsText( + `Collectors installed with this token join fleet ${fleet.name}, which has no configuration yet.`, + ); + + const target = { ...(await pm.fleetInstallPage.installTarget(pm.fleetSeed, newer.token)), http: true }; + await pm.fleetInstallPage.revealToken(); + const lines = FleetInstallPage.expectedInstallLines('linux', target); + await pm.fleetInstallPage.expectCommandLines(lines); + }); +}); diff --git a/tests/ui-testing/playwright-tests/Fleet/fleet-navigation.spec.js b/tests/ui-testing/playwright-tests/Fleet/fleet-navigation.spec.js new file mode 100644 index 0000000000..5085bf531b --- /dev/null +++ b/tests/ui-testing/playwright-tests/Fleet/fleet-navigation.spec.js @@ -0,0 +1,94 @@ +const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js'); +const testLogger = require('../utils/test-logger.js'); +const PageManager = require('../../pages/page-manager.js'); +const { FLEET_YAML } = require('../../pages/fleetPages/fleetSeed.js'); + +test.describe('Fleet management navigation', { tag: ['@fleet', '@enterprise', '@all'] }, () => { + test.describe.configure({ mode: 'parallel' }); + + let pm; + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + // Cleared first so a failed navigation never leaves afterEach the previous test's closed page. + pm = undefined; + await navigateToBase(page); + pm = new PageManager(page); + await pm.fleetManagerPage.assertEnabled(); + }); + + test.afterEach(async () => { + if (!pm) return; + await pm.fleetSeed.cleanup(); + expect(await pm.fleetSeed.survivors()).toEqual([]); + }); + + test('FL-GATE-01 · Data → Fleet management opens /web/fleet on the Overview', { + tag: ['@fleetNav', '@smoke', '@P0'], + }, async () => { + await pm.fleetManagerPage.openFromNav(); + await pm.fleetManagerPage.expectOnFleetRoute({ org_identifier: pm.fleetSeed.orgId() }); + await pm.fleetManagerPage.expectOverviewVisible(); + }); + + test('FL-NAV-01 · tabs set ?tab= by replace and keep org_identifier, so Back leaves the page', { + tag: ['@fleetNav', '@P1'], + }, async () => { + const org = pm.fleetSeed.orgId(); + await pm.fleetManagerPage.gotoFleet(); + await pm.fleetManagerPage.expectTabActive('overview'); + + await pm.fleetManagerPage.clickTab('fleets'); + await pm.fleetManagerPage.expectOnFleetRoute({ tab: 'fleets', org_identifier: org }); + await pm.fleetManagerPage.clickTab('configurations'); + await pm.fleetManagerPage.expectOnFleetRoute({ tab: 'configurations', org_identifier: org }); + await pm.fleetManagerPage.clickTab('audit'); + await pm.fleetManagerPage.expectOnFleetRoute({ tab: 'audit', org_identifier: org }); + + await pm.fleetManagerPage.goBackAndExpectLeftFleet(); + }); + + test('FL-NAV-04 · an unknown ?fleet= says the fleet no longer exists', { + tag: ['@fleetNav', '@P2'], + }, async () => { + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: pm.fleetSeed.fleetName('missing') }); + await pm.fleetManagerPage.expectFleetNotFound('This fleet no longer exists'); + }); + + test('FL-NAV-05 · ?scope=all&status=offline shows the Fleet column, Offline pressed and no fleet panel', { + tag: ['@fleetNav', '@P1'], + }, async () => { + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', scope: 'all', status: 'offline' }); + await pm.fleetManagerPage.expectAllFleetsScope(); + await pm.fleetManagerPage.expectStatPressed('offline'); + await pm.fleetManagerPage.expectFleetPanelHidden(); + }); + + test('FL-NAV-08 · ?rollout= of fleet B with ?fleet=A opens that rollout and switches to fleet B', { + tag: ['@fleetNav', '@P1'], + }, async () => { + const fleetA = await pm.fleetSeed.createFleet('a'); + const fleetB = await pm.fleetSeed.createFleet('b'); + const config = await pm.fleetSeed.createConfig('nav', FLEET_YAML.publishable); + const version = await pm.fleetSeed.publishConfig(config, FLEET_YAML.publishable); + const rollout = await pm.fleetSeed.setDesired(fleetB.id, config, version); + await pm.fleetSeed.waitForRolloutStatus(rollout.id, 'completed'); + + await pm.fleetManagerPage.gotoFleet({ tab: 'fleets', fleet: fleetA.id, rollout: rollout.id }); + await pm.fleetManagerPage.expectQueryParam('fleet', fleetB.id); + await pm.fleetFleetsPage.expectRailItemActive(fleetB.id); + await pm.fleetFleetsPage.expectRolloutDrawerStatus('No collectors targeted'); + }); + + test('FL-NAV-10 · the config editor deep link opens the editor and Back returns to ?tab=configurations', { + tag: ['@fleetNav', '@P2'], + }, async () => { + const config = await pm.fleetSeed.createConfig('deeplink', FLEET_YAML.seed); + await pm.fleetConfigPage.goto(config); + await pm.fleetConfigPage.expectTitle(config); + + await pm.fleetConfigPage.clickBack(); + await pm.fleetManagerPage.expectOnFleetRoute({ tab: 'configurations', org_identifier: pm.fleetSeed.orgId() }); + await pm.fleetManagerPage.expectTabActive('configurations'); + }); +}); diff --git a/tests/ui-testing/playwright-tests/IAM/iam-roles-editor.spec.js b/tests/ui-testing/playwright-tests/IAM/iam-roles-editor.spec.js index 6f121c87ea..c1349f7127 100644 --- a/tests/ui-testing/playwright-tests/IAM/iam-roles-editor.spec.js +++ b/tests/ui-testing/playwright-tests/IAM/iam-roles-editor.spec.js @@ -373,4 +373,19 @@ test.describe('IAM · Edit Role · navigation and filtering', { tag: '@enterpris expect(pairs.length, 'JSON view rendered no grants').toBeGreaterThan(0); expect(pairs).toEqual(stored.map((p) => `${p.object}|${p.permission}`).sort()); }); + + // FL-RBAC-01 · guards openobserve#15949: the no-op "Fleet Manager" permission must not return. + test('U-23 · the Fleet group lists Fleets, Fleet configurations and Fleet management, and no Fleet Manager', { + tag: ['@iam', '@iamRolesEditor', '@fleet', '@regression', '@P0', '@all'] + }, async () => { + await pm.fleetManagerPage.assertEnabled(); + const labels = await pm.rolesPage.railModuleLabels(); + expect(labels).toMatchObject({ + fleets: 'Fleets', + fleet_configs: 'Fleet configurations', + fleet_management: 'Fleet management', + }); + expect(Object.keys(labels)).not.toContain('fleet'); + expect(Object.values(labels)).not.toContain('Fleet Manager'); + }); }); diff --git a/tests/ui-testing/playwright-tests/cleanup.spec.js b/tests/ui-testing/playwright-tests/cleanup.spec.js index d3bd7ffce7..c911ab610a 100644 --- a/tests/ui-testing/playwright-tests/cleanup.spec.js +++ b/tests/ui-testing/playwright-tests/cleanup.spec.js @@ -494,6 +494,9 @@ test.describe("Pre-Test Cleanup", () => { // quietly rather than failing the cleanup every other suite depends on. await pm.apiCleanup.cleanupIamArtifacts('ui_auto'); + // Fleet management leftovers (Fleet/*.spec.js); enterprise-only, quiet on OSS. + await pm.apiCleanup.cleanupFleetArtifacts(['fl_auto_'], ['fl-auto-']); + testLogger.info('Pre-test cleanup completed successfully'); }); }); diff --git a/web/coverage.sh b/web/coverage.sh index 0abf398dae..fad39eff01 100755 --- a/web/coverage.sh +++ b/web/coverage.sh @@ -12,6 +12,7 @@ usage() { Usage: $0 [-h | --help] [] Commands: + merge Merge sharded blob reports and verify thresholds check Run frontend tests with coverage and verify thresholds html Generate coverage report in HTML format show-env Show the coverage thresholds @@ -50,7 +51,16 @@ EOF cmd_check() { echo "Running frontend tests with coverage..." npm run test:unit:coverage - + check_thresholds +} + +cmd_merge() { + echo "Merging shard reports from .vitest-reports..." + npx vitest --merge-reports --coverage --run + check_thresholds +} + +check_thresholds() { if [ -f "coverage/coverage-summary.json" ]; then echo "Checking coverage thresholds..." python3 <( @@ -119,6 +129,10 @@ main() { shift cmd_test "$@" ;; + merge) + shift + cmd_merge "$@" + ;; html) shift cmd_html "$@" diff --git a/web/package-lock.json b/web/package-lock.json index 580b8c39f1..ab4b434df0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -115,6 +115,7 @@ "eslint-plugin-cypress": "^5.2.0", "eslint-plugin-vue": "^10.11.1", "eslint-plugin-vuejs-accessibility": "2.6.0", + "flatted": "^3.4.2", "fs-extra": "^11.4.1", "happy-dom": "^21.0.4", "jsdom": "^27.4.0", diff --git a/web/package.json b/web/package.json index 44bac94c96..33521acbe1 100644 --- a/web/package.json +++ b/web/package.json @@ -148,6 +148,7 @@ "eslint-plugin-cypress": "^5.2.0", "eslint-plugin-vue": "^10.11.1", "eslint-plugin-vuejs-accessibility": "2.6.0", + "flatted": "^3.4.2", "fs-extra": "^11.4.1", "happy-dom": "^21.0.4", "jsdom": "^27.4.0", diff --git a/web/scripts/ui-shard-durations.json b/web/scripts/ui-shard-durations.json new file mode 100644 index 0000000000..620e8bd558 --- /dev/null +++ b/web/scripts/ui-shard-durations.json @@ -0,0 +1,2641 @@ +{"src/App.spec.ts":5.5, +"src/assets/dashboards/host_metrics.dashboard.spec.ts":2.4, +"src/assets/dashboards/nvidia_gpu.dashboard.spec.ts":0.8, +"src/components/AIObservability/textDiff.spec.ts":1.1, +"src/components/AttributeValueCell.spec.ts":2.2, +"src/components/AutoRefreshInterval.spec.ts":4.2, +"src/components/CodeQueryEditor.changeOrigin.spec.ts":2.3, +"src/components/CodeQueryEditor.completion.spec.ts":29.1, +"src/components/CodeQueryEditor.monacoLoad.spec.ts":3.7, +"src/components/CodeQueryEditor.spec.ts":4.4, +"src/components/CommunitySlackInvite.spec.ts":3.6, +"src/components/ConfirmDialog.spec.ts":1.4, +"src/components/ConfirmDialogProvider.spec.ts":1.6, +"src/components/ConnectDataSourcePopup.spec.ts":2.9, +"src/components/CopyContent.spec.ts":3.1, +"src/components/CustomDateTimePicker.spec.ts":2.8, +"src/components/DatabaseDeprecationBanner.spec.ts":1.5, +"src/components/DateTime.paste.spec.ts":6.2, +"src/components/DateTime.rail.spec.ts":14, +"src/components/DateTime.spec.ts":3.1, +"src/components/DateTimePickerDashboard.spec.ts":6.7, +"src/components/EnterpriseUpgradeDialog.spec.ts":3.9, +"src/components/Header.spec.ts":8.9, +"src/components/JsonPreview.spec.ts":2.7, +"src/components/MenuLink.spec.ts":4.1, +"src/components/O2AIChat.content.spec.ts":1.2, +"src/components/O2AIChat.framing.spec.ts":0.9, +"src/components/O2AIChat.history.spec.ts":1.7, +"src/components/O2AIChat.keyboard.spec.ts":6, +"src/components/O2AIChat.navigation.spec.ts":0.7, +"src/components/O2AIChat.reducer.spec.ts":0.8, +"src/components/O2AIChat.session.spec.ts":16.3, +"src/components/O2AIChat.spec.js":6.3, +"src/components/O2AIChat.spec.ts":6.5, +"src/components/O2AIChat.stream.spec.ts":23.1, +"src/components/O2AIChat.toolcall.spec.ts":1.8, +"src/components/O2AIConfirmDialog.spec.ts":1.7, +"src/components/OrganizationSelector.spec.ts":1.2, +"src/components/PaidOverageConsentDialog.spec.ts":1.3, +"src/components/PredefinedThemes.spec.ts":3.6, +"src/components/QueryEditor.spec.ts":3, +"src/components/QueryPlanDialog.spec.ts":7.5, +"src/components/ResumePipelineDialog.spec.ts":3.3, +"src/components/RichTextInput.spec.ts":1.5, +"src/components/SanitizedHtmlRenderer.spec.ts":0.7, +"src/components/ThemeSwitcher.spec.ts":2.9, +"src/components/about/FeatureComparisonTable.spec.ts":1.8, +"src/components/ai-assistant/chat/O2AIChatHistoryMenu.spec.ts":1.9, +"src/components/ai-assistant/chat/O2AIChatInput.spec.ts":2.9, +"src/components/ai-assistant/chat/O2AIChatMessage.spec.ts":1.5, +"src/components/ai-assistant/chat/O2AIChatToolCallBlock.spec.ts":1.7, +"src/components/ai-assistant/chat/O2AIChatToolCallIndicator.spec.ts":1.3, +"src/components/ai-assistant/chat/O2AIChatTranscript.spec.ts":1.6, +"src/components/ai-assistant/chat/O2AICreditsNotice.spec.ts":1.6, +"src/components/ai-assistant/share/AiChatShareRow.spec.ts":2.5, +"src/components/ai-assistant/share/AiChatSharedByMe.spec.ts":4.2, +"src/components/ai-assistant/share/chatShare.spec.ts":2.1, +"src/components/ai-assistant/share/useChatShareDialog.spec.ts":1.9, +"src/components/ai_toolsets/AddAiToolset.drafts.spec.ts":15.8, +"src/components/ai_toolsets/AddAiToolset.schema.spec.ts":0.8, +"src/components/ai_toolsets/AddAiToolset.spec.ts":4.4, +"src/components/alertLibrary/LibraryCard.spec.ts":2.8, +"src/components/alertLibrary/LibraryDrawer.spec.ts":5.9, +"src/components/alertLibrary/LibraryEmptyState.spec.ts":1.9, +"src/components/alertLibrary/LibraryInstallDialog.spec.ts":11.5, +"src/components/alertLibrary/LibraryRail.spec.ts":2.2, +"src/components/alertLibrary/libraryFacets.spec.ts":1.8, +"src/components/alertLibrary/libraryInstall.spec.ts":2.8, +"src/components/alertLibrary/libraryTunables.spec.ts":1, +"src/components/alert_calibration/AlertCalibrationDashboard.spec.ts":5.4, +"src/components/alert_calibration/AlertCalibrationDigests.spec.ts":9.2, +"src/components/alert_calibration/AlertCalibrationFindingCard.spec.ts":4.5, +"src/components/alert_calibration/alertCalibrationSettings.spec.ts":1.4, +"src/components/alert_calibration/digests.spec.ts":1.5, +"src/components/alerts/AckChip.spec.ts":4.2, +"src/components/alerts/AddAlert.anomalyEdit.spec.ts":9.6, +"src/components/alerts/AddAlert.drafts.spec.ts":15.2, +"src/components/alerts/AddAlert.panelPrefill.spec.ts":5.7, +"src/components/alerts/AddAlert.prefill.spec.ts":16.3, +"src/components/alerts/AddAlert.schema.spec.ts":1.8, +"src/components/alerts/AddAlert.spec.ts":21.2, +"src/components/alerts/AddDestination.drafts.spec.ts":9.6, +"src/components/alerts/AddDestination.schema.spec.ts":2.4, +"src/components/alerts/AddDestination.slackAppReview.spec.ts":4.9, +"src/components/alerts/AddDestination.spec.ts":11, +"src/components/alerts/AddExternalAlertSource.spec.ts":3.2, +"src/components/alerts/AddTemplate.drafts.spec.ts":7.4, +"src/components/alerts/AddTemplate.schema.spec.ts":3, +"src/components/alerts/AddTemplate.spec.ts":8.7, +"src/components/alerts/AlertConfigSummary.spec.ts":2.9, +"src/components/alerts/AlertDestinationsField.spec.ts":3.2, +"src/components/alerts/AlertEvaluationHistory.spec.ts":9.4, +"src/components/alerts/AlertGroupChart.spec.ts":4.5, +"src/components/alerts/AlertGroupHistory.spec.ts":6.1, +"src/components/alerts/AlertGroupsTable.spec.ts":4.5, +"src/components/alerts/AlertHeaderActions.spec.ts":5.9, +"src/components/alerts/AlertHistory.ack.spec.ts":4.2, +"src/components/alerts/AlertHistory.spec.ts":5.9, +"src/components/alerts/AlertHistoryDrawer.spec.ts":15.8, +"src/components/alerts/AlertHistoryTimeline.spec.ts":2.5, +"src/components/alerts/AlertInsights.spec.ts":8.9, +"src/components/alerts/AlertInsightsContextMenu.spec.ts":1.7, +"src/components/alerts/AlertList.editorDrafts.spec.ts":6.3, +"src/components/alerts/AlertList.spec.ts":118.4, +"src/components/alerts/AlertListSlo.spec.ts":16.8, +"src/components/alerts/AlertMultiToggle.spec.ts":2.5, +"src/components/alerts/AlertSectionTabs.spec.ts":1.8, +"src/components/alerts/AlertSettingsHelpDrawer.spec.ts":4.2, +"src/components/alerts/AlertSummary.spec.ts":2.3, +"src/components/alerts/AlertTargetsSelect.spec.ts":1.4, +"src/components/alerts/AlertsDestinationList.deeplink.spec.ts":4.9, +"src/components/alerts/AlertsDestinationList.spec.ts":17, +"src/components/alerts/AnomalyDetectionChart.spec.ts":6.6, +"src/components/alerts/CreateAlertAction.spec.ts":5.7, +"src/components/alerts/CreateAlertDialogProvider.spec.ts":1.9, +"src/components/alerts/CreateAlertFromSourceDialog.spec.ts":2.9, +"src/components/alerts/CustomConfirmDialog.spec.ts":2.8, +"src/components/alerts/DependencyImpactDialog.spec.ts":8.8, +"src/components/alerts/DependencyUsageCell.spec.ts":5.3, +"src/components/alerts/DestinationList.spec.ts":3.8, +"src/components/alerts/DestinationPreview.spec.ts":3.1, +"src/components/alerts/DestinationTestResult.spec.ts":3.6, +"src/components/alerts/ExternalAlertSourcesList.spec.ts":10.5, +"src/components/alerts/FieldsInput.spec.ts":4.5, +"src/components/alerts/FilterCondition.spec.ts":6.4, +"src/components/alerts/FilterCondition.suggest.spec.ts":6.1, +"src/components/alerts/FilterGroup.spec.ts":5.1, +"src/components/alerts/ImportAlert.spec.ts":6.8, +"src/components/alerts/ImportDestination.spec.ts":2.6, +"src/components/alerts/ImportSemanticGroups.spec.ts":2.3, +"src/components/alerts/ImportSemanticGroupsDrawer.spec.ts":3.9, +"src/components/alerts/ImportTemplate.spec.ts":2.8, +"src/components/alerts/IncidentAiAnalysisCell.spec.ts":4.3, +"src/components/alerts/IncidentAlertTriggersTable.spec.ts":5.5, +"src/components/alerts/IncidentDetailDrawer.spec.ts":21.2, +"src/components/alerts/IncidentList.spec.ts":8.2, +"src/components/alerts/IncidentRCAAnalysis.spec.ts":11.2, +"src/components/alerts/IncidentServiceGraph.spec.ts":11.8, +"src/components/alerts/IncidentTimeline.spec.ts":2.9, +"src/components/alerts/JiraDestinationFields.spec.ts":5.6, +"src/components/alerts/PipelinesDestinationList.spec.ts":9.5, +"src/components/alerts/PrebuiltDestinationForm.schema.spec.ts":0.9, +"src/components/alerts/PrebuiltDestinationForm.spec.ts":2.2, +"src/components/alerts/PrebuiltDestinationSelector.spec.ts":6.9, +"src/components/alerts/PreviewAlert.spec.ts":9.2, +"src/components/alerts/QueryEditorDialog.spec.ts":25.7, +"src/components/alerts/SemanticFieldGroupsConfig.spec.ts":2.5, +"src/components/alerts/SemanticGroupItem.spec.ts":3, +"src/components/alerts/SlackAppDestinationForm.spec.ts":4.3, +"src/components/alerts/SlackDestinationSetup.spec.ts":6.5, +"src/components/alerts/TemplateList.spec.ts":22.3, +"src/components/alerts/VariablesInput.spec.ts":3.6, +"src/components/alerts/composite/CompositeAlertDetail.spec.ts":2, +"src/components/alerts/composite/CompositeAlertForm.spec.ts":2.9, +"src/components/alerts/composite/CompositeAlertPreview.spec.ts":3.1, +"src/components/alerts/composite/CompositeChildSelector.spec.ts":1.8, +"src/components/alerts/composite/CompositeExpressionBuilder.spec.ts":2.9, +"src/components/alerts/composite/CompositeReferencesDrawer.spec.ts":2.2, +"src/components/alerts/composite/CompositeStatusTimeline.spec.ts":2.2, +"src/components/alerts/composite/expression.spec.ts":0.6, +"src/components/alerts/downtimes/AddDowntime.drafts.spec.ts":17.1, +"src/components/alerts/downtimes/AddDowntime.schema.spec.ts":3, +"src/components/alerts/downtimes/AddDowntime.spec.ts":8.4, +"src/components/alerts/downtimes/DowntimeCountdown.spec.ts":1.3, +"src/components/alerts/downtimes/DowntimeDetail.notifications.spec.ts":4.2, +"src/components/alerts/downtimes/DowntimeDetail.spec.ts":8.8, +"src/components/alerts/downtimes/DowntimeList.actions.spec.ts":16.8, +"src/components/alerts/downtimes/DowntimeList.spec.ts":5.6, +"src/components/alerts/downtimes/DowntimeResourcePicker.spec.ts":3.6, +"src/components/alerts/downtimes/DowntimeScheduleFields.spec.ts":3.9, +"src/components/alerts/downtimes/DowntimeTargetCard.spec.ts":5.5, +"src/components/alerts/downtimes/ExtendDowntimeDialog.spec.ts":3.7, +"src/components/alerts/downtimes/ExtendDowntimeMenu.spec.ts":5.6, +"src/components/alerts/downtimes/MuteMenuItems.spec.ts":5, +"src/components/alerts/downtimes/MutedChip.spec.ts":3, +"src/components/alerts/downtimes/QuickMuteDialog.alert.spec.ts":5.7, +"src/components/alerts/downtimes/QuickMuteDialog.spec.ts":4.6, +"src/components/alerts/rca/RcaAnalysisBar.spec.ts":2.7, +"src/components/alerts/rca/RcaBlindSpots.spec.ts":4, +"src/components/alerts/rca/RcaEvidenceCard.spec.ts":3.6, +"src/components/alerts/rca/RcaEvidenceList.spec.ts":4.4, +"src/components/alerts/rca/RcaGuidanceDialog.spec.ts":4.9, +"src/components/alerts/rca/RcaHypotheses.spec.ts":2.6, +"src/components/alerts/rca/RcaPill.spec.ts":2.4, +"src/components/alerts/rca/RcaProposedActions.spec.ts":5.1, +"src/components/alerts/rca/RcaReportContent.spec.ts":1, +"src/components/alerts/rca/RcaStageStepper.spec.ts":2.9, +"src/components/alerts/rca/RcaSteerComposer.spec.ts":2.3, +"src/components/alerts/rca/RcaThreadColumn.spec.ts":4.2, +"src/components/alerts/rca/RcaThreadStep.spec.ts":3.6, +"src/components/alerts/rca/RcaToolCallRow.spec.ts":2.4, +"src/components/alerts/rca/RcaVerdictCard.spec.ts":4.9, +"src/components/alerts/rca/RcaWhatHappened.spec.ts":3.8, +"src/components/alerts/steps/Advanced.spec.ts":3.5, +"src/components/alerts/steps/AlertSettings.spec.ts":6.5, +"src/components/alerts/steps/CompareWithPast.spec.ts":3.6, +"src/components/alerts/steps/QueryConfig.schema.spec.ts":1.6, +"src/components/alerts/steps/QueryConfig.spec.ts":42.2, +"src/components/alerts/template-content/ContentFieldsEditor.spec.ts":2, +"src/components/alerts/template-content/ContentTemplateForm.spec.ts":6.9, +"src/components/alerts/template-content/TemplatePreviewPanel.spec.ts":2.8, +"src/components/alerts/template-content/contentSpec.spec.ts":1.6, +"src/components/announcements/AnnouncementBanner.spec.ts":3, +"src/components/anomaly_detection/AnomalyDataPreview.spec.ts":7.2, +"src/components/anomaly_detection/steps/AnomalyAlerting.spec.ts":2.4, +"src/components/anomaly_detection/steps/AnomalyDetectionConfig.spec.ts":13.2, +"src/components/autoobserve/AutoObserveIssueDrawer.spec.ts":4.5, +"src/components/cipherkeys/AddAkeylessType.spec.ts":3.2, +"src/components/cipherkeys/AddCipherKey.drafts.spec.ts":6.1, +"src/components/cipherkeys/AddCipherKey.schema.spec.ts":1.9, +"src/components/cipherkeys/AddCipherKey.spec.ts":7.4, +"src/components/cipherkeys/AddEncryptionMechanism.spec.ts":4.5, +"src/components/cipherkeys/AddOpenobserveType.spec.ts":2.8, +"src/components/common/AppTabs.spec.ts":1.9, +"src/components/common/BaseImport.spec.ts":1.8, +"src/components/common/ExportResourceDialog.spec.ts":2.6, +"src/components/common/FieldValuesPanel.spec.ts":4.5, +"src/components/common/FlameGraphView.spec.ts":2.4, +"src/components/common/GroupHeader.spec.ts":1.2, +"src/components/common/IacRegistryLinks.spec.ts":3.6, +"src/components/common/JsonEditor.spec.ts":3.2, +"src/components/common/LoadingProgress.spec.ts":1.1, +"src/components/common/O2AIContextAddBtn.spec.js":1.9, +"src/components/common/PanelBar.spec.ts":2.8, +"src/components/common/PasswordRequirementList.spec.ts":2.3, +"src/components/common/QueryErrorState.spec.ts":3.9, +"src/components/common/RelativeTime.spec.ts":1, +"src/components/common/SectionRail.spec.ts":1.8, +"src/components/common/ShareButton.spec.ts":1.3, +"src/components/common/UpdatePasswordDialog.spec.ts":7, +"src/components/common/drafts/EditorDraftDialogs.spec.ts":1.6, +"src/components/common/drafts/EditorDraftDot.spec.ts":3, +"src/components/common/drafts/EditorDraftLeaveDialog.spec.ts":4.1, +"src/components/common/drafts/EditorDraftRestoreDialog.spec.ts":2.1, +"src/components/common/drafts/EditorDraftSignOutDialog.spec.ts":2.5, +"src/components/common/flameGraphPackage.spec.ts":1.2, +"src/components/common/sidebar/AddFolder.spec.ts":4.7, +"src/components/common/sidebar/FolderIcon.spec.ts":1.2, +"src/components/common/sidebar/FolderIconField.spec.ts":3, +"src/components/common/sidebar/FolderList.spec.ts":3.6, +"src/components/common/sidebar/InlineSelectFolderDropdown.spec.ts":2.3, +"src/components/common/sidebar/MoveAcrossFolders.spec.ts":1.5, +"src/components/common/sidebar/SearchFieldList.spec.ts":5.9, +"src/components/common/sidebar/SelectFolderDropDown.spec.ts":1.6, +"src/components/cross-linking/CrossLinkDialog.schema.spec.ts":1, +"src/components/cross-linking/CrossLinkDialog.spec.ts":7.2, +"src/components/cross-linking/CrossLinkManager.spec.ts":2.4, +"src/components/cross-linking/CrossLinkUserGuide.spec.ts":4.3, +"src/components/dashboards/AddDashboard.spec.ts":5.5, +"src/components/dashboards/AddDashboardFromGitHub.spec.ts":2.3, +"src/components/dashboards/AddFolder.spec.ts":1.6, +"src/components/dashboards/AlertContextMenu.spec.ts":2.9, +"src/components/dashboards/ColorSwatchPicker.spec.ts":2.7, +"src/components/dashboards/ColumnFormatControls.spec.ts":3.1, +"src/components/dashboards/DashboardSaveConflictDialog.spec.ts":7.1, +"src/components/dashboards/ExportDashboard.spec.ts":1.7, +"src/components/dashboards/MoveDashboardToAnotherFolder.spec.ts":2.2, +"src/components/dashboards/OverrideConfigPopup.chartMode.spec.ts":8.3, +"src/components/dashboards/OverrideConfigPopup.seriesStyle.spec.ts":5.1, +"src/components/dashboards/OverrideConfigPopup.spec.ts":4.6, +"src/components/dashboards/PanelContainer.spec.ts":22.7, +"src/components/dashboards/PanelContainer.title.spec.ts":5.6, +"src/components/dashboards/PanelEditor/PanelEditor.spec.ts":5.8, +"src/components/dashboards/PanelEditor/composables/usePanelEditor.spec.ts":3.9, +"src/components/dashboards/PanelEditor/types/panelEditor.spec.ts":1.5, +"src/components/dashboards/PanelErrorButtons.spec.ts":7.8, +"src/components/dashboards/PanelSchemaRenderer.spec.ts":9.1, +"src/components/dashboards/PublicLinkForm.schema.spec.ts":2.8, +"src/components/dashboards/PublicLinksPanel.spec.ts":10.9, +"src/components/dashboards/PublicLinksTable.spec.ts":9, +"src/components/dashboards/QueryAnnotationTooltip.spec.ts":6.4, +"src/components/dashboards/QueryInspector.spec.ts":7, +"src/components/dashboards/SelectDashboardDropdown.spec.ts":5.2, +"src/components/dashboards/SelectFolderDropdown.spec.ts":5.8, +"src/components/dashboards/SelectTabDropdown.spec.ts":1.7, +"src/components/dashboards/SeriesFormatControls.spec.ts":7.4, +"src/components/dashboards/TemplateSuggestionCards.spec.ts":1.4, +"src/components/dashboards/VariablesValueSelector.spec.ts":36.8, +"src/components/dashboards/addPanel/AddAnnotation.spec.ts":8.3, +"src/components/dashboards/addPanel/AxisFieldChipLabel.spec.ts":1.6, +"src/components/dashboards/addPanel/BackGroundColorConfig.spec.ts":2.3, +"src/components/dashboards/addPanel/ChartSelection.spec.ts":6.6, +"src/components/dashboards/addPanel/ColorBySeries.spec.ts":6.4, +"src/components/dashboards/addPanel/ColorBySeriesPopUp.spec.ts":3.3, +"src/components/dashboards/addPanel/ColorPaletteDropDown.spec.ts":6.1, +"src/components/dashboards/addPanel/ColumnOrderPopUp.spec.ts":1.9, +"src/components/dashboards/addPanel/ConfigPanel.alert-list.spec.ts":7.4, +"src/components/dashboards/addPanel/ConfigPanel.boxplot.spec.ts":6.8, +"src/components/dashboards/addPanel/ConfigPanel.cartesian.spec.ts":19.1, +"src/components/dashboards/addPanel/ConfigPanel.f2.spec.ts":9.4, +"src/components/dashboards/addPanel/ConfigPanel.f4.spec.ts":20.7, +"src/components/dashboards/addPanel/ConfigPanel.flame-graph.spec.ts":11.4, +"src/components/dashboards/addPanel/ConfigPanel.funnel.spec.ts":8.8, +"src/components/dashboards/addPanel/ConfigPanel.hostmap.spec.ts":5.1, +"src/components/dashboards/addPanel/ConfigPanel.logs.spec.ts":9.5, +"src/components/dashboards/addPanel/ConfigPanel.node-graph.spec.ts":6.7, +"src/components/dashboards/addPanel/ConfigPanel.panelTime.spec.ts":7.8, +"src/components/dashboards/addPanel/ConfigPanel.seriesOverrides.spec.ts":24.1, +"src/components/dashboards/addPanel/ConfigPanel.spec.ts":72.1, +"src/components/dashboards/addPanel/ConfigPanel.sunburst.spec.ts":5.5, +"src/components/dashboards/addPanel/ConfigPanel.thresholds.spec.ts":31, +"src/components/dashboards/addPanel/ConfigPanel.unitSelect.spec.ts":15.3, +"src/components/dashboards/addPanel/ConfigPanelSearch.spec.ts":1.3, +"src/components/dashboards/addPanel/CustomChartEditor.spec.ts":1, +"src/components/dashboards/addPanel/CustomHTMLEditor.spec.ts":2.4, +"src/components/dashboards/addPanel/CustomMarkdownEditor.spec.ts":2.2, +"src/components/dashboards/addPanel/DashboardErrors.spec.ts":2.7, +"src/components/dashboards/addPanel/DashboardGeoMapsQueryBuilder.spec.ts":6.3, +"src/components/dashboards/addPanel/DashboardMapsQueryBuilder.spec.ts":3.2, +"src/components/dashboards/addPanel/DashboardQueryBuilder.spec.ts":7.8, +"src/components/dashboards/addPanel/DashboardQueryEditor.completionInputs.spec.ts":5.4, +"src/components/dashboards/addPanel/DashboardQueryEditor.spec.ts":11.7, +"src/components/dashboards/addPanel/DashboardSankeyChartBuilder.spec.ts":5.4, +"src/components/dashboards/addPanel/Drilldown.spec.ts":3.5, +"src/components/dashboards/addPanel/DrilldownPopUp.schema.spec.ts":1.1, +"src/components/dashboards/addPanel/DrilldownPopUp.spec.ts":8.8, +"src/components/dashboards/addPanel/DrilldownUserGuide.spec.ts":5.4, +"src/components/dashboards/addPanel/HistogramIntervalDropDown.spec.ts":1.4, +"src/components/dashboards/addPanel/MarkLineConfig.spec.ts":5.3, +"src/components/dashboards/addPanel/OverrideConfig.chartMode.spec.ts":10.2, +"src/components/dashboards/addPanel/OverrideConfig.spec.ts":4.6, +"src/components/dashboards/addPanel/PanelFieldList.spec.ts":6.7, +"src/components/dashboards/addPanel/PanelSidebar.spec.ts":2, +"src/components/dashboards/addPanel/PromQLChartConfig.spec.ts":3.9, +"src/components/dashboards/addPanel/QueryTypeSelector.spec.ts":4, +"src/components/dashboards/addPanel/QueryTypeSelector.switch.spec.ts":2.3, +"src/components/dashboards/addPanel/ShowLegendsPopup.spec.ts":2.7, +"src/components/dashboards/addPanel/SortByBtnGrp.spec.ts":6.5, +"src/components/dashboards/addPanel/StreamFieldSelect.spec.ts":3.4, +"src/components/dashboards/addPanel/TablePaginationControls.spec.ts":2.7, +"src/components/dashboards/addPanel/ThresholdsConfig.spec.ts":11.7, +"src/components/dashboards/addPanel/ValueMapping.spec.ts":3, +"src/components/dashboards/addPanel/ValueMappingList.spec.ts":4.2, +"src/components/dashboards/addPanel/ValueMappingPopUp.spec.ts":5.1, +"src/components/dashboards/addPanel/customChartExamples/CustomChartConfirmDialog.spec.ts":2.4, +"src/components/dashboards/addPanel/customChartExamples/CustomChartTypeSelector.spec.ts":4.2, +"src/components/dashboards/addPanel/customChartExamples/customChartTemplates.spec.ts":0.7, +"src/components/dashboards/addPanel/dynamicFunction/DynamicFunctionPopUp.spec.ts":4.3, +"src/components/dashboards/addPanel/dynamicFunction/RawQueryBuilder.spec.ts":3, +"src/components/dashboards/addPanel/dynamicFunction/SelectFunction.spec.ts":4.8, +"src/components/dashboards/exemplars/ExemplarCard.spec.ts":6.7, +"src/components/dashboards/exemplars/ExemplarToggle.spec.ts":3, +"src/components/dashboards/kiosk/KioskCountdownRing.spec.ts":1.9, +"src/components/dashboards/kiosk/KioskSetupPopover.spec.ts":4.2, +"src/components/dashboards/kiosk/KioskStrip.spec.ts":2.8, +"src/components/dashboards/kiosk/PlaylistDialog.spec.ts":8, +"src/components/dashboards/kiosk/PlaylistPlayer.spec.ts":2.5, +"src/components/dashboards/kiosk/PlaylistStrip.spec.ts":4.6, +"src/components/dashboards/panelDraft/PanelLeaveDialog.spec.ts":3.8, +"src/components/dashboards/panels/AlertListPanelRenderer.spec.ts":4.7, +"src/components/dashboards/panels/ChartRenderer.hostmap.spec.ts":9.1, +"src/components/dashboards/panels/ChartRenderer.spec.ts":5.1, +"src/components/dashboards/panels/CustomChartRenderer.spec.ts":5.4, +"src/components/dashboards/panels/FlameGraphPanelRenderer.spec.ts":3.4, +"src/components/dashboards/panels/GeoMapRenderer.spec.ts":3.8, +"src/components/dashboards/panels/HTMLRenderer.spec.ts":1.6, +"src/components/dashboards/panels/JsonFieldRenderer.spec.ts":1, +"src/components/dashboards/panels/LogsPanelRenderer.spec.ts":3.3, +"src/components/dashboards/panels/MapsRenderer.spec.ts":2, +"src/components/dashboards/panels/MarkdownRenderer.spec.ts":2, +"src/components/dashboards/panels/PromQLTableChart.spec.ts":9.2, +"src/components/dashboards/panels/TableRenderer.cells.spec.ts":4.6, +"src/components/dashboards/panels/TableRenderer.spec.ts":10.3, +"src/components/dashboards/panels/TextPanelVariableEscaping.spec.ts":1.8, +"src/components/dashboards/publicLinkDisplay.spec.ts":3.4, +"src/components/dashboards/settings/AddSettingVariable.spec.ts":11.4, +"src/components/dashboards/settings/AnnotationSettings.spec.ts":17.4, +"src/components/dashboards/settings/DashboardSettingsDraftScope.spec.ts":1.6, +"src/components/dashboards/settings/GeneralSettings.spec.ts":10, +"src/components/dashboards/settings/SinglePanelMove.spec.ts":3.8, +"src/components/dashboards/settings/TabsDeletePopUp.spec.ts":4.7, +"src/components/dashboards/settings/TabsSettings.spec.ts":7.3, +"src/components/dashboards/settings/VariableAdHocValueSelector.spec.ts":4.6, +"src/components/dashboards/settings/VariableCustomValueSelector.spec.ts":1.7, +"src/components/dashboards/settings/VariableQueryValueSelector.spec.ts":3, +"src/components/dashboards/settings/VariableSettings.spec.ts":5.1, +"src/components/dashboards/settings/VariablesDependenciesGraph.spec.ts":1.3, +"src/components/dashboards/settings/common/DashboardHeader.spec.ts":3.2, +"src/components/dashboards/tabs/AddTab.spec.ts":3.8, +"src/components/dashboards/tabs/TabList.spec.ts":4.8, +"src/components/dashboards/viewPanel/ViewPanel.spec.ts":5.2, +"src/components/dbm/DbmAppSourceLegend.spec.ts":1.9, +"src/components/dbm/DbmAppSourceMarker.spec.ts":1.9, +"src/components/dbm/DbmCoverageLine.spec.ts":2.5, +"src/components/dbm/DbmDeltaCell.spec.ts":2.1, +"src/components/dbm/DbmEmptyState.spec.ts":2.8, +"src/components/dbm/DbmHistoryPanel.spec.ts":1.4, +"src/components/dbm/DbmInsightStrip.spec.ts":1.5, +"src/components/dbm/DbmInstanceHealthCell.spec.ts":2.4, +"src/components/dbm/DbmLockCoverageLine.spec.ts":0.9, +"src/components/dbm/DbmLockEmptyState.spec.ts":2, +"src/components/dbm/DbmMetricTiles.spec.ts":2.2, +"src/components/dbm/DbmMetricsRail.spec.ts":2.7, +"src/components/dbm/DbmOverlapValue.spec.ts":1.7, +"src/components/dbm/DbmPageChrome.spec.ts":1.9, +"src/components/dbm/DbmQueryCell.spec.ts":2.3, +"src/components/dbm/DbmQueryText.spec.ts":0.9, +"src/components/dbm/DbmRowChips.spec.ts":1.5, +"src/components/dbm/DbmScopeFilters.spec.ts":1.8, +"src/components/dbm/DbmSection.spec.ts":0.9, +"src/components/dbm/DbmSectionTabs.spec.ts":5.7, +"src/components/dbm/DbmSectionTabsScopeCarry.spec.ts":2.4, +"src/components/dbm/DbmShareBar.spec.ts":1.1, +"src/components/dbm/DbmStateNote.spec.ts":0.9, +"src/components/dbm/DbmSubheaderBand.spec.ts":1, +"src/components/dbm/DbmTableToolbar.spec.ts":1.2, +"src/components/dbm/DbmTerminateSql.spec.ts":2.6, +"src/components/dbm/DbmToolbarNote.spec.ts":1.6, +"src/components/fleet/FleetAudit.spec.ts":6.2, +"src/components/fleet/FleetCollectorDrawer.spec.ts":8.5, +"src/components/fleet/FleetCollectorMetrics.spec.ts":5.3, +"src/components/fleet/FleetCollectorVersions.spec.ts":10.1, +"src/components/fleet/FleetCollectors.spec.ts":24.6, +"src/components/fleet/FleetConfigurations.spec.ts":8.9, +"src/components/fleet/FleetFleets.spec.ts":6.2, +"src/components/fleet/FleetFlowEdge.spec.ts":3.4, +"src/components/fleet/FleetFlowLegend.spec.ts":5, +"src/components/fleet/FleetFlowNode.spec.ts":3.2, +"src/components/fleet/FleetFlowOverview.spec.ts":12, +"src/components/fleet/FleetInstallCommand.spec.ts":4, +"src/components/fleet/FleetInstallDialog.spec.ts":7.5, +"src/components/fleet/FleetNewRolloutDialog.spec.ts":10.1, +"src/components/fleet/FleetOverview.spec.ts":2.9, +"src/components/fleet/FleetRail.spec.ts":3.9, +"src/components/fleet/FleetRemoveDialog.spec.ts":4.4, +"src/components/fleet/FleetRolloutOutcome.spec.ts":3.2, +"src/components/fleet/FleetRolloutPhases.spec.ts":5.9, +"src/components/fleet/FleetRollouts.spec.ts":13.1, +"src/components/fleet/FleetSettings.spec.ts":8.3, +"src/components/fleet/FleetTokenExpiryBanner.spec.ts":2.8, +"src/components/fleet/FleetTokens.spec.ts":25.1, +"src/components/fleet/FleetUninstallCommand.spec.ts":1.4, +"src/components/fleet/FleetVersionBars.spec.ts":2.6, +"src/components/fleet/auditKind.spec.ts":1.1, +"src/components/fleet/configDiff.spec.ts":1, +"src/components/fleet/editor/FleetConfigHistory.spec.ts":3, +"src/components/fleet/editor/FleetEditorCanvas.spec.ts":3.7, +"src/components/fleet/editor/FleetEditorNode.spec.ts":1.4, +"src/components/fleet/editor/editorGraph.spec.ts":1.3, +"src/components/fleet/fleetFormat.spec.ts":1.2, +"src/components/fleet/fleetHelpers.spec.ts":0.8, +"src/components/fleet/fleetInstall.spec.ts":1.9, +"src/components/fleet/fleetTokenSecrets.spec.ts":1.3, +"src/components/fleet/flowIssues.spec.ts":2, +"src/components/fleet/pipelineGraph.spec.ts":5.4, +"src/components/fleet/useFleetApi.spec.ts":3.6, +"src/components/flow/FlowEdge.spec.ts":3, +"src/components/flow/FlowNodeCard.spec.ts":2.3, +"src/components/flow/NodePalette.spec.ts":2.7, +"src/components/flow/StepPickerDialog.spec.ts":1.1, +"src/components/flow/forms/ConditionBuilder.spec.ts":1.9, +"src/components/flow/forms/DestinationPicker.spec.ts":5.8, +"src/components/flow/forms/FunctionPicker.spec.ts":6.6, +"src/components/flow/forms/conditionColumnCheck.spec.ts":2.2, +"src/components/functions/AddEnrichmentTable.schema.spec.ts":1.2, +"src/components/functions/AddEnrichmentTable.spec.ts":7.1, +"src/components/functions/AddFunction.drafts.spec.ts":6.6, +"src/components/functions/AddFunction.schema.spec.ts":1.4, +"src/components/functions/AddFunction.spec.js":7.3, +"src/components/functions/AddFunction.spec.ts":3.1, +"src/components/functions/AssociatedStreamFunction.spec.ts":9.2, +"src/components/functions/EnrichmentSchema.spec.ts":3.2, +"src/components/functions/EnrichmentTableList.spec.js":21.4, +"src/components/functions/EnrichmentTableList.spec.ts":25.8, +"src/components/functions/FullViewContainer.spec.ts":1.7, +"src/components/functions/FunctionList.spec.ts":31.7, +"src/components/functions/FunctionsToolbar.spec.ts":4.6, +"src/components/functions/ImportFunction.spec.ts":4.3, +"src/components/functions/TestFunction.spec.js":3.9, +"src/components/functions/TestFunction.spec.ts":8.2, +"src/components/iam/IngestionTokens.spec.ts":5.9, +"src/components/iam/groups/AddGroup.spec.ts":1.3, +"src/components/iam/groups/AppGroups.spec.ts":16.3, +"src/components/iam/groups/EditGroup.leave.spec.ts":2.2, +"src/components/iam/groups/EditGroup.spec.ts":7.4, +"src/components/iam/groups/GroupRoles.debug.spec.ts":4.1, +"src/components/iam/groups/GroupRoles.spec.ts":4.6, +"src/components/iam/groups/GroupUsers.spec.ts":3.1, +"src/components/iam/organizations/AddUpdateOrganization.spec.ts":2.9, +"src/components/iam/organizations/ListOrganizations.spec.js":10.6, +"src/components/iam/roles/AddRole.spec.ts":5.6, +"src/components/iam/roles/AppRoles.spec.ts":35.4, +"src/components/iam/roles/EditRole.drafts.spec.ts":17.6, +"src/components/iam/roles/EditRole.spec.js":59.7, +"src/components/iam/roles/ModulePane.spec.ts":8.7, +"src/components/iam/roles/ModulePaneBulkHeader.spec.ts":0.7, +"src/components/iam/roles/ModuleRail.spec.ts":5.9, +"src/components/iam/roles/RolePresetsPanel.spec.ts":4.1, +"src/components/iam/roles/RoleSummary.spec.ts":3.8, +"src/components/iam/roles/UnsavedChangesDrawer.spec.ts":4.2, +"src/components/iam/roles/dbmViewerPreset.spec.ts":1.1, +"src/components/iam/roles/k8sViewerPreset.spec.ts":2.6, +"src/components/iam/roles/readonlyPreset.spec.ts":1.6, +"src/components/iam/roles/roleCompanions.spec.ts":1.1, +"src/components/iam/roles/roleModules.spec.ts":1.5, +"src/components/iam/roles/rumPresets.spec.ts":0.9, +"src/components/iam/serviceAccounts/AddServiceAccount.spec.ts":2.6, +"src/components/iam/serviceAccounts/ServiceAccountsList.spec.js":27.4, +"src/components/iam/users/AddUser.spec.ts":8.9, +"src/components/iam/users/InvitationList.spec.ts":12.9, +"src/components/iam/users/InviteMembersDialog.spec.ts":7.9, +"src/components/iam/users/UpdateRole.spec.ts":1.6, +"src/components/iam/users/User.spec.js":20.2, +"src/components/iam/users/User.spec.ts":34.5, +"src/components/icons/AscSort.spec.ts":2.5, +"src/components/icons/DeployedCode.spec.ts":0.8, +"src/components/icons/DescSort.spec.ts":1.2, +"src/components/icons/DynamicFilterIcon.spec.ts":1.3, +"src/components/icons/EqualIcon.spec.ts":0.8, +"src/components/icons/InnerJoinTypeSvg.spec.ts":1.2, +"src/components/icons/LeftJoinLineSvg.spec.ts":0.9, +"src/components/icons/LeftJoinSvg.spec.ts":0.8, +"src/components/icons/LeftJoinTypeSvg.spec.ts":1.8, +"src/components/icons/ManagementIcon.spec.ts":1.4, +"src/components/icons/NotEqualIcon.spec.ts":1.5, +"src/components/icons/RightJoinLineSvg.spec.ts":0.7, +"src/components/icons/RightJoinSvg.spec.ts":1.4, +"src/components/icons/RightJoinTypeSvg.spec.ts":1, +"src/components/icons/ServiceMapIcon.spec.ts":1.3, +"src/components/icons/SlackIcon.spec.ts":1, +"src/components/icons/SubTaskArrow.spec.ts":1.2, +"src/components/icons/TraceTimelineIcon.spec.ts":0.9, +"src/components/icons/dashboards/LinearIcon.spec.ts":1.6, +"src/components/icons/dashboards/NoSymbol.spec.ts":1.4, +"src/components/icons/dashboards/Smooth.spec.ts":1.1, +"src/components/icons/dashboards/StepAfter.spec.ts":0.7, +"src/components/icons/dashboards/StepBefore.spec.ts":1.4, +"src/components/icons/dashboards/StepMiddle.spec.ts":1, +"src/components/ingestion/AIIntegrations.spec.ts":12.4, +"src/components/ingestion/CredentialCodeBlock.spec.ts":4.6, +"src/components/ingestion/Custom.spec.ts":3.2, +"src/components/ingestion/DataSourceSidebarLayout.spec.ts":3.2, +"src/components/ingestion/Database.spec.ts":2.7, +"src/components/ingestion/DevOps.spec.ts":2.8, +"src/components/ingestion/FirstDataPanel.spec.ts":6.5, +"src/components/ingestion/FirstEventStatus.spec.ts":4.4, +"src/components/ingestion/IngestionContent.spec.ts":4.8, +"src/components/ingestion/IngestionDocLink.spec.ts":1.5, +"src/components/ingestion/Languages.spec.ts":3.4, +"src/components/ingestion/MessageQueues.spec.ts":3.2, +"src/components/ingestion/Networking.spec.ts":3.4, +"src/components/ingestion/Others.spec.ts":2.2, +"src/components/ingestion/Recommended.spec.ts":6.5, +"src/components/ingestion/Security.spec.ts":5.4, +"src/components/ingestion/Server.spec.ts":3.9, +"src/components/ingestion/ai/AIIntegrationDetail.spec.ts":4.8, +"src/components/ingestion/ai/McpServerCard.spec.ts":7.4, +"src/components/ingestion/ai/content/AIIntegrationCard.spec.ts":2.7, +"src/components/ingestion/ai/content/parseCard.spec.ts":1.2, +"src/components/ingestion/ai/content/renderMarkdown.spec.ts":2.6, +"src/components/ingestion/configGuides.spec.ts":3.1, +"src/components/ingestion/copyContentMigration.spec.ts":11.7, +"src/components/ingestion/databases/Aerospike.spec.ts":4, +"src/components/ingestion/databases/Cassandra.spec.ts":2.7, +"src/components/ingestion/databases/CouchDB.spec.ts":1.7, +"src/components/ingestion/databases/Databricks.spec.ts":4.5, +"src/components/ingestion/databases/DynamoDB.spec.ts":2.9, +"src/components/ingestion/databases/Elasticsearch.spec.ts":3.5, +"src/components/ingestion/databases/MongoDB.spec.ts":3.4, +"src/components/ingestion/databases/MySQL.spec.ts":5, +"src/components/ingestion/databases/Oracle.spec.ts":3.1, +"src/components/ingestion/databases/Postgres.spec.ts":3.4, +"src/components/ingestion/databases/Redis.spec.ts":4.7, +"src/components/ingestion/databases/SAPHana.spec.ts":1.6, +"src/components/ingestion/databases/Snowflake.spec.ts":3.8, +"src/components/ingestion/databases/SqlServer.spec.ts":2.2, +"src/components/ingestion/databases/Zookeeper.spec.ts":5.7, +"src/components/ingestion/devops/Ansible.spec.ts":1.5, +"src/components/ingestion/devops/GithubActions.spec.ts":1.9, +"src/components/ingestion/devops/Jenkins.spec.ts":2.2, +"src/components/ingestion/devops/Terraform.spec.ts":2.3, +"src/components/ingestion/firstEventCopy.spec.ts":2.9, +"src/components/ingestion/languages/DotNetLogs.spec.ts":1.8, +"src/components/ingestion/languages/DotNetTracing.spec.ts":3.6, +"src/components/ingestion/languages/FastAPI.spec.ts":2.6, +"src/components/ingestion/languages/Go.spec.ts":1.8, +"src/components/ingestion/languages/Java.spec.ts":1.8, +"src/components/ingestion/languages/NodeJS.spec.ts":2, +"src/components/ingestion/languages/Python.spec.ts":2, +"src/components/ingestion/languages/Rust.spec.ts":1.7, +"src/components/ingestion/logs/Curl.spec.ts":4.1, +"src/components/ingestion/logs/FileBeat.spec.ts":3.5, +"src/components/ingestion/logs/FluentBit.spec.ts":5.3, +"src/components/ingestion/logs/Fluentd.spec.ts":2.4, +"src/components/ingestion/logs/Index.spec.ts":3.3, +"src/components/ingestion/logs/KinesisFirehose.spec.ts":2.1, +"src/components/ingestion/logs/LogstashDatasource.spec.ts":4.4, +"src/components/ingestion/logs/LoongCollector.spec.ts":1.3, +"src/components/ingestion/logs/SplunkHec.spec.ts":5.4, +"src/components/ingestion/logs/SyslogNg.spec.ts":3.2, +"src/components/ingestion/logs/Vector.spec.ts":2.2, +"src/components/ingestion/logs/ingestionIndex.spec.ts":7.3, +"src/components/ingestion/logs/targetStream.spec.ts":5.9, +"src/components/ingestion/messagequeues/Kafka.spec.ts":2.2, +"src/components/ingestion/messagequeues/Nats.spec.ts":2.8, +"src/components/ingestion/messagequeues/RabbitMQ.spec.ts":2, +"src/components/ingestion/metrics/CategrafConfig.spec.ts":1.8, +"src/components/ingestion/metrics/CloudWatchMetrics.spec.ts":1.6, +"src/components/ingestion/metrics/Index.spec.ts":3.1, +"src/components/ingestion/metrics/NightingaleConfig.spec.ts":1.8, +"src/components/ingestion/metrics/OtelCollector.spec.ts":2.6, +"src/components/ingestion/metrics/PrometheusConfig.spec.ts":3, +"src/components/ingestion/metrics/TelegrafConfig.spec.ts":5.4, +"src/components/ingestion/metrics/VMagentConfig.spec.ts":1.4, +"src/components/ingestion/networking/Netflow.spec.ts":2.4, +"src/components/ingestion/others/Airbyte.spec.ts":2.1, +"src/components/ingestion/others/Airflow.spec.ts":3.2, +"src/components/ingestion/others/Cribl.spec.ts":2.3, +"src/components/ingestion/others/Heroku.spec.ts":2.1, +"src/components/ingestion/others/Vercel.spec.ts":4.1, +"src/components/ingestion/profiles/OtelCollector.spec.ts":1.6, +"src/components/ingestion/recommended/AWSConfig.spec.ts":3.5, +"src/components/ingestion/recommended/AWSIndividualServices.spec.ts":3.3, +"src/components/ingestion/recommended/AWSIntegrationTile.spec.ts":2, +"src/components/ingestion/recommended/AWSQuickSetup.spec.ts":6.3, +"src/components/ingestion/recommended/AzureConfig.spec.ts":5.6, +"src/components/ingestion/recommended/FrontendRumConfig.spec.ts":3.5, +"src/components/ingestion/recommended/GCPConfig.spec.ts":2.8, +"src/components/ingestion/recommended/GpuConfig.spec.ts":2.6, +"src/components/ingestion/recommended/KubernetesConfig.spec.ts":3.7, +"src/components/ingestion/recommended/LinuxConfig.spec.ts":5.2, +"src/components/ingestion/recommended/MacOSConfig.spec.ts":3, +"src/components/ingestion/recommended/OtelConfig.spec.ts":2.9, +"src/components/ingestion/recommended/WindowsConfig.spec.ts":3, +"src/components/ingestion/security/Falco.spec.ts":3.4, +"src/components/ingestion/security/GoogleWorkspace.spec.ts":2.2, +"src/components/ingestion/security/Jumpcloud.spec.ts":4, +"src/components/ingestion/security/OSQuery.spec.ts":2.5, +"src/components/ingestion/security/Office365.spec.ts":2.9, +"src/components/ingestion/security/Okta.spec.ts":3.3, +"src/components/ingestion/security/OpenVPN.spec.ts":2.4, +"src/components/ingestion/servers/Apache.spec.ts":4, +"src/components/ingestion/servers/IIS.spec.ts":4.5, +"src/components/ingestion/servers/Nginx.spec.ts":4.8, +"src/components/ingestion/setupCard/DataSourceSetupCard.spec.ts":3.5, +"src/components/ingestion/setupCard/SetupCardRenderer.spec.ts":12.1, +"src/components/ingestion/setupCard/content/dbInstaller.spec.ts":1.7, +"src/components/ingestion/setupCard/content/dbmMariadb.spec.ts":4, +"src/components/ingestion/setupCard/content/dbmVerifyCopy.spec.ts":2.7, +"src/components/ingestion/setupCard/content/nvidiaDcgm.spec.ts":2.9, +"src/components/ingestion/setupCard/content/osAgent.spec.ts":3.4, +"src/components/ingestion/setupCard/content/rum.spec.ts":1.1, +"src/components/ingestion/setupCard/content/rumAndroid.spec.ts":1.9, +"src/components/ingestion/setupCard/content/rumIOS.spec.ts":1.9, +"src/components/ingestion/setupCard/content/rumReactNative.spec.ts":1.1, +"src/components/ingestion/setupCard/useStreamDetect.spec.ts":2.9, +"src/components/ingestion/traces/Index.spec.ts":3.2, +"src/components/ingestion/traces/OpenTelemetry.spec.ts":2.4, +"src/components/login/GetStarted.spec.ts":11.2, +"src/components/login/Login.spec.ts":4.2, +"src/components/login/firstSourceOptions.spec.ts":1, +"src/components/logs/ChunkedContent.spec.ts":2.4, +"src/components/logs/LogsHighLighting.spec.ts":2.9, +"src/components/logs/useQueryPlaceholder.spec.ts":2.3, +"src/components/logstream/AddStream.spec.ts":5.3, +"src/components/logstream/AssociatedRegexPatterns.spec.ts":15.9, +"src/components/logstream/PerformanceFieldsDialog.spec.ts":2.9, +"src/components/logstream/Schema.spec.ts":36.5, +"src/components/logstream/StreamFieldInputs.spec.ts":4.2, +"src/components/logstream/explore/SearchBar.spec.ts":1.3, +"src/components/onboardingPopupCoordination.spec.ts":3, +"src/components/oncall/OnCallAboutPage.spec.ts":3.5, +"src/components/oncall/OnCallCoverForm.spec.ts":4.1, +"src/components/oncall/OnCallCoverList.spec.ts":2.4, +"src/components/oncall/OnCallCoverageStrip.spec.ts":3.7, +"src/components/oncall/OnCallDefaultTeamCard.spec.ts":4.4, +"src/components/oncall/OnCallDeliveryLedger.spec.ts":3.7, +"src/components/oncall/OnCallEscalation.spec.ts":4.2, +"src/components/oncall/OnCallEscalationCell.spec.ts":2.8, +"src/components/oncall/OnCallEscalationLadder.spec.ts":4.3, +"src/components/oncall/OnCallEscalationSkeleton.spec.ts":1.2, +"src/components/oncall/OnCallFiringHistory.spec.ts":6.2, +"src/components/oncall/OnCallL0Editor.spec.ts":3.3, +"src/components/oncall/OnCallMembers.spec.ts":4.4, +"src/components/oncall/OnCallMyDeliveries.spec.ts":2.7, +"src/components/oncall/OnCallOwnership.spec.ts":5.2, +"src/components/oncall/OnCallOwnershipRules.spec.ts":4.6, +"src/components/oncall/OnCallPhoneCard.spec.ts":5.2, +"src/components/oncall/OnCallPolicyEditor.drafts.spec.ts":5.1, +"src/components/oncall/OnCallPolicyEditor.spec.ts":9.1, +"src/components/oncall/OnCallPriorCauses.spec.ts":1.9, +"src/components/oncall/OnCallReachAlarm.spec.ts":2.9, +"src/components/oncall/OnCallRecentPages.spec.ts":2.7, +"src/components/oncall/OnCallRemoveRotationDialog.spec.ts":2.9, +"src/components/oncall/OnCallReportCard.spec.ts":2.3, +"src/components/oncall/OnCallRoutingList.spec.ts":7.8, +"src/components/oncall/OnCallRoutingSimulator.spec.ts":5.8, +"src/components/oncall/OnCallRuleEditor.spec.ts":5.8, +"src/components/oncall/OnCallScheduleAnswer.spec.ts":2.1, +"src/components/oncall/OnCallScheduleEditor.drafts.spec.ts":5.5, +"src/components/oncall/OnCallScheduleEditor.spec.ts":14.6, +"src/components/oncall/OnCallSchedulePresets.shape.spec.ts":2.2, +"src/components/oncall/OnCallSchedulePresets.spec.ts":3.7, +"src/components/oncall/OnCallScheduleTimeline.spec.ts":10.8, +"src/components/oncall/OnCallScopePicker.spec.ts":2.9, +"src/components/oncall/OnCallSetupChecklist.spec.ts":2.5, +"src/components/oncall/OnCallShiftBanner.spec.ts":4.5, +"src/components/oncall/OnCallTeamAttention.spec.ts":2.4, +"src/components/oncall/OnCallTeamForm.spec.ts":7.1, +"src/components/oncall/OnCallUnroutedQueue.spec.ts":7.4, +"src/components/oncall/OnCallVerdictCard.spec.ts":3.7, +"src/components/oncall/OnCallWhoIsOn.spec.ts":4.7, +"src/components/oncall/stubs.contract.spec.ts":1.9, +"src/components/pipeline/ImportPipeline.spec.ts":4.3, +"src/components/pipeline/ImportPipelineOrOperator.spec.ts":5.2, +"src/components/pipeline/NodeForm/AssociateFunction.spec.js":2.4, +"src/components/pipeline/NodeForm/Condition.spec.js":4.8, +"src/components/pipeline/NodeForm/ConditionOrOperator.spec.js":2.9, +"src/components/pipeline/NodeForm/CreateDestinationForm.schema.spec.ts":1.1, +"src/components/pipeline/NodeForm/CreateDestinationForm.spec.ts":4.6, +"src/components/pipeline/NodeForm/ExternalDestination.spec.ts":6.4, +"src/components/pipeline/NodeForm/PreviewPromqlQuery.spec.ts":6.2, +"src/components/pipeline/NodeForm/Query.spec.js":8.2, +"src/components/pipeline/NodeForm/Query.spec.ts":3.4, +"src/components/pipeline/NodeForm/ScheduledPipeline.spec.js":25.4, +"src/components/pipeline/NodeForm/Stream.spec.js":5.7, +"src/components/pipeline/PipelineDestinationEditor.spec.ts":4.9, +"src/components/pipeline/PipelineEditor.drafts.spec.ts":7, +"src/components/pipeline/PipelineEditor.spec.js":5.8, +"src/components/pipeline/PipelineView.spec.ts":0.8, +"src/components/pipeline/PipelinesList.spec.ts":4.6, +"src/components/pipeline/StreamSelection.spec.ts":2.2, +"src/components/pipeline/utils/pipelineCommonValidation.spec.ts":1.6, +"src/components/pipelines/BackfillJobDetails.spec.ts":5.3, +"src/components/pipelines/BackfillJobsList.spec.ts":6.4, +"src/components/pipelines/CreateBackfillJobDialog.spec.ts":4, +"src/components/pipelines/EditBackfillJobDialog.spec.ts":3.3, +"src/components/pipelines/PipelineHistory.spec.ts":3.8, +"src/components/promql/astToBuilder.spec.ts":1.9, +"src/components/promql/components/LabelFilterEditor.spec.ts":6.7, +"src/components/promql/components/OperationsList.spec.ts":5.5, +"src/components/promql/components/PromQLBuilderOptions.spec.ts":3.7, +"src/components/promql/labelBoolean.spec.ts":1, +"src/components/promql/operations/index.spec.ts":1.4, +"src/components/promql/operations/queryModeller.spec.ts":1.1, +"src/components/promql/types/index.spec.ts":0.9, +"src/components/queries/QueryList.spec.ts":5.9, +"src/components/queries/RunningQueries.drillthrough.spec.ts":8.9, +"src/components/queries/RunningQueries.spec.ts":7.8, +"src/components/queries/RunningQueriesDrillThrough.spec.ts":5.4, +"src/components/queries/RunningQueriesList.spec.ts":7.2, +"src/components/queries/SummaryList.spec.ts":5.5, +"src/components/query-plan/MetricsSummaryCard.spec.ts":1.4, +"src/components/query-plan/QueryPlanNode.spec.ts":2.6, +"src/components/query-plan/QueryPlanTree.spec.ts":2.2, +"src/components/queryHistory/RecentSearchesButton.spec.ts":3.7, +"src/components/queryHistory/RecentSearchesDayPicker.spec.ts":2.9, +"src/components/queryHistory/RecentSearchesDrawer.spec.ts":3.7, +"src/components/queryHistory/RecentSearchesDrawer.timezone.spec.ts":3.1, +"src/components/queryHistory/RecentSearchesFocusDropdown.editorBox.spec.ts":3.8, +"src/components/queryHistory/RecentSearchesFocusDropdown.spec.ts":4.3, +"src/components/queryHistory/RecentSearchesPopover.spec.ts":7.8, +"src/components/reports/CreateReport.drafts.spec.ts":7, +"src/components/reports/CreateReport.schema.spec.ts":1.9, +"src/components/reports/CreateReport.spec.ts":13.8, +"src/components/reports/ReportList.spec.js":24, +"src/components/reports/ReportList.spec.ts":12.3, +"src/components/reports/ReportRunDrawer.spec.ts":3.7, +"src/components/reports/ReportRunHistory.spec.ts":2.6, +"src/components/reports/ReportRunNowDialog.spec.ts":1.3, +"src/components/rum/EventDetailDrawer.spec.ts":0.7, +"src/components/rum/EventDetailDrawerContent.spec.ts":8.4, +"src/components/rum/FrustrationEventBadge.spec.ts":2.2, +"src/components/rum/MobileSessionPlayer.spec.ts":2.9, +"src/components/rum/PlayerEventsSidebar.spec.ts":3.9, +"src/components/rum/PlayerTracesTab.spec.ts":5.4, +"src/components/rum/ReplayPlaybackOverlay.spec.ts":1.7, +"src/components/rum/ReplayStatusChip.spec.ts":2.7, +"src/components/rum/VideoPlayer.spec.ts":7.7, +"src/components/rum/common/EventDetailsSection.spec.ts":1.2, +"src/components/rum/common/EventTypeBadge.spec.ts":1.6, +"src/components/rum/common/KeyValueRow.spec.ts":1.8, +"src/components/rum/correlation/TraceCorrelationCard.spec.ts":6.7, +"src/components/rum/errorTracking/list/ErrorIssueCell.spec.ts":3.7, +"src/components/rum/errorTracking/list/ErrorTrendCell.spec.ts":2.4, +"src/components/rum/errorTracking/list/ErrorsFilterBar.spec.ts":2.2, +"src/components/rum/errorTracking/list/ErrorsKpiCards.spec.ts":4.1, +"src/components/rum/errorTracking/list/ErrorsOverTimeChart.spec.ts":3.4, +"src/components/rum/errorTracking/view/ErrorContextCard.spec.ts":1.6, +"src/components/rum/errorTracking/view/ErrorEventDescription.spec.ts":1.2, +"src/components/rum/errorTracking/view/ErrorEvents.spec.ts":1.9, +"src/components/rum/errorTracking/view/ErrorFacetBreakdown.spec.ts":1.7, +"src/components/rum/errorTracking/view/ErrorHeader.spec.ts":4.4, +"src/components/rum/errorTracking/view/ErrorImpactStrip.spec.ts":2.7, +"src/components/rum/errorTracking/view/ErrorOccurrencesChart.spec.ts":2.8, +"src/components/rum/errorTracking/view/ErrorSessionReplay.spec.ts":1.3, +"src/components/rum/errorTracking/view/ErrorStackTrace.spec.ts":4.9, +"src/components/rum/errorTracking/view/ErrorTag.spec.ts":1.5, +"src/components/rum/errorTracking/view/ErrorTypeIcons.spec.ts":1, +"src/components/rum/performance/ApiDashboard.spec.ts":3.5, +"src/components/rum/performance/ErrorsDashboard.spec.ts":5.4, +"src/components/rum/performance/PerformanceOverview.spec.ts":5.9, +"src/components/rum/performance/PerformanceSummary.spec.ts":4.4, +"src/components/rum/performance/WebVitalsDashboard.spec.ts":1.3, +"src/components/rum/performance/overview/FrustrationPanel.spec.ts":4.3, +"src/components/rum/performance/overview/KpiTile.spec.ts":2.6, +"src/components/rum/performance/overview/OverviewPanelState.spec.ts":3.1, +"src/components/rum/performance/overview/PagesToFix.spec.ts":3.5, +"src/components/rum/performance/overview/ReleaseComparison.spec.ts":2.5, +"src/components/rum/performance/overview/SegmentPanel.spec.ts":2.5, +"src/components/rum/performance/overview/SessionsErrorsChart.spec.ts":4.7, +"src/components/rum/performance/overview/VerdictCard.spec.ts":4, +"src/components/rum/productAnalytics/AnalyticsFunnels.spec.ts":217, +"src/components/rum/productAnalytics/AnalyticsOverview.spec.ts":43.3, +"src/components/rum/productAnalytics/AnalyticsPaths.spec.ts":9.2, +"src/components/rum/productAnalytics/AnalyticsRetention.spec.ts":17.9, +"src/components/rum/productAnalytics/DropoffDrawer.spec.ts":9.6, +"src/components/rum/productAnalytics/FunnelBreakdown.spec.ts":2.8, +"src/components/rum/productAnalytics/FunnelQuickStarts.spec.ts":2.7, +"src/components/rum/productAnalytics/FunnelTrend.spec.ts":6.3, +"src/components/rum/productAnalytics/NamedEventsListPage.spec.ts":12, +"src/components/rum/productAnalytics/PathSessionsDrawer.spec.ts":3.9, +"src/components/rum/productAnalytics/RankedKeysTable.spec.ts":6.5, +"src/components/rum/productAnalytics/SaveFunnelDialog.spec.ts":3.8, +"src/components/rum/productAnalytics/SavedFunnelBar.spec.ts":6.3, +"src/components/rum/productAnalytics/SavedFunnelDialogs.spec.ts":3.7, +"src/components/rum/productAnalytics/SavedFunnelsPage.spec.ts":17.7, +"src/components/rum/productAnalytics/SidePanelCtas.spec.ts":3.7, +"src/components/rum/productAnalytics/StepPicker.spec.ts":3.3, +"src/components/rum/productAnalytics/TrendsPanel.spec.ts":6.1, +"src/components/rum/productAnalytics/funnelLayouts/rail/FunnelLayoutRail.spec.ts":6.8, +"src/components/rum/productAnalytics/funnelParts/AppFiltersSummary.spec.ts":3.6, +"src/components/rum/productAnalytics/funnelParts/FunnelBreakdownControl.spec.ts":5.2, +"src/components/rum/productAnalytics/funnelParts/exclusionPicker.spec.ts":6.4, +"src/components/rum/productAnalytics/funnelParts/funnelEdits.spec.ts":3.2, +"src/components/rum/productAnalytics/funnelParts/funnelFilterParts.spec.ts":7.4, +"src/components/rum/productAnalytics/funnelParts/funnelFilters.spec.ts":3.6, +"src/components/rum/sessionReplay/SessionLocationColumn.spec.ts":3.3, +"src/components/rum/sessionReplay/SessionsMetricsStrip.spec.ts":2.9, +"src/components/savedViews/SavedViewsMenu.spec.ts":9, +"src/components/savedViews/savedViewsSort.spec.ts":1.1, +"src/components/settings/AddRegexPattern.drafts.spec.ts":15, +"src/components/settings/AddRegexPattern.spec.ts":8.2, +"src/components/settings/AiToolsets.spec.ts":3.1, +"src/components/settings/AnnouncementBannerEditor.drafts.spec.ts":10.4, +"src/components/settings/AnnouncementBannerEditor.spec.ts":10.4, +"src/components/settings/AnnouncementBannerList.spec.ts":3.5, +"src/components/settings/BuiltInPatternsTab.spec.ts":5.5, +"src/components/settings/CipherKeys.spec.ts":8.3, +"src/components/settings/CorrelationSettings.spec.ts":2.8, +"src/components/settings/DiscoveredServices.spec.ts":7.2, +"src/components/settings/DomainManagement.integration.spec.ts":18.7, +"src/components/settings/DomainManagement.spec.ts":10.8, +"src/components/settings/General.spec.ts":8.9, +"src/components/settings/GeneralDangerZone.spec.ts":4.7, +"src/components/settings/ImportRegexPattern.spec.ts":4.9, +"src/components/settings/ImportSyntheticsLocations.spec.ts":5.4, +"src/components/settings/License.spec.ts":9.9, +"src/components/settings/LicenseStatusCard.spec.ts":2.2, +"src/components/settings/LicenseWaiversCard.spec.ts":2.6, +"src/components/settings/LinkedAccountsDialog.spec.ts":3.5, +"src/components/settings/LlmProvidersSettings.spec.ts":3.3, +"src/components/settings/ModelPricingEditor.drafts.spec.ts":10.2, +"src/components/settings/ModelPricingEditor.spec.ts":18.1, +"src/components/settings/ModelPricingList.spec.ts":21.3, +"src/components/settings/Nodes.spec.ts":16.8, +"src/components/settings/OrgDomainDetailsDialog.spec.ts":4.2, +"src/components/settings/OrgDomainMapping.schema.spec.ts":1.4, +"src/components/settings/OrgDomainMappings.spec.ts":4.2, +"src/components/settings/OrgDomainRequests.spec.ts":3.6, +"src/components/settings/OrgDomainRestrictions.rules.spec.ts":3.8, +"src/components/settings/OrgLinkedDomains.spec.ts":11.7, +"src/components/settings/OrgStorageEditor.spec.ts":5.6, +"src/components/settings/OrgStorageSettings.spec.ts":3.1, +"src/components/settings/OrganizationManagement.spec.ts":6.6, +"src/components/settings/OrganizationSettings.spec.ts":6.7, +"src/components/settings/PasswordPolicy.spec.ts":9.8, +"src/components/settings/RegexPatternList.spec.ts":5.3, +"src/components/settings/ServiceIdentitySetup.spec.ts":3.5, +"src/components/settings/Slack.spec.ts":5, +"src/components/settings/SlackChannelDefaults.spec.ts":8.7, +"src/components/settings/SlackConnectEmpty.spec.ts":1.7, +"src/components/settings/SlackCredentialsDialog.spec.ts":2.8, +"src/components/settings/SlackLinkedPeople.spec.ts":3.9, +"src/components/settings/SlackLinkingSection.spec.ts":5.6, +"src/components/settings/SlackOncallUnlinked.spec.ts":4.3, +"src/components/settings/SlackOrgAccess.spec.ts":5.5, +"src/components/settings/SlackPrerequisites.spec.ts":2.4, +"src/components/settings/SlackSelfHostedWizard.spec.ts":10.7, +"src/components/settings/SlackSwitchAllCard.spec.ts":2.7, +"src/components/settings/SlackUninstallDialog.spec.ts":4.3, +"src/components/settings/SlackWorkspaceCard.spec.ts":8.1, +"src/components/settings/SyntheticsLocationForm.spec.ts":3.7, +"src/components/settings/SyntheticsLocationsList.schema.spec.ts":1.9, +"src/components/settings/SyntheticsLocationsList.spec.ts":3.5, +"src/components/settings/TelephonySettings.spec.ts":9.3, +"src/components/settings/TestModelMatchDialog.spec.ts":4.1, +"src/components/settings/UtcHoursBar.spec.ts":2.4, +"src/components/settings/announcementDrafts.spec.ts":1.1, +"src/components/settings/index.spec.ts":2.6, +"src/components/settings/orgDomainRestrictions.spec.ts":1.4, +"src/components/settings/settingsIndex.spec.ts":1.8, +"src/components/settings/telephony.spec.ts":1.2, +"src/components/settings/useAnnouncementConfigUpdate.spec.ts":1.5, +"src/components/shared/HomeViewSkeleton.spec.ts":1.2, +"src/components/shared/OAgentBadges.spec.ts":1.3, +"src/components/shared/grid/NoData.spec.ts":1, +"src/components/shared/grid/NoOrganizationSelected.spec.ts":2.1, +"src/components/shared/grid/NoPanel.spec.ts":2.2, +"src/components/shared/grid/Pagination.spec.ts":2.2, +"src/components/slos/SloAlertCondition.spec.ts":1.7, +"src/components/slos/SloAlertForm.schema.spec.ts":2.4, +"src/components/slos/SloAlertForm.spec.ts":6.9, +"src/components/slos/SloAlertPreview.spec.ts":1.2, +"src/components/slos/SloBurndownChart.spec.ts":5.7, +"src/components/slos/SloExpressionField.spec.ts":1.7, +"src/components/slos/SloPreviewChart.spec.ts":3.5, +"src/components/slos/SloPreviewChartPromql.spec.ts":7.1, +"src/components/slos/SloTimeSlicePreview.spec.ts":6.1, +"src/components/synthetic-monitoring/AgentSetupDrawer.spec.ts":2.4, +"src/components/synthetic-monitoring/MonitorTable.spec.ts":4.5, +"src/components/synthetics/CheckTypePicker.spec.ts":3.5, +"src/components/synthetics/CreateBrowserTest.schema.spec.ts":2, +"src/components/synthetics/ExtensionSetupChecklist.spec.ts":2.1, +"src/components/synthetics/StepEvidence.spec.ts":1.7, +"src/components/synthetics/configure/CheckAlerts.spec.ts":2, +"src/components/synthetics/configure/CheckAuthNetwork.spec.ts":2.7, +"src/components/synthetics/configure/CheckBrowserDevices.spec.ts":2.1, +"src/components/synthetics/configure/CheckCapture.spec.ts":2.1, +"src/components/synthetics/configure/CheckConfigure.spec.ts":5.1, +"src/components/synthetics/configure/CheckDetails.spec.ts":3, +"src/components/synthetics/configure/CheckEnvironments.spec.ts":3.2, +"src/components/synthetics/configure/CheckLocations.spec.ts":2.7, +"src/components/synthetics/configure/CheckRUM.spec.ts":1.9, +"src/components/synthetics/configure/CheckRetries.spec.ts":1.9, +"src/components/synthetics/configure/CheckSchedule.spec.ts":6.2, +"src/components/synthetics/configure/CheckVariablesPanel.spec.ts":2.4, +"src/components/synthetics/configure/types/CheckHttpConfig.spec.ts":3.4, +"src/components/synthetics/configure/types/CheckSshConfig.spec.ts":1.3, +"src/components/synthetics/configure/types/CheckTcpConfig.spec.ts":1.6, +"src/components/synthetics/configure/types/CheckTlsConfig.spec.ts":1.3, +"src/components/synthetics/journey/BrowserJourney.spec.ts":24.8, +"src/components/synthetics/journey/BrowserJourneyAssertion.spec.ts":3, +"src/components/synthetics/journey/BrowserJourneyLocator.spec.ts":3.2, +"src/components/synthetics/journey/BrowserJourneyStep.spec.ts":9.3, +"src/components/synthetics/journey/BrowserJourneyStepEditor.spec.ts":10.7, +"src/components/synthetics/journey/BrowserJourneyStepError.spec.ts":1.6, +"src/components/synthetics/journey/ExtensionSetupDialog.spec.ts":2.5, +"src/components/synthetics/journey/ExtractSubtestDialog.spec.ts":5.9, +"src/components/synthetics/journey/JourneyStartPill.spec.ts":6.8, +"src/components/synthetics/journey/JourneyStepCount.spec.ts":1.4, +"src/components/synthetics/journey/JourneySteps.spec.ts":6.5, +"src/components/synthetics/journey/JourneySuggestions.spec.ts":4.8, +"src/components/synthetics/journey/JourneyUsedByPopover.spec.ts":5.1, +"src/components/synthetics/journey/LocatorComposeDialog.spec.ts":3.1, +"src/components/synthetics/journey/MissingValueDialog.spec.ts":3, +"src/components/synthetics/journey/ReplayEnvironmentMenu.spec.ts":1.5, +"src/components/synthetics/journey/ReplaySecretsDialog.spec.ts":1.9, +"src/components/synthetics/journey/SubtestMenu.spec.ts":9.9, +"src/components/synthetics/journey/SubtestMenuRow.spec.ts":1.9, +"src/components/synthetics/journey/SubtestPicker.spec.ts":4.4, +"src/components/synthetics/journey/startPillRules.spec.ts":0.7, +"src/components/synthetics/journey/useReplaySecrets.spec.ts":1.2, +"src/components/synthetics/journey/useSubtestCandidates.spec.ts":2.2, +"src/components/synthetics/results/EvidenceEventDetail.spec.ts":2.7, +"src/components/synthetics/results/EvidenceEvents.spec.ts":10.5, +"src/components/synthetics/results/EvidenceFilters.spec.ts":2.1, +"src/components/synthetics/results/EvidencePanel.spec.ts":12.1, +"src/components/synthetics/results/ExecutionDetailDrawer.spec.ts":1.6, +"src/components/synthetics/results/ProtocolRunSummary.spec.ts":4.9, +"src/components/synthetics/results/ProtocolRunSummarySkeleton.spec.ts":0.8, +"src/components/synthetics/results/RunDetailDrawer.spec.ts":2.3, +"src/components/synthetics/results/RunRowExpansion.spec.ts":2.4, +"src/components/synthetics/results/StepPageActivity.spec.ts":5.1, +"src/components/synthetics/variables/PromoteCheckVariableDialog.spec.ts":1.6, +"src/components/synthetics/variables/SyntheticsEnvironmentForm.spec.ts":1.5, +"src/components/synthetics/variables/SyntheticsInheritedVariables.spec.ts":3, +"src/components/synthetics/variables/SyntheticsScopeRail.spec.ts":2, +"src/components/synthetics/variables/SyntheticsSplitVariableDialog.spec.ts":2.2, +"src/components/synthetics/variables/SyntheticsVariableForm.spec.ts":2.5, +"src/components/synthetics/variables/SyntheticsVariablesList.spec.ts":4.7, +"src/components/synthetics/variables/SyntheticsVariablesTab.spec.ts":2.3, +"src/components/synthetics/variables/VariableSuggestionRow.spec.ts":2.1, +"src/components/synthetics/variables/notify.spec.ts":1, +"src/components/synthetics/variables/placeholders.spec.ts":1.4, +"src/components/synthetics/variables/replayInputs.spec.ts":0.9, +"src/components/synthetics/variables/resolved.spec.ts":1.1, +"src/components/synthetics/variables/scope.spec.ts":1.4, +"src/components/synthetics/variables/serverMessage.spec.ts":2.9, +"src/components/synthetics/variables/suggestions.spec.ts":1.2, +"src/components/synthetics/variables/usage.spec.ts":0.7, +"src/components/synthetics/variables/useSharedVariables.spec.ts":1.6, +"src/components/traces/FlameGraphView.spec.ts":13.9, +"src/components/workflows/WorkflowEditor.spec.ts":5.8, +"src/components/workflows/WorkflowLinkAlertsDialog.spec.ts":4.7, +"src/components/workflows/WorkflowNodeDrawer.spec.ts":6.6, +"src/components/workflows/WorkflowRunSwitcher.spec.ts":3.1, +"src/components/workflows/WorkflowRuns.spec.ts":3, +"src/components/workflows/WorkflowRunsPanel.spec.ts":6.8, +"src/components/workflows/WorkflowTestDialog.spec.ts":10.1, +"src/components/workflows/WorkflowView.spec.ts":3.5, +"src/components/workflows/WorkflowsList.spec.ts":6.6, +"src/components/workflows/nodeRunOutcome.spec.ts":4.8, +"src/composables/alertCalibration/useAlertCalibrationEnabled.spec.ts":1.4, +"src/composables/alerts/useAlertActions.spec.ts":1.7, +"src/composables/alerts/useAlertCreation.spec.ts":2.5, +"src/composables/alerts/useAlertLibrary.spec.ts":1.7, +"src/composables/alerts/useDependencyGraph.spec.ts":3.6, +"src/composables/alerts/useTemplatePreview.spec.ts":0.8, +"src/composables/contextProviders/alertsContextProvider.spec.ts":1.1, +"src/composables/contextProviders/dashboardsContextProvider.spec.ts":0.7, +"src/composables/contextProviders/dbmContextProvider.spec.ts":0.8, +"src/composables/contextProviders/defaultContextProvider.spec.ts":1.2, +"src/composables/contextProviders/incidentsContextProvider.spec.ts":0.8, +"src/composables/contextProviders/index.spec.ts":2.9, +"src/composables/contextProviders/logsContextProvider.spec.ts":1.9, +"src/composables/contextProviders/pipelinesContextProvider.spec.ts":1, +"src/composables/contextProviders/registry.spec.ts":0.9, +"src/composables/contextProviders/types.spec.ts":0.8, +"src/composables/dashboard/dashboardFreshPanelTime.spec.ts":2.2, +"src/composables/dashboard/panelRunOnce.spec.ts":2, +"src/composables/dashboard/promqlChunkProcessor.spec.ts":2.7, +"src/composables/dashboard/useAnnotations.spec.ts":1.9, +"src/composables/dashboard/useAnnotationsData.spec.ts":1.8, +"src/composables/dashboard/useAutoRunToggle.spec.ts":1.3, +"src/composables/dashboard/useCancelQuery.spec.ts":3.1, +"src/composables/dashboard/useColumnFormatting.spec.ts":4.1, +"src/composables/dashboard/useConfigPanel.search.spec.ts":3.3, +"src/composables/dashboard/useConfigPanel.spec.ts":2.5, +"src/composables/dashboard/useDashboardAnnotationSources.spec.ts":4, +"src/composables/dashboard/useDashboardPanel.filterValues.spec.ts":5.7, +"src/composables/dashboard/useDashboardPanel.groupedFieldsRace.spec.ts":2.4, +"src/composables/dashboard/useDashboardPanel.promqlLabels.spec.ts":3.1, +"src/composables/dashboard/useDashboardPanel.spec.ts":2.1, +"src/composables/dashboard/useDashboardPanelDefaults.spec.ts":1.9, +"src/composables/dashboard/useDashboardSaveConflict.spec.ts":2.4, +"src/composables/dashboard/useDashboardTimeGate.spec.ts":4.1, +"src/composables/dashboard/useDefaultPanelFields.spec.ts":1.1, +"src/composables/dashboard/useExemplarOverride.spec.ts":1.2, +"src/composables/dashboard/useExemplarTraceLookup.spec.ts":0.9, +"src/composables/dashboard/useKioskDisplay.spec.ts":1.5, +"src/composables/dashboard/useKioskRefresh.spec.ts":3.7, +"src/composables/dashboard/useLayoutEditDraft.spec.ts":1, +"src/composables/dashboard/usePanelActions.spec.ts":3.8, +"src/composables/dashboard/usePanelAggregation.spec.ts":3.4, +"src/composables/dashboard/usePanelCache.spec.ts":1.4, +"src/composables/dashboard/usePanelDataLoader.freshOnOpen.spec.ts":4.1, +"src/composables/dashboard/usePanelDataLoader.spec.ts":4, +"src/composables/dashboard/usePanelDraft.spec.ts":1.5, +"src/composables/dashboard/usePanelDrilldown.bar-gauge.spec.ts":2.8, +"src/composables/dashboard/usePanelDrilldown.boxplot.spec.ts":4.1, +"src/composables/dashboard/usePanelDrilldown.distribution.spec.ts":3.9, +"src/composables/dashboard/usePanelDrilldown.funnel.spec.ts":4.9, +"src/composables/dashboard/usePanelDrilldown.node-graph.spec.ts":3.7, +"src/composables/dashboard/usePanelDrilldown.spec.ts":3.5, +"src/composables/dashboard/usePanelDrilldown.sunburst.spec.ts":3.7, +"src/composables/dashboard/usePanelDrilldown.treemap.spec.ts":5.1, +"src/composables/dashboard/usePanelEditorFocus.spec.ts":1.4, +"src/composables/dashboard/usePanelExemplarInteraction.spec.ts":1.9, +"src/composables/dashboard/usePanelExemplars.spec.ts":1.5, +"src/composables/dashboard/usePanelFields.spec.ts":2.1, +"src/composables/dashboard/usePanelLivePreview.spec.ts":2, +"src/composables/dashboard/usePanelNotices.spec.ts":3.7, +"src/composables/dashboard/usePanelPromQLExecutor.spec.ts":2.7, +"src/composables/dashboard/usePanelRefreshAfterRestore.spec.ts":2.3, +"src/composables/dashboard/usePanelSQLExecutor.spec.ts":1.2, +"src/composables/dashboard/usePanelSearchHandlers.spec.ts":0.9, +"src/composables/dashboard/usePanelVariableSubstitution.spec.ts":1.2, +"src/composables/dashboard/usePlaylistFrame.spec.ts":2.6, +"src/composables/dashboard/usePlaylistPlayer.spec.ts":1.7, +"src/composables/dashboard/useQueryFields.spec.ts":2.2, +"src/composables/dashboard/useSeriesFormatting.promql.spec.ts":5.6, +"src/composables/dashboard/useSeriesFormatting.spec.ts":2.2, +"src/composables/dashboard/useValuesWebSocket.spec.ts":1.7, +"src/composables/dashboard/useVariableDebugger.spec.ts":1, +"src/composables/dashboard/useVariablesManager.spec.ts":1.9, +"src/composables/dbm/dbmQueryDetailSeed.spec.ts":1.5, +"src/composables/dbm/dbmScopeFilterSync.spec.ts":0.9, +"src/composables/dbm/dbmTabCountsResilience.spec.ts":3, +"src/composables/dbm/dbmTabCountsTier.spec.ts":1, +"src/composables/dbm/useDbmChartTheme.spec.ts":1.1, +"src/composables/dbm/useDbmFleetInstances.spec.ts":0.9, +"src/composables/dbm/useDbmListPage.spec.ts":4.1, +"src/composables/dbm/useDbmQueryDetailHop.spec.ts":1.5, +"src/composables/dbm/useDbmRequestSeq.spec.ts":1, +"src/composables/dbm/useDbmScope.spec.ts":0.7, +"src/composables/dbm/useDbmScopeFilters.spec.ts":1.7, +"src/composables/dbm/useDbmScopeSync.spec.ts":1.1, +"src/composables/dbm/useDbmSearchEmpty.spec.ts":2.1, +"src/composables/dbm/useDbmTracePresence.spec.ts":1.6, +"src/composables/dbm/useDbmTraceVantage.spec.ts":1, +"src/composables/downtimes/useCountdown.spec.ts":3.9, +"src/composables/downtimes/useDowntimeLookup.spec.ts":0.8, +"src/composables/downtimes/useDowntimeValueSuggest.spec.ts":3.5, +"src/composables/downtimes/useDowntimesEnabled.spec.ts":4, +"src/composables/downtimes/useExtendDowntime.spec.ts":2.5, +"src/composables/downtimes/useListBoundaryRefetch.spec.ts":1.3, +"src/composables/downtimes/useNowMicros.spec.ts":1.1, +"src/composables/downtimes/useViewerTimezone.spec.ts":2.2, +"src/composables/fieldValueDB.spec.ts":2, +"src/composables/fieldValueStore.serverFetch.spec.ts":1.3, +"src/composables/fieldValueStore.spec.ts":1.5, +"src/composables/firstEvent/confirmQuery.spec.ts":0.8, +"src/composables/firstEvent/firstEventBudget.spec.ts":1.7, +"src/composables/firstEvent/useFirstDataNotice.spec.ts":1.1, +"src/composables/firstEvent/useFirstEventWatch.spec.ts":3.6, +"src/composables/flow/detectCycle.spec.ts":0.9, +"src/composables/flow/makeEdge.spec.ts":1.1, +"src/composables/iam/useModuleNavigation.spec.ts":1.1, +"src/composables/iam/usePermissions.spec.ts":1.3, +"src/composables/iam/useRoleEntityLoaders.spec.ts":3.3, +"src/composables/iam/useRoleGrants.spec.ts":1, +"src/composables/iam/useRolePermissionRows.spec.ts":1.7, +"src/composables/iam/useRolePermissionTree.spec.ts":0.8, +"src/composables/iam/useRolePresets.spec.ts":2, +"src/composables/iam/useRoleSave.spec.ts":1.1, +"src/composables/iam/useRoleScopes.spec.ts":1.1, +"src/composables/iam/useRoleSummary.spec.ts":0.9, +"src/composables/metrics/metricExportInterval.spec.ts":1, +"src/composables/metrics/metricsUrlState.spec.ts":2, +"src/composables/metrics/useFilterHint.spec.ts":1.2, +"src/composables/metrics/useMetricDrilldown.spec.ts":3.8, +"src/composables/metrics/useMetricsExplorerExemplars.spec.ts":1.3, +"src/composables/metrics/useMetricsExplorerGrid.spec.ts":4.3, +"src/composables/metrics/useMetricsPreviewQueue.spec.ts":3, +"src/composables/query/fetchInto.spec.ts":1.1, +"src/composables/query/queryClient.spec.ts":0.8, +"src/composables/queryHistory/useRecentEntryText.spec.ts":4.1, +"src/composables/queryHistory/useRecentSearchesEditorHost.spec.ts":0.8, +"src/composables/router.spec.ts":1, +"src/composables/rum/scopedList.spec.ts":0.8, +"src/composables/rum/useAnalyticsSearch.spec.ts":2.8, +"src/composables/rum/useCorrelatedTracesStream.spec.ts":3.8, +"src/composables/rum/useErrorDetail.spec.ts":4, +"src/composables/rum/useErrorIssuesData.spec.ts":2, +"src/composables/rum/useFunnelDraft.spec.ts":3.5, +"src/composables/rum/useFunnelTrend.spec.ts":2.3, +"src/composables/rum/useMobileSessionReplay.spec.ts":0.8, +"src/composables/rum/useNamedEvents.spec.ts":4.6, +"src/composables/rum/usePerformance.spec.ts":1.2, +"src/composables/rum/useProductAnalytics.spec.ts":3.6, +"src/composables/rum/useRum.spec.ts":0.7, +"src/composables/rum/useRumOverviewData.spec.ts":3, +"src/composables/rum/useRumPerformanceTab.spec.ts":1.5, +"src/composables/rum/useRumPlatforms.spec.ts":1.9, +"src/composables/rum/useRumSpanBuilder.spec.ts":3.3, +"src/composables/rum/useSavedFunnels.spec.ts":1.5, +"src/composables/rum/useSessionHeatmap.spec.ts":2.7, +"src/composables/rum/useTraceCorrelation.spec.ts":2, +"src/composables/shared/router.spec.ts":7.6, +"src/composables/shared/slackRouteGuard.spec.ts":0.9, +"src/composables/shared/useEnterpriseRoutes.spec.ts":2.5, +"src/composables/shared/useIngestionRoutes.spec.ts":1.6, +"src/composables/shared/useManagementRoutes.spec.ts":1.2, +"src/composables/slack/useSlackAppSwitch.spec.ts":1.9, +"src/composables/slack/useSlackOAuthPopup.spec.ts":1, +"src/composables/synthetics/attemptViews.spec.ts":0.9, +"src/composables/synthetics/syntheticResultsSchema.spec.ts":1.9, +"src/composables/synthetics/useEvidenceFilters.spec.ts":1.6, +"src/composables/traces/useSpanEvents.spec.ts":1.1, +"src/composables/traces/useTraceDetails.spec.ts":1.6, +"src/composables/traces/useTraceProcessing.spec.ts":8.8, +"src/composables/useAccessStore.classify.spec.ts":1.8, +"src/composables/useAccessStore.spec.ts":2.2, +"src/composables/useAiChat.spec.ts":1.7, +"src/composables/useAlertForm.anomalyDefaults.spec.ts":4.2, +"src/composables/useAlertForm.spec.ts":4.2, +"src/composables/useAlertInsights.spec.ts":2, +"src/composables/useAnalyzingMessage.spec.ts":0.7, +"src/composables/useAnnouncementBanners.spec.ts":1.4, +"src/composables/useAutoName.spec.ts":1.1, +"src/composables/useAutoNavigationPreferences.spec.ts":0.8, +"src/composables/useBreakpoint.spec.ts":1.1, +"src/composables/useChatHistory.spec.ts":1.2, +"src/composables/useChatHistoryList.spec.ts":1.7, +"src/composables/useChatImages.spec.ts":1.4, +"src/composables/useChatScroll.spec.ts":0.9, +"src/composables/useChatStream.spec.ts":2.5, +"src/composables/useChunkedContent.spec.ts":1.3, +"src/composables/useColorizedQuery.spec.ts":1.4, +"src/composables/useConfirmDialog.spec.ts":1.1, +"src/composables/useCopyFeedback.spec.ts":1, +"src/composables/useCorrelatedLogs.spec.ts":3, +"src/composables/useCorrelationDefaultSlug.spec.ts":0.7, +"src/composables/useDashboardGallery.spec.ts":1.8, +"src/composables/useDashboardPanel.spec.ts":5.4, +"src/composables/useDimensionSelector.spec.ts":1.6, +"src/composables/useDurationPercentiles.spec.ts":3.2, +"src/composables/useEditorDraft.router.spec.ts":2, +"src/composables/useEditorDraft.spec.ts":4.4, +"src/composables/useEditorSuggestMode.spec.ts":1, +"src/composables/useErrorTracking.spec.ts":0.8, +"src/composables/useEventFormatters.spec.ts":1.3, +"src/composables/useFavoriteDashboards.spec.ts":2, +"src/composables/useFavoriteSavedViews.spec.ts":2.3, +"src/composables/useFieldValuesStream.spec.ts":0.9, +"src/composables/useFolderIcons.spec.ts":0.7, +"src/composables/useFullscreenBox.spec.ts":1.3, +"src/composables/useFunctions.spec.ts":1.2, +"src/composables/useHideInternalStreams.spec.ts":1.2, +"src/composables/useHomeDashboard.spec.ts":0.7, +"src/composables/useHostMetricsDashboard.spec.ts":1.1, +"src/composables/useIncidentRca.spec.ts":3.8, +"src/composables/useIngestion.spec.ts":1.6, +"src/composables/useIsMetaOrg.spec.ts":0.9, +"src/composables/useLatencyInsightsAnalysis.spec.ts":3, +"src/composables/useLatencyInsightsDashboard.spec.ts":0.9, +"src/composables/useLicenseRefresh.spec.ts":1, +"src/composables/useListBackNavigation.spec.ts":0.9, +"src/composables/useListViewState.spec.ts":1.9, +"src/composables/useLoading.spec.ts":1.1, +"src/composables/useLogs.landingLoading.spec.ts":5.3, +"src/composables/useLogs.landingSupersede.spec.ts":4.2, +"src/composables/useLogs.schemaRefresh.spec.ts":4.6, +"src/composables/useLogs.spec.ts":6.6, +"src/composables/useLogs.updateStreams.spec.ts":3.2, +"src/composables/useLogs.urlPrefetch.spec.ts":3.1, +"src/composables/useLogs/freeTextSearch.spec.ts":3.6, +"src/composables/useLogs/histogramMixedBreakdownCapture.spec.ts":2.1, +"src/composables/useLogs/histogramStreamCapture.spec.ts":3.9, +"src/composables/useLogs/logsAutoRun.spec.ts":2.5, +"src/composables/useLogs/logsRowNav.spec.ts":1.1, +"src/composables/useLogs/logsUtils.spec.ts":3.5, +"src/composables/useLogs/logsUtils.urlRegistry.spec.ts":2.5, +"src/composables/useLogs/logsVisualization.spec.ts":2.3, +"src/composables/useLogs/searchAround.spec.ts":3.8, +"src/composables/useLogs/searchState.spec.ts":6.2, +"src/composables/useLogs/settledResults.spec.ts":1.2, +"src/composables/useLogs/useAutoRun.spec.ts":2.4, +"src/composables/useLogs/useFreeTextPreview.spec.ts":4.5, +"src/composables/useLogs/useHistogram.spec.ts":3, +"src/composables/useLogs/useLogLineLink.spec.ts":1.8, +"src/composables/useLogs/useLogPermalink.spec.ts":1.9, +"src/composables/useLogs/useLogSeverity.spec.ts":3.6, +"src/composables/useLogs/useLogsUrl.spec.ts":2.8, +"src/composables/useLogs/useLogsUrlSync.spec.ts":3.9, +"src/composables/useLogs/usePagination.spec.ts":4.4, +"src/composables/useLogs/useResultsLoadingIndicator.spec.ts":1.1, +"src/composables/useLogs/useSearchBar.spec.ts":2.7, +"src/composables/useLogs/useSearchHistogramManager.spec.ts":3.3, +"src/composables/useLogs/useSearchPagination.spec.ts":0.7, +"src/composables/useLogs/useSearchQuery.spec.ts":3.6, +"src/composables/useLogs/useSearchResponseHandler.spec.ts":3.2, +"src/composables/useLogs/useSearchStream.history.spec.ts":2.3, +"src/composables/useLogs/useSearchStream.spec.ts":3.5, +"src/composables/useLogs/useStreamFields.correlation.spec.ts":5.2, +"src/composables/useLogs/useStreamFields.spec.ts":2.1, +"src/composables/useLogs/useTemplateTokenizer.spec.ts":1, +"src/composables/useLogs/useViewTraceAction.spec.ts":0.8, +"src/composables/useLogsHighlighter.fieldScope.spec.ts":1.4, +"src/composables/useLogsHighlighter.spec.ts":1.2, +"src/composables/useMcpCredential.spec.ts":1.6, +"src/composables/useMetrics.spec.ts":1.1, +"src/composables/useMetricsCorrelationDashboard.spec.ts":1.2, +"src/composables/useMetricsExplorer.spec.ts":1.9, +"src/composables/useNLQuery.spec.ts":1.6, +"src/composables/useNotifications.spec.ts":1.2, +"src/composables/useNow.spec.ts":2.1, +"src/composables/useOnCallClock.spec.ts":1, +"src/composables/useOnCallPermissions.spec.ts":1.1, +"src/composables/useOrgCredential.spec.ts":1.4, +"src/composables/usePaidOverageConsent.spec.ts":1.2, +"src/composables/useParser.spec.ts":0.8, +"src/composables/usePasswordReset.spec.ts":0.9, +"src/composables/usePipelines.spec.ts":2.4, +"src/composables/usePrebuiltDestinations.spec.ts":2.5, +"src/composables/usePredefinedThemes.spec.ts":0.8, +"src/composables/usePromptHistory.spec.ts":0.8, +"src/composables/usePromqlSuggestions.spec.ts":3.2, +"src/composables/usePromqlSuggestions.streamSources.spec.ts":5.3, +"src/composables/useQuery.spec.ts":2, +"src/composables/useRateLimiter.spec.ts":1.1, +"src/composables/useRcaSteering.spec.ts":2.3, +"src/composables/useRcaTrigger.spec.ts":2.1, +"src/composables/useResizer.spec.ts":1.8, +"src/composables/useRoutePrefetch.spec.ts":0.9, +"src/composables/useSearchInputUsingRegex.spec.ts":0.8, +"src/composables/useSearchWebSocket.spec.ts":1.8, +"src/composables/useSelectAutocomplete.spec.ts":1, +"src/composables/useServiceCorrelation.spec.ts":1.6, +"src/composables/useSessionReplay.spec.ts":1.3, +"src/composables/useSetupDashboardImport.spec.ts":1.1, +"src/composables/useSmartBack.spec.ts":0.9, +"src/composables/useSqlEditorDiagnostics.spec.ts":2, +"src/composables/useStickyColumns.spec.ts":1.9, +"src/composables/useStreamPickerOptions.spec.ts":1.8, +"src/composables/useStreamingSearch.spec.ts":1.9, +"src/composables/useStreams.spec.ts":2.1, +"src/composables/useSuggestions.serverCatalog.spec.ts":1, +"src/composables/useSuggestions.spec.ts":1.9, +"src/composables/useSyntheticEvidence.spec.ts":3.5, +"src/composables/useSyntheticResults.spec.ts":1, +"src/composables/useSyntheticsRecorder.spec.ts":1.2, +"src/composables/useTextHighlighter.spec.ts":1, +"src/composables/useThemeCustomizer.spec.ts":1.8, +"src/composables/useTimeRangeHistory.spec.ts":1, +"src/composables/useTimezoneOptions.spec.ts":2.3, +"src/composables/useToolbarPins.spec.ts":2, +"src/composables/useTracePatternTree.spec.ts":4.8, +"src/composables/useTraces.spec.ts":3.8, +"src/composables/useTreeVisualization.spec.ts":2.7, +"src/composables/useTrialPaywall.spec.ts":2, +"src/composables/useTypewriter.spec.ts":0.9, +"src/composables/useWebSocket.spec.ts":1.1, +"src/composables/useWidenRange.spec.ts":1.7, +"src/composables/useWorker.spec.ts":1.9, +"src/composables/useWorkloadDetection.spec.ts":1.8, +"src/constants/alertLibrary.spec.ts":0.9, +"src/constants/synthetics.spec.ts":1.1, +"src/constants/themes.spec.ts":0.9, +"src/enterprise/components/AIObservability/AddToDatasetDrawer.spec.ts":1.6, +"src/enterprise/components/AIObservability/AddToQueueMenu.spec.ts":4.7, +"src/enterprise/components/AIObservability/AgentScopeCascade.spec.ts":2.6, +"src/enterprise/components/AIObservability/AiNoAgentsEmpty.spec.ts":1.5, +"src/enterprise/components/AIObservability/AiPageShell.spec.ts":4.5, +"src/enterprise/components/AIObservability/AiScopeBar.spec.ts":1.8, +"src/enterprise/components/AIObservability/AnnotateDrawer.spec.ts":2, +"src/enterprise/components/AIObservability/DatasetItemDetail.spec.ts":2.4, +"src/enterprise/components/AIObservability/EnqueueStatusChip.spec.ts":1.4, +"src/enterprise/components/AIObservability/ExperimentBrowser.spec.ts":5.5, +"src/enterprise/components/AIObservability/ExperimentComparePickerDialog.spec.ts":3.2, +"src/enterprise/components/AIObservability/ExperimentComparisonPanel.spec.ts":1.6, +"src/enterprise/components/AIObservability/ExperimentComparisonRowDrawer.spec.ts":3.5, +"src/enterprise/components/AIObservability/ExperimentForm.drafts.spec.ts":10.5, +"src/enterprise/components/AIObservability/ExperimentForm.schema.prefill.spec.ts":0.8, +"src/enterprise/components/AIObservability/ExperimentForm.spec.ts":14.6, +"src/enterprise/components/AIObservability/ExperimentRowContentBox.spec.ts":1.9, +"src/enterprise/components/AIObservability/ExperimentRowDetailDrawer.spec.ts":2.3, +"src/enterprise/components/AIObservability/PlaygroundExpectedBar.spec.ts":1.8, +"src/enterprise/components/AIObservability/PlaygroundMessageList.spec.ts":1.4, +"src/enterprise/components/AIObservability/PlaygroundOutputCell.spec.ts":2.4, +"src/enterprise/components/AIObservability/PlaygroundSchemaDialog.spec.ts":1.7, +"src/enterprise/components/AIObservability/PlaygroundToolsDialog.spec.ts":2.1, +"src/enterprise/components/AIObservability/PlaygroundVariableBar.spec.ts":1.9, +"src/enterprise/components/AIObservability/PlaygroundVariablesMenu.spec.ts":2.1, +"src/enterprise/components/AIObservability/PlaygroundVariantColumn.spec.ts":5.5, +"src/enterprise/components/AIObservability/PlaygroundVariantHeader.spec.ts":4.2, +"src/enterprise/components/AIObservability/RemoteTaskCredentialsDialog.spec.ts":4.7, +"src/enterprise/components/AIObservability/RemoteTaskForm.schema.spec.ts":1.4, +"src/enterprise/components/AIObservability/RemoteTaskFormPage.drafts.spec.ts":5.4, +"src/enterprise/components/AIObservability/RemoteTaskFormPage.spec.ts":18.1, +"src/enterprise/components/AIObservability/RemoteTaskSigningPanel.spec.ts":3.7, +"src/enterprise/components/AIObservability/RemoteTaskTestPanel.spec.ts":1.7, +"src/enterprise/components/AIObservability/RemoteTaskTestRunPanel.spec.ts":2.4, +"src/enterprise/components/AIObservability/TraceAnnotateMenu.spec.ts":3, +"src/enterprise/components/AIObservability/VersionCompareBanner.spec.ts":1.1, +"src/enterprise/components/AIObservability/VersionCompareBar.spec.ts":1.4, +"src/enterprise/components/AIObservability/VersionCompareView.spec.ts":5.1, +"src/enterprise/components/AIObservability/VersionDeltaStrip.spec.ts":1.5, +"src/enterprise/components/AIObservability/VersionErrorDiff.spec.ts":1.6, +"src/enterprise/components/AIObservability/VersionOverlayChart.spec.ts":3.2, +"src/enterprise/components/AIObservability/VersionWindowCard.spec.ts":1.3, +"src/enterprise/components/AIObservability/experimentRowContent.spec.ts":0.9, +"src/enterprise/components/AIObservability/remoteTaskContent.spec.ts":1.5, +"src/enterprise/components/OnlineEvals.drafts.spec.ts":21.1, +"src/enterprise/components/OnlineEvals.spec.ts":6.7, +"src/enterprise/components/billings/Billing.spec.ts":6.6, +"src/enterprise/components/billings/BillingGroup.spec.ts":5.5, +"src/enterprise/components/billings/LicensePeriod.spec.ts":1.8, +"src/enterprise/components/billings/LicenseSearchBanner.spec.ts":4.1, +"src/enterprise/components/billings/PaidAiUsageControl.spec.ts":1.4, +"src/enterprise/components/billings/TrialPeriod.spec.ts":2.4, +"src/enterprise/components/billings/enterprisePlan.spec.ts":3.5, +"src/enterprise/components/billings/invoiceHistory.spec.ts":1.6, +"src/enterprise/components/billings/invoiceTable.spec.ts":8.4, +"src/enterprise/components/billings/plans.spec.ts":2.7, +"src/enterprise/components/billings/proPlan.spec.ts":1.8, +"src/enterprise/components/billings/usage/UsageBudget.spec.ts":6.4, +"src/enterprise/components/billings/usage/UsageCachedChart.spec.ts":3.5, +"src/enterprise/components/billings/usage/UsageEstimator.spec.ts":4.1, +"src/enterprise/components/billings/usage/UsageMeterBreakdown.spec.ts":3.7, +"src/enterprise/components/billings/usage/UsageMeterTable.spec.ts":2.9, +"src/enterprise/components/billings/usage/UsageOverview.spec.ts":11, +"src/enterprise/components/billings/usage/UsageTrends.spec.ts":10.2, +"src/enterprise/components/billings/usage/UsageView.spec.ts":6.6, +"src/enterprise/components/billings/usage/budgetModel.spec.ts":2, +"src/enterprise/components/billings/usage/meteringMock.spec.ts":1.8, +"src/enterprise/components/billings/usage/meteringModel.spec.ts":3.8, +"src/enterprise/components/billings/usage/totalsBar.spec.ts":1, +"src/enterprise/components/billings/usage/trendsModel.spec.ts":4, +"src/enterprise/components/billings/usage/usageCharts.spec.ts":5.5, +"src/enterprise/components/billings/usage/usageQueries.spec.ts":1.6, +"src/enterprise/components/billings/usage/useOrgCosts.spec.ts":3.2, +"src/enterprise/components/billings/usage/useUsageSearch.spec.ts":1.3, +"src/enterprise/components/onlineEvals/EvalJobList.spec.ts":3.3, +"src/enterprise/components/onlineEvals/EvalListShell.spec.ts":1.1, +"src/enterprise/components/onlineEvals/ManualEvaluationDialog.spec.ts":3.5, +"src/enterprise/components/onlineEvals/QualityPage.spec.ts":7.8, +"src/enterprise/components/onlineEvals/TraceScoreChips.spec.ts":4.3, +"src/enterprise/components/onlineEvals/composables/useEvalJobRuns.spec.ts":1.1, +"src/enterprise/components/onlineEvals/composables/useJobMatchedTargets.spec.ts":2.6, +"src/enterprise/components/onlineEvals/composables/useOnlineEvalsData.spec.ts":2.3, +"src/enterprise/components/onlineEvals/composables/useQualityDetail.spec.ts":2.5, +"src/enterprise/components/onlineEvals/composables/useQualityList.spec.ts":2, +"src/enterprise/components/onlineEvals/composables/useScorerRuns.spec.ts":1.4, +"src/enterprise/components/onlineEvals/composables/useTraceScoreChips.spec.ts":3.6, +"src/enterprise/components/onlineEvals/forms/JobFormPage.spec.ts":9.7, +"src/enterprise/components/onlineEvals/forms/ProviderFormPage.spec.ts":4.9, +"src/enterprise/components/onlineEvals/forms/ScoreConfigDialog.spec.ts":3, +"src/enterprise/components/onlineEvals/forms/ScorerFormPage.spec.ts":8, +"src/enterprise/components/onlineEvals/forms/job/JobFilterBuilder.spec.ts":3.9, +"src/enterprise/components/onlineEvals/forms/job/JobInputMapping.spec.ts":7.9, +"src/enterprise/components/onlineEvals/forms/job/JobPreviewPanel.spec.ts":1.3, +"src/enterprise/components/onlineEvals/forms/job/SpanSelectorBinding.schema.spec.ts":1.1, +"src/enterprise/components/onlineEvals/forms/job/SpanSelectorBinding.spec.ts":3.4, +"src/enterprise/components/onlineEvals/quality/QualityConfigDetail.spec.ts":3.6, +"src/enterprise/components/onlineEvals/quality/QualityConfigsTable.spec.ts":6.3, +"src/enterprise/components/onlineEvals/quality/QualityDistributionChart.spec.ts":2.3, +"src/enterprise/components/onlineEvals/quality/QualityScorePane.spec.ts":7.9, +"src/enterprise/components/onlineEvals/quality/QualityTrendChart.spec.ts":2.1, +"src/enterprise/components/onlineEvals/utils/agentFilterSql.spec.ts":0.9, +"src/enterprise/components/onlineEvals/utils/completionWindow.spec.ts":0.8, +"src/enterprise/components/onlineEvals/utils/evalEntity.spec.ts":1.3, +"src/enterprise/components/onlineEvals/utils/evalFormat.spec.ts":1, +"src/enterprise/components/onlineEvals/utils/exportScoreConfig.spec.ts":1.6, +"src/enterprise/components/onlineEvals/utils/exportScorer.spec.ts":1.1, +"src/enterprise/components/onlineEvals/utils/importScoreConfig.spec.ts":1.4, +"src/enterprise/components/onlineEvals/utils/importScorer.spec.ts":2.1, +"src/enterprise/components/onlineEvals/utils/jobFilter.spec.ts":2.4, +"src/enterprise/components/onlineEvals/utils/jobMappings.spec.ts":1.8, +"src/enterprise/components/onlineEvals/utils/jobSampling.spec.ts":1, +"src/enterprise/components/onlineEvals/utils/latestScoreSql.spec.ts":1.1, +"src/enterprise/components/onlineEvals/utils/qualityFormat.spec.ts":4.3, +"src/enterprise/components/onlineEvals/utils/routeSync.spec.ts":0.9, +"src/enterprise/components/onlineEvals/utils/systemProvidedVariables.spec.ts":0.7, +"src/enterprise/composables/router.spec.ts":1.5, +"src/enterprise/composables/useAgentScope.spec.ts":1.5, +"src/enterprise/composables/useAiDateController.spec.ts":0.7, +"src/enterprise/composables/useAiDateRange.spec.ts":1, +"src/enterprise/composables/useAiFilterUrlSync.spec.ts":0.9, +"src/enterprise/composables/useChildRefresh.spec.ts":1.1, +"src/enterprise/composables/useFilterMode.spec.ts":0.9, +"src/enterprise/composables/useLlmTraceStreams.spec.ts":1, +"src/enterprise/mixins/mainLayout.mixin.spec.ts":3.5, +"src/enterprise/views/AIObservability/AgentBehaviorPanel.spec.ts":1.2, +"src/enterprise/views/AIObservability/AgentGraphPage.spec.ts":3.3, +"src/enterprise/views/AIObservability/AgentSignalDetailPanel.spec.ts":2.5, +"src/enterprise/views/AIObservability/DatasetDetailPage.spec.ts":2.8, +"src/enterprise/views/AIObservability/DiscoveryPage.spec.ts":4.5, +"src/enterprise/views/AIObservability/ExperimentComparePage.spec.ts":1.8, +"src/enterprise/views/AIObservability/ExperimentDetailPage.spec.ts":4.8, +"src/enterprise/views/AIObservability/ExperimentsPage.spec.ts":4.3, +"src/enterprise/views/AIObservability/Index.spec.ts":4.3, +"src/enterprise/views/AIObservability/PlaygroundPage.spec.ts":5.5, +"src/enterprise/views/AIObservability/QueueDetailPage.spec.ts":4.8, +"src/enterprise/views/AIObservability/QueueWorkbenchPage.backTarget.spec.ts":2.7, +"src/enterprise/views/AIObservability/QueueWorkbenchPage.nav.spec.ts":5, +"src/enterprise/views/AIObservability/RemoteTaskDetailPage.spec.ts":4.1, +"src/enterprise/views/AIObservability/RemoteTasksPage.spec.ts":7.1, +"src/enterprise/views/AIObservability/ReviewContentBox.spec.ts":1.8, +"src/enterprise/views/AIObservability/SessionsPage.spec.ts":1, +"src/enterprise/views/AIObservability/experimentDiscovery.spec.ts":0.7, +"src/enterprise/views/AIObservability/experimentPreview.spec.ts":1.5, +"src/enterprise/views/AIObservability/experimentResults.spec.ts":1, +"src/enterprise/views/AIObservability/experimentRoutes.spec.ts":1.1, +"src/enterprise/views/AIObservability/navSection.spec.ts":1, +"src/enterprise/views/AIObservability/playgroundDraft.spec.ts":1, +"src/enterprise/views/AIObservability/playgroundHandoff.spec.ts":1.1, +"src/layouts/MainLayout.aiChat.spec.ts":8.6, +"src/layouts/MainLayout.bootGate.spec.ts":4.5, +"src/layouts/MainLayout.emptyData.spec.ts":5.6, +"src/layouts/MainLayout.keepalive.spec.ts":3.2, +"src/layouts/MainLayout.nav.spec.ts":7.1, +"src/layouts/MainLayout.orgSettings.spec.ts":5.2, +"src/layouts/MainLayout.rum.spec.ts":5.9, +"src/layouts/MainLayout.setupContext.spec.ts":5.5, +"src/layouts/MainLayout.shortcuts.spec.ts":6.5, +"src/layouts/MainLayout.signOutDrafts.spec.ts":5.5, +"src/layouts/MainLayout.signOutEditor.spec.ts":5.8, +"src/layouts/MainLayout.spec.ts":4.1, +"src/lib/core/Avatar/OAvatar.spec.ts":1.4, +"src/lib/core/Badge/OBadge.spec.ts":2.5, +"src/lib/core/Badge/ODimensionChip.spec.ts":1.6, +"src/lib/core/Badge/OTag.spec.ts":1.5, +"src/lib/core/Badge/badgeGroups.spec.ts":1.4, +"src/lib/core/Button/OButton.spec.ts":2.8, +"src/lib/core/Button/OButtonGroup.spec.ts":1.4, +"src/lib/core/Button/iconOnlyName.spec.ts":1.2, +"src/lib/core/Card/OCard.spec.ts":1.3, +"src/lib/core/Card/OCardActions.spec.ts":1.1, +"src/lib/core/Card/OCardSection.spec.ts":1, +"src/lib/core/Code/OCode.spec.ts":2.2, +"src/lib/core/Code/OCodeBlock.spec.ts":3.4, +"src/lib/core/Code/highlighter.spec.ts":0.7, +"src/lib/core/Collapsible/OCollapsible.spec.ts":2.9, +"src/lib/core/EmptyState/EmptyStateActionCard.spec.ts":1.7, +"src/lib/core/EmptyState/OEmptyState.spec.ts":2, +"src/lib/core/EmptyState/illustrations/EmptyServiceGraph.spec.ts":1.9, +"src/lib/core/EmptyState/illustrations/EmptyServicesCatalog.spec.ts":1.6, +"src/lib/core/Icon/OIcon.spec.ts":2, +"src/lib/core/Navbar/ONavGroup.spec.ts":5.2, +"src/lib/core/Navbar/ONavbar.spec.ts":2.4, +"src/lib/core/Navbar/hoverIntent.spec.ts":1.4, +"src/lib/core/Navbar/navGroups.spec.ts":1.8, +"src/lib/core/Navbar/railFocus.spec.ts":1.5, +"src/lib/core/Navbar/railReveal.spec.ts":1.3, +"src/lib/core/PageLayout/OPageLayout.spec.ts":2.2, +"src/lib/core/RefreshButton/ORefreshButton.spec.ts":1.7, +"src/lib/core/Separator/OSeparator.spec.ts":1.8, +"src/lib/core/SettingRow/OSettingRow.spec.ts":1.6, +"src/lib/core/SettingRow/OSettingRowPair.spec.ts":1.3, +"src/lib/core/Shortcut/OShortcut.spec.ts":1.3, +"src/lib/core/Splitter/OSplitter.spec.ts":0.9, +"src/lib/core/Table/OTable.bench.spec.ts":4, +"src/lib/core/Table/OTable.callSites.spec.ts":5, +"src/lib/core/Table/OTable.flexwidth.spec.ts":4.6, +"src/lib/core/Table/OTable.refetch.spec.ts":6, +"src/lib/core/Table/OTable.spec.ts":40, +"src/lib/core/Table/OTable.wrapMeasure.spec.ts":3, +"src/lib/core/Table/cells/OCodeCell.spec.ts":2.7, +"src/lib/core/Table/cells/ODataBarCell.spec.ts":1.8, +"src/lib/core/Table/cells/OTimeCell.spec.ts":3.1, +"src/lib/core/Table/cells/OUserCell.spec.ts":2, +"src/lib/core/Table/cells/statusVariant.spec.ts":0.9, +"src/lib/core/Table/composables/useTableVirtualization.spec.ts":0.9, +"src/lib/core/Table/sub-components/OTableBodyRow.spec.ts":1.6, +"src/lib/core/Table/sub-components/OTableColumnToggle.spec.ts":1.7, +"src/lib/core/Table/sub-components/OTableOverflowTooltip.spec.ts":2.1, +"src/lib/core/ToggleGroup/OFormToggleGroup.spec.ts":1.6, +"src/lib/core/ToggleGroup/OToggleGroup.spec.ts":1.5, +"src/lib/core/ToggleGroup/OToggleGroupItem.spec.ts":2.1, +"src/lib/core/Typography/OText.spec.ts":1.1, +"src/lib/core/Typography/OTruncatedText.spec.ts":1.4, +"src/lib/core/VirtualScroll/OVirtualScroll.spec.ts":1.2, +"src/lib/data/CoverageMeter/OCoverageMeter.spec.ts":3.1, +"src/lib/data/ProgressBar/OProgressBar.spec.ts":1.3, +"src/lib/data/ScheduleTimeline/OScheduleTimeline.spec.ts":2.7, +"src/lib/data/SegmentedBar/OSegmentedBar.spec.ts":1.2, +"src/lib/data/Sparkline/OSparkline.spec.ts":1.2, +"src/lib/data/StatStrip/OStatStrip.spec.ts":2.4, +"src/lib/data/Timeline/OTimeline.spec.ts":0.7, +"src/lib/data/Timeline/OTimelineItem.spec.ts":2, +"src/lib/data/Tree/OTree.spec.ts":1.1, +"src/lib/feedback/Banner/OBanner.spec.ts":1.2, +"src/lib/feedback/InnerLoading/OInnerLoading.spec.ts":1.2, +"src/lib/feedback/Skeleton/OSkeleton.spec.ts":0.9, +"src/lib/feedback/Spinner/OSpinner.spec.ts":0.8, +"src/lib/feedback/Toast/OToast.spec.ts":2, +"src/lib/feedback/Toast/OToastProvider.spec.ts":3, +"src/lib/feedback/Toast/useToast.spec.ts":2.1, +"src/lib/forms/Checkbox/OCheckbox.spec.ts":1.5, +"src/lib/forms/Checkbox/OCheckboxGroup.spec.ts":0.8, +"src/lib/forms/Checkbox/OFormCheckbox.spec.ts":1, +"src/lib/forms/Color/OColor.spec.ts":3.1, +"src/lib/forms/Color/OFormColor.spec.ts":2, +"src/lib/forms/Combobox/OFormCombobox.spec.ts":3.3, +"src/lib/forms/Date/ODate.spec.ts":2.4, +"src/lib/forms/Date/OFormDate.spec.ts":2.4, +"src/lib/forms/DateTime/OFormDateTimeRange.spec.ts":3.5, +"src/lib/forms/DateTimeRange/ODateRangeCalendar.spec.ts":2.6, +"src/lib/forms/DateTimeRange/ODateTimeRange.spec.ts":2.4, +"src/lib/forms/EmojiPicker/OEmojiPicker.spec.ts":5.5, +"src/lib/forms/EmojiPicker/OGlyph.spec.ts":1.1, +"src/lib/forms/EmojiPicker/emojiCatalog.spec.ts":1, +"src/lib/forms/File/OFile.spec.ts":2.4, +"src/lib/forms/File/OFormFile.spec.ts":3.2, +"src/lib/forms/Form/OForm.reset-on-remount.spec.ts":2.5, +"src/lib/forms/Form/OForm.revalidation.spec.ts":1.9, +"src/lib/forms/Form/OForm.shared-field.spec.ts":2.7, +"src/lib/forms/Form/OForm.spec.ts":1.6, +"src/lib/forms/Form/OForm.submit-state.spec.ts":1.5, +"src/lib/forms/Form/scrollToFirstError.spec.ts":1.7, +"src/lib/forms/InlineEdit/OInlineEdit.spec.ts":3.4, +"src/lib/forms/Input/OFormInput.spec.ts":1.4, +"src/lib/forms/Input/OFormTextarea.spec.ts":1.7, +"src/lib/forms/Input/OInput.required.spec.ts":1.5, +"src/lib/forms/Input/OInput.spec.ts":2.1, +"src/lib/forms/Input/OTextarea.spec.ts":1.7, +"src/lib/forms/OptionGroup/OFormOptionGroup.spec.ts":2, +"src/lib/forms/OptionGroup/OOptionGroup.spec.ts":3.2, +"src/lib/forms/Radio/ORadio.spec.ts":1.5, +"src/lib/forms/Radio/ORadioGroup.spec.ts":1.6, +"src/lib/forms/Range/OFormRange.spec.ts":2.5, +"src/lib/forms/Range/ORange.spec.ts":1.7, +"src/lib/forms/SearchInput/OSearchInput.spec.ts":3.5, +"src/lib/forms/Select/OFormSelect.spec.ts":1.8, +"src/lib/forms/Select/OSelect.expandable.spec.ts":3, +"src/lib/forms/Select/OSelect.headerAction.spec.ts":6.9, +"src/lib/forms/Select/OSelect.pinSelected.spec.ts":2.5, +"src/lib/forms/Select/OSelect.searchText.spec.ts":1.7, +"src/lib/forms/Select/OSelect.selectAll.spec.ts":3.2, +"src/lib/forms/Select/OSelect.spec.ts":5.7, +"src/lib/forms/Select/OSelectGroup.spec.ts":1, +"src/lib/forms/Select/OSelectHeaderRow.spec.ts":2.3, +"src/lib/forms/Select/OSelectItem.spec.ts":1.9, +"src/lib/forms/Slider/OFormSlider.spec.ts":1.6, +"src/lib/forms/Slider/OSlider.spec.ts":1.8, +"src/lib/forms/Switch/OFormSwitch.spec.ts":3.9, +"src/lib/forms/Switch/OSwitch.spec.ts":2.3, +"src/lib/forms/TagInput/OTagInput.spec.ts":2.4, +"src/lib/forms/TemplateInput/OFormTemplateInput.spec.ts":2.8, +"src/lib/forms/TemplateInput/OTemplateInput.spec.ts":2.9, +"src/lib/forms/TemplateInput/OTemplateSuggestList.spec.ts":3.2, +"src/lib/forms/TemplateInput/OTemplateTextarea.spec.ts":5.9, +"src/lib/forms/TemplateInput/useTemplateSuggest.spec.ts":0.9, +"src/lib/forms/Time/OFormTime.spec.ts":1.3, +"src/lib/forms/Time/OTime.spec.ts":3.9, +"src/lib/forms/controlAttrs.spec.ts":1.6, +"src/lib/lists/DescriptionList/ODescriptionList.spec.ts":1.1, +"src/lib/lists/FieldList/OFieldList.spec.ts":2.1, +"src/lib/navigation/Pagination/OPagination.spec.ts":1.2, +"src/lib/navigation/Stepper/OStep.spec.ts":1.3, +"src/lib/navigation/Stepper/OStepper.spec.ts":2.6, +"src/lib/navigation/Tabs/OTab.spec.ts":2.6, +"src/lib/navigation/Tabs/OTabPanel.spec.ts":1.2, +"src/lib/navigation/Tabs/OTabPanels.spec.ts":1.3, +"src/lib/navigation/Tabs/OTabs.spec.ts":4.7, +"src/lib/overlay/ContextMenu/OContextMenu.spec.ts":3, +"src/lib/overlay/ContextMenu/OContextMenuItem.spec.ts":3.6, +"src/lib/overlay/ContextMenu/OContextMenuLabel.spec.ts":0.8, +"src/lib/overlay/ContextMenu/OContextMenuSeparator.spec.ts":1.3, +"src/lib/overlay/Dialog/ODialog.contract.spec.ts":1.4, +"src/lib/overlay/Dialog/ODialog.spec.ts":5.1, +"src/lib/overlay/Dialog/dialogStacking.spec.ts":1.8, +"src/lib/overlay/Drawer/ODrawer.modal.spec.ts":3.4, +"src/lib/overlay/Drawer/ODrawer.spec.ts":2.5, +"src/lib/overlay/Dropdown/ODropdown.context.spec.ts":1.3, +"src/lib/overlay/Dropdown/ODropdown.spec.ts":2.5, +"src/lib/overlay/Dropdown/ODropdownGroup.spec.ts":1.2, +"src/lib/overlay/Dropdown/ODropdownItem.spec.ts":4.5, +"src/lib/overlay/Dropdown/ODropdownItem.unavailable.spec.ts":2.9, +"src/lib/overlay/Dropdown/ODropdownSeparator.spec.ts":1.3, +"src/lib/overlay/Dropdown/ODropdownSub.spec.ts":3.1, +"src/lib/overlay/Popover/OPopover.spec.ts":3.9, +"src/lib/overlay/Tooltip/OTooltip.spec.ts":6.2, +"src/lib/overlay/Tooltip/triggerName.spec.ts":0.7, +"src/lib/overlay/Tooltip/useIsTruncated.spec.ts":0.9, +"src/lib/overlay/keptAliveOverlays.spec.ts":9.9, +"src/lib/overlay/useScrollShadow.spec.ts":1.5, +"src/lib/styles/tokens/brandThemes.spec.ts":1.3, +"src/lib/styles/tokens/colorMath.spec.ts":1.9, +"src/lib/styles/tokens/contrast.spec.ts":0.8, +"src/lib/vue-shortcut-manager/__tests__/manager.spec.ts":1.2, +"src/lib/vue-shortcut-manager/__tests__/shortcutRegistry.spec.ts":0.6, +"src/locales/alertLibraryParity.spec.ts":1.3, +"src/locales/anomalySensitivityParity.spec.ts":0.9, +"src/locales/dbmMessages.spec.ts":1.2, +"src/locales/index.spec.ts":1, +"src/locales/localeKeys.spec.ts":1, +"src/locales/localeMessages.spec.ts":21.3, +"src/locales/localeParity.spec.ts":2.6, +"src/locales/menuTerms.spec.ts":1.8, +"src/locales/oncallMessages.spec.ts":2, +"src/main.spec.ts":1.1, +"src/plugins/correlation/CorrelatedLogsTable.spec.ts":5.6, +"src/plugins/correlation/CorrelatedLogsTable.viewTrace.spec.ts":2.9, +"src/plugins/correlation/CorrelationEventHeader.spec.ts":4.9, +"src/plugins/correlation/DimensionFiltersBar.spec.ts":2.2, +"src/plugins/correlation/TelemetryCorrelationDashboard.spec.ts":5.8, +"src/plugins/logs/BuildQueryPage.spec.ts":6.2, +"src/plugins/logs/DetailTable.copyLink.spec.ts":10, +"src/plugins/logs/DetailTable.spec.ts":15.6, +"src/plugins/logs/FunctionSelector.spec.ts":3, +"src/plugins/logs/Index.autoRun.spec.ts":26.9, +"src/plugins/logs/Index.buildFreeText.spec.ts":7, +"src/plugins/logs/Index.buildParserLoad.spec.ts":5, +"src/plugins/logs/Index.noFtsRecovery.spec.ts":18.5, +"src/plugins/logs/Index.permalink.spec.ts":10.4, +"src/plugins/logs/Index.recentReapply.spec.ts":20.1, +"src/plugins/logs/Index.searchCancelled.spec.ts":14.5, +"src/plugins/logs/Index.spec.ts":41.6, +"src/plugins/logs/IndexList.freeText.spec.ts":3.4, +"src/plugins/logs/IndexList.spec.ts":7.7, +"src/plugins/logs/IndexList.typeSwitch.spec.ts":6, +"src/plugins/logs/JsonPreview.spec.ts":7.4, +"src/plugins/logs/LogsAutoRunGuard.spec.ts":3.9, +"src/plugins/logs/LogsErrorState.spec.ts":3.4, +"src/plugins/logs/LogsMissingStreamBanner.spec.ts":2.6, +"src/plugins/logs/LogsNoDataState.spec.ts":4.3, +"src/plugins/logs/LogsNoEventsState.spec.ts":4.1, +"src/plugins/logs/LogsNoFtsFieldSearch.spec.ts":5.3, +"src/plugins/logs/LogsNoFtsPanel.spec.ts":2.7, +"src/plugins/logs/QueryEditor.spec.ts":2.5, +"src/plugins/logs/SearchBar.addSearchTerm.spec.ts":5, +"src/plugins/logs/SearchBar.autoFlip.spec.ts":4.9, +"src/plugins/logs/SearchBar.autoRun.spec.ts":16.2, +"src/plugins/logs/SearchBar.downloadLogs.spec.ts":4, +"src/plugins/logs/SearchBar.drillDownToggle.spec.ts":4, +"src/plugins/logs/SearchBar.fieldTypes.spec.ts":4.6, +"src/plugins/logs/SearchBar.heldFilter.spec.ts":4.8, +"src/plugins/logs/SearchBar.nlDetection.spec.ts":5.4, +"src/plugins/logs/SearchBar.permalink.spec.ts":7.5, +"src/plugins/logs/SearchBar.programmaticClear.spec.ts":7.2, +"src/plugins/logs/SearchBar.recentFocusEscape.spec.ts":8.9, +"src/plugins/logs/SearchBar.refreshCacheDropdown.spec.ts":4.5, +"src/plugins/logs/SearchBar.savedDialogs.spec.ts":2.2, +"src/plugins/logs/SearchBar.savedViewsDialog.spec.ts":12.9, +"src/plugins/logs/SearchBar.savedViewsPinned.spec.ts":9.3, +"src/plugins/logs/SearchBar.sessionValues.spec.ts":3.1, +"src/plugins/logs/SearchBar.spec.ts":4.5, +"src/plugins/logs/SearchBar.streamNotFound.spec.ts":4.7, +"src/plugins/logs/SearchBar.textPreview.spec.ts":3.5, +"src/plugins/logs/SearchBar.toolbarHeight.spec.ts":8.5, +"src/plugins/logs/SearchBar.toolbarSpacing.spec.ts":4.7, +"src/plugins/logs/SearchHistory.spec.js":5.8, +"src/plugins/logs/SearchJobInspector.spec.ts":5.2, +"src/plugins/logs/SearchResult.freeText.spec.ts":3.7, +"src/plugins/logs/SearchResult.jk.spec.ts":17.3, +"src/plugins/logs/SearchResult.permalink.spec.ts":19.2, +"src/plugins/logs/SearchResult.spec.ts":16.4, +"src/plugins/logs/SearchResult.viewTrace.spec.ts":5.9, +"src/plugins/logs/SearchResultSkeleton.spec.ts":2.6, +"src/plugins/logs/SearchSchedulersList.spec.js":5.8, +"src/plugins/logs/SearchSchedulersList.spec.ts":18.8, +"src/plugins/logs/SyntaxGuide.spec.ts":4.3, +"src/plugins/logs/VisualizeLogsQuery.spec.ts":9.1, +"src/plugins/logs/components/FieldExpansion.spec.ts":1.6, +"src/plugins/logs/components/FieldRow.spec.ts":2.3, +"src/plugins/logs/data-table/CellActions.spec.ts":6.2, +"src/plugins/logs/filterUtils.spec.ts":1.5, +"src/plugins/logs/patterns/PatternCard.spec.ts":3.7, +"src/plugins/logs/patterns/PatternDetailsDialog.spec.ts":2.9, +"src/plugins/logs/patterns/PatternList.spec.ts":8.2, +"src/plugins/logs/patterns/PatternVolumeCell.spec.ts":2.4, +"src/plugins/logs/patterns/WildcardValuePopover.spec.ts":3.7, +"src/plugins/logs/patterns/patternUtils.spec.ts":5.2, +"src/plugins/logs/patterns/usePatternActions.spec.ts":3.9, +"src/plugins/logs/patterns/usePatternVolume.spec.ts":3.3, +"src/plugins/logs/patterns/useWildcardHover.spec.ts":1.7, +"src/plugins/metrics/AddToDashboard.spec.ts":3.2, +"src/plugins/metrics/QueryHistoryDrawer.spec.ts":2.9, +"src/plugins/metrics/explorer/ExplorerSavedViews.spec.ts":4.1, +"src/plugins/metrics/explorer/FunctionConfigDialog.spec.ts":4.2, +"src/plugins/metrics/explorer/LabelFilterBar.spec.ts":11.9, +"src/plugins/metrics/explorer/MetricBreakdown.spec.ts":30.9, +"src/plugins/metrics/explorer/MetricCard.spec.ts":5.1, +"src/plugins/metrics/explorer/MetricCardChart.spec.ts":9.8, +"src/plugins/metrics/explorer/MetricChartTile.spec.ts":6.3, +"src/plugins/metrics/explorer/MetricDetailView.spec.ts":9.7, +"src/plugins/metrics/explorer/MetricUsageList.spec.ts":3.1, +"src/plugins/metrics/explorer/MetricsExplorer.spec.ts":11.4, +"src/plugins/metrics/explorer/MetricsVisualize.historyStatus.spec.ts":4, +"src/plugins/metrics/explorer/MetricsVisualize.spec.ts":2.4, +"src/plugins/metrics/explorer/PrefixFilterPanel.spec.ts":1.4, +"src/plugins/pipelines/CustomNode.spec.ts":3.7, +"src/plugins/pipelines/EdgeWithButton.spec.ts":0.7, +"src/plugins/pipelines/PipelineFlow.spec.ts":4.6, +"src/plugins/pipelines/useDnD.spec.ts":1.8, +"src/plugins/profiles/Index.spec.ts":21, +"src/plugins/profiles/ProfilesCompare.spec.ts":2.2, +"src/plugins/profiles/ProfilesFunctionDetails.spec.ts":2.9, +"src/plugins/profiles/format.spec.ts":1.1, +"src/plugins/profiles/sandwich.spec.ts":0.9, +"src/plugins/traces/DbSpanDetails.spec.ts":3.9, +"src/plugins/traces/Index.loadStates.spec.ts":5.6, +"src/plugins/traces/Index.spec.ts":16.9, +"src/plugins/traces/IndexList.spec.ts":6.4, +"src/plugins/traces/KpiSparkline.spec.ts":1.5, +"src/plugins/traces/LLMContentRenderer.spec.ts":2.6, +"src/plugins/traces/LLMInsightsDashboard.spec.ts":7.6, +"src/plugins/traces/LLMInsightsDashboard.versionCompare.spec.ts":23.3, +"src/plugins/traces/SearchBar.spec.ts":27.3, +"src/plugins/traces/SearchResult.spec.ts":7, +"src/plugins/traces/ServiceGraph.spec.ts":10.2, +"src/plugins/traces/ServiceGraphNoDataState.spec.ts":3.7, +"src/plugins/traces/ServiceGraphNodeSidePanel.spec.ts":5.2, +"src/plugins/traces/ServiceGraphSidePanel.spec.ts":9.1, +"src/plugins/traces/ServicesCatalog.spec.ts":16.2, +"src/plugins/traces/ServicesCatalogNoDataState.spec.ts":5, +"src/plugins/traces/SessionDetails.spec.ts":4.9, +"src/plugins/traces/SessionsList.spec.ts":2.9, +"src/plugins/traces/SpanBlock.spec.ts":4.1, +"src/plugins/traces/SyntaxGuide.spec.ts":5.6, +"src/plugins/traces/ThreadView.spec.ts":6.2, +"src/plugins/traces/TraceDAG.spec.ts":3.6, +"src/plugins/traces/TraceDetails.all-missing-fields.spec.ts":6.2, +"src/plugins/traces/TraceDetails.error-spans-menu.spec.ts":3.5, +"src/plugins/traces/TraceDetails.missing-span-kind.spec.ts":14.1, +"src/plugins/traces/TraceDetails.paging.spec.ts":48.5, +"src/plugins/traces/TraceDetails.rum-bridge.spec.ts":7.9, +"src/plugins/traces/TraceDetails.spec.ts":39.8, +"src/plugins/traces/TraceDetailsSidebar.spec.ts":73.4, +"src/plugins/traces/TraceHeader.spec.ts":3.1, +"src/plugins/traces/TraceTree.spec.ts":6.7, +"src/plugins/traces/TracesNoDataState.spec.ts":3.3, +"src/plugins/traces/TracesNoEventsState.spec.ts":6.4, +"src/plugins/traces/agentOptionFormat.spec.ts":1.6, +"src/plugins/traces/components/SpanErrorEventsCells.spec.ts":4.4, +"src/plugins/traces/components/SpanKindBadge.spec.ts":1.7, +"src/plugins/traces/components/SpanStatusCodeBadge.spec.ts":1.6, +"src/plugins/traces/components/SpanStatusPill.spec.ts":2.4, +"src/plugins/traces/components/TraceErrorSpansMenu.spec.ts":4.5, +"src/plugins/traces/components/TraceErrorTab.spec.ts":5.9, +"src/plugins/traces/components/TraceLatencyCell.spec.ts":2.8, +"src/plugins/traces/components/TraceServiceCell.spec.ts":2.3, +"src/plugins/traces/components/TraceStatusCell.spec.ts":1.3, +"src/plugins/traces/components/TraceTimestampCell.spec.ts":1.1, +"src/plugins/traces/components/TracesSearchResultList.jk.spec.ts":6.8, +"src/plugins/traces/components/TracesSearchResultList.spec.ts":3.6, +"src/plugins/traces/components/spanResultCells.spec.ts":0.8, +"src/plugins/traces/composables/useLLMInsights.spec.ts":2, +"src/plugins/traces/composables/useLLMStreamQuery.spec.ts":1.9, +"src/plugins/traces/composables/useSessions.spec.ts":3.7, +"src/plugins/traces/composables/useTracesTableColumns.spec.ts":1.5, +"src/plugins/traces/composables/useVersionCompare.spec.ts":3.7, +"src/plugins/traces/config/llmInsightsPanels.spec.ts":3.5, +"src/plugins/traces/genAiParts.spec.ts":1.7, +"src/plugins/traces/llmAgentFilter.spec.ts":1.2, +"src/plugins/traces/llmInsightsCache.spec.ts":1.6, +"src/plugins/traces/llmInsightsDashboard.utils.spec.ts":1.1, +"src/plugins/traces/llmTrendPanel.utils.spec.ts":1.6, +"src/plugins/traces/metrics/TracesAnalysisDashboard.spec.ts":3, +"src/plugins/traces/metrics/TracesComparison.spec.ts":31.7, +"src/plugins/traces/metrics/TracesComparisonField.spec.ts":5.6, +"src/plugins/traces/metrics/TracesDrillDownPage.spec.ts":2.7, +"src/plugins/traces/metrics/TracesLatencyHeatmap.spec.ts":8, +"src/plugins/traces/metrics/TracesMetricsDashboard.heatmap.spec.ts":6.5, +"src/plugins/traces/metrics/TracesMetricsDashboard.spec.ts":5.9, +"src/plugins/traces/metrics/latencyHeatmap.spec.ts":4.1, +"src/plugins/traces/metrics/traceComparison.spec.ts":2.1, +"src/plugins/traces/threadView.utils.spec.ts":2.1, +"src/plugins/traces/traceDetails.utils.spec.ts":2.6, +"src/plugins/traces/tracesIndex.utils.spec.ts":1.2, +"src/plugins/traces/tracesSearchBar.utils.spec.ts":3.7, +"src/plugins/traces/tracesUrlState.spec.ts":1.9, +"src/plugins/traces/utils/traceErrorSummary.spec.ts":0.9, +"src/plugins/traces/versionCompare/compareResult.spec.ts":10.1, +"src/plugins/traces/versionCompare/formatDuration.spec.ts":1.3, +"src/plugins/traces/versionCompare/rawSample.spec.ts":2.2, +"src/plugins/traces/versionCompare/stats.bootstrap.spec.ts":2.1, +"src/plugins/traces/versionCompare/stats.spec.ts":1.1, +"src/plugins/traces/versionCompare/windows.spec.ts":1.1, +"src/plugins/traces/viewTracesHandoff.spec.ts":3.1, +"src/plugins/workflows/WorkflowCanvas.spec.ts":2.3, +"src/plugins/workflows/WorkflowNode.spec.ts":7.6, +"src/plugins/workflows/alertFields.spec.ts":1.1, +"src/plugins/workflows/incidentFields.spec.ts":0.9, +"src/plugins/workflows/incidentSample.spec.ts":2.3, +"src/plugins/workflows/nodes/WorkflowBranch.spec.ts":2.9, +"src/plugins/workflows/nodes/WorkflowCondition.spec.ts":5.2, +"src/plugins/workflows/nodes/WorkflowConfigSummary.spec.ts":3.8, +"src/plugins/workflows/nodes/WorkflowDestination.spec.ts":4.1, +"src/plugins/workflows/nodes/WorkflowFunction.spec.ts":5.8, +"src/plugins/workflows/nodes/WorkflowTrigger.spec.ts":4.1, +"src/plugins/workflows/testSample.spec.ts":1.7, +"src/plugins/workflows/triggers.spec.ts":1, +"src/plugins/workflows/useWorkflowCanvas.spec.ts":4.2, +"src/router/index.spec.ts":1.3, +"src/router/routes.spec.ts":0.8, +"src/services/ai_chat.spec.ts":1.4, +"src/services/ai_chat_share.spec.ts":1.1, +"src/services/ai_toolsets.spec.ts":1.3, +"src/services/alertInsights.spec.ts":1.6, +"src/services/alert_calibration.queries.spec.ts":1.7, +"src/services/alert_destination.queries.spec.ts":1.4, +"src/services/alert_destination.spec.ts":1.2, +"src/services/alert_sources.spec.ts":1.3, +"src/services/alert_templates.spec.ts":1.8, +"src/services/alerts.queries.spec.ts":0.9, +"src/services/alerts.spec.ts":0.8, +"src/services/anomaly_detection.spec.ts":1.5, +"src/services/api_keys.spec.ts":1.5, +"src/services/attachments.spec.ts":1, +"src/services/auth.spec.ts":3.8, +"src/services/awsMarketplace.spec.ts":1.6, +"src/services/azureMarketplace.spec.ts":1, +"src/services/backfill.spec.ts":0.7, +"src/services/billings.spec.ts":4.5, +"src/services/cipher_keys.spec.ts":1.3, +"src/services/common.queries.spec.ts":0.8, +"src/services/common.spec.ts":2.2, +"src/services/config.queries.spec.ts":1.5, +"src/services/config.spec.ts":1.8, +"src/services/dashboard_annotations.queries.spec.ts":1.6, +"src/services/dashboard_annotations.spec.ts":1.4, +"src/services/dashboards.spec.ts":1.6, +"src/services/db_monitoring.spec.ts":1.2, +"src/services/domainManagement.spec.ts":1.6, +"src/services/downtimes.queries.spec.ts":1.5, +"src/services/fleet.queries.pages.spec.ts":2.6, +"src/services/fleet.spec.ts":2.6, +"src/services/gen-ai-agent-mapping.service.spec.ts":1, +"src/services/health.spec.ts":1, +"src/services/http.spec.ts":0.8, +"src/services/iam.queries.spec.ts":1.4, +"src/services/iam.spec.ts":1.3, +"src/services/incidents.spec.ts":1.6, +"src/services/index.spec.ts":1.7, +"src/services/ingestion.spec.ts":1.1, +"src/services/jstransform.spec.ts":0.6, +"src/services/kubernetes.queries.spec.ts":2.9, +"src/services/license_server.spec.ts":1.6, +"src/services/llm-annotations.service.spec.ts":0.9, +"src/services/llm-datasets.service.spec.ts":2, +"src/services/llm-discovery.service.spec.ts":1.5, +"src/services/llm-experiments.service.spec.ts":0.7, +"src/services/llm-playground.service.spec.ts":1.4, +"src/services/llm-prompts.service.queries.spec.ts":0.8, +"src/services/llm-prompts.service.spec.ts":1.3, +"src/services/llm-queues.service.spec.ts":0.7, +"src/services/metrics.spec.ts":0.7, +"src/services/nativequery.spec.ts":1.7, +"src/services/oncall.queries.spec.ts":1.9, +"src/services/oncall.spec.ts":0.7, +"src/services/online-evals.service.spec.ts":1.5, +"src/services/organizations.spec.ts":3.2, +"src/services/patterns.spec.ts":0.8, +"src/services/pipelines.queries.spec.ts":1.6, +"src/services/pipelines.spec.ts":0.8, +"src/services/product_analytics.spec.ts":0.9, +"src/services/profiles.spec.ts":1, +"src/services/public_dashboards.spec.ts":1.2, +"src/services/public_dashboards_admin.spec.ts":1.2, +"src/services/query_functions.spec.ts":0.8, +"src/services/query_history.spec.ts":1.4, +"src/services/rate_limit.spec.ts":1.6, +"src/services/regex_pattern.spec.ts":0.7, +"src/services/remote-tasks.service.spec.ts":0.9, +"src/services/reodotdev_analytics.spec.ts":0.7, +"src/services/reports.queries.spec.ts":1, +"src/services/reports.spec.ts":1.4, +"src/services/rumProductAnalytics.spec.ts":1.5, +"src/services/saved_views.spec.ts":1.3, +"src/services/search.spec.ts":1.2, +"src/services/service_accounts.spec.ts":1.2, +"src/services/service_graph.spec.ts":1.6, +"src/services/service_streams.queries.spec.ts":0.8, +"src/services/service_streams.spec.ts":1, +"src/services/settings.spec.ts":0.8, +"src/services/short_url.spec.ts":0.7, +"src/services/slack.queries.spec.ts":2.1, +"src/services/slack.spec.ts":1.4, +"src/services/slos.spec.ts":0.8, +"src/services/sourcemaps.spec.ts":1.1, +"src/services/stream.spec.ts":1.3, +"src/services/streaming_search.spec.ts":1.2, +"src/services/synthetics.queries.spec.ts":1.4, +"src/services/synthetics.spec.ts":3.7, +"src/services/tickets.spec.ts":3.2, +"src/services/users.spec.ts":3, +"src/services/workflows.spec.ts":0.7, +"src/stores/calendarTimezone.spec.ts":3.5, +"src/stores/incidents.spec.ts":1.4, +"src/stores/index.spec.ts":4, +"src/stores/logs.spec.ts":1.3, +"src/stores/streams.spec.ts":1.7, +"src/test/unit/HomeView.spec.ts":8.6, +"src/test/unit/Nodes.spec.ts":7.6, +"src/test/unit/PinnedDashboardTab.spec.ts":5.7, +"src/test/unit/components/ingestion/security/Falco.spec.ts":4.7, +"src/test/unit/components/ingestion/security/GoogleWorkspace.spec.ts":3.3, +"src/test/unit/components/ingestion/security/Jumpcloud.spec.ts":3.8, +"src/test/unit/components/ingestion/security/OSQuery.spec.ts":2.3, +"src/test/unit/components/ingestion/security/Office365.spec.ts":3.4, +"src/test/unit/components/ingestion/security/Okta.spec.ts":5.2, +"src/test/unit/components/ingestion/security/OpenVPN.spec.ts":2.5, +"src/test/unit/components/pipelines/PipelineHistory.spec.ts":6.9, +"src/test/unit/composables/logsUtils.spec.ts":3.6, +"src/test/unit/plugins/logs/components/FieldExpansion.spec.ts":5, +"src/test/unit/plugins/logs/components/FieldRow.spec.ts":2.6, +"src/test/unit/plugins/pipelines/CustomNode.spec.ts":5.2, +"src/test/unit/utils/alerts/alertPayload.spec.ts":3.6, +"src/test/unit/utils/alerts/promqlSamples.spec.ts":1.5, +"src/test/unit/utils/alerts/runOutcome.spec.ts":0.8, +"src/test/unit/utils/alerts/thresholdMarkLines.spec.ts":1, +"src/test/unit/utils/alerts/triggerStatus.spec.ts":1.6, +"src/test/unit/views/RUM/RealUserMonitoring.spec.ts":5.7, +"src/test/unit/workers/rumcssworker.spec.js":1, +"src/test/unit/workers/streamWorker.spec.js":1.4, +"src/ts/interfaces/alert.composite.spec.ts":1.4, +"src/ts/interfaces/oncall.contract.spec.ts":2.2, +"src/ts/interfaces/query.spec.ts":1.1, +"src/ts/interfaces/report.spec.ts":0.7, +"src/utils/aiCredits.spec.ts":0.6, +"src/utils/alertSourceStatus.spec.ts":1.3, +"src/utils/alerts/ackState.spec.ts":1.5, +"src/utils/alerts/aggregationPreviewQuery.spec.ts":1.7, +"src/utils/alerts/alertChartData.spec.ts":3.3, +"src/utils/alerts/alertCondition.spec.ts":3.6, +"src/utils/alerts/alertDataTransforms.spec.ts":1.6, +"src/utils/alerts/alertEditorDraft.spec.ts":1.3, +"src/utils/alerts/alertPayload.spec.ts":3.9, +"src/utils/alerts/alertPrefill.spec.ts":1.5, +"src/utils/alerts/alertPrefillStorage.spec.ts":1.5, +"src/utils/alerts/alertQueryBuilder.spec.ts":2.6, +"src/utils/alerts/alertSourceRegistry.spec.ts":1.2, +"src/utils/alerts/alertSqlUtils.spec.ts":1.6, +"src/utils/alerts/alertSummaryGenerator.spec.ts":1.1, +"src/utils/alerts/alertTerraform.spec.ts":1, +"src/utils/alerts/alertValidation.spec.ts":2.8, +"src/utils/alerts/anomalyBandChart.spec.ts":3.3, +"src/utils/alerts/anomalyChartQuery.spec.ts":1.1, +"src/utils/alerts/anomalyFilterOperators.spec.ts":2.4, +"src/utils/alerts/anomalySqlBuilder.spec.ts":3.8, +"src/utils/alerts/anomalySummaryGenerator.spec.ts":2.2, +"src/utils/alerts/conditionsFormatter.spec.ts":1.7, +"src/utils/alerts/deliveryTarget.spec.ts":1.3, +"src/utils/alerts/forecastAlert.spec.ts":1.4, +"src/utils/alerts/prefill/conformance.spec.ts":2.7, +"src/utils/alerts/prefill/dbmLocksMessages.spec.ts":1.7, +"src/utils/alerts/prefill/fromDbm.spec.ts":2.5, +"src/utils/alerts/prefill/fromDbmLocks.spec.ts":1.6, +"src/utils/alerts/prefill/fromLibrary.spec.ts":3.5, +"src/utils/alerts/prefill/fromLogs.spec.ts":2.7, +"src/utils/alerts/prefill/fromPanel.spec.ts":3.5, +"src/utils/alerts/prefill/fromPatterns.spec.ts":2.4, +"src/utils/alerts/prefill/fromRumFunnel.spec.ts":2.2, +"src/utils/alerts/promRuleHistory.spec.ts":1.4, +"src/utils/alerts/sloAlertPayload.spec.ts":3.3, +"src/utils/alerts/sloAlertRouting.spec.ts":1.1, +"src/utils/announcementAppearance.spec.ts":1.7, +"src/utils/announcementMarkdown.spec.ts":1.3, +"src/utils/announcementOrder.spec.ts":1.2, +"src/utils/auth.spec.ts":1.1, +"src/utils/autoName.spec.ts":0.6, +"src/utils/azureIntegrations.spec.ts":1.4, +"src/utils/beforeAppReload.spec.ts":0.7, +"src/utils/billing/convertBillingData.spec.ts":0.9, +"src/utils/buildVersionChecker.spec.ts":4.2, +"src/utils/chartTheme.fallbacks.spec.ts":1.3, +"src/utils/chartTheme.spec.ts":1.1, +"src/utils/chatSelection.spec.ts":1.6, +"src/utils/clipboard.spec.ts":0.7, +"src/utils/common.spec.ts":1.7, +"src/utils/commons.spec.ts":3.7, +"src/utils/conditionPreview.spec.ts":3, +"src/utils/cookies.spec.ts":1.2, +"src/utils/csv.spec.ts":2.1, +"src/utils/dashboard/alertListPanel.spec.ts":1.4, +"src/utils/dashboard/aliasUtils.spec.ts":1.1, +"src/utils/dashboard/annotationSources.spec.ts":2.2, +"src/utils/dashboard/autoRefresh.spec.ts":2.7, +"src/utils/dashboard/axisTickFormat.spec.ts":3.6, +"src/utils/dashboard/barTrack.spec.ts":1.3, +"src/utils/dashboard/calculateGridForSubPlot.spec.ts":1.5, +"src/utils/dashboard/calendarPeriods.spec.ts":2.8, +"src/utils/dashboard/cartesianOptions.spec.ts":4.5, +"src/utils/dashboard/castSuggestion.spec.ts":0.9, +"src/utils/dashboard/categoryNodes.spec.ts":3.2, +"src/utils/dashboard/chartColorUtils.spec.ts":1, +"src/utils/dashboard/chartDimensionUtils.spec.ts":0.9, +"src/utils/dashboard/chartLabelContrast.spec.ts":0.9, +"src/utils/dashboard/checkConfigChangeApiCall.spec.ts":0.8, +"src/utils/dashboard/chunkingDirection.spec.ts":1.2, +"src/utils/dashboard/colorPalette.spec.ts":4.5, +"src/utils/dashboard/colorPatette.spec.ts":5, +"src/utils/dashboard/configUtils.spec.ts":1.4, +"src/utils/dashboard/convertBarGaugeData.spec.ts":2.1, +"src/utils/dashboard/convertBoxplotData.spec.ts":5.8, +"src/utils/dashboard/convertCustomChartData.spec.ts":0.9, +"src/utils/dashboard/convertDashboardSchemaVersion.spec.ts":0.8, +"src/utils/dashboard/convertDataIntoUnitValue.newUnits.spec.ts":1, +"src/utils/dashboard/convertDataIntoUnitValue.spec.ts":5.3, +"src/utils/dashboard/convertDistributionData.spec.ts":4.3, +"src/utils/dashboard/convertFlameGraphPanelData.spec.ts":2.3, +"src/utils/dashboard/convertFunnelData.spec.ts":5.3, +"src/utils/dashboard/convertGeoMapData.spec.ts":1.9, +"src/utils/dashboard/convertHostmapData.spec.ts":4.8, +"src/utils/dashboard/convertLogsPanelData.spec.ts":3.5, +"src/utils/dashboard/convertMapsData.spec.ts":0.9, +"src/utils/dashboard/convertNodeGraphData.spec.ts":5.7, +"src/utils/dashboard/convertPanelData.spec.ts":2.6, +"src/utils/dashboard/convertPromQLData.forecast.spec.ts":4.5, +"src/utils/dashboard/convertPromQLData.formulaLegend.spec.ts":2.4, +"src/utils/dashboard/convertPromQLData.gauge.spec.ts":4.4, +"src/utils/dashboard/convertPromQLData.metric.spec.ts":2.8, +"src/utils/dashboard/convertPromQLData.options-comparison.spec.ts":4.8, +"src/utils/dashboard/convertPromQLData.spec.ts":5.2, +"src/utils/dashboard/convertPromQLData.timeShift.spec.ts":5.8, +"src/utils/dashboard/convertSQLData.barTrack.spec.ts":2.2, +"src/utils/dashboard/convertSQLData.categoryMerge.spec.ts":4.9, +"src/utils/dashboard/convertSQLData.gauge.spec.ts":2.5, +"src/utils/dashboard/convertSQLData.spec.ts":7.2, +"src/utils/dashboard/convertSankeyData.spec.ts":5.4, +"src/utils/dashboard/convertStateTimelineData.spec.ts":4.7, +"src/utils/dashboard/convertSunburstData.spec.ts":7.9, +"src/utils/dashboard/convertTableData.spec.ts":2.1, +"src/utils/dashboard/convertTreemapData.spec.ts":2.5, +"src/utils/dashboard/countryMappings.spec.ts":1, +"src/utils/dashboard/dashboardAutoQueryBuilder.spec.ts":3.4, +"src/utils/dashboard/dateTimeUtils.spec.ts":3.6, +"src/utils/dashboard/datetimeStartPoint.spec.ts":1.2, +"src/utils/dashboard/defaultFields.spec.ts":1.8, +"src/utils/dashboard/exemplars/applyExemplarSeries.spec.ts":3.2, +"src/utils/dashboard/exemplars/buildExemplarMarkers.spec.ts":1.9, +"src/utils/dashboard/exemplars/exemplarCardPosition.spec.ts":1, +"src/utils/dashboard/exemplars/exemplarEligibility.spec.ts":1.7, +"src/utils/dashboard/exemplars/exemplarPlacement.spec.ts":0.9, +"src/utils/dashboard/exemplars/exemplarTraceRoute.spec.ts":0.8, +"src/utils/dashboard/exemplars/heatmapExemplars.spec.ts":2.4, +"src/utils/dashboard/fieldLabel.spec.ts":1, +"src/utils/dashboard/findPanel.spec.ts":1.2, +"src/utils/dashboard/formattingT0.spec.ts":3.2, +"src/utils/dashboard/gaugeDetailText.spec.ts":2.7, +"src/utils/dashboard/gaugeDial.spec.ts":3.6, +"src/utils/dashboard/geoMapCluster.spec.ts":5.3, +"src/utils/dashboard/getAnnotationsData.spec.ts":0.9, +"src/utils/dashboard/heatmapDefaults.spec.ts":3.5, +"src/utils/dashboard/heatmapTimeAxis.spec.ts":2.8, +"src/utils/dashboard/heatmapYAxis.spec.ts":1.8, +"src/utils/dashboard/histogramIntervalReplacer.spec.ts":0.9, +"src/utils/dashboard/htmlPanelSanitizer.spec.ts":0.8, +"src/utils/dashboard/kiosk.spec.ts":2.2, +"src/utils/dashboard/legendConfiguration.spec.ts":4.1, +"src/utils/dashboard/lineSeriesAtPoint.spec.ts":1, +"src/utils/dashboard/lineStrokeEvents.spec.ts":2.7, +"src/utils/dashboard/markdownPanel.spec.ts":1.2, +"src/utils/dashboard/markdownPanelSanitizer.spec.ts":1.7, +"src/utils/dashboard/menuPlacement.spec.ts":0.9, +"src/utils/dashboard/metricOptions.spec.ts":2.9, +"src/utils/dashboard/noDataText.spec.ts":0.9, +"src/utils/dashboard/optionDefaults.spec.ts":1.4, +"src/utils/dashboard/overrideConfig.isolation.spec.ts":2.3, +"src/utils/dashboard/panelDiagnostics.spec.ts":1, +"src/utils/dashboard/panelTimeUtils.effectiveRelative.spec.ts":2.9, +"src/utils/dashboard/panelTimeUtils.pickerValues.spec.ts":2.4, +"src/utils/dashboard/panelTimeUtils.queryChange.spec.ts":2.6, +"src/utils/dashboard/panelTitle.spec.ts":1.1, +"src/utils/dashboard/panelTypeOptions.spec.ts":3.2, +"src/utils/dashboard/panelValidation.spec.ts":4.1, +"src/utils/dashboard/playlist.spec.ts":2.7, +"src/utils/dashboard/promql/convertPromQLBarChart.spec.ts":3.8, +"src/utils/dashboard/promql/convertPromQLChartData.spec.ts":2.3, +"src/utils/dashboard/promql/convertPromQLGaugeChart.spec.ts":1.7, +"src/utils/dashboard/promql/convertPromQLGeoChart.spec.ts":1.6, +"src/utils/dashboard/promql/convertPromQLHeatmapChart.spec.ts":4.3, +"src/utils/dashboard/promql/convertPromQLMapsChart.spec.ts":0.9, +"src/utils/dashboard/promql/convertPromQLMetricChart.spec.ts":0.8, +"src/utils/dashboard/promql/convertPromQLPieChart.spec.ts":3, +"src/utils/dashboard/promql/convertPromQLTableChart.spec.ts":2.2, +"src/utils/dashboard/promql/convertPromQLTimeSeriesChart.spec.ts":1.5, +"src/utils/dashboard/promql/formula.spec.ts":1.5, +"src/utils/dashboard/promql/shared/axisBuilder.spec.ts":3.2, +"src/utils/dashboard/promql/shared/dataProcessor.spec.ts":2.7, +"src/utils/dashboard/promql/shared/gridBuilder.spec.ts":1.3, +"src/utils/dashboard/promql/shared/histogramBuckets.spec.ts":1.6, +"src/utils/dashboard/promql/shared/legendBuilder.spec.ts":1.9, +"src/utils/dashboard/promql/shared/types.spec.ts":1.7, +"src/utils/dashboard/promqlChartSeriesProps.spec.ts":1.4, +"src/utils/dashboard/promqlSeed.spec.ts":1.8, +"src/utils/dashboard/pruneAnnotationSourcePanels.spec.ts":0.7, +"src/utils/dashboard/rawRowCap.spec.ts":3.2, +"src/utils/dashboard/saveConflict.spec.ts":2, +"src/utils/dashboard/scatterPointStyle.spec.ts":1.3, +"src/utils/dashboard/searchLabelsConfig.spec.ts":1.5, +"src/utils/dashboard/seriesOverrides.builders.spec.ts":3.4, +"src/utils/dashboard/seriesOverrides.spec.ts":4.1, +"src/utils/dashboard/seriesStyleOverrides.builders.spec.ts":5.5, +"src/utils/dashboard/seriesStyleOverrides.spec.ts":2.4, +"src/utils/dashboard/sql/charts/convertSQLCharts.spec.ts":4, +"src/utils/dashboard/sql/charts/convertSQLGaugeChart.spec.ts":3, +"src/utils/dashboard/sql/charts/convertSQLHBarChart.spec.ts":1, +"src/utils/dashboard/sql/charts/convertSQLHStackedChart.spec.ts":1.6, +"src/utils/dashboard/sql/charts/convertSQLHeatmapChart.spec.ts":4.4, +"src/utils/dashboard/sql/charts/convertSQLLineAreaChart.spec.ts":3.4, +"src/utils/dashboard/sql/charts/convertSQLMetricChart.spec.ts":3.5, +"src/utils/dashboard/sql/charts/convertSQLPieDonutChart.spec.ts":3.7, +"src/utils/dashboard/sql/charts/convertSQLStackedChart.spec.ts":1, +"src/utils/dashboard/sql/convertSQLChartData.spec.ts":2.3, +"src/utils/dashboard/sql/shared/contextBuilder.spec.ts":2.2, +"src/utils/dashboard/sql/shared/contextBuilderYAxisTicks.spec.ts":2.5, +"src/utils/dashboard/sql/shared/seriesBuilder.spec.ts":1, +"src/utils/dashboard/sql/shared/trellisConfig.spec.ts":1.2, +"src/utils/dashboard/sqlChartSeriesProps.spec.ts":1.4, +"src/utils/dashboard/sqlMissingValueFiller.spec.ts":0.7, +"src/utils/dashboard/sqlProcessData.spec.ts":2.4, +"src/utils/dashboard/sqlTimeSeriesConverter.spec.ts":1.1, +"src/utils/dashboard/stackTotals.spec.ts":3.2, +"src/utils/dashboard/streamLifetime.spec.ts":1.3, +"src/utils/dashboard/streaming/index.spec.ts":1, +"src/utils/dashboard/streaming/overlayNewDataOnOldOptions.spec.ts":1.3, +"src/utils/dashboard/tableConfigUtils.spec.ts":3.7, +"src/utils/dashboard/tableOptions.spec.ts":3.4, +"src/utils/dashboard/thresholds.builders.spec.ts":4.3, +"src/utils/dashboard/thresholds.spec.ts":4.4, +"src/utils/dashboard/timestampAliasRewrite.spec.ts":1.5, +"src/utils/dashboard/tooltipSort.spec.ts":1.2, +"src/utils/dashboard/urlTimeParams.spec.ts":3.4, +"src/utils/dashboard/useCustomDebouncer.spec.ts":1.2, +"src/utils/dashboard/variableSuggestions.spec.ts":1.1, +"src/utils/dashboard/variables/variablesDependencyUtils.spec.ts":1.1, +"src/utils/dashboard/variables/variablesUtils.spec.ts":1.4, +"src/utils/dashboard/visualizeEmptyFilter.spec.ts":2.1, +"src/utils/date.spec.ts":3.8, +"src/utils/dateTimeRangeParse.spec.ts":1.9, +"src/utils/dbm/activity.spec.ts":1.4, +"src/utils/dbm/aiPrompts.spec.ts":0.8, +"src/utils/dbm/blocking.spec.ts":1.2, +"src/utils/dbm/breakdown.spec.ts":1, +"src/utils/dbm/breakdownRows.spec.ts":1.6, +"src/utils/dbm/callingServices.spec.ts":1.3, +"src/utils/dbm/deadlocks.spec.ts":2, +"src/utils/dbm/emptyAction.spec.ts":1.2, +"src/utils/dbm/filters.spec.ts":0.9, +"src/utils/dbm/fleetRows.spec.ts":1, +"src/utils/dbm/format.spec.ts":1.1, +"src/utils/dbm/healthScalar.spec.ts":1.8, +"src/utils/dbm/history.spec.ts":1.4, +"src/utils/dbm/historyChart.spec.ts":1.7, +"src/utils/dbm/historyPanelSchema.spec.ts":2.4, +"src/utils/dbm/incidentSummary.spec.ts":0.9, +"src/utils/dbm/insights.spec.ts":0.9, +"src/utils/dbm/instanceMetrics.spec.ts":0.8, +"src/utils/dbm/instanceMetricsRead.spec.ts":1.2, +"src/utils/dbm/metricSections.spec.ts":2, +"src/utils/dbm/metricsPanels.spec.ts":1.6, +"src/utils/dbm/notCollecting.spec.ts":1.5, +"src/utils/dbm/overlapJoin.spec.ts":1, +"src/utils/dbm/overlapMetrics.spec.ts":0.8, +"src/utils/dbm/plans.spec.ts":1.1, +"src/utils/dbm/queryDetailTabs.spec.ts":1, +"src/utils/dbm/recommendations.spec.ts":1.1, +"src/utils/dbm/samples.spec.ts":1.3, +"src/utils/dbm/serverMetrics.spec.ts":0.9, +"src/utils/dbm/tableHealth.spec.ts":1.4, +"src/utils/dbm/tones.spec.ts":0.9, +"src/utils/dbm/whereItRuns.spec.ts":1.5, +"src/utils/destinationType.spec.ts":1.2, +"src/utils/downtimes/banner.spec.ts":2.3, +"src/utils/downtimes/conditionBridge.spec.ts":2, +"src/utils/downtimes/downtimeForm.spec.ts":2, +"src/utils/downtimes/folderDefault.spec.ts":1, +"src/utils/downtimes/listOrder.spec.ts":0.7, +"src/utils/downtimes/notifyTest.spec.ts":1, +"src/utils/downtimes/quickMute.spec.ts":4.5, +"src/utils/downtimes/schedule.spec.ts":3.5, +"src/utils/downtimes/summary.spec.ts":3.9, +"src/utils/downtimes/suppressed.spec.ts":0.7, +"src/utils/downtimes/targetSummary.spec.ts":1.1, +"src/utils/drafts/editorDraftStore.spec.ts":1.4, +"src/utils/drafts/redactDraftSecrets.spec.ts":1.2, +"src/utils/drafts/signOutFlow.spec.ts":2.1, +"src/utils/drafts/urlCredentials.spec.ts":0.9, +"src/utils/enterpriseFeatures.completeness.spec.ts":3.6, +"src/utils/enterpriseFeatures.spec.ts":1.2, +"src/utils/fieldCategories.spec.ts":1.3, +"src/utils/fieldValueUtils.spec.ts":0.9, +"src/utils/focusNextControl.spec.ts":1.7, +"src/utils/folderIcons.spec.ts":1.3, +"src/utils/formatters.spec.ts":1, +"src/utils/functionLanguage.spec.ts":1, +"src/utils/identityConfig.spec.ts":1.5, +"src/utils/incidents/rcaAskAttachment.spec.ts":3.6, +"src/utils/incidents/rcaCell.spec.ts":2.9, +"src/utils/incidents/rcaEvidenceLinks.spec.ts":2.2, +"src/utils/incidents/rcaReportMarkdown.spec.ts":1.5, +"src/utils/incidents/rcaSteering.spec.ts":2, +"src/utils/incidents/rcaSummary.spec.ts":2.8, +"src/utils/incidents/rcaThread.spec.ts":2.4, +"src/utils/internalStreams.spec.ts":0.9, +"src/utils/json.spec.ts":1.6, +"src/utils/licenseWaivers.spec.ts":1.9, +"src/utils/llmUtils.spec.ts":1.5, +"src/utils/logs/breakdownFilter.spec.ts":2.2, +"src/utils/logs/convertLogData.spec.ts":1.9, +"src/utils/logs/detailRowMatch.spec.ts":3.6, +"src/utils/logs/estimateScanMb.spec.ts":3.2, +"src/utils/logs/freeTextScan.spec.ts":1.7, +"src/utils/logs/interestingFields.spec.ts":1.9, +"src/utils/logs/logPermalink.spec.ts":2.3, +"src/utils/logs/sqlSources.spec.ts":1.2, +"src/utils/logs/statusParser.bench.spec.ts":1.1, +"src/utils/logs/statusParser.spec.ts":1.4, +"src/utils/logs/streamNameColumn.spec.ts":1.2, +"src/utils/logs/transientSearchKeys.spec.ts":1.5, +"src/utils/logs/visualizeStreamFields.spec.ts":1.3, +"src/utils/metrics/breakdownStats.spec.ts":1.4, +"src/utils/metrics/buildMetricsUrl.spec.ts":2, +"src/utils/metrics/explorerSavedView.spec.ts":0.9, +"src/utils/metrics/explorerUrlState.spec.ts":1.8, +"src/utils/metrics/forecast.spec.ts":0.8, +"src/utils/metrics/labelFilterSql.spec.ts":1.7, +"src/utils/metrics/metricDefaults.builder.spec.ts":1.2, +"src/utils/metrics/metricDefaults.spec.ts":1.7, +"src/utils/metrics/metricDrilldown.spec.ts":2.1, +"src/utils/metrics/metricFamily.spec.ts":0.6, +"src/utils/metrics/metricGrouping.spec.ts":1, +"src/utils/metrics/metricPalette.spec.ts":2.7, +"src/utils/metrics/metricPanelSeed.spec.ts":1.5, +"src/utils/metrics/metricsHandoff.spec.ts":0.7, +"src/utils/metrics/metricsParamRegistry.spec.ts":4.1, +"src/utils/metrics/outlierScore.spec.ts":0.7, +"src/utils/metrics/prefixGrouping.spec.ts":0.8, +"src/utils/metrics/queryHistory.spec.ts":2.9, +"src/utils/metrics/relatedMetrics.spec.ts":1.1, +"src/utils/nsFieldsPatch.spec.ts":1.4, +"src/utils/oncall.spec.ts":5.3, +"src/utils/pageCrossing.spec.ts":2, +"src/utils/passwordComplexity.spec.ts":0.9, +"src/utils/passwordResetErrors.spec.ts":0.9, +"src/utils/pipelines/nodeErrors.spec.ts":1.3, +"src/utils/prebuilt-templates/discord.spec.ts":1, +"src/utils/prebuilt-templates/email.spec.ts":1.1, +"src/utils/prebuilt-templates/index.spec.ts":1.2, +"src/utils/prebuilt-templates/jira.spec.ts":1.1, +"src/utils/prebuilt-templates/msteams.spec.ts":0.9, +"src/utils/prebuilt-templates/opsgenie.spec.ts":1.1, +"src/utils/prebuilt-templates/pagerduty.spec.ts":1.5, +"src/utils/prebuilt-templates/servicenow.spec.ts":1.3, +"src/utils/prebuilt-templates/slack.spec.ts":1.9, +"src/utils/prebuilt-templates/templates.spec.ts":1.2, +"src/utils/prebuilt-templates/types.spec.ts":1.6, +"src/utils/query/builderToggle.integration.spec.ts":5.3, +"src/utils/query/colorizeQuery.monacoLoad.spec.ts":1.2, +"src/utils/query/colorizeQuery.spec.ts":1.1, +"src/utils/query/doubleQuoteWarnings.spec.ts":1.2, +"src/utils/query/editorKeyContract.spec.ts":1.8, +"src/utils/query/editorProviders.spec.ts":1.2, +"src/utils/query/editorSuggestStats.spec.ts":1.3, +"src/utils/query/editorWiring.spec.ts":1.5, +"src/utils/query/formatSql.spec.ts":1.7, +"src/utils/query/freeTextFilter.spec.ts":3.6, +"src/utils/query/nonSqlLimit.spec.ts":0.8, +"src/utils/query/promQLUtils.spec.ts":0.7, +"src/utils/query/promqlCompletion.spec.ts":0.9, +"src/utils/query/queryErrorExplain.spec.ts":1.6, +"src/utils/query/quickModeFieldList.spec.ts":0.8, +"src/utils/query/searchError.spec.ts":1, +"src/utils/query/serverFunctions.spec.ts":0.9, +"src/utils/query/sqlComments.spec.ts":1.1, +"src/utils/query/sqlCompletion.spec.ts":1.7, +"src/utils/query/sqlComplexity.spec.ts":1.3, +"src/utils/query/sqlDiagnostics.spec.ts":14.2, +"src/utils/query/sqlFilterBuilder.spec.ts":3.3, +"src/utils/query/sqlQueryParser.spec.ts":1.5, +"src/utils/query/sqlUtils.spec.ts":2.2, +"src/utils/query/visualizationUtils.spec.ts":0.9, +"src/utils/query/vrlLanguageDefinition.spec.ts":1.3, +"src/utils/queryHistory/adapters.spec.ts":2.9, +"src/utils/queryHistory/logs.spec.ts":2.9, +"src/utils/queryHistory/observeMoves.spec.ts":0.8, +"src/utils/queryHistory/queryHistoryContract.spec.ts":2.2, +"src/utils/queryHistory/recentSearches.spec.ts":1.5, +"src/utils/queryHistory/routes.spec.ts":3.8, +"src/utils/queryPlanParser.spec.ts":1.1, +"src/utils/queryUtils.spec.ts":0.8, +"src/utils/reports/reportRun.spec.ts":2.3, +"src/utils/routeTabMaps.spec.ts":0.7, +"src/utils/rowNavigation.spec.ts":2.3, +"src/utils/rum/clickHeatmap.spec.ts":1.7, +"src/utils/rum/clickHeatmapQueries.spec.ts":3.3, +"src/utils/rum/dashboardCapability.spec.ts":1, +"src/utils/rum/ensureBrowserRum.spec.ts":2.3, +"src/utils/rum/errorDetailQueries.spec.ts":2, +"src/utils/rum/errorIssueQueries.spec.ts":3.8, +"src/utils/rum/errorIssueUtils.spec.ts":2, +"src/utils/rum/errorTraceCorrelation.spec.ts":0.9, +"src/utils/rum/extensionFrames.spec.ts":1.2, +"src/utils/rum/fields.spec.ts":1.7, +"src/utils/rum/funnelCompare.spec.ts":2.8, +"src/utils/rum/funnelFields.spec.ts":3, +"src/utils/rum/funnelTrend.spec.ts":3.1, +"src/utils/rum/ignoredNoisePatterns.spec.ts":1.6, +"src/utils/rum/initBrowserRum.spec.ts":1.3, +"src/utils/rum/performanceOverview.spec.ts":2.3, +"src/utils/rum/performanceOverviewPanel.spec.ts":3.8, +"src/utils/rum/productAnalyticsModel.attributeBreakdown.spec.ts":2.9, +"src/utils/rum/productAnalyticsModel.exclusions.spec.ts":2.7, +"src/utils/rum/productAnalyticsModel.funnelFilters.spec.ts":2.4, +"src/utils/rum/productAnalyticsModel.spec.ts":3.1, +"src/utils/rum/productAnalyticsPanels.spec.ts":2.5, +"src/utils/rum/productAnalyticsQueries.attributeBreakdown.spec.ts":1.8, +"src/utils/rum/productAnalyticsQueries.funnelFilters.spec.ts":3.2, +"src/utils/rum/productAnalyticsQueries.spec.ts":4.6, +"src/utils/rum/productAnalyticsQueries.trend.spec.ts":3.3, +"src/utils/rum/productAnalyticsRoutes.spec.ts":0.7, +"src/utils/rum/replayCanvasPlaceholder.spec.ts":1, +"src/utils/rum/rumPaApiError.spec.ts":1.2, +"src/utils/rum/scrubAuthParams.spec.ts":1.8, +"src/utils/rum/sessionReplayChangeFormat.spec.ts":1.5, +"src/utils/rum/sessionReplayLive.spec.ts":1.3, +"src/utils/rum/sessionReplayLoadPlan.spec.ts":0.8, +"src/utils/rum/sessionReplayLoader.spec.ts":1.6, +"src/utils/rum/sessionReplayManifest.spec.ts":1.6, +"src/utils/rum/sessionReplaySeekPlan.spec.ts":1.7, +"src/utils/rum/sessionReplayTimeline.spec.ts":1, +"src/utils/rum/sessionReplayUrls.spec.ts":1, +"src/utils/rum/sessionReplayViews.spec.ts":0.8, +"src/utils/rum/traceWindow.spec.ts":0.9, +"src/utils/rum/tracingOrigin.spec.ts":1.4, +"src/utils/rum/viewDocuments.spec.ts":1.2, +"src/utils/safeUrl.spec.ts":0.8, +"src/utils/semanticGroupsCache.spec.ts":1.6, +"src/utils/serviceStreamEnvs.spec.ts":1.1, +"src/utils/sessionServices.spec.ts":2.3, +"src/utils/slackAppDestination.spec.ts":1.7, +"src/utils/slackCommunityInvite.spec.ts":1.3, +"src/utils/slackLink.spec.ts":1.6, +"src/utils/slackManifest.spec.ts":1.3, +"src/utils/slackSettings.spec.ts":1.4, +"src/utils/slos/alertSource.spec.ts":1.4, +"src/utils/slos/burndownQuery.spec.ts":1.2, +"src/utils/slos/chartScale.spec.ts":1.5, +"src/utils/slos/previewQuery.spec.ts":2.3, +"src/utils/slos/sloTerraform.spec.ts":1.9, +"src/utils/sourceEventSeverity.spec.ts":0.8, +"src/utils/sourceHygiene.spec.ts":3.4, +"src/utils/storage.spec.ts":1.8, +"src/utils/streamPersist.spec.ts":1.4, +"src/utils/synthetics/buildExtractedChild.spec.ts":4.6, +"src/utils/synthetics/buildPayload.spec.ts":3.3, +"src/utils/synthetics/buildV2Steps.spec.ts":1.1, +"src/utils/synthetics/checkDraftSecrets.spec.ts":1, +"src/utils/synthetics/composeLocator.spec.ts":1.2, +"src/utils/synthetics/compositionContract.spec.ts":1.3, +"src/utils/synthetics/deriveLocatorKind.spec.ts":1, +"src/utils/synthetics/expandJourney.spec.ts":1.1, +"src/utils/synthetics/extractEligibility.spec.ts":0.7, +"src/utils/synthetics/journeySuggestions.spec.ts":2.2, +"src/utils/synthetics/locationLiveStatus.spec.ts":1.1, +"src/utils/synthetics/locatorStability.spec.ts":0.9, +"src/utils/synthetics/mapRecordedStep.spec.ts":0.9, +"src/utils/synthetics/payloadChanged.spec.ts":4, +"src/utils/synthetics/payloadContract.spec.ts":2.3, +"src/utils/synthetics/replayFailure.spec.ts":0.9, +"src/utils/synthetics/rollUpStatus.spec.ts":1.8, +"src/utils/synthetics/routes.spec.ts":1, +"src/utils/synthetics/runBudget.spec.ts":2.1, +"src/utils/synthetics/stepTarget.spec.ts":1, +"src/utils/telemetryCorrelation.spec.ts":1.6, +"src/utils/templates/validation.spec.ts":1.5, +"src/utils/theme.spec.ts":1.7, +"src/utils/themeManager.spec.ts":1.8, +"src/utils/timeRangeRail.spec.ts":2.2, +"src/utils/timezone.spec.ts":2.1, +"src/utils/timezoneAliases.spec.ts":0.9, +"src/utils/traces/applyGraphCollapse.spec.ts":1.2, +"src/utils/traces/computeTreeLayout.spec.ts":0.9, +"src/utils/traces/constants.spec.ts":1.4, +"src/utils/traces/convertTraceData.spec.ts":3.3, +"src/utils/traces/criticalPath.spec.ts":2.1, +"src/utils/traces/filterUtils.spec.ts":4.5, +"src/utils/traces/modelVendorLogo.spec.ts":1.5, +"src/utils/traces/patternDetection.spec.ts":1.1, +"src/utils/traces/serviceClassification.spec.ts":2.1, +"src/utils/traces/serviceColorRegistry.spec.ts":2.1, +"src/utils/traces/spanPalette.spec.ts":0.7, +"src/utils/traces/streamSelection.spec.ts":0.9, +"src/utils/traces/traceColors.spec.ts":1.5, +"src/utils/traces/traceServiceGraph.spec.ts":1.2, +"src/utils/traces/treeTooltipHelpers.spec.ts":1.7, +"src/utils/traces/treeVisualizationEngine.spec.ts":0.7, +"src/utils/traces/useSpanServiceDetection.spec.ts":1.3, +"src/utils/trialPaywallNotice.spec.ts":1.3, +"src/utils/url/deepLinkParams.spec.ts":1.8, +"src/utils/uuid.spec.ts":1.2, +"src/utils/validateAlerts.spec.ts":1.8, +"src/utils/validatePipeline.spec.ts":1, +"src/utils/zincutils.spec.ts":3.3, +"src/views/AIObservability/PromptDetailDrawer.spec.ts":5.3, +"src/views/AIObservability/PromptEditor.schema.spec.ts":1.1, +"src/views/AIObservability/PromptEditorPage.drafts.spec.ts":7.3, +"src/views/AIObservability/PromptSettings.schema.spec.ts":1.4, +"src/views/AIObservability/PromptSettingsDrawer.spec.ts":1.8, +"src/views/AIObservability/PromptTrafficPanel.spec.ts":1.7, +"src/views/AIObservability/PromptsPage.drafts.spec.ts":4.9, +"src/views/AIObservability/SaveAsPromptDialog.spec.ts":3.5, +"src/views/AIObservability/usePromptAnalytics.spec.ts":1.1, +"src/views/About.license.spec.ts":4.4, +"src/views/About.spec.ts":2.6, +"src/views/AddAlertView.spec.ts":6.2, +"src/views/AlertLibrary/AlertLibrary.spec.ts":22.7, +"src/views/AppAlerts.spec.ts":0.9, +"src/views/AwsMarketplaceSetup.spec.ts":4.2, +"src/views/AzureMarketplaceSetup.spec.ts":5.6, +"src/views/Dashboards/DashboardJsonEditor.spec.ts":1.2, +"src/views/Dashboards/DashboardSettings.drafts.spec.ts":11.4, +"src/views/Dashboards/DashboardSettings.spec.ts":8.8, +"src/views/Dashboards/Dashboards.publicLinks.spec.ts":6.4, +"src/views/Dashboards/Dashboards.spec.ts":4.3, +"src/views/Dashboards/ImportDashboard.spec.ts":6, +"src/views/Dashboards/PanelLayoutSettings.spec.ts":3.4, +"src/views/Dashboards/PublicDashboard.spec.ts":5, +"src/views/Dashboards/RenderDashboardCharts.spec.ts":11.9, +"src/views/Dashboards/ScheduledDashboards.spec.ts":5.3, +"src/views/Dashboards/TvPlaylist.spec.ts":2.4, +"src/views/Dashboards/addPanel/AddCondition.spec.ts":4.8, +"src/views/Dashboards/addPanel/AddJoinPopUp.spec.ts":4.8, +"src/views/Dashboards/addPanel/AddPanel.drafts.spec.ts":16, +"src/views/Dashboards/addPanel/AddPanel.spec.ts":8, +"src/views/Dashboards/addPanel/DashboardFiltersOption.spec.ts":0.9, +"src/views/Dashboards/addPanel/DashboardJoinsOption.spec.ts":3.6, +"src/views/Dashboards/addPanel/Group.spec.ts":2.3, +"src/views/Dashboards/publicDashboardPolling.spec.ts":1.9, +"src/views/Dashboards/viewDashboard.defaultRefresh.spec.ts":7.6, +"src/views/Dashboards/viewDashboard.spec.ts":20, +"src/views/Dashboards/viewDashboard.timeRange.spec.ts":7.9, +"src/views/DatabaseMonitoring/dbmActivityRowNavigation.spec.ts":0.9, +"src/views/DatabaseMonitoring/dbmBlockedFooterGrammar.spec.ts":1.1, +"src/views/DatabaseMonitoring/dbmComparisonBaseline.spec.ts":1, +"src/views/DatabaseMonitoring/dbmEmptyStateTraceCount.spec.ts":0.9, +"src/views/DatabaseMonitoring/dbmEndpointsJoinKey.spec.ts":1, +"src/views/DatabaseMonitoring/dbmEventTabScope.spec.ts":2.2, +"src/views/DatabaseMonitoring/dbmFallbackCoversLoading.spec.ts":1.9, +"src/views/DatabaseMonitoring/dbmFallbackScopeVisible.spec.ts":0.9, +"src/views/DatabaseMonitoring/dbmFleetDefaultSort.spec.ts":0.9, +"src/views/DatabaseMonitoring/dbmListOverlapVantage.spec.ts":0.9, +"src/views/DatabaseMonitoring/dbmListPagination.spec.ts":1, +"src/views/DatabaseMonitoring/dbmLockAlertAction.spec.ts":1.5, +"src/views/DatabaseMonitoring/dbmMetricsTabWiring.spec.ts":1.5, +"src/views/DatabaseMonitoring/dbmOverlapSortOrder.spec.ts":1.3, +"src/views/DatabaseMonitoring/dbmPlanProvenance.spec.ts":1.4, +"src/views/DatabaseMonitoring/dbmPlansEmptyState.spec.ts":0.7, +"src/views/DatabaseMonitoring/dbmPlansJump.spec.ts":1.2, +"src/views/DatabaseMonitoring/dbmQueryDetailEntry.spec.ts":1.2, +"src/views/DatabaseMonitoring/dbmQueryDetailNotFound.spec.ts":0.9, +"src/views/DatabaseMonitoring/dbmQueryDetailOriginSurvives.spec.ts":1.3, +"src/views/DatabaseMonitoring/dbmQueryDetailTabs.spec.ts":2.1, +"src/views/DatabaseMonitoring/dbmRecommendationsSection.spec.ts":1.1, +"src/views/DatabaseMonitoring/dbmRequestGuard.spec.ts":0.9, +"src/views/DatabaseMonitoring/dbmSamplesLogOff.spec.ts":0.7, +"src/views/DatabaseMonitoring/dbmScopeRowLayout.spec.ts":2.1, +"src/views/DatabaseMonitoring/dbmSectionTabCounts.spec.ts":1.1, +"src/views/DatabaseMonitoring/dbmServerMetricsSection.spec.ts":0.7, +"src/views/DatabaseMonitoring/dbmStatStripFilter.spec.ts":0.8, +"src/views/DatabaseMonitoring/dbmSummaryTileWithholding.spec.ts":2.5, +"src/views/DatabaseMonitoring/dbmTableHealthCellRendering.spec.ts":3, +"src/views/DatabaseMonitoring/dbmZeroTraceQueryDetail.spec.ts":1.4, +"src/views/Error404.spec.ts":1.9, +"src/views/FleetConfigEditor.drafts.spec.ts":11.6, +"src/views/FleetConfigEditor.spec.ts":13.7, +"src/views/FleetManager.spec.ts":4.4, +"src/views/Functions.spec.ts":2.2, +"src/views/HomeChatHistory.spec.ts":3.7, +"src/views/HomeNoDataState.spec.ts":3.4, +"src/views/HomeView.spec.ts":3.5, +"src/views/IdentityAccessManagement.spec.ts":5.9, +"src/views/Infrastructure/HostDetailDrawer.spec.ts":4.6, +"src/views/Infrastructure/HostsDependenciesView.spec.ts":6.8, +"src/views/Infrastructure/HostsPage.spec.ts":11.1, +"src/views/Infrastructure/curated/CuratedPageView.spec.ts":18.9, +"src/views/Infrastructure/curated/packs/hosts.page.spec.ts":2.7, +"src/views/Infrastructure/curated/packs/kubernetes.health.spec.ts":3, +"src/views/Infrastructure/curated/packs/kubernetes.page.spec.ts":3.2, +"src/views/Infrastructure/curated/packs/kubernetes.summary.spec.ts":3.3, +"src/views/Infrastructure/curated/packs/kubernetes.utilization.spec.ts":2.4, +"src/views/Infrastructure/curated/packs/lint.spec.ts":5.7, +"src/views/Infrastructure/curated/resolve.spec.ts":4, +"src/views/Infrastructure/curated/useCuratedPage.spec.ts":9.4, +"src/views/Infrastructure/hostsDependencyGraph.spec.ts":1.1, +"src/views/Infrastructure/hostsQueries.spec.ts":0.6, +"src/views/Infrastructure/kubernetes2/ClusterOverview.spec.ts":8.5, +"src/views/Infrastructure/kubernetes2/DependenciesView.spec.ts":9.1, +"src/views/Infrastructure/kubernetes2/DependencyMap.spec.ts":4.5, +"src/views/Infrastructure/kubernetes2/K8sAttentionList.spec.ts":4.4, +"src/views/Infrastructure/kubernetes2/K8sClusterHotbar.spec.ts":3.4, +"src/views/Infrastructure/kubernetes2/K8sCommandPalette.spec.ts":4.8, +"src/views/Infrastructure/kubernetes2/K8sDetailsDrawer.spec.ts":12.1, +"src/views/Infrastructure/kubernetes2/K8sHexMap.spec.ts":4.3, +"src/views/Infrastructure/kubernetes2/K8sListHeader.spec.ts":2.5, +"src/views/Infrastructure/kubernetes2/K8sListView.spec.ts":21.2, +"src/views/Infrastructure/kubernetes2/K8sLogsPanel.spec.ts":5, +"src/views/Infrastructure/kubernetes2/K8sResourceTree.spec.ts":3.6, +"src/views/Infrastructure/kubernetes2/K8sRowMenu.spec.ts":1.8, +"src/views/Infrastructure/kubernetes2/K8sYamlPanel.spec.ts":2, +"src/views/Infrastructure/kubernetes2/KubernetesPage.spec.ts":39.3, +"src/views/Infrastructure/kubernetes2/MapView.spec.ts":21.5, +"src/views/Infrastructure/kubernetes2/NodeDetails.spec.ts":6, +"src/views/Infrastructure/kubernetes2/PodDetails.spec.ts":3.6, +"src/views/Infrastructure/kubernetes2/RelatedPodsTable.spec.ts":4.8, +"src/views/Infrastructure/kubernetes2/WorkloadDetails.spec.ts":6.1, +"src/views/Infrastructure/kubernetes2/WorkloadsOverview.spec.ts":11.8, +"src/views/Infrastructure/kubernetes2/dependencyGraph.spec.ts":1, +"src/views/Infrastructure/kubernetes2/dependencyMapGeometry.spec.ts":1.7, +"src/views/Infrastructure/kubernetes2/dockState.spec.ts":1.7, +"src/views/Infrastructure/kubernetes2/hexLayout.spec.ts":7.8, +"src/views/Infrastructure/kubernetes2/hexViewport.spec.ts":1, +"src/views/Infrastructure/kubernetes2/hoverCard.spec.ts":3.4, +"src/views/Infrastructure/kubernetes2/k8sDock.spec.ts":4, +"src/views/Infrastructure/kubernetes2/k8sNavigation.spec.ts":3.2, +"src/views/Infrastructure/kubernetes2/k8sPalette.spec.ts":3.3, +"src/views/Infrastructure/kubernetes2/k8sPodLogs.spec.ts":1.5, +"src/views/Infrastructure/kubernetes2/k8sTree.spec.ts":3, +"src/views/Infrastructure/kubernetes2/k8sYaml.spec.ts":1.1, +"src/views/Infrastructure/kubernetes2/kubernetesColumns.spec.ts":1.7, +"src/views/Infrastructure/kubernetes2/kubernetesEvents.spec.ts":2, +"src/views/Infrastructure/kubernetes2/kubernetesModel.spec.ts":3.8, +"src/views/Infrastructure/kubernetes2/kubernetesObjects.spec.ts":1.9, +"src/views/Infrastructure/kubernetes2/kubernetesPanels.spec.ts":1.1, +"src/views/Infrastructure/kubernetes2/kubernetesQueries.spec.ts":1.9, +"src/views/Infrastructure/kubernetes2/kubernetesStatus.spec.ts":2.2, +"src/views/Infrastructure/kubernetes2/kubernetesStatusChips.spec.ts":3.9, +"src/views/Infrastructure/kubernetes2/kubernetesUrlState.spec.ts":5, +"src/views/Infrastructure/kubernetes2/mapFill.spec.ts":1.9, +"src/views/Infrastructure/kubernetes2/mapFilter.spec.ts":2.3, +"src/views/Infrastructure/kubernetes2/overviewModel.spec.ts":2.7, +"src/views/Infrastructure/kubernetes2/podLogsLink.spec.ts":3.1, +"src/views/Infrastructure/kubernetes2/useKubernetesInventory.spec.ts":3.4, +"src/views/Infrastructure/kubernetes2/usePaletteIndex.spec.ts":2.9, +"src/views/Infrastructure/kubernetes2/workloadProblems.spec.ts":3.1, +"src/views/Infrastructure/kubernetes2/workloadStatusLabel.spec.ts":1.5, +"src/views/Infrastructure/useHostsList.spec.ts":1.2, +"src/views/Ingestion.spec.ts":8.3, +"src/views/Invitations.spec.ts":5.6, +"src/views/LogStream.spec.ts":10.2, +"src/views/Login.spec.ts":5.7, +"src/views/MemberSubscription.spec.ts":1.9, +"src/views/OnCall/OnCallMine.spec.ts":4.5, +"src/views/OnCall/OnCallPolicies.spec.ts":4.6, +"src/views/OnCall/OnCallResponseDetail.spec.ts":7.8, +"src/views/OnCall/OnCallResponseDetailSkeleton.spec.ts":1.1, +"src/views/OnCall/OnCallResponses.spec.ts":26.1, +"src/views/OnCall/OnCallRouting.spec.ts":4.5, +"src/views/OnCall/OnCallTeamDetail.spec.ts":21.5, +"src/views/OnCall/OnCallTeams.spec.ts":11.6, +"src/views/OverviewTab.spec.ts":6.6, +"src/views/PinnedDashboardTab.spec.ts":6.1, +"src/views/RUM/AppAnalytics.spec.ts":22.1, +"src/views/RUM/AppErrors.spec.ts":9, +"src/views/RUM/AppPerformance.spec.ts":6.8, +"src/views/RUM/AppSessions.spec.ts":17.7, +"src/views/RUM/ClickHeatmap.spec.ts":69, +"src/views/RUM/ErrorViewer.spec.ts":4.5, +"src/views/RUM/NamedEventEditor.drafts.spec.ts":4.6, +"src/views/RUM/NamedEventEditor.spec.ts":13.9, +"src/views/RUM/RealUserMonitoring.spec.ts":3.8, +"src/views/RUM/SessionViewer.spec.ts":13.3, +"src/views/RUM/SourceMaps.spec.ts":3.8, +"src/views/RUM/UploadSourceMaps.spec.ts":4.1, +"src/views/SharedChatView.spec.ts":5.1, +"src/views/ShortUrl.spec.ts":6.7, +"src/views/SlackConnectCallback.spec.ts":2.2, +"src/views/SlackLink.spec.ts":2.1, +"src/views/SlackLinkCallback.spec.ts":4.2, +"src/views/SlackOAuthCallback.spec.ts":1.4, +"src/views/StreamExplorer.spec.ts":3.1, +"src/views/SyntheticMonitoring.spec.ts":12.3, +"src/views/UsageTab.spec.ts":9.5, +"src/views/alerts/AlertDetail.spec.ts":16.9, +"src/views/alerts/AlertDetailSlo.spec.ts":13, +"src/views/reports/ReportDetail.drafts.spec.ts":6.4, +"src/views/slos/AddSlo.drafts.spec.ts":12.1, +"src/views/slos/AddSlo.schema.spec.ts":3.4, +"src/views/slos/AddSloAlertSli.spec.ts":14.1, +"src/views/slos/AddSloCountPromql.spec.ts":17.2, +"src/views/slos/AddSloMetricsLanguage.spec.ts":17.4, +"src/views/slos/AddSloTimeSlicePromql.spec.ts":17.4, +"src/views/slos/SloDetailAlertSli.spec.ts":6.1, +"src/views/slos/SloDetailAlerts.spec.ts":3.2, +"src/views/slos/SloDetailCorrections.spec.ts":3.9, +"src/views/slos/SloList.drafts.spec.ts":5, +"src/views/slos/SloListDelete.spec.ts":14.3, +"src/views/slos/SloListExport.spec.ts":7.6, +"src/views/slos/SloListPagination.spec.ts":8.8, +"src/views/synthetics/CreateBrowserTest.drafts.spec.ts":10.8, +"src/views/synthetics/CreateBrowserTest.spec.ts":19.3, +"src/views/synthetics/CreateCheck.spec.ts":2.9, +"src/views/synthetics/CreateProtocolCheck.drafts.spec.ts":5.4, +"src/views/synthetics/CreateProtocolCheck.spec.ts":4.5, +"src/views/synthetics/MonitorResults.spec.ts":3.2, +"src/views/synthetics/MonitorRuns.spec.ts":5.5, +"src/views/synthetics/MonitorStatusTimeline.spec.ts":3.1, +"src/views/synthetics/PrivateLocationDetail.spec.ts":7.2, +"src/views/synthetics/PrivateLocations.spec.ts":3.3, +"src/views/synthetics/RunDetail.spec.ts":11.5, +"src/views/synthetics/status-pages/DomainsDialog.spec.ts":3.3, +"src/views/synthetics/status-pages/NoticeHistoryDialog.spec.ts":2.7, +"src/views/synthetics/status-pages/StatusPageEditor.drafts.spec.ts":10.6, +"src/views/synthetics/status-pages/StatusPageEditor.spec.ts":3.3, +"src/views/synthetics/status-pages/StatusPagesList.spec.ts":3.2, +"src/workers/rumcssworker.spec.ts":0.8} diff --git a/web/scripts/ui-shard-durations.mjs b/web/scripts/ui-shard-durations.mjs new file mode 100644 index 0000000000..c6aee8b06f --- /dev/null +++ b/web/scripts/ui-shard-durations.mjs @@ -0,0 +1,16 @@ +// Regenerate ui-shard-durations.json: download the ui-unit-blob-* artifacts of a green CI run (they expire after 1 day), then `node scripts/ui-shard-durations.mjs `. +import { readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { parse } from 'flatted' + +const dir = process.argv[2] ?? '.vitest-reports' +const out = {} +for (const f of readdirSync(dir, { recursive: true }).filter((n) => n.endsWith('.json'))) { + for (const t of parse(readFileSync(join(dir, f), 'utf8'))[1]) { + const ms = (t.result?.duration ?? 0) + (t.collectDuration ?? 0) + (t.setupDuration ?? 0) + (t.environmentLoad ?? 0) + (t.prepareDuration ?? 0) + out[t.filepath.slice(t.filepath.indexOf('/web/') + 5)] = Math.round(ms / 100) / 10 + } +} +const sorted = Object.fromEntries(Object.entries(out).sort(([a], [b]) => (a < b ? -1 : 1))) +writeFileSync(new URL('./ui-shard-durations.json', import.meta.url), JSON.stringify(sorted, null, 0).replace(/,"/g, ',\n"') + '\n') +console.log(Object.keys(sorted).length, 'files') diff --git a/web/scripts/ui-shard-sequencer.ts b/web/scripts/ui-shard-sequencer.ts new file mode 100644 index 0000000000..e5eb84c11b --- /dev/null +++ b/web/scripts/ui-shard-sequencer.ts @@ -0,0 +1,34 @@ +// Balances --shard=i/N by recorded per-file seconds (greedy longest-first); refresh with scripts/ui-shard-durations.mjs. +import { readFileSync } from 'node:fs' +import { relative } from 'node:path' +import { BaseSequencer } from 'vitest/node' +import type { TestSpecification } from 'vitest/node' + +const durations: Record = JSON.parse( + readFileSync(new URL('./ui-shard-durations.json', import.meta.url), 'utf8'), +) + +export function partition(paths: string[], count: number): string[][] { + const known = Object.values(durations).sort((a, b) => a - b) + const median = known[Math.floor(known.length / 2)] ?? 1 + const secs = (p: string) => durations[p] ?? median + const bins: string[][] = Array.from({ length: count }, () => []) + const load = new Array(count).fill(0) + for (const p of [...paths].sort((a, b) => secs(b) - secs(a) || (a < b ? -1 : 1))) { + const i = load.indexOf(Math.min(...load)) + bins[i].push(p) + load[i] += secs(p) + } + return bins +} + +export default class UiShardSequencer extends BaseSequencer { + async shard(files: TestSpecification[]) { + const { root, shard } = this.ctx.config + if (!shard) return files + const rel = (f: TestSpecification) => relative(root, f.moduleId) + const bins = partition(files.map(rel), shard.count) + const mine = new Set(bins[shard.index - 1]) + return files.filter((f) => mine.has(rel(f))) + } +} diff --git a/web/src/components/dashboards/PanelContainer.spec.ts b/web/src/components/dashboards/PanelContainer.spec.ts index d026bfa37c..8e2101a7db 100644 --- a/web/src/components/dashboards/PanelContainer.spec.ts +++ b/web/src/components/dashboards/PanelContainer.spec.ts @@ -371,6 +371,62 @@ describe("PanelContainer", () => { }); describe("Panel Controls", () => { + // OTable is covered by PanelDataView.spec; this file's ResizeObserver mock cannot construct. + const dataViewStub = { + PanelDataView: { template: '
' }, + ODropdown: { template: "
" }, + ODropdownItem: { + name: "ODropdownItem", + emits: ["select"], + template: '
', + }, + }; + const TOGGLE = '[data-test="dashboard-panel-data-view-toggle"]'; + const DATA_VIEW = '[data-test="dashboard-panel-data-view"]'; + + it("switches a chart panel to its raw rows and back from the panel menu", async () => { + wrapper = createWrapper({}, dataViewStub); + + expect(wrapper.find(TOGGLE).text()).toBe("Show raw data"); + expect(wrapper.find(DATA_VIEW).exists()).toBe(false); + + await wrapper.find(TOGGLE).trigger("click"); + expect(wrapper.find(DATA_VIEW).exists()).toBe(true); + expect(wrapper.find('[data-test="panel-schema-renderer"]').exists()).toBe(true); + expect(wrapper.find(TOGGLE).text()).toBe("Show chart"); + + await wrapper.find(TOGGLE).trigger("click"); + expect(wrapper.find(DATA_VIEW).exists()).toBe(false); + }); + + it("hides the legends button while the rows are shown", async () => { + wrapper = createWrapper({ showLegendsButton: true }, dataViewStub); + await wrapper.find('[data-test="dashboard-panel-container"]').trigger("mouseover"); + expect(wrapper.find('[data-test="dashboard-show-legends-btn"]').exists()).toBe(true); + + await wrapper.find(TOGGLE).trigger("click"); + expect(wrapper.find('[data-test="dashboard-show-legends-btn"]').exists()).toBe(false); + }); + + it("returns to the chart when layout editing starts", async () => { + wrapper = createWrapper({ layoutMode: "view" }, dataViewStub); + await wrapper.find(TOGGLE).trigger("click"); + expect(wrapper.find(DATA_VIEW).exists()).toBe(true); + + await wrapper.setProps({ layoutMode: "edit" }); + expect(wrapper.find(DATA_VIEW).exists()).toBe(false); + }); + + it.each([ + ["table panels", { data: { ...defaultProps.data, type: "table" } }], + ["kiosk mode", { kiosk: true }], + ["view-only panels", { viewOnly: true }], + ["simplified panels", { simplifiedPanelView: true }], + ])("offers no Data view on %s", (_name, props) => { + wrapper = createWrapper(props, dataViewStub); + expect(wrapper.find(TOGGLE).exists()).toBe(false); + }); + it("should show fullscreen button on hover when not view-only", async () => { wrapper = createWrapper({ viewOnly: false }); diff --git a/web/src/components/dashboards/PanelContainer.vue b/web/src/components/dashboards/PanelContainer.vue index 46da5f4580..6ba90214c6 100644 --- a/web/src/components/dashboards/PanelContainer.vue +++ b/web/src/components/dashboards/PanelContainer.vue @@ -138,6 +138,7 @@ along with this program. If not, see . :class="hoverRevealClass" v-if=" props.showLegendsButton && + !isDataView && !PanleSchemaRendererRef?.noData && ![ 'table', @@ -177,6 +178,7 @@ along with this program. If not, see . . + + + {{ isDataView ? t("panel.showChart") : t("panel.showRawData") }} + . @series-data-update="onCuratedSeriesData" @send-to-ai-chat="(value, append) => $emit('sendToAiChat', value, append)" > +
props.data?.type, + renderer: PanleSchemaRendererRef, + enabled: () => !props.kiosk && !props.viewOnly && !props.simplifiedPanelView, + }); const exemplarsEligible = computed(() => isExemplarEligible(props.data)); const exemplarKey = computed(() => @@ -1049,6 +1083,10 @@ export default defineComponent({ // for full screen button const isCurrentlyHoveredPanel: any = ref(false); const isLayoutEditing = computed(() => props.layoutMode === "edit"); + // Layout editing hides the panel bar controls, which would leave the Data view with no way back. + watch(isLayoutEditing, (editing) => { + if (editing) previewView.value = "chart"; + }); // Editor actions follow the "Edit panel" menu item; Refresh is a viewer action, so layout edit and viewOnly keep it. const canEditFromNotice = computed( @@ -1585,6 +1623,11 @@ export default defineComponent({ isLayoutEditing, hoverRevealClass, doorRevealClass, + previewView, + showDataViewToggle, + isDataView, + showDataOverlay, + previewRawData, hasNoticeChips, showViewPanel, dependentAdHocVariable, diff --git a/web/src/components/dashboards/PanelEditor/PanelEditor.spec.ts b/web/src/components/dashboards/PanelEditor/PanelEditor.spec.ts index e83a938bf8..3603ef851d 100644 --- a/web/src/components/dashboards/PanelEditor/PanelEditor.spec.ts +++ b/web/src/components/dashboards/PanelEditor/PanelEditor.spec.ts @@ -1087,6 +1087,47 @@ describe("PanelEditor.vue", () => { }); }); + describe("Preview Chart | Data view", () => { + const TOGGLE = { name: "PanelViewToggle" }; + + beforeEach(() => { + mockChartData.value = {}; + mockDashboardPanelData.data.type = "line"; + }); + + afterEach(() => { + mockChartData.value = undefined; + }); + + const mountEditor = (pageType = "dashboard") => + shallowMount(PanelEditor, { props: { pageType: pageType as any }, global: mountGlobal }); + + it("offers the switch on a dashboard chart and starts on Chart", () => { + wrapper = mountEditor(); + expect(wrapper.findComponent(TOGGLE).exists()).toBe(true); + expect(wrapper.findComponent({ name: "PanelDataView" }).exists()).toBe(false); + }); + + it("hides the switch for table panels and outside dashboards", () => { + mockDashboardPanelData.data.type = "table"; + wrapper = mountEditor(); + expect(wrapper.findComponent(TOGGLE).exists()).toBe(false); + wrapper.unmount(); + + mockDashboardPanelData.data.type = "line"; + wrapper = mountEditor("metrics"); + expect(wrapper.findComponent(TOGGLE).exists()).toBe(false); + }); + + it("overlays the raw rows when Data is picked", async () => { + wrapper = mountEditor(); + wrapper.findComponent({ name: "PanelViewToggle" }).vm.$emit("update:modelValue", "data"); + await nextTick(); + expect(wrapper.findComponent({ name: "PanelDataView" }).exists()).toBe(true); + expect(wrapper.findComponent({ name: "PanelSchemaRenderer" }).exists()).toBe(true); + }); + }); + describe("renderer errors", () => { beforeEach(() => { mockChartData.value = {}; diff --git a/web/src/components/dashboards/PanelEditor/PanelEditor.vue b/web/src/components/dashboards/PanelEditor/PanelEditor.vue index 4abe3e88a4..812db39c96 100644 --- a/web/src/components/dashboards/PanelEditor/PanelEditor.vue +++ b/web/src/components/dashboards/PanelEditor/PanelEditor.vue @@ -197,8 +197,16 @@ along with this program. If not, see .
+ + . @format-column="openColumnFormatting" />
+ +
@@ -808,6 +826,9 @@ import DashboardQueryBuilder from "@/components/dashboards/addPanel/DashboardQue import DashboardErrorsComponent from "@/components/dashboards/addPanel/DashboardErrors.vue"; import PanelSchemaRenderer from "@/components/dashboards/PanelSchemaRenderer.vue"; import PanelErrorButtons from "@/components/dashboards/PanelErrorButtons.vue"; +import PanelDataView from "@/components/dashboards/addPanel/PanelDataView.vue"; +import PanelViewToggle from "@/components/dashboards/PanelViewToggle.vue"; +import { usePanelDataView } from "@/composables/dashboard/usePanelDataView"; import { usePanelEditorFocus } from "@/composables/dashboard/usePanelEditorFocus"; import type { PanelNoticeActionId } from "@/utils/dashboard/panelNotices"; import OButton from "@/lib/core/Button/OButton.vue"; @@ -1017,6 +1038,18 @@ const seriesOverridePreviewSource = computed(() => { }; }); +const { + previewView, + showDataViewToggle, + isDataView, + showDataOverlay, + rawData: previewRawData, +} = usePanelDataView({ + panelType: () => dashboardPanelData.data.type, + renderer: panelSchemaRendererRef, + enabled: () => props.pageType === "dashboard", +}); + // The desktop 20% field-list splitter is unusable on a phone, so there it opens at ~half width. const { isMobile } = useBreakpoint(); const mobileFieldsOpen = ref(false); diff --git a/web/src/components/dashboards/PanelSchemaRenderer.vue b/web/src/components/dashboards/PanelSchemaRenderer.vue index 3bc2fc34e1..6ca8010788 100644 --- a/web/src/components/dashboards/PanelSchemaRenderer.vue +++ b/web/src/components/dashboards/PanelSchemaRenderer.vue @@ -2636,6 +2636,7 @@ export default defineComponent({ chartPanelRef, chartRendererRef, data, + filteredData, loading, searchRequestTraceIds, errorDetail, diff --git a/web/src/components/dashboards/PanelViewToggle.vue b/web/src/components/dashboards/PanelViewToggle.vue new file mode 100644 index 0000000000..cd429f8128 --- /dev/null +++ b/web/src/components/dashboards/PanelViewToggle.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/web/src/components/dashboards/addPanel/PanelDataView.spec.ts b/web/src/components/dashboards/addPanel/PanelDataView.spec.ts new file mode 100644 index 0000000000..28f92c9fbb --- /dev/null +++ b/web/src/components/dashboards/addPanel/PanelDataView.spec.ts @@ -0,0 +1,101 @@ +// Copyright 2026 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { describe, expect, it } from "vitest"; +import { mount } from "@vue/test-utils"; +import i18n from "@/locales"; +import PanelDataView from "./PanelDataView.vue"; + +const mountView = (props: Record) => + mount(PanelDataView, { + props: { dataTestPrefix: "test", ...props }, + global: { plugins: [i18n] }, + }); + +describe("PanelDataView", () => { + it("shows the query result columns and rows", () => { + const wrapper = mountView({ + data: [ + [ + { x_axis_1: "api", "a.b": 3 }, + { x_axis_1: "web", "a.b": 4 }, + ], + ], + }); + const table = wrapper.findComponent({ name: "OTable" }); + expect(table.props("columns").map((c: any) => c.id)).toEqual(["x_axis_1", "a.b"]); + expect(table.props("data")).toHaveLength(2); + expect(table.props("columns")[1].accessorFn(table.props("data")[0])).toBe(3); + }); + + it("gives duplicate rows distinct keys", () => { + const row = { x_axis_1: "api", y_axis_1: 1 }; + const wrapper = mountView({ data: [[row, { ...row }]] }); + const table = wrapper.findComponent({ name: "OTable" }); + const rowKey = table.props("rowKey") as (r: any) => string; + const [a, b] = table.props("data"); + expect(rowKey(a)).not.toBe(rowKey(b)); + }); + + it("flattens PromQL range and instant series into timestamp, value and labels", () => { + const wrapper = mountView({ + queryType: "promql", + data: [ + { + result: [ + { metric: { job: "api" }, values: [[1, "5"]] }, + { metric: { job: "web" }, value: [2, "7"] }, + ], + }, + ], + }); + const table = wrapper.findComponent({ name: "OTable" }); + expect(table.props("columns").map((c: any) => c.id)).toEqual(["timestamp", "value", "job"]); + expect(table.props("data")).toHaveLength(2); + }); + + it("shows its own empty message when the query returned nothing", () => { + const table = mountView({ data: [[]] }).findComponent({ name: "OTable" }); + expect(table.props("data")).toHaveLength(0); + expect(table.props("emptyMessage")).toBe("No rows returned"); + }); + + it("keeps the rows on screen while a refresh streams in", () => { + const table = mountView({ data: [[{ a: 1 }]], loading: true }).findComponent({ + name: "OTable", + }); + expect(table.props("loading")).toBe(false); + expect(table.props("streaming")).toBe(true); + }); + + it("caps very large results and says so", () => { + const rows = Array.from({ length: 100005 }, (_, i) => ({ i })); + // jsdom has no layout, so a real virtual-scrolled OTable would draw every row. + const wrapper = mount(PanelDataView, { + props: { dataTestPrefix: "test", data: [rows] }, + global: { + plugins: [i18n], + stubs: { OTable: { name: "OTable", props: ["data"], template: "
" } }, + }, + }); + expect(wrapper.findComponent({ name: "OTable" }).props("data")).toHaveLength(100000); + expect(wrapper.find('[data-test="test-data-view-capped"]').text()).toContain("100,005"); + }); + + it("aligns a numeric column right even when its first value is empty", () => { + const table = mountView({ data: [[{ n: null }, { n: 4 }]] }).findComponent({ name: "OTable" }); + expect(table.props("columns")[0].meta.align).toBe("right"); + }); +}); diff --git a/web/src/components/dashboards/addPanel/PanelDataView.vue b/web/src/components/dashboards/addPanel/PanelDataView.vue new file mode 100644 index 0000000000..ed79f6754c --- /dev/null +++ b/web/src/components/dashboards/addPanel/PanelDataView.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/web/src/components/dashboards/panels/LogsPanelRenderer.spec.ts b/web/src/components/dashboards/panels/LogsPanelRenderer.spec.ts index ef4b6fa886..6455b55d62 100644 --- a/web/src/components/dashboards/panels/LogsPanelRenderer.spec.ts +++ b/web/src/components/dashboards/panels/LogsPanelRenderer.spec.ts @@ -102,6 +102,12 @@ describe("LogsPanelRenderer", () => { expect(table().props("pinnedFirstColumn")).toBe("_timestamp"); }); + it("leaves the empty state to the panel so no second No Data message stacks under it", async () => { + await mountPanel({}, []); + expect(table().exists()).toBe(false); + expect(wrapper!.find('[data-test="o2-table-empty"]').exists()).toBe(false); + }); + it("colours each row's severity rail from the level field, like the logs table", async () => { await mountPanel(); const data = table().props("data") as any[]; diff --git a/web/src/components/dashboards/panels/LogsPanelRenderer.vue b/web/src/components/dashboards/panels/LogsPanelRenderer.vue index ec1603411a..4e32609411 100644 --- a/web/src/components/dashboards/panels/LogsPanelRenderer.vue +++ b/web/src/components/dashboards/panels/LogsPanelRenderer.vue @@ -16,7 +16,9 @@ along with this program. If not, see .