diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3689d75..a13b70b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -10,10 +10,13 @@ on: env: CARGO_TERM_COLOR: always RUST_MIN_STACK: 16777216 + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 jobs: - rust-blocking: - name: Rust Blocking Checks + rust-fmt: + name: Rustfmt runs-on: ubuntu-latest steps: - name: Checkout @@ -22,19 +25,119 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: - components: rustfmt, clippy + components: rustfmt - name: Rustfmt run: cargo fmt --all -- --check + rust-clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache Cargo registry + index + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-registry- + + - name: Cache Cargo build artifacts + uses: actions/cache@v4 + with: + path: target + key: cargo-clippy-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }} + restore-keys: | + cargo-clippy-${{ hashFiles('**/Cargo.lock') }}- + cargo-clippy- + - name: Clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings + rust-test: + name: Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo registry + index + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-registry- + + - name: Cache Cargo build artifacts + uses: actions/cache@v4 + with: + path: target + key: cargo-test-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }} + restore-keys: | + cargo-test-${{ hashFiles('**/Cargo.lock') }}- + cargo-test- + - name: Tests run: cargo test --workspace --all-features - - name: Install Rust security tooling - run: cargo install --locked cargo-audit cargo-deny + rust-audit: + name: Cargo Audit & Deny + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo registry + index + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-registry- + + - name: Cache cargo-binstall and installed binaries + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/cargo-binstall + ~/.cargo/bin/cargo-audit + ~/.cargo/bin/cargo-deny + key: cargo-security-tools-v1 + + - name: Install cargo-binstall + run: | + if ! command -v cargo-binstall &> /dev/null; then + curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + fi + + - name: Install security tools (pre-built binaries) + run: | + command -v cargo-audit &> /dev/null || cargo binstall --no-confirm --locked cargo-audit + command -v cargo-deny &> /dev/null || cargo binstall --no-confirm --locked cargo-deny - name: Cargo Audit run: cargo audit diff --git a/web/src/api/models/ActionSummary.ts b/web/src/api/models/ActionSummary.ts index cb9421f..930284e 100644 --- a/web/src/api/models/ActionSummary.ts +++ b/web/src/api/models/ActionSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Simplified action response (for list endpoints) */ diff --git a/web/src/api/models/ApiResponse_CurrentUserResponse.ts b/web/src/api/models/ApiResponse_CurrentUserResponse.ts index 6b1b58d..9f5604c 100644 --- a/web/src/api/models/ApiResponse_CurrentUserResponse.ts +++ b/web/src/api/models/ApiResponse_CurrentUserResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Standard API response wrapper */ diff --git a/web/src/api/models/ApiResponse_KeyResponse.ts b/web/src/api/models/ApiResponse_KeyResponse.ts index 38cc0a5..1738b41 100644 --- a/web/src/api/models/ApiResponse_KeyResponse.ts +++ b/web/src/api/models/ApiResponse_KeyResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { OwnerType } from './OwnerType'; /** diff --git a/web/src/api/models/ApiResponse_PackInstallResponse.ts b/web/src/api/models/ApiResponse_PackInstallResponse.ts index 340251c..e1d928c 100644 --- a/web/src/api/models/ApiResponse_PackInstallResponse.ts +++ b/web/src/api/models/ApiResponse_PackInstallResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { PackResponse } from './PackResponse'; import type { PackTestResult } from './PackTestResult'; /** diff --git a/web/src/api/models/ApiResponse_QueueStatsResponse.ts b/web/src/api/models/ApiResponse_QueueStatsResponse.ts index 03eff9c..a7e623e 100644 --- a/web/src/api/models/ApiResponse_QueueStatsResponse.ts +++ b/web/src/api/models/ApiResponse_QueueStatsResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Standard API response wrapper */ diff --git a/web/src/api/models/ApiResponse_String.ts b/web/src/api/models/ApiResponse_String.ts index 4a2cb12..5fba4c1 100644 --- a/web/src/api/models/ApiResponse_String.ts +++ b/web/src/api/models/ApiResponse_String.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Standard API response wrapper */ diff --git a/web/src/api/models/ApiResponse_TokenResponse.ts b/web/src/api/models/ApiResponse_TokenResponse.ts index 9208959..e684e12 100644 --- a/web/src/api/models/ApiResponse_TokenResponse.ts +++ b/web/src/api/models/ApiResponse_TokenResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { UserInfo } from './UserInfo'; /** * Standard API response wrapper diff --git a/web/src/api/models/ApiResponse_WebhookReceiverResponse.ts b/web/src/api/models/ApiResponse_WebhookReceiverResponse.ts index e056064..8670802 100644 --- a/web/src/api/models/ApiResponse_WebhookReceiverResponse.ts +++ b/web/src/api/models/ApiResponse_WebhookReceiverResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Standard API response wrapper */ diff --git a/web/src/api/models/ChangePasswordRequest.ts b/web/src/api/models/ChangePasswordRequest.ts index 4f3ed13..590d80f 100644 --- a/web/src/api/models/ChangePasswordRequest.ts +++ b/web/src/api/models/ChangePasswordRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Change password request */ diff --git a/web/src/api/models/CreateKeyRequest.ts b/web/src/api/models/CreateKeyRequest.ts index 864fb75..3698467 100644 --- a/web/src/api/models/CreateKeyRequest.ts +++ b/web/src/api/models/CreateKeyRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { OwnerType } from './OwnerType'; /** diff --git a/web/src/api/models/CurrentUserResponse.ts b/web/src/api/models/CurrentUserResponse.ts index f3197e1..f13ef4b 100644 --- a/web/src/api/models/CurrentUserResponse.ts +++ b/web/src/api/models/CurrentUserResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Current user response */ diff --git a/web/src/api/models/EnforcementCondition.ts b/web/src/api/models/EnforcementCondition.ts index 84cfee7..1d48f0c 100644 --- a/web/src/api/models/EnforcementCondition.ts +++ b/web/src/api/models/EnforcementCondition.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + export enum EnforcementCondition { ANY = 'any', ALL = 'all', diff --git a/web/src/api/models/EnforcementStatus.ts b/web/src/api/models/EnforcementStatus.ts index 0f34187..f8de91e 100644 --- a/web/src/api/models/EnforcementStatus.ts +++ b/web/src/api/models/EnforcementStatus.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + export enum EnforcementStatus { CREATED = 'created', PROCESSED = 'processed', diff --git a/web/src/api/models/EnforcementSummary.ts b/web/src/api/models/EnforcementSummary.ts index 5527d05..84ff524 100644 --- a/web/src/api/models/EnforcementSummary.ts +++ b/web/src/api/models/EnforcementSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { EnforcementCondition } from './EnforcementCondition'; import type { EnforcementStatus } from './EnforcementStatus'; import type { i64 } from './i64'; diff --git a/web/src/api/models/EventSummary.ts b/web/src/api/models/EventSummary.ts index 611f4f1..18bce8c 100644 --- a/web/src/api/models/EventSummary.ts +++ b/web/src/api/models/EventSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; /** * Summary event response for list views diff --git a/web/src/api/models/ExecutionStatus.ts b/web/src/api/models/ExecutionStatus.ts index 97eb85e..67dc334 100644 --- a/web/src/api/models/ExecutionStatus.ts +++ b/web/src/api/models/ExecutionStatus.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + export enum ExecutionStatus { REQUESTED = 'requested', SCHEDULING = 'scheduling', diff --git a/web/src/api/models/ExecutionSummary.ts b/web/src/api/models/ExecutionSummary.ts index 0be0eb5..eb1ee42 100644 --- a/web/src/api/models/ExecutionSummary.ts +++ b/web/src/api/models/ExecutionSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ExecutionStatus } from "./ExecutionStatus"; /** * Simplified execution response (for list endpoints) diff --git a/web/src/api/models/HealthResponse.ts b/web/src/api/models/HealthResponse.ts index d4dbb93..ab16625 100644 --- a/web/src/api/models/HealthResponse.ts +++ b/web/src/api/models/HealthResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Health check response */ diff --git a/web/src/api/models/InquiryStatus.ts b/web/src/api/models/InquiryStatus.ts index 4475bc8..553aca4 100644 --- a/web/src/api/models/InquiryStatus.ts +++ b/web/src/api/models/InquiryStatus.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + export enum InquiryStatus { PENDING = 'pending', RESPONDED = 'responded', diff --git a/web/src/api/models/InquirySummary.ts b/web/src/api/models/InquirySummary.ts index f5678a5..43f59cb 100644 --- a/web/src/api/models/InquirySummary.ts +++ b/web/src/api/models/InquirySummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { InquiryStatus } from './InquiryStatus'; /** diff --git a/web/src/api/models/InstallPackRequest.ts b/web/src/api/models/InstallPackRequest.ts index 83729e7..6de65ee 100644 --- a/web/src/api/models/InstallPackRequest.ts +++ b/web/src/api/models/InstallPackRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Request DTO for installing a pack from remote source */ diff --git a/web/src/api/models/KeyResponse.ts b/web/src/api/models/KeyResponse.ts index d01d130..de2cbb8 100644 --- a/web/src/api/models/KeyResponse.ts +++ b/web/src/api/models/KeyResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { OwnerType } from './OwnerType'; /** diff --git a/web/src/api/models/KeySummary.ts b/web/src/api/models/KeySummary.ts index 689b525..9fbe25a 100644 --- a/web/src/api/models/KeySummary.ts +++ b/web/src/api/models/KeySummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { OwnerType } from './OwnerType'; /** diff --git a/web/src/api/models/LoginRequest.ts b/web/src/api/models/LoginRequest.ts index 5db7ae2..be1a08e 100644 --- a/web/src/api/models/LoginRequest.ts +++ b/web/src/api/models/LoginRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Login request */ diff --git a/web/src/api/models/OwnerType.ts b/web/src/api/models/OwnerType.ts index c3ad5cd..ab88d2b 100644 --- a/web/src/api/models/OwnerType.ts +++ b/web/src/api/models/OwnerType.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + export enum OwnerType { SYSTEM = 'system', IDENTITY = 'identity', diff --git a/web/src/api/models/PackInstallResponse.ts b/web/src/api/models/PackInstallResponse.ts index 186c204..70af751 100644 --- a/web/src/api/models/PackInstallResponse.ts +++ b/web/src/api/models/PackInstallResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { PackResponse } from './PackResponse'; import type { PackTestResult } from './PackTestResult'; /** diff --git a/web/src/api/models/PackSummary.ts b/web/src/api/models/PackSummary.ts index bd18e03..80ca267 100644 --- a/web/src/api/models/PackSummary.ts +++ b/web/src/api/models/PackSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Simplified pack response (for list endpoints) */ diff --git a/web/src/api/models/PackTestExecution.ts b/web/src/api/models/PackTestExecution.ts index d292713..3aae18c 100644 --- a/web/src/api/models/PackTestExecution.ts +++ b/web/src/api/models/PackTestExecution.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { Value } from './Value'; /** diff --git a/web/src/api/models/PackTestResult.ts b/web/src/api/models/PackTestResult.ts index 0680f10..87bdba9 100644 --- a/web/src/api/models/PackTestResult.ts +++ b/web/src/api/models/PackTestResult.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { TestSuiteResult } from './TestSuiteResult'; /** * Pack test result structure (not from DB, used for test execution) diff --git a/web/src/api/models/PackTestSummary.ts b/web/src/api/models/PackTestSummary.ts index 58e5a4b..33a1bbd 100644 --- a/web/src/api/models/PackTestSummary.ts +++ b/web/src/api/models/PackTestSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; /** * Pack test summary view diff --git a/web/src/api/models/PackWorkflowSyncResponse.ts b/web/src/api/models/PackWorkflowSyncResponse.ts index c346da1..93bcebb 100644 --- a/web/src/api/models/PackWorkflowSyncResponse.ts +++ b/web/src/api/models/PackWorkflowSyncResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { WorkflowSyncResult } from './WorkflowSyncResult'; /** * Response for pack workflow sync operation diff --git a/web/src/api/models/PackWorkflowValidationResponse.ts b/web/src/api/models/PackWorkflowValidationResponse.ts index 883e946..5ebe21e 100644 --- a/web/src/api/models/PackWorkflowValidationResponse.ts +++ b/web/src/api/models/PackWorkflowValidationResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Response for pack workflow validation operation */ diff --git a/web/src/api/models/PaginatedResponse_ActionSummary.ts b/web/src/api/models/PaginatedResponse_ActionSummary.ts index 8fe79dc..666ba63 100644 --- a/web/src/api/models/PaginatedResponse_ActionSummary.ts +++ b/web/src/api/models/PaginatedResponse_ActionSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { PaginationMeta } from "./PaginationMeta"; /** * Paginated response wrapper diff --git a/web/src/api/models/PaginatedResponse_EnforcementSummary.ts b/web/src/api/models/PaginatedResponse_EnforcementSummary.ts index a7634e7..6d7f8c6 100644 --- a/web/src/api/models/PaginatedResponse_EnforcementSummary.ts +++ b/web/src/api/models/PaginatedResponse_EnforcementSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { EnforcementCondition } from './EnforcementCondition'; import type { EnforcementStatus } from './EnforcementStatus'; import type { i64 } from './i64'; diff --git a/web/src/api/models/PaginatedResponse_EventSummary.ts b/web/src/api/models/PaginatedResponse_EventSummary.ts index 4880c2f..5bf8e0e 100644 --- a/web/src/api/models/PaginatedResponse_EventSummary.ts +++ b/web/src/api/models/PaginatedResponse_EventSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { PaginationMeta } from './PaginationMeta'; /** diff --git a/web/src/api/models/PaginatedResponse_ExecutionSummary.ts b/web/src/api/models/PaginatedResponse_ExecutionSummary.ts index 1a7c442..9c874df 100644 --- a/web/src/api/models/PaginatedResponse_ExecutionSummary.ts +++ b/web/src/api/models/PaginatedResponse_ExecutionSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ExecutionStatus } from "./ExecutionStatus"; import type { PaginationMeta } from "./PaginationMeta"; /** diff --git a/web/src/api/models/PaginatedResponse_InquirySummary.ts b/web/src/api/models/PaginatedResponse_InquirySummary.ts index 45bf350..cdb71bd 100644 --- a/web/src/api/models/PaginatedResponse_InquirySummary.ts +++ b/web/src/api/models/PaginatedResponse_InquirySummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { InquiryStatus } from './InquiryStatus'; import type { PaginationMeta } from './PaginationMeta'; diff --git a/web/src/api/models/PaginatedResponse_KeySummary.ts b/web/src/api/models/PaginatedResponse_KeySummary.ts index 70945c9..fe32449 100644 --- a/web/src/api/models/PaginatedResponse_KeySummary.ts +++ b/web/src/api/models/PaginatedResponse_KeySummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { OwnerType } from './OwnerType'; import type { PaginationMeta } from './PaginationMeta'; diff --git a/web/src/api/models/PaginatedResponse_PackSummary.ts b/web/src/api/models/PaginatedResponse_PackSummary.ts index 032f009..1340770 100644 --- a/web/src/api/models/PaginatedResponse_PackSummary.ts +++ b/web/src/api/models/PaginatedResponse_PackSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { PaginationMeta } from './PaginationMeta'; /** * Paginated response wrapper diff --git a/web/src/api/models/PaginatedResponse_PackTestSummary.ts b/web/src/api/models/PaginatedResponse_PackTestSummary.ts index aa5806f..30635c3 100644 --- a/web/src/api/models/PaginatedResponse_PackTestSummary.ts +++ b/web/src/api/models/PaginatedResponse_PackTestSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { i64 } from './i64'; import type { PaginationMeta } from './PaginationMeta'; /** diff --git a/web/src/api/models/PaginatedResponse_SensorSummary.ts b/web/src/api/models/PaginatedResponse_SensorSummary.ts index ff69804..6dc50a4 100644 --- a/web/src/api/models/PaginatedResponse_SensorSummary.ts +++ b/web/src/api/models/PaginatedResponse_SensorSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { PaginationMeta } from './PaginationMeta'; /** * Paginated response wrapper diff --git a/web/src/api/models/PaginatedResponse_TriggerSummary.ts b/web/src/api/models/PaginatedResponse_TriggerSummary.ts index 0690a9a..b410e71 100644 --- a/web/src/api/models/PaginatedResponse_TriggerSummary.ts +++ b/web/src/api/models/PaginatedResponse_TriggerSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { PaginationMeta } from './PaginationMeta'; /** * Paginated response wrapper diff --git a/web/src/api/models/PaginatedResponse_WorkflowSummary.ts b/web/src/api/models/PaginatedResponse_WorkflowSummary.ts index 2954e43..17fd948 100644 --- a/web/src/api/models/PaginatedResponse_WorkflowSummary.ts +++ b/web/src/api/models/PaginatedResponse_WorkflowSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { PaginationMeta } from './PaginationMeta'; /** * Paginated response wrapper diff --git a/web/src/api/models/PaginationMeta.ts b/web/src/api/models/PaginationMeta.ts index bc08279..b5595b6 100644 --- a/web/src/api/models/PaginationMeta.ts +++ b/web/src/api/models/PaginationMeta.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Pagination metadata */ diff --git a/web/src/api/models/QueueStatsResponse.ts b/web/src/api/models/QueueStatsResponse.ts index 9c616a0..b642cdd 100644 --- a/web/src/api/models/QueueStatsResponse.ts +++ b/web/src/api/models/QueueStatsResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Response DTO for queue statistics */ diff --git a/web/src/api/models/RefreshTokenRequest.ts b/web/src/api/models/RefreshTokenRequest.ts index d8d2c5c..5302b60 100644 --- a/web/src/api/models/RefreshTokenRequest.ts +++ b/web/src/api/models/RefreshTokenRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Refresh token request */ diff --git a/web/src/api/models/RegisterPackRequest.ts b/web/src/api/models/RegisterPackRequest.ts index c561364..549ee38 100644 --- a/web/src/api/models/RegisterPackRequest.ts +++ b/web/src/api/models/RegisterPackRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Request DTO for registering a pack from local filesystem */ diff --git a/web/src/api/models/RegisterRequest.ts b/web/src/api/models/RegisterRequest.ts index ec5e176..b9931d3 100644 --- a/web/src/api/models/RegisterRequest.ts +++ b/web/src/api/models/RegisterRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Register request */ diff --git a/web/src/api/models/SensorSummary.ts b/web/src/api/models/SensorSummary.ts index 05cdd53..5b734ea 100644 --- a/web/src/api/models/SensorSummary.ts +++ b/web/src/api/models/SensorSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Simplified sensor response (for list endpoints) */ diff --git a/web/src/api/models/SuccessResponse.ts b/web/src/api/models/SuccessResponse.ts index 24948a8..73a478a 100644 --- a/web/src/api/models/SuccessResponse.ts +++ b/web/src/api/models/SuccessResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Success message response (for operations that don't return data) */ diff --git a/web/src/api/models/TestCaseResult.ts b/web/src/api/models/TestCaseResult.ts index 42d1bc5..5000377 100644 --- a/web/src/api/models/TestCaseResult.ts +++ b/web/src/api/models/TestCaseResult.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { TestStatus } from './TestStatus'; /** * Individual test case result diff --git a/web/src/api/models/TestStatus.ts b/web/src/api/models/TestStatus.ts index 3f8ed79..71ca37f 100644 --- a/web/src/api/models/TestStatus.ts +++ b/web/src/api/models/TestStatus.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Test status enum */ diff --git a/web/src/api/models/TestSuiteResult.ts b/web/src/api/models/TestSuiteResult.ts index ada7ef3..4ec0223 100644 --- a/web/src/api/models/TestSuiteResult.ts +++ b/web/src/api/models/TestSuiteResult.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { TestCaseResult } from './TestCaseResult'; /** * Test suite result (collection of test cases) diff --git a/web/src/api/models/TokenResponse.ts b/web/src/api/models/TokenResponse.ts index 89f3695..3424984 100644 --- a/web/src/api/models/TokenResponse.ts +++ b/web/src/api/models/TokenResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { UserInfo } from './UserInfo'; /** * Token response diff --git a/web/src/api/models/TriggerSummary.ts b/web/src/api/models/TriggerSummary.ts index d5a68ad..3fbb9f7 100644 --- a/web/src/api/models/TriggerSummary.ts +++ b/web/src/api/models/TriggerSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Simplified trigger response (for list endpoints) */ diff --git a/web/src/api/models/UpdateKeyRequest.ts b/web/src/api/models/UpdateKeyRequest.ts index f1039f3..a6153b3 100644 --- a/web/src/api/models/UpdateKeyRequest.ts +++ b/web/src/api/models/UpdateKeyRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Request to update an existing key/secret */ diff --git a/web/src/api/models/UserInfo.ts b/web/src/api/models/UserInfo.ts index 671ba2a..7bb92b5 100644 --- a/web/src/api/models/UserInfo.ts +++ b/web/src/api/models/UserInfo.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * User information included in token response */ diff --git a/web/src/api/models/WebhookReceiverRequest.ts b/web/src/api/models/WebhookReceiverRequest.ts index a06c696..7f805c0 100644 --- a/web/src/api/models/WebhookReceiverRequest.ts +++ b/web/src/api/models/WebhookReceiverRequest.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { Value } from './Value'; /** * Request body for webhook receiver endpoint diff --git a/web/src/api/models/WebhookReceiverResponse.ts b/web/src/api/models/WebhookReceiverResponse.ts index 2e4e74c..0d33d4a 100644 --- a/web/src/api/models/WebhookReceiverResponse.ts +++ b/web/src/api/models/WebhookReceiverResponse.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Response from webhook receiver endpoint */ diff --git a/web/src/api/models/WorkflowSummary.ts b/web/src/api/models/WorkflowSummary.ts index 3062c19..3047eae 100644 --- a/web/src/api/models/WorkflowSummary.ts +++ b/web/src/api/models/WorkflowSummary.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Simplified workflow response (for list endpoints) */ diff --git a/web/src/api/models/WorkflowSyncResult.ts b/web/src/api/models/WorkflowSyncResult.ts index 38da9a7..5fae670 100644 --- a/web/src/api/models/WorkflowSyncResult.ts +++ b/web/src/api/models/WorkflowSyncResult.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + /** * Individual workflow sync result */ diff --git a/web/src/api/models/i64.ts b/web/src/api/models/i64.ts index 6129ad9..a63f3b7 100644 --- a/web/src/api/models/i64.ts +++ b/web/src/api/models/i64.ts @@ -1,5 +1,5 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + export type i64 = number; diff --git a/web/src/api/services/AuthService.ts b/web/src/api/services/AuthService.ts index f3b029a..78ef880 100644 --- a/web/src/api/services/AuthService.ts +++ b/web/src/api/services/AuthService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ChangePasswordRequest } from '../models/ChangePasswordRequest'; import type { LoginRequest } from '../models/LoginRequest'; import type { RefreshTokenRequest } from '../models/RefreshTokenRequest'; diff --git a/web/src/api/services/EnforcementsService.ts b/web/src/api/services/EnforcementsService.ts index acce322..42948de 100644 --- a/web/src/api/services/EnforcementsService.ts +++ b/web/src/api/services/EnforcementsService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ApiResponse_EnforcementResponse } from '../models/ApiResponse_EnforcementResponse'; import type { EnforcementStatus } from '../models/EnforcementStatus'; import type { i64 } from '../models/i64'; diff --git a/web/src/api/services/EventsService.ts b/web/src/api/services/EventsService.ts index 63e62c9..edd2fa9 100644 --- a/web/src/api/services/EventsService.ts +++ b/web/src/api/services/EventsService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ApiResponse_EventResponse } from '../models/ApiResponse_EventResponse'; import type { i64 } from '../models/i64'; import type { PaginatedResponse_EventSummary } from '../models/PaginatedResponse_EventSummary'; diff --git a/web/src/api/services/InquiriesService.ts b/web/src/api/services/InquiriesService.ts index 5611f43..721ee83 100644 --- a/web/src/api/services/InquiriesService.ts +++ b/web/src/api/services/InquiriesService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ApiResponse_InquiryResponse } from '../models/ApiResponse_InquiryResponse'; import type { CreateInquiryRequest } from '../models/CreateInquiryRequest'; import type { i64 } from '../models/i64'; diff --git a/web/src/api/services/RulesService.ts b/web/src/api/services/RulesService.ts index b9e15df..b2bc9cb 100644 --- a/web/src/api/services/RulesService.ts +++ b/web/src/api/services/RulesService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ApiResponse_RuleResponse } from '../models/ApiResponse_RuleResponse'; import type { CreateRuleRequest } from '../models/CreateRuleRequest'; import type { PaginatedResponse_RuleSummary } from '../models/PaginatedResponse_RuleSummary'; diff --git a/web/src/api/services/SecretsService.ts b/web/src/api/services/SecretsService.ts index fb2ddcc..4c313b1 100644 --- a/web/src/api/services/SecretsService.ts +++ b/web/src/api/services/SecretsService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { CreateKeyRequest } from '../models/CreateKeyRequest'; import type { i64 } from '../models/i64'; import type { OwnerType } from '../models/OwnerType'; diff --git a/web/src/api/services/SensorsService.ts b/web/src/api/services/SensorsService.ts index c239574..92cb3e9 100644 --- a/web/src/api/services/SensorsService.ts +++ b/web/src/api/services/SensorsService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ApiResponse_SensorResponse } from '../models/ApiResponse_SensorResponse'; import type { CreateSensorRequest } from '../models/CreateSensorRequest'; import type { PaginatedResponse_SensorSummary } from '../models/PaginatedResponse_SensorSummary'; diff --git a/web/src/api/services/TriggersService.ts b/web/src/api/services/TriggersService.ts index 33ef196..cdaeb89 100644 --- a/web/src/api/services/TriggersService.ts +++ b/web/src/api/services/TriggersService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { ApiResponse_TriggerResponse } from '../models/ApiResponse_TriggerResponse'; import type { CreateTriggerRequest } from '../models/CreateTriggerRequest'; import type { PaginatedResponse_TriggerSummary } from '../models/PaginatedResponse_TriggerSummary'; diff --git a/web/src/api/services/WebhooksService.ts b/web/src/api/services/WebhooksService.ts index cafbeb6..0ff6df6 100644 --- a/web/src/api/services/WebhooksService.ts +++ b/web/src/api/services/WebhooksService.ts @@ -1,7 +1,7 @@ /* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ /* tslint:disable */ -/* eslint-disable */ + import type { TriggerResponse } from '../models/TriggerResponse'; import type { WebhookReceiverRequest } from '../models/WebhookReceiverRequest'; import type { WebhookReceiverResponse } from '../models/WebhookReceiverResponse'; diff --git a/web/src/components/common/AnalyticsWidgets.tsx b/web/src/components/common/AnalyticsWidgets.tsx index 6a8ad4c..a490953 100644 --- a/web/src/components/common/AnalyticsWidgets.tsx +++ b/web/src/components/common/AnalyticsWidgets.tsx @@ -550,29 +550,34 @@ export default function AnalyticsDashboard({ hours, onHoursChange, }: AnalyticsDashboardProps) { + // Extract sub-properties so useMemo deps match what the React Compiler infers + const executionThroughput = data?.execution_throughput; + const eventVolume = data?.event_volume; + const enforcementVolume = data?.enforcement_volume; + const executionBuckets = useMemo(() => { - if (!data?.execution_throughput) return []; - const agg = aggregateByBucket(data.execution_throughput); + if (!executionThroughput) return []; + const agg = aggregateByBucket(executionThroughput); return Array.from(agg.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([bucket, v]) => ({ bucket, value: v.total })); - }, [data?.execution_throughput]); + }, [executionThroughput]); const eventBuckets = useMemo(() => { - if (!data?.event_volume) return []; - const agg = aggregateByBucket(data.event_volume); + if (!eventVolume) return []; + const agg = aggregateByBucket(eventVolume); return Array.from(agg.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([bucket, v]) => ({ bucket, value: v.total })); - }, [data?.event_volume]); + }, [eventVolume]); const enforcementBuckets = useMemo(() => { - if (!data?.enforcement_volume) return []; - const agg = aggregateByBucket(data.enforcement_volume); + if (!enforcementVolume) return []; + const agg = aggregateByBucket(enforcementVolume); return Array.from(agg.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([bucket, v]) => ({ bucket, value: v.total })); - }, [data?.enforcement_volume]); + }, [enforcementVolume]); const totalExecutions = useMemo( () => executionBuckets.reduce((s, b) => s + b.value, 0), diff --git a/web/src/components/common/ErrorDisplay.tsx b/web/src/components/common/ErrorDisplay.tsx index a5b0217..b08563b 100644 --- a/web/src/components/common/ErrorDisplay.tsx +++ b/web/src/components/common/ErrorDisplay.tsx @@ -1,5 +1,17 @@ import { AlertCircle, ShieldAlert } from "lucide-react"; +/** Shape of an axios-like error with a response property */ +interface AxiosLikeError { + response?: { + status?: number; + data?: { + message?: string; + }; + }; + isAuthorizationError?: boolean; + message?: string; +} + interface ErrorDisplayProps { error: Error | unknown; title?: string; @@ -21,30 +33,39 @@ export default function ErrorDisplay({ showRetry = false, onRetry, }: ErrorDisplayProps) { + const asAxios = (err: unknown): AxiosLikeError | null => { + if (err && typeof err === "object") return err as AxiosLikeError; + return null; + }; + // Type guard for axios errors - const isAxiosError = (err: any): boolean => { - return err?.response?.status !== undefined; + const isAxiosError = (err: unknown): boolean => { + const e = asAxios(err); + return e?.response?.status !== undefined; }; // Check if this is a 403 (Forbidden) error - const is403Error = (err: any): boolean => { - return ( - err?.response?.status === 403 || - err?.isAuthorizationError === true - ); + const is403Error = (err: unknown): boolean => { + const e = asAxios(err); + return e?.response?.status === 403 || e?.isAuthorizationError === true; }; // Check if this is a 401 (Unauthorized) error - const is401Error = (err: any): boolean => { - return err?.response?.status === 401; + const is401Error = (err: unknown): boolean => { + const e = asAxios(err); + return e?.response?.status === 401; }; // Extract error message - const getErrorMessage = (err: any): string => { - if (err?.response?.data?.message) { - return err.response.data.message; + const getErrorMessage = (err: unknown): string => { + const e = asAxios(err); + if (e?.response?.data?.message) { + return e.response.data.message; } - if (err?.message) { + if (e?.message) { + return e.message; + } + if (err instanceof Error) { return err.message; } return "An unexpected error occurred"; @@ -67,8 +88,8 @@ export default function ErrorDisplay({ role or permissions do not allow this action.

- If you believe you should have access, please contact your - system administrator. + If you believe you should have access, please contact your system + administrator.

@@ -110,12 +131,10 @@ export default function ErrorDisplay({

{title || "Error"}

-

- {getErrorMessage(error)} -

- {isAxiosError(error) && (error as any)?.response?.status && ( +

{getErrorMessage(error)}

+ {isAxiosError(error) && asAxios(error)?.response?.status && (

- Status Code: {(error as any).response.status} + Status Code: {asAxios(error)?.response?.status}

)} {showRetry && onRetry && ( diff --git a/web/src/components/common/ExecuteActionModal.tsx b/web/src/components/common/ExecuteActionModal.tsx index b8fa6e8..93f499d 100644 --- a/web/src/components/common/ExecuteActionModal.tsx +++ b/web/src/components/common/ExecuteActionModal.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { OpenAPI } from "@/api"; +import type { ActionResponse } from "@/api"; import { Play, X } from "lucide-react"; import ParamSchemaForm, { validateParamSchema, @@ -8,10 +9,13 @@ import ParamSchemaForm, { type ParamSchema, } from "@/components/common/ParamSchemaForm"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type JsonValue = any; + interface ExecuteActionModalProps { - action: any; + action: ActionResponse; onClose: () => void; - initialParameters?: Record; + initialParameters?: Record; } /** @@ -32,9 +36,9 @@ export default function ExecuteActionModal({ const paramProperties = extractProperties(paramSchema); // If initialParameters are provided, use them (stripping out any keys not in the schema) - const buildInitialValues = (): Record => { + const buildInitialValues = (): Record => { if (!initialParameters) return {}; - const values: Record = {}; + const values: Record = {}; // Include all initial parameters - even those not in the schema // so users can see exactly what was run before for (const [key, value] of Object.entries(initialParameters)) { @@ -52,7 +56,7 @@ export default function ExecuteActionModal({ }; const [parameters, setParameters] = - useState>(buildInitialValues); + useState>(buildInitialValues); const [paramErrors, setParamErrors] = useState>({}); const [envVars, setEnvVars] = useState>( [{ key: "", value: "" }], @@ -60,12 +64,12 @@ export default function ExecuteActionModal({ const executeAction = useMutation({ mutationFn: async (params: { - parameters: Record; + parameters: Record; envVars: Array<{ key: string; value: string }>; }) => { const token = typeof OpenAPI.TOKEN === "function" - ? await OpenAPI.TOKEN({} as any) + ? await OpenAPI.TOKEN({} as Parameters[0]) : OpenAPI.TOKEN; const response = await fetch( diff --git a/web/src/components/common/ParamSchemaDisplay.tsx b/web/src/components/common/ParamSchemaDisplay.tsx index 5c32ef0..51194d1 100644 --- a/web/src/components/common/ParamSchemaDisplay.tsx +++ b/web/src/components/common/ParamSchemaDisplay.tsx @@ -8,9 +8,13 @@ import type { ParamSchema } from "./ParamSchemaForm"; export type { ParamSchema }; import { extractProperties } from "./ParamSchemaForm"; +/** A JSON-compatible value that can appear in display data */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type JsonValue = any; + interface ParamSchemaDisplayProps { schema: ParamSchema; - values: Record; + values: Record; className?: string; emptyMessage?: string; } @@ -53,7 +57,7 @@ export default function ParamSchemaDisplay({ * Returns both the formatted value and whether it should be displayed inline */ const formatValue = ( - value: any, + value: JsonValue, type?: string, ): { element: React.JSX.Element; isInline: boolean } => { if (value === undefined || value === null) { diff --git a/web/src/components/common/ParamSchemaForm.tsx b/web/src/components/common/ParamSchemaForm.tsx index eb4c0fb..6ed6e90 100644 --- a/web/src/components/common/ParamSchemaForm.tsx +++ b/web/src/components/common/ParamSchemaForm.tsx @@ -1,5 +1,10 @@ +/* eslint-disable react-refresh/only-export-components -- extractProperties and validateParamSchema are shared utilities co-located with the form component */ import { useState, useEffect } from "react"; +/** A JSON-compatible value that can appear in form data */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type JsonValue = any; + /** * StackStorm-style parameter schema format. * Parameters are defined as a flat map of parameter name to definition, @@ -14,7 +19,7 @@ import { useState, useEffect } from "react"; export interface ParamSchemaProperty { type?: "string" | "number" | "integer" | "boolean" | "array" | "object"; description?: string; - default?: any; + default?: JsonValue; enum?: string[]; minimum?: number; maximum?: number; @@ -23,7 +28,7 @@ export interface ParamSchemaProperty { secret?: boolean; required?: boolean; position?: number; - items?: any; + items?: Record; } export interface ParamSchema { @@ -40,7 +45,7 @@ export interface ParamSchema { * { param_name: { type, description, required, secret, ... }, ... } */ export function extractProperties( - schema: ParamSchema | any, + schema: ParamSchema | Record | null | undefined, ): Record { if (!schema || typeof schema !== "object") return {}; // StackStorm-style flat format: { param_name: { type, description, required, ... }, ... } @@ -56,8 +61,8 @@ export function extractProperties( interface ParamSchemaFormProps { schema: ParamSchema; - values: Record; - onChange: (values: Record) => void; + values: Record; + onChange: (values: Record) => void; errors?: Record; disabled?: boolean; className?: string; @@ -79,7 +84,7 @@ interface ParamSchemaFormProps { /** * Check if a string value contains a template expression ({{ ... }}) */ -function isTemplateExpression(value: any): boolean { +function isTemplateExpression(value: JsonValue): boolean { return typeof value === "string" && /\{\{.*\}\}/.test(value); } @@ -88,7 +93,7 @@ function isTemplateExpression(value: any): boolean { * Non-string values (booleans, numbers, objects, arrays) are JSON-stringified * so the user can edit them as text. */ -function valueToString(value: any): string { +function valueToString(value: JsonValue): string { if (value === undefined || value === null) return ""; if (typeof value === "string") return value; return JSON.stringify(value); @@ -99,7 +104,7 @@ function valueToString(value: any): string { * Template expressions are always kept as strings. * Plain values are coerced to the schema type when possible. */ -function parseTemplateValue(raw: string, type: string): any { +function parseTemplateValue(raw: string, type: string): JsonValue { if (raw === "") return ""; // Template expressions stay as strings - resolved server-side if (isTemplateExpression(raw)) return raw; @@ -164,19 +169,20 @@ export default function ParamSchemaForm({ } return acc; }, - { ...values } as Record, + { ...values } as Record, ); // Only update if there are new defaults if (JSON.stringify(initialValues) !== JSON.stringify(values)) { onChange(initialValues); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [schema]); // Only run when schema changes /** * Handle input change for a specific field */ - const handleInputChange = (key: string, value: any) => { + const handleInputChange = (key: string, value: JsonValue) => { const newValues = { ...values, [key]: value }; onChange(newValues); @@ -200,7 +206,10 @@ export default function ParamSchemaForm({ /** * Get a placeholder hint for template-mode inputs */ - const getTemplatePlaceholder = (key: string, param: any): string => { + const getTemplatePlaceholder = ( + key: string, + param: ParamSchemaProperty | undefined, + ) => { const type = param?.type || "string"; switch (type) { case "boolean": @@ -225,7 +234,10 @@ export default function ParamSchemaForm({ /** * Render a template-mode text input for any parameter type */ - const renderTemplateInput = (key: string, param: any) => { + const renderTemplateInput = ( + key: string, + param: ParamSchemaProperty | undefined, + ) => { const type = param?.type || "string"; const rawValue = values[key] ?? param?.default ?? ""; const isDisabled = disabled; @@ -264,7 +276,7 @@ export default function ParamSchemaForm({ /** * Render input field based on parameter type (standard mode) */ - const renderInput = (key: string, param: any) => { + const renderInput = (key: string, param: ParamSchemaProperty | undefined) => { const type = param?.type || "string"; const value = values[key] ?? param?.default ?? ""; const isDisabled = disabled; @@ -388,7 +400,7 @@ export default function ParamSchemaForm({ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" > - {param.enum.map((option: any) => ( + {param.enum.map((option: string) => ( @@ -416,7 +428,10 @@ export default function ParamSchemaForm({ /** * Render type hint badge and additional context for template-mode fields */ - const renderTemplateHints = (_key: string, param: any) => { + const renderTemplateHints = ( + _key: string, + param: ParamSchemaProperty | undefined, + ) => { const type = param?.type || "string"; const hints: string[] = []; @@ -537,7 +552,7 @@ export default function ParamSchemaForm({ */ export function validateParamSchema( schema: ParamSchema, - values: Record, + values: Record, allowTemplates: boolean = false, ): Record { const errors: Record = {}; diff --git a/web/src/components/common/SchemaBuilder.tsx b/web/src/components/common/SchemaBuilder.tsx index 76325c3..55e8f2b 100644 --- a/web/src/components/common/SchemaBuilder.tsx +++ b/web/src/components/common/SchemaBuilder.tsx @@ -1,6 +1,25 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Plus, Trash2, ChevronDown, ChevronRight, Code } from "lucide-react"; +/** A single property definition within a flat schema object */ +interface SchemaPropertyDef { + type?: string; + description?: string; + required?: boolean; + secret?: boolean; + default?: unknown; + minimum?: number; + maximum?: number; + minLength?: number; + maxLength?: number; + pattern?: string; + enum?: string[]; + [key: string]: unknown; +} + +/** The flat schema format: each key is a parameter name mapped to its definition */ +type FlatSchema = Record; + interface SchemaProperty { name: string; type: string; @@ -17,8 +36,8 @@ interface SchemaProperty { } interface SchemaBuilderProps { - value: Record; - onChange: (schema: Record) => void; + value: FlatSchema; + onChange: (schema: FlatSchema) => void; label?: string; placeholder?: string; error?: string; @@ -58,24 +77,23 @@ export default function SchemaBuilder({ if (!value || typeof value !== "object") return; const props: SchemaProperty[] = []; - Object.entries(value).forEach(([name, propDef]: [string, any]) => { + Object.entries(value).forEach(([name, propDef]) => { if (propDef && typeof propDef === "object" && !Array.isArray(propDef)) { + const def = propDef as SchemaPropertyDef; props.push({ name, - type: propDef.type || "string", - description: propDef.description || "", - required: propDef.required === true, - secret: propDef.secret === true, + type: def.type || "string", + description: def.description || "", + required: def.required === true, + secret: def.secret === true, default: - propDef.default !== undefined - ? JSON.stringify(propDef.default) - : undefined, - minimum: propDef.minimum, - maximum: propDef.maximum, - minLength: propDef.minLength, - maxLength: propDef.maxLength, - pattern: propDef.pattern, - enum: propDef.enum, + def.default !== undefined ? JSON.stringify(def.default) : undefined, + minimum: def.minimum, + maximum: def.maximum, + minLength: def.minLength, + maxLength: def.maxLength, + pattern: def.pattern, + enum: def.enum, }); } }); @@ -83,26 +101,19 @@ export default function SchemaBuilder({ if (props.length > 0) { setProperties(props); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Update raw JSON when switching to raw view - useEffect(() => { - if (showRawJson) { - setRawJson(JSON.stringify(buildSchema(), null, 2)); - setRawJsonError(""); - } - }, [showRawJson]); - // Build StackStorm-style flat parameter schema - const buildSchema = (): Record => { + const buildSchema = useCallback((): FlatSchema => { if (properties.length === 0) { return {}; } - const schema: Record = {}; + const schema: FlatSchema = {}; properties.forEach((prop) => { - const propSchema: Record = { + const propSchema: SchemaPropertyDef = { type: prop.type, }; @@ -143,7 +154,15 @@ export default function SchemaBuilder({ }); return schema; - }; + }, [properties]); + + // Update raw JSON when switching to raw view + useEffect(() => { + if (showRawJson) { + setRawJson(JSON.stringify(buildSchema(), null, 2)); + setRawJsonError(""); + } + }, [showRawJson, buildSchema]); const handlePropertiesChange = (newProperties: SchemaProperty[]) => { setProperties(newProperties); @@ -152,17 +171,15 @@ export default function SchemaBuilder({ }; // Build StackStorm-style flat parameter schema from properties array - const buildSchemaFromProperties = ( - props: SchemaProperty[], - ): Record => { + const buildSchemaFromProperties = (props: SchemaProperty[]): FlatSchema => { if (props.length === 0) { return {}; } - const schema: Record = {}; + const schema: FlatSchema = {}; props.forEach((prop) => { - const propSchema: Record = { + const propSchema: SchemaPropertyDef = { type: prop.type, }; @@ -266,31 +283,32 @@ export default function SchemaBuilder({ // Expects StackStorm-style flat format: { param_name: { type, required, secret, ... }, ... } const props: SchemaProperty[] = []; - Object.entries(parsed).forEach(([name, propDef]: [string, any]) => { + Object.entries(parsed).forEach(([name, propDef]) => { if (propDef && typeof propDef === "object" && !Array.isArray(propDef)) { + const def = propDef as SchemaPropertyDef; props.push({ name, - type: propDef.type || "string", - description: propDef.description || "", - required: propDef.required === true, - secret: propDef.secret === true, + type: def.type || "string", + description: def.description || "", + required: def.required === true, + secret: def.secret === true, default: - propDef.default !== undefined - ? JSON.stringify(propDef.default) + def.default !== undefined + ? JSON.stringify(def.default) : undefined, - minimum: propDef.minimum, - maximum: propDef.maximum, - minLength: propDef.minLength, - maxLength: propDef.maxLength, - pattern: propDef.pattern, - enum: propDef.enum, + minimum: def.minimum, + maximum: def.maximum, + minLength: def.minLength, + maxLength: def.maxLength, + pattern: def.pattern, + enum: def.enum, }); } }); setProperties(props); - } catch (e: any) { - setRawJsonError(e.message); + } catch (e: unknown) { + setRawJsonError(e instanceof Error ? e.message : "Invalid JSON"); } }; diff --git a/web/src/components/forms/PackForm.tsx b/web/src/components/forms/PackForm.tsx index 6ac000d..5fc3029 100644 --- a/web/src/components/forms/PackForm.tsx +++ b/web/src/components/forms/PackForm.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { useCreatePack, useUpdatePack } from "@/hooks/usePacks"; import type { PackResponse } from "@/api"; @@ -7,6 +7,24 @@ import SchemaBuilder from "@/components/common/SchemaBuilder"; import ParamSchemaForm from "@/components/common/ParamSchemaForm"; import { RotateCcw } from "lucide-react"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type JsonValue = any; + +/** A single property definition within a flat schema object */ +interface SchemaPropertyDef { + type?: string; + description?: string; + required?: boolean; + secret?: boolean; + default?: JsonValue; + minimum?: number; + maximum?: number; + [key: string]: unknown; +} + +/** The flat schema format: each key is a parameter name mapped to its definition */ +type FlatSchema = Record; + interface PackFormProps { pack?: PackResponse; onSuccess?: () => void; @@ -31,9 +49,10 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) { const [isStandard, setIsStandard] = useState(pack?.is_standard ?? false); const [configValues, setConfigValues] = - useState>(initialConfig); - const [confSchema, setConfSchema] = - useState>(initialConfSchema); + useState>(initialConfig); + const [confSchema, setConfSchema] = useState( + initialConfSchema as FlatSchema, + ); const [meta, setMeta] = useState( pack?.meta ? JSON.stringify(pack.meta, null, 2) : "{}", ); @@ -49,14 +68,22 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) { typeof confSchema === "object" && Object.keys(confSchema).length > 0; + // Track previous confSchema to detect changes without re-running on every render + const prevConfSchemaRef = useRef(confSchema); + // Sync config values when schema changes (for ad-hoc packs only) + /* eslint-disable react-hooks/set-state-in-effect -- intentional sync of dependent state */ useEffect(() => { + // Only sync when confSchema actually changed + if (prevConfSchemaRef.current === confSchema) return; + prevConfSchemaRef.current = confSchema; + if (!isStandard && hasSchemaProperties) { // Get current schema property names (flat format: keys are parameter names) const schemaKeys = Object.keys(confSchema); // Create new config with only keys that exist in schema - const syncedConfig: Record = {}; + const syncedConfig: Record = {}; schemaKeys.forEach((key) => { if (configValues[key] !== undefined) { // Preserve existing value @@ -77,7 +104,8 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) { setConfigValues(syncedConfig); } } - }, [confSchema, isStandard]); + }, [confSchema, isStandard, hasSchemaProperties, configValues]); + /* eslint-enable react-hooks/set-state-in-effect */ const validateForm = (): boolean => { const newErrors: Record = {}; @@ -111,7 +139,7 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) { if (meta.trim()) { try { JSON.parse(meta); - } catch (e) { + } catch { newErrors.meta = "Invalid JSON format"; } } @@ -179,12 +207,14 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) { onSuccess(); } } - } catch (error: any) { + } catch (error: unknown) { + const errMsg = + error instanceof Error ? error.message : "Failed to save pack"; + const axiosErr = error as { + response?: { data?: { message?: string } }; + }; setErrors({ - submit: - error.response?.data?.message || - error.message || - "Failed to save pack", + submit: axiosErr?.response?.data?.message || errMsg, }); } }; @@ -203,7 +233,7 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) { }; const insertSchemaExample = (type: "api" | "database" | "webhook") => { - let example: Record; + let example: FlatSchema; switch (type) { case "api": example = { @@ -280,8 +310,8 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) { setConfSchema(example); // Immediately sync config values with schema defaults - const syncedConfig: Record = {}; - Object.entries(example).forEach(([key, propDef]: [string, any]) => { + const syncedConfig: Record = {}; + Object.entries(example).forEach(([key, propDef]) => { if (propDef.default !== undefined) { syncedConfig[key] = propDef.default; } diff --git a/web/src/components/forms/RuleForm.tsx b/web/src/components/forms/RuleForm.tsx index 4df32e1..c5cf7cf 100644 --- a/web/src/components/forms/RuleForm.tsx +++ b/web/src/components/forms/RuleForm.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { usePacks } from "@/hooks/usePacks"; import { useTriggers, useTrigger } from "@/hooks/useTriggers"; @@ -9,9 +9,17 @@ import ParamSchemaForm, { type ParamSchema, } from "@/components/common/ParamSchemaForm"; import SearchableSelect from "@/components/common/SearchableSelect"; -import type { RuleResponse } from "@/types/api"; +import type { + RuleResponse, + ActionSummary, + TriggerResponse, + ActionResponse, +} from "@/types/api"; import { labelToRef, extractLocalRef, combineRefs } from "@/lib/format-utils"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type JsonValue = any; + interface RuleFormProps { rule?: RuleResponse; onSuccess?: () => void; @@ -35,11 +43,11 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { rule?.conditions ? JSON.stringify(rule.conditions, null, 2) : "", ); const [triggerParameters, setTriggerParameters] = useState< - Record + Record >(rule?.trigger_params || {}); - const [actionParameters, setActionParameters] = useState>( - rule?.action_params || {}, - ); + const [actionParameters, setActionParameters] = useState< + Record + >(rule?.action_params || {}); const [enabled, setEnabled] = useState(rule?.enabled ?? true); const [errors, setErrors] = useState>({}); const [triggerParamErrors, setTriggerParamErrors] = useState< @@ -51,7 +59,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { // Data fetching const { data: packsData } = usePacks({ pageSize: 1000 }); - const packs = packsData?.data || []; + const packs = useMemo(() => packsData?.data || [], [packsData?.data]); const selectedPack = packs.find((p) => p.id === packId); @@ -65,7 +73,9 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { // Get selected trigger and action refs for detail fetching const selectedTriggerSummary = triggers.find((t) => t.id === triggerId); - const selectedActionSummary = actions.find((a: any) => a.id === actionId); + const selectedActionSummary = actions.find( + (a: ActionSummary) => a.id === actionId, + ); // Fetch full trigger details (including param_schema) when a trigger is selected const { data: triggerDetailsData } = useTrigger( @@ -81,15 +91,18 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { // Extract param schemas from full details const triggerParamSchema: ParamSchema = - ((selectedTrigger as any)?.param_schema as ParamSchema) || {}; + ((selectedTrigger as TriggerResponse | undefined) + ?.param_schema as ParamSchema) || {}; const actionParamSchema: ParamSchema = - ((selectedAction as any)?.param_schema as ParamSchema) || {}; + ((selectedAction as ActionResponse | undefined) + ?.param_schema as ParamSchema) || {}; // Mutations const createRule = useCreateRule(); const updateRule = useUpdateRule(); // Reset triggers, actions, and parameters when pack changes + /* eslint-disable react-hooks/set-state-in-effect -- intentional dependent-state reset */ useEffect(() => { if (!isEditing) { setTriggerId(0); @@ -98,20 +111,25 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { setActionParameters({}); } }, [packId, isEditing]); + /* eslint-enable react-hooks/set-state-in-effect */ // Reset trigger parameters when trigger changes + /* eslint-disable react-hooks/set-state-in-effect -- intentional dependent-state reset */ useEffect(() => { if (!isEditing) { setTriggerParameters({}); } }, [triggerId, isEditing]); + /* eslint-enable react-hooks/set-state-in-effect */ // Reset action parameters when action changes + /* eslint-disable react-hooks/set-state-in-effect -- intentional dependent-state reset */ useEffect(() => { if (!isEditing) { setActionParameters({}); } }, [actionId, isEditing]); + /* eslint-enable react-hooks/set-state-in-effect */ const validateForm = (): boolean => { const newErrors: Record = {}; @@ -144,7 +162,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { if (conditions.trim()) { try { JSON.parse(conditions); - } catch (e) { + } catch { newErrors.conditions = "Invalid JSON format"; } } @@ -187,7 +205,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { // Combine pack ref and local ref to create full ref const fullRef = combineRefs(selectedPackData?.ref || "", localRef.trim()); - const formData: any = { + const formData: Record = { pack_ref: selectedPackData?.ref || "", ref: fullRef, label: label.trim(), @@ -267,7 +285,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { id="pack" value={packId} onChange={(v) => setPackId(Number(v))} - options={packs.map((pack: any) => ({ + options={packs.map((pack) => ({ value: pack.id, label: `${pack.label} (${pack.version})`, }))} @@ -408,7 +426,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { id="trigger" value={triggerId} onChange={(v) => setTriggerId(Number(v))} - options={triggers.map((trigger: any) => ({ + options={triggers.map((trigger) => ({ value: trigger.id, label: `${trigger.ref} - ${trigger.label}`, }))} @@ -494,7 +512,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { id="action" value={actionId} onChange={(v) => setActionId(Number(v))} - options={actions.map((action: any) => ({ + options={actions.map((action) => ({ value: action.id, label: `${action.ref} - ${action.label}`, }))} diff --git a/web/src/components/forms/TriggerForm.tsx b/web/src/components/forms/TriggerForm.tsx index f70b105..4782b76 100644 --- a/web/src/components/forms/TriggerForm.tsx +++ b/web/src/components/forms/TriggerForm.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; import { usePacks } from "@/hooks/usePacks"; @@ -11,9 +11,14 @@ import { import SchemaBuilder from "@/components/common/SchemaBuilder"; import SearchableSelect from "@/components/common/SearchableSelect"; import { WebhooksService } from "@/api"; +import type { TriggerResponse, PackSummary } from "@/api"; + +/** Flat schema format: each key is a parameter name mapped to its definition */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FlatSchema = Record; interface TriggerFormProps { - initialData?: any; + initialData?: TriggerResponse; isEditing?: boolean; } @@ -31,14 +36,14 @@ export default function TriggerForm({ const [description, setDescription] = useState(""); const [webhookEnabled, setWebhookEnabled] = useState(false); const [enabled, setEnabled] = useState(true); - const [paramSchema, setParamSchema] = useState>({}); - const [outSchema, setOutSchema] = useState>({}); + const [paramSchema, setParamSchema] = useState({}); + const [outSchema, setOutSchema] = useState({}); const [errors, setErrors] = useState>({}); // Fetch packs const { data: packsData } = usePacks({ page: 1, pageSize: 100 }); - const packs = packsData?.data || []; - const selectedPack = packs.find((p: any) => p.id === packId); + const packs = useMemo(() => packsData?.data || [], [packsData?.data]); + const selectedPack = packs.find((p: PackSummary) => p.id === packId); // Mutations const createTrigger = useCreateTrigger(); @@ -56,7 +61,9 @@ export default function TriggerForm({ if (isEditing) { // Find pack by pack_ref - const pack = packs.find((p: any) => p.ref === initialData.pack_ref); + const pack = packs.find( + (p: PackSummary) => p.ref === initialData.pack_ref, + ); if (pack) { setPackId(pack.id); } @@ -96,7 +103,7 @@ export default function TriggerForm({ } try { - const selectedPackData = packs.find((p: any) => p.id === packId); + const selectedPackData = packs.find((p: PackSummary) => p.id === packId); if (!selectedPackData) { throw new Error("Selected pack not found"); } @@ -166,13 +173,15 @@ export default function TriggerForm({ } navigate("/triggers"); - } catch (error: any) { + } catch (error: unknown) { console.error("Error submitting trigger:", error); + const errMsg = + error instanceof Error ? error.message : "Failed to save trigger"; + const axiosErr = error as { + response?: { data?: { message?: string } }; + }; setErrors({ - submit: - error.response?.data?.message || - error.message || - "Failed to save trigger", + submit: axiosErr?.response?.data?.message || errMsg, }); } }; @@ -211,7 +220,7 @@ export default function TriggerForm({ id="pack" value={packId} onChange={(v) => setPackId(Number(v))} - options={packs.map((pack: any) => ({ + options={packs.map((pack: PackSummary) => ({ value: pack.id, label: `${pack.label} (${pack.version})`, }))} diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index eab1040..3df1a30 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components -- exporting useAuth hook alongside AuthProvider is standard React pattern */ import { createContext, useContext, diff --git a/web/src/contexts/WebSocketContext.tsx b/web/src/contexts/WebSocketContext.tsx index 33e8b25..df669be 100644 --- a/web/src/contexts/WebSocketContext.tsx +++ b/web/src/contexts/WebSocketContext.tsx @@ -1,7 +1,9 @@ +/* eslint-disable react-refresh/only-export-components -- exporting hooks alongside WebSocketProvider is standard React pattern */ import { createContext, useContext, useEffect, + useMemo, useRef, useState, useCallback, @@ -306,36 +308,33 @@ export function useEntityNotifications( ) { const { connected, subscribe, unsubscribe } = useWebSocketContext(); - // Stable reference to the handler + // Stable reference to the handler — updated on every render via effect const handlerRef = useRef(onNotification); - // Stable reference to the wrapper function (created once, never changes) - const stableHandlerRef = useRef(null); - - // Initialize the stable handler once - if (stableHandlerRef.current === null) { - stableHandlerRef.current = (notification) => { - handlerRef.current(notification); - }; - } - // Update ref when handler changes (but don't cause re-subscription) useEffect(() => { handlerRef.current = onNotification; }, [onNotification]); + // Create a stable wrapper function once via useMemo (no ref access during render) + const stableHandler = useMemo( + () => (notification) => { + handlerRef.current(notification); + }, + [], // intentionally empty — handlerRef is stable + ); + useEffect(() => { if (!connected || !enabled) return; const filter = `entity_type:${entityType}`; - const stableHandler = stableHandlerRef.current!; subscribe(filter, stableHandler); return () => { unsubscribe(filter, stableHandler); }; - }, [connected, enabled, entityType, subscribe, unsubscribe]); + }, [connected, enabled, entityType, subscribe, unsubscribe, stableHandler]); return { connected }; } diff --git a/web/src/hooks/useArtifactStream.ts b/web/src/hooks/useArtifactStream.ts index 5fca62c..afbe764 100644 --- a/web/src/hooks/useArtifactStream.ts +++ b/web/src/hooks/useArtifactStream.ts @@ -16,6 +16,26 @@ interface UseArtifactStreamOptions { enabled?: boolean; } +/** Shape of data coming from WebSocket notifications for artifacts */ +interface ArtifactNotification { + entity_id: number; + entity_type: string; + notification_type: string; + payload: ArtifactNotificationPayload; + timestamp: string; +} + +/** The raw payload from the PostgreSQL trigger for artifact notifications */ +interface ArtifactNotificationPayload { + execution?: number; + type?: string; + name?: string | null; + progress_percent?: number | null; + progress_message?: string | null; + progress_entries?: number | null; + [key: string]: unknown; +} + /** * Hook to subscribe to real-time artifact updates via WebSocket. * @@ -42,8 +62,8 @@ export function useArtifactStream(options: UseArtifactStreamOptions = {}) { const queryClient = useQueryClient(); const handleNotification = useCallback( - (notification: any) => { - const payload = notification.payload as any; + (notification: ArtifactNotification) => { + const payload = notification.payload; // If we're filtering by execution ID, only process matching artifacts if (executionId && payload?.execution !== executionId) { @@ -71,11 +91,11 @@ export function useArtifactStream(options: UseArtifactStreamOptions = {}) { if (payload?.type === "progress" && payload?.progress_percent != null) { queryClient.setQueryData( ["artifact_progress", artifactExecution], - (old: any) => ({ + (old: ArtifactProgressSummary | undefined) => ({ ...old, artifactId, - name: payload.name, - percent: payload.progress_percent, + name: payload.name ?? null, + percent: payload.progress_percent as number, message: payload.progress_message ?? null, entries: payload.progress_entries ?? 0, timestamp: notification.timestamp, diff --git a/web/src/hooks/useEnforcementStream.ts b/web/src/hooks/useEnforcementStream.ts index 8942419..23be47e 100644 --- a/web/src/hooks/useEnforcementStream.ts +++ b/web/src/hooks/useEnforcementStream.ts @@ -1,6 +1,7 @@ import { useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useEntityNotifications } from "@/contexts/WebSocketContext"; +import type { EnforcementSummary } from "@/api"; interface UseEnforcementStreamOptions { /** @@ -16,11 +17,47 @@ interface UseEnforcementStreamOptions { enabled?: boolean; } +/** Shape of data coming from WebSocket notifications for enforcements */ +interface EnforcementNotification { + entity_id: number; + entity_type: string; + notification_type: string; + payload: Partial & Record; + timestamp: string; +} + +/** Query params shape used in enforcement list query keys */ +interface EnforcementQueryParams { + status?: string; + event?: number; + rule?: number; + triggerRef?: string; + ruleRef?: string; +} + +/** Shape of the paginated API response stored in React Query cache */ +interface EnforcementListCache { + data: EnforcementSummary[]; + pagination?: { + total_items?: number; + page?: number; + page_size?: number; + }; +} + +/** Shape of a single enforcement detail response stored in React Query cache */ +interface EnforcementDetailCache { + data: EnforcementSummary; +} + /** * Check if an enforcement matches the given query parameters * Only checks fields that are reliably present in WebSocket payloads */ -function enforcementMatchesParams(enforcement: any, params: any): boolean { +function enforcementMatchesParams( + enforcement: Partial, + params: EnforcementQueryParams | undefined, +): boolean { if (!params) return true; // Check status filter @@ -53,8 +90,10 @@ function enforcementMatchesParams(enforcement: any, params: any): boolean { /** * Check if query params include filters not present in WebSocket payloads */ -function hasUnsupportedFilters(params: any): boolean { - if (!params) return false; +function hasUnsupportedFilters( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _params: EnforcementQueryParams | undefined, +): boolean { // Currently all enforcement filters are supported in WebSocket payloads return false; } @@ -81,19 +120,20 @@ export function useEnforcementStream( const queryClient = useQueryClient(); const handleNotification = useCallback( - (notification: any) => { + (notification: EnforcementNotification) => { // Filter by enforcement ID if specified if (enforcementId && notification.entity_id !== enforcementId) { return; } // Extract enforcement data from notification payload (flat structure) - const enforcementData = notification.payload as any; + const enforcementData = + notification.payload as Partial; // Update specific enforcement query if it exists queryClient.setQueryData( ["enforcements", notification.entity_id], - (old: any) => { + (old: EnforcementDetailCache | undefined) => { if (!old) return old; return { ...old, @@ -108,21 +148,24 @@ export function useEnforcementStream( // Update enforcement list queries by modifying existing data // We need to iterate manually to access query keys for filtering const queries = queryClient - .getQueriesData({ queryKey: ["enforcements"], exact: false }) - .filter(([, data]) => data && Array.isArray((data as any)?.data)); + .getQueriesData({ + queryKey: ["enforcements"], + exact: false, + }) + .filter(([, data]) => data && Array.isArray(data?.data)); queries.forEach(([queryKey, oldData]) => { // Extract query params from the query key (format: ["enforcements", params]) - const queryParams = queryKey[1]; + const queryParams = queryKey[1] as EnforcementQueryParams | undefined; - const old = oldData as any; + const old = oldData as EnforcementListCache; // Check if enforcement already exists in the list const existingIndex = old.data.findIndex( - (enf: any) => enf.id === notification.entity_id, + (enf) => enf.id === notification.entity_id, ); - let updatedData; + let updatedData: EnforcementSummary[]; if (existingIndex >= 0) { // Always update existing enforcement in the list updatedData = [...old.data]; @@ -144,7 +187,10 @@ export function useEnforcementStream( // Only add new enforcement if it matches the query parameters if (enforcementMatchesParams(enforcementData, queryParams)) { // Add to beginning and cap at 50 items to prevent performance issues - updatedData = [enforcementData, ...old.data].slice(0, 50); + updatedData = [ + enforcementData as EnforcementSummary, + ...old.data, + ].slice(0, 50); } else { // Don't modify the list if the new enforcement doesn't match the query return; diff --git a/web/src/hooks/useExecutionStream.ts b/web/src/hooks/useExecutionStream.ts index e1bb437..2c61b17 100644 --- a/web/src/hooks/useExecutionStream.ts +++ b/web/src/hooks/useExecutionStream.ts @@ -1,6 +1,7 @@ import { useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useEntityNotifications } from "@/contexts/WebSocketContext"; +import type { ExecutionSummary } from "@/api"; interface UseExecutionStreamOptions { /** @@ -28,11 +29,57 @@ const NOTIFICATION_META_FIELDS = [ "action_id", ] as const; +/** Shape of data coming from WebSocket notifications for executions */ +interface ExecutionNotification { + entity_id: number; + entity_type: string; + notification_type: string; + payload: ExecutionNotificationPayload; + timestamp: string; +} + +/** The raw payload from the PostgreSQL trigger, which includes extra meta fields */ +interface ExecutionNotificationPayload extends Partial { + entity_type?: string; + entity_id?: number; + old_status?: string; + action_id?: number; +} + +/** Query params shape used in execution list query keys */ +interface ExecutionQueryParams { + topLevelOnly?: boolean; + parent?: number; + status?: string; + actionRef?: string; + packName?: string; + executor?: number; + ruleRef?: string; + triggerRef?: string; +} + +/** Shape of the paginated API response stored in React Query cache */ +interface ExecutionListCache { + data: ExecutionSummary[]; + pagination?: { + total_items?: number; + page?: number; + page_size?: number; + }; +} + +/** Shape of a single execution detail response stored in React Query cache */ +interface ExecutionDetailCache { + data: ExecutionSummary; +} + /** * Strip notification-only metadata fields from the payload so cached data * matches the shape returned by the API (ExecutionSummary / ExecutionResponse). */ -function stripNotificationMeta(payload: any): any { +function stripNotificationMeta( + payload: ExecutionNotificationPayload, +): Partial { if (!payload || typeof payload !== "object") return payload; const cleaned = { ...payload }; for (const key of NOTIFICATION_META_FIELDS) { @@ -45,7 +92,10 @@ function stripNotificationMeta(payload: any): any { * Check if an execution matches the given query parameters. * Only checks fields that are reliably present in WebSocket payloads. */ -function executionMatchesParams(execution: any, params: any): boolean { +function executionMatchesParams( + execution: Partial & { parent?: number | null }, + params: ExecutionQueryParams | undefined, +): boolean { if (!params) return true; // Check topLevelOnly filter — child executions (with a parent) must not @@ -98,7 +148,9 @@ function executionMatchesParams(execution: any, params: any): boolean { /** * Check if query params include filters not present in WebSocket payloads. */ -function hasUnsupportedFilters(params: any): boolean { +function hasUnsupportedFilters( + params: ExecutionQueryParams | undefined, +): boolean { if (!params) return false; return !!(params.ruleRef || params.triggerRef); } @@ -123,7 +175,7 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) { const queryClient = useQueryClient(); const handleNotification = useCallback( - (notification: any) => { + (notification: ExecutionNotification) => { // Filter by execution ID if specified if (executionId && notification.entity_id !== executionId) { return; @@ -131,14 +183,14 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) { // Extract execution data from notification payload (flat structure). // Keep raw payload for old_status inspection, but use cleaned data for cache. - const rawPayload = notification.payload as any; + const rawPayload = notification.payload; const oldStatus: string | undefined = rawPayload?.old_status; const executionData = stripNotificationMeta(rawPayload); // Update specific execution query if it exists queryClient.setQueryData( ["executions", notification.entity_id], - (old: any) => { + (old: ExecutionDetailCache | undefined) => { if (!old) return old; return { ...old, @@ -153,35 +205,38 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) { // Update execution list queries by modifying existing data. // We need to iterate manually to access query keys for filtering. const queries = queryClient - .getQueriesData({ queryKey: ["executions"], exact: false }) - .filter(([, data]) => data && Array.isArray((data as any)?.data)); + .getQueriesData({ + queryKey: ["executions"], + exact: false, + }) + .filter(([, data]) => data && Array.isArray(data?.data)); queries.forEach(([queryKey, oldData]) => { // Extract query params from the query key (format: ["executions", params]) - const queryParams = queryKey[1]; + const queryParams = queryKey[1] as ExecutionQueryParams | undefined; // Child execution queries (keyed by { parent: id }) fetch all pages // and must not be capped — the timeline DAG needs every child. - const isChildQuery = !!(queryParams as any)?.parent; + const isChildQuery = !!queryParams?.parent; - const old = oldData as any; + const old = oldData as ExecutionListCache; // Check if execution already exists in the list const existingIndex = old.data.findIndex( - (exec: any) => exec.id === notification.entity_id, + (exec) => exec.id === notification.entity_id, ); // Merge the updated fields to determine if the execution matches the query const mergedExecution = existingIndex >= 0 ? { ...old.data[existingIndex], ...executionData } - : executionData; + : (executionData as ExecutionSummary); const matchesQuery = executionMatchesParams( mergedExecution, queryParams, ); - let updatedData; + let updatedData: ExecutionSummary[]; let totalItemsDelta = 0; if (existingIndex >= 0) { @@ -189,12 +244,13 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) { if (matchesQuery) { // Still matches — update in place, no total_items change updatedData = [...old.data]; - updatedData[existingIndex] = mergedExecution; + updatedData[existingIndex] = { + ...updatedData[existingIndex], + ...executionData, + }; } else { // No longer matches the query filter — remove it - updatedData = old.data.filter( - (_: any, i: number) => i !== existingIndex, - ); + updatedData = old.data.filter((_, i) => i !== existingIndex); totalItemsDelta = -1; } } else { @@ -229,8 +285,8 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) { return; } updatedData = isChildQuery - ? [...old.data, executionData] - : [executionData, ...old.data].slice(0, 50); + ? [...old.data, executionData as ExecutionSummary] + : [executionData as ExecutionSummary, ...old.data].slice(0, 50); totalItemsDelta = 1; } else { // No boundary crossing: either both match (execution was @@ -249,8 +305,8 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) { // Add to the list. Child queries keep all items (no cap); // other lists cap at 50 to prevent unbounded growth. updatedData = isChildQuery - ? [...old.data, executionData] - : [executionData, ...old.data].slice(0, 50); + ? [...old.data, executionData as ExecutionSummary] + : [executionData as ExecutionSummary, ...old.data].slice(0, 50); totalItemsDelta = 1; } else { return; diff --git a/web/src/hooks/useSensors.ts b/web/src/hooks/useSensors.ts index 773b003..3113c7d 100644 --- a/web/src/hooks/useSensors.ts +++ b/web/src/hooks/useSensors.ts @@ -1,5 +1,6 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { SensorsService } from "@/api"; +import type { CreateSensorRequest, UpdateSensorRequest } from "@/api"; interface SensorsQueryParams { page?: number; @@ -69,7 +70,7 @@ export function useCreateSensor() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: any) => { + mutationFn: async (data: CreateSensorRequest) => { return await SensorsService.createSensor({ requestBody: data }); }, onSuccess: () => { @@ -83,7 +84,13 @@ export function useUpdateSensor() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ ref, data }: { ref: string; data: any }) => { + mutationFn: async ({ + ref, + data, + }: { + ref: string; + data: UpdateSensorRequest; + }) => { return await SensorsService.updateSensor({ ref, requestBody: data }); }, onSuccess: (_, variables) => { diff --git a/web/src/hooks/useTriggers.ts b/web/src/hooks/useTriggers.ts index b17664d..b632ab5 100644 --- a/web/src/hooks/useTriggers.ts +++ b/web/src/hooks/useTriggers.ts @@ -1,5 +1,6 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { TriggersService } from "@/api"; +import type { CreateTriggerRequest, UpdateTriggerRequest } from "@/api"; interface TriggersQueryParams { page?: number; @@ -69,7 +70,7 @@ export function useCreateTrigger() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: any) => { + mutationFn: async (data: CreateTriggerRequest) => { return await TriggersService.createTrigger({ requestBody: data }); }, onSuccess: () => { @@ -83,7 +84,13 @@ export function useUpdateTrigger() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ ref, data }: { ref: string; data: any }) => { + mutationFn: async ({ + ref, + data, + }: { + ref: string; + data: UpdateTriggerRequest; + }) => { return await TriggersService.updateTrigger({ ref, requestBody: data }); }, onSuccess: (_, variables) => { diff --git a/web/src/lib/api-config.ts b/web/src/lib/api-config.ts index e60986e..d2df3a3 100644 --- a/web/src/lib/api-config.ts +++ b/web/src/lib/api-config.ts @@ -1,13 +1,22 @@ import { OpenAPI } from "../api"; import { apiClient } from "./api-client"; +declare global { + interface Window { + __ATTUNE_CONFIG__?: { + API_BASE_URL: string; + WITH_CREDENTIALS: boolean; + }; + } +} + // Configure the OpenAPI client // Use empty string to make requests relative to current origin (uses Vite proxy) const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ""; // API configuration (silent - check window.__ATTUNE_CONFIG__ for debug info) if (import.meta.env.DEV) { - (window as any).__ATTUNE_CONFIG__ = { + window.__ATTUNE_CONFIG__ = { API_BASE_URL, WITH_CREDENTIALS: true, }; diff --git a/web/src/lib/api-wrapper.ts b/web/src/lib/api-wrapper.ts index 0f30f83..b29ec2a 100644 --- a/web/src/lib/api-wrapper.ts +++ b/web/src/lib/api-wrapper.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { type InternalAxiosRequestConfig } from "axios"; /** * This module configures the generated API client to properly handle token refresh @@ -65,8 +65,7 @@ export function isTokenExpiringSoon( // Return true if token expires within threshold seconds (default 5 minutes) return timeUntilExpiry <= thresholdSeconds; - } catch (error) { - console.error("Failed to parse JWT:", error); + } catch { return true; } } @@ -84,8 +83,7 @@ export function isTokenExpired(token: string): boolean { const now = Math.floor(Date.now() / 1000); return exp <= now; - } catch (error) { - console.error("Failed to parse JWT:", error); + } catch { return true; } } @@ -114,7 +112,7 @@ async function attemptTokenRefresh(): Promise { } return true; - } catch (error) { + } catch { console.error( "Token refresh failed, clearing session and redirecting to login", ); @@ -193,7 +191,9 @@ export function configureAxiosDefaults(): void { axios.interceptors.response.use( (response) => response, async (error) => { - const originalRequest = error.config as any; + const originalRequest = error.config as InternalAxiosRequestConfig & { + _retry?: boolean; + }; // Handle 401 Unauthorized — token expired or invalid if (error.response?.status === 401 && !originalRequest._retry) { @@ -215,13 +215,12 @@ export function configureAxiosDefaults(): void { // Handle 403 Forbidden — valid token but insufficient permissions if (error.response?.status === 403) { - const enhancedError = error as any; - enhancedError.isAuthorizationError = true; + error.isAuthorizationError = true; console.warn( "Access forbidden - insufficient permissions for this resource", ); - return Promise.reject(enhancedError); + return Promise.reject(error); } return Promise.reject(error); diff --git a/web/src/lib/query-client.ts b/web/src/lib/query-client.ts index 2d4740c..058f873 100644 --- a/web/src/lib/query-client.ts +++ b/web/src/lib/query-client.ts @@ -1,9 +1,15 @@ import { QueryClient } from "@tanstack/react-query"; +interface HttpError extends Error { + response?: { + status: number; + }; +} + export const queryClient = new QueryClient({ defaultOptions: { queries: { - retry: (failureCount, error: any) => { + retry: (failureCount, error: HttpError) => { // Don't retry on 401 (handled by interceptor) or 403 (permission denied) if ( error?.response?.status === 401 || diff --git a/web/src/pages/actions/ActionsPage.tsx b/web/src/pages/actions/ActionsPage.tsx index 83b6abc..ab98b8d 100644 --- a/web/src/pages/actions/ActionsPage.tsx +++ b/web/src/pages/actions/ActionsPage.tsx @@ -2,6 +2,8 @@ import { Link, useParams, useNavigate } from "react-router-dom"; import { useActions, useAction, useDeleteAction } from "@/hooks/useActions"; import { useExecutions } from "@/hooks/useExecutions"; import { useState, useMemo } from "react"; +import type { ActionSummary, ExecutionSummary } from "@/api"; +import type { ParamSchemaProperty } from "@/components/common/ParamSchemaForm"; import { ChevronDown, ChevronRight, @@ -20,7 +22,7 @@ export default function ActionsPage() { const { ref } = useParams<{ ref?: string }>(); const navigate = useNavigate(); const { data, isLoading, error } = useActions(); - const actions = data?.data || []; + const actions = useMemo(() => data?.data || [], [data?.data]); const [collapsedPacks, setCollapsedPacks] = useState>(new Set()); const [searchQuery, setSearchQuery] = useState(""); @@ -28,7 +30,7 @@ export default function ActionsPage() { const filteredActions = useMemo(() => { if (!searchQuery.trim()) return actions; const query = searchQuery.toLowerCase(); - return actions.filter((action: any) => { + return actions.filter((action: ActionSummary) => { return ( action.label?.toLowerCase().includes(query) || action.ref?.toLowerCase().includes(query) || @@ -40,8 +42,8 @@ export default function ActionsPage() { // Group filtered actions by pack const actionsByPack = useMemo(() => { - const grouped = new Map(); - filteredActions.forEach((action: any) => { + const grouped = new Map(); + filteredActions.forEach((action: ActionSummary) => { const packRef = action.pack_ref; if (!grouped.has(packRef)) { grouped.set(packRef, []); @@ -176,7 +178,7 @@ export default function ActionsPage() { {/* Actions List */} {!isCollapsed && (
- {packActions.map((action: any) => ( + {packActions.map((action: ActionSummary) => (
- {paramEntries.map(([key, param]: [string, any]) => ( -
-
-
-
- - {key} - - {param?.required && ( - - Required + {paramEntries.map( + ([key, param]: [string, ParamSchemaProperty]) => ( +
+
+
+
+ + {key} - )} - {param?.secret && ( - - Secret + {param?.required && ( + + Required + + )} + {param?.secret && ( + + Secret + + )} + + {param?.type || "any"} +
+ {param?.description && ( +

+ {param.description} +

+ )} + {param?.default !== undefined && ( +

+ Default:{" "} + + {JSON.stringify(param.default)} + +

+ )} + {param?.enum && param.enum.length > 0 && ( +

+ Values:{" "} + {param.enum + .map((v: string) => `"${v}"`) + .join(", ")} +

)} - - {param?.type || "any"} -
- {param?.description && ( -

- {param.description} -

- )} - {param?.default !== undefined && ( -

- Default:{" "} - - {JSON.stringify(param.default)} - -

- )} - {param?.enum && param.enum.length > 0 && ( -

- Values:{" "} - {param.enum.map((v: any) => `"${v}"`).join(", ")} -

- )}
-
- ))} + ), + )}
)} @@ -532,7 +538,7 @@ function ActionDetail({ actionRef }: { actionRef: string }) {

) : (
- {executions.map((execution: any) => ( + {executions.map((execution: ExecutionSummary) => ( { e.preventDefault(); @@ -27,16 +44,16 @@ export default function LoginPage() { sessionStorage.removeItem("redirect_after_login"); navigate(from, { replace: true }); - } catch (err: any) { - console.error("Login error:", err); - console.error("Full error object:", JSON.stringify(err, null, 2)); - if (err.response) { - console.error("Response status:", err.response.status); - console.error("Response data:", err.response.data); + } catch (err: unknown) { + const loginErr = err as LoginError; + console.error("Login error:", loginErr); + if (loginErr.response) { + console.error("Response status:", loginErr.response.status); + console.error("Response data:", loginErr.response.data); } const errorMessage = - err.response?.data?.message || - err.message || + loginErr.response?.data?.message || + loginErr.message || "Login failed. Please check your credentials."; setError(errorMessage); // Don't navigate on error - stay on login page diff --git a/web/src/pages/enforcements/EnforcementsPage.tsx b/web/src/pages/enforcements/EnforcementsPage.tsx index dd9030f..f1105ca 100644 --- a/web/src/pages/enforcements/EnforcementsPage.tsx +++ b/web/src/pages/enforcements/EnforcementsPage.tsx @@ -2,6 +2,7 @@ import { Link, useSearchParams } from "react-router-dom"; import { useEnforcements } from "@/hooks/useEvents"; import { useEnforcementStream } from "@/hooks/useEnforcementStream"; import { EnforcementStatus } from "@/api"; +import type { EnforcementSummary } from "@/api"; import { useState, useMemo, memo, useCallback, useEffect } from "react"; import { Search, X } from "lucide-react"; import MultiSelect from "@/components/common/MultiSelect"; @@ -99,7 +100,7 @@ const EnforcementsResultsTable = memo( pageSize, total, }: { - enforcements: any[]; + enforcements: EnforcementSummary[]; isLoading: boolean; isFetching: boolean; error: Error | null; @@ -195,7 +196,7 @@ const EnforcementsResultsTable = memo( - {enforcements.map((enforcement: any) => ( + {enforcements.map((enforcement: EnforcementSummary) => ( { - const params: any = { page, pageSize }; + const params: { + page: number; + pageSize: number; + triggerRef?: string; + event?: number; + status?: EnforcementStatus; + } = { page, pageSize }; if (debouncedFilters.trigger) params.triggerRef = debouncedFilters.trigger; if (debouncedFilters.event) { const eventId = parseInt(debouncedFilters.event, 10); @@ -416,16 +423,16 @@ export default function EnforcementsPage() { // Filter by rule_ref (client-side since API doesn't support it) if (debouncedFilters.rule) { - filtered = filtered.filter((enf: any) => + filtered = filtered.filter((enf: EnforcementSummary) => enf.rule_ref - .toLowerCase() + ?.toLowerCase() .includes(debouncedFilters.rule.toLowerCase()), ); } // If multiple statuses selected, filter client-side if (debouncedStatuses.length > 1) { - filtered = filtered.filter((enf: any) => + filtered = filtered.filter((enf: EnforcementSummary) => debouncedStatuses.includes(enf.status), ); } diff --git a/web/src/pages/events/EventsPage.tsx b/web/src/pages/events/EventsPage.tsx index a896b44..dc8c947 100644 --- a/web/src/pages/events/EventsPage.tsx +++ b/web/src/pages/events/EventsPage.tsx @@ -310,7 +310,12 @@ export default function EventsPage() { // --- Build query params from debounced state --- const queryParams = useMemo(() => { - const params: any = { page, pageSize }; + const params: { + page: number; + pageSize: number; + triggerRef?: string; + ruleRef?: string; + } = { page, pageSize }; if (debouncedFilters.trigger) params.triggerRef = debouncedFilters.trigger; if (debouncedFilters.rule) params.ruleRef = debouncedFilters.rule; return params; @@ -320,19 +325,21 @@ export default function EventsPage() { const handleEventNotification = useCallback( (notification: Notification) => { if (notification.notification_type === "event_created") { - const payload = notification.payload as any; + const payload = notification.payload as Partial & { + payload?: unknown; + }; const newEvent: EventSummary = { - id: payload.id, - trigger: payload.trigger, - trigger_ref: payload.trigger_ref, + id: payload.id ?? 0, + trigger: payload.trigger ?? 0, + trigger_ref: payload.trigger_ref ?? "", rule: payload.rule, rule_ref: payload.rule_ref, source: payload.source, source_ref: payload.source_ref, has_payload: payload.payload !== null && payload.payload !== undefined, - created: payload.created, + created: payload.created ?? new Date().toISOString(), }; // Augment autocomplete suggestions with new refs from notification @@ -357,44 +364,54 @@ export default function EventsPage() { }; }); - queryClient.setQueryData(["events", queryParams], (oldData: any) => { - if (!oldData) return oldData; + queryClient.setQueryData( + ["events", queryParams], + ( + oldData: + | { + data: EventSummary[]; + pagination?: { total_items?: number }; + } + | undefined, + ) => { + if (!oldData) return oldData; - // Check if filtering and event matches filter - if ( - debouncedFilters.trigger && - newEvent.trigger_ref !== debouncedFilters.trigger - ) { - return oldData; - } - if ( - debouncedFilters.rule && - newEvent.rule_ref !== debouncedFilters.rule - ) { - return oldData; - } + // Check if filtering and event matches filter + if ( + debouncedFilters.trigger && + newEvent.trigger_ref !== debouncedFilters.trigger + ) { + return oldData; + } + if ( + debouncedFilters.rule && + newEvent.rule_ref !== debouncedFilters.rule + ) { + return oldData; + } - // Add new event to the beginning of the list if on first page - if (page === 1) { + // Add new event to the beginning of the list if on first page + if (page === 1) { + return { + ...oldData, + data: [newEvent, ...oldData.data].slice(0, pageSize), + pagination: { + ...oldData.pagination, + total_items: (oldData.pagination?.total_items || 0) + 1, + }, + }; + } + + // For other pages, just update the total count return { ...oldData, - data: [newEvent, ...oldData.data].slice(0, pageSize), pagination: { ...oldData.pagination, total_items: (oldData.pagination?.total_items || 0) + 1, }, }; - } - - // For other pages, just update the total count - return { - ...oldData, - pagination: { - ...oldData.pagination, - total_items: (oldData.pagination?.total_items || 0) + 1, - }, - }; - }); + }, + ); } }, [queryClient, queryParams, page, pageSize, debouncedFilters], diff --git a/web/src/pages/executions/ExecutionsPage.tsx b/web/src/pages/executions/ExecutionsPage.tsx index 6c8a102..60d22b2 100644 --- a/web/src/pages/executions/ExecutionsPage.tsx +++ b/web/src/pages/executions/ExecutionsPage.tsx @@ -2,6 +2,7 @@ import { Link, useSearchParams } from "react-router-dom"; import { useExecutions } from "@/hooks/useExecutions"; import { useExecutionStream } from "@/hooks/useExecutionStream"; import { ExecutionStatus } from "@/api"; +import type { ExecutionSummary } from "@/api"; import { useState, useMemo, memo, useCallback, useEffect } from "react"; import { Search, X, List, GitBranch } from "lucide-react"; import MultiSelect from "@/components/common/MultiSelect"; @@ -96,7 +97,7 @@ const ExecutionsResultsTable = memo( selectedExecutionId, onSelectExecution, }: { - executions: any[]; + executions: ExecutionSummary[]; isLoading: boolean; isFetching: boolean; error: Error | null; @@ -191,7 +192,7 @@ const ExecutionsResultsTable = memo( - {executions.map((exec: any) => ( + {executions.map((exec: ExecutionSummary) => ( { - const params: any = { page, pageSize }; + const params: { + page: number; + pageSize: number; + packName?: string; + ruleRef?: string; + actionRef?: string; + triggerRef?: string; + executor?: number; + status?: ExecutionStatus; + topLevelOnly?: boolean; + } = { page, pageSize }; if (debouncedFilters.pack) params.packName = debouncedFilters.pack; if (debouncedFilters.rule) params.ruleRef = debouncedFilters.rule; if (debouncedFilters.action) params.actionRef = debouncedFilters.action; @@ -429,7 +440,7 @@ export default function ExecutionsPage() { // Client-side filtering for multiple status selection (when > 1 selected) const filteredExecutions = useMemo(() => { if (debouncedStatuses.length <= 1) return executions; - return executions.filter((exec: any) => + return executions.filter((exec: ExecutionSummary) => debouncedStatuses.includes(exec.status), ); }, [executions, debouncedStatuses]); @@ -500,7 +511,9 @@ export default function ExecutionsPage() { return nextId; } - const currentIndex = list.findIndex((ex: any) => ex.id === prevId); + const currentIndex = list.findIndex( + (ex: ExecutionSummary) => ex.id === prevId, + ); if (currentIndex === -1) { const nextId = list[0].id; requestAnimationFrame(() => { diff --git a/web/src/pages/keys/KeyCreateModal.tsx b/web/src/pages/keys/KeyCreateModal.tsx index 3d7b135..1f2dcc4 100644 --- a/web/src/pages/keys/KeyCreateModal.tsx +++ b/web/src/pages/keys/KeyCreateModal.tsx @@ -22,7 +22,8 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) { const createKeyMutation = useCreateKey(); // Determine if encryption is allowed based on format - const canEncrypt = format === "text" || format === "json" || format === "yaml"; + const canEncrypt = + format === "text" || format === "json" || format === "yaml"; // Auto-disable encryption for non-encryptable formats const isEncrypted = canEncrypt && encrypted; @@ -33,12 +34,13 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) { // Validate ref format if (!/^[a-zA-Z0-9_.-]+$/.test(ref)) { - setError("Reference must contain only letters, numbers, underscores, hyphens, and dots"); + setError( + "Reference must contain only letters, numbers, underscores, hyphens, and dots", + ); return; } // Validate value based on format - let validatedValue = value; try { if (format === "json") { JSON.parse(value); @@ -70,14 +72,14 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) { await createKeyMutation.mutateAsync({ ref, name, - value: validatedValue, + value, encrypted: isEncrypted, owner_type: ownerType, owner: owner || undefined, }); onClose(); - } catch (err: any) { - setError(err.message || "Failed to create key"); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to create key"); } }; @@ -102,7 +104,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) { )}
-