eslint
Some checks failed
CI / Rustfmt (push) Successful in 22s
CI / Cargo Audit & Deny (push) Failing after 1m2s
CI / Web Blocking Checks (push) Failing after 35s
CI / Security Blocking Checks (push) Successful in 8s
CI / Clippy (push) Successful in 2m43s
CI / Web Advisory Checks (push) Successful in 35s
CI / Security Advisory Checks (push) Successful in 37s
CI / Tests (push) Failing after 9m28s

This commit is contained in:
2026-03-05 06:52:55 -06:00
parent f54eef3a14
commit 179180d604
102 changed files with 1031 additions and 532 deletions

View File

@@ -10,10 +10,13 @@ on:
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
RUST_MIN_STACK: 16777216 RUST_MIN_STACK: 16777216
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
jobs: jobs:
rust-blocking: rust-fmt:
name: Rust Blocking Checks name: Rustfmt
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -22,19 +25,119 @@ jobs:
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: rustfmt, clippy components: rustfmt
- name: Rustfmt - name: Rustfmt
run: cargo fmt --all -- --check 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 - name: Clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings 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 - name: Tests
run: cargo test --workspace --all-features run: cargo test --workspace --all-features
- name: Install Rust security tooling rust-audit:
run: cargo install --locked cargo-audit cargo-deny 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 - name: Cargo Audit
run: cargo audit run: cargo audit

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Simplified action response (for list endpoints) * Simplified action response (for list endpoints)
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Standard API response wrapper * Standard API response wrapper
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { OwnerType } from './OwnerType'; import type { OwnerType } from './OwnerType';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { PackResponse } from './PackResponse'; import type { PackResponse } from './PackResponse';
import type { PackTestResult } from './PackTestResult'; import type { PackTestResult } from './PackTestResult';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Standard API response wrapper * Standard API response wrapper
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Standard API response wrapper * Standard API response wrapper
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { UserInfo } from './UserInfo'; import type { UserInfo } from './UserInfo';
/** /**
* Standard API response wrapper * Standard API response wrapper

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Standard API response wrapper * Standard API response wrapper
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Change password request * Change password request
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { OwnerType } from './OwnerType'; import type { OwnerType } from './OwnerType';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Current user response * Current user response
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
export enum EnforcementCondition { export enum EnforcementCondition {
ANY = 'any', ANY = 'any',
ALL = 'all', ALL = 'all',

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
export enum EnforcementStatus { export enum EnforcementStatus {
CREATED = 'created', CREATED = 'created',
PROCESSED = 'processed', PROCESSED = 'processed',

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { EnforcementCondition } from './EnforcementCondition'; import type { EnforcementCondition } from './EnforcementCondition';
import type { EnforcementStatus } from './EnforcementStatus'; import type { EnforcementStatus } from './EnforcementStatus';
import type { i64 } from './i64'; import type { i64 } from './i64';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
/** /**
* Summary event response for list views * Summary event response for list views

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
export enum ExecutionStatus { export enum ExecutionStatus {
REQUESTED = 'requested', REQUESTED = 'requested',
SCHEDULING = 'scheduling', SCHEDULING = 'scheduling',

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ExecutionStatus } from "./ExecutionStatus"; import type { ExecutionStatus } from "./ExecutionStatus";
/** /**
* Simplified execution response (for list endpoints) * Simplified execution response (for list endpoints)

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Health check response * Health check response
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
export enum InquiryStatus { export enum InquiryStatus {
PENDING = 'pending', PENDING = 'pending',
RESPONDED = 'responded', RESPONDED = 'responded',

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { InquiryStatus } from './InquiryStatus'; import type { InquiryStatus } from './InquiryStatus';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Request DTO for installing a pack from remote source * Request DTO for installing a pack from remote source
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { OwnerType } from './OwnerType'; import type { OwnerType } from './OwnerType';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { OwnerType } from './OwnerType'; import type { OwnerType } from './OwnerType';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Login request * Login request
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
export enum OwnerType { export enum OwnerType {
SYSTEM = 'system', SYSTEM = 'system',
IDENTITY = 'identity', IDENTITY = 'identity',

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { PackResponse } from './PackResponse'; import type { PackResponse } from './PackResponse';
import type { PackTestResult } from './PackTestResult'; import type { PackTestResult } from './PackTestResult';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Simplified pack response (for list endpoints) * Simplified pack response (for list endpoints)
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { Value } from './Value'; import type { Value } from './Value';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { TestSuiteResult } from './TestSuiteResult'; import type { TestSuiteResult } from './TestSuiteResult';
/** /**
* Pack test result structure (not from DB, used for test execution) * Pack test result structure (not from DB, used for test execution)

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
/** /**
* Pack test summary view * Pack test summary view

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { WorkflowSyncResult } from './WorkflowSyncResult'; import type { WorkflowSyncResult } from './WorkflowSyncResult';
/** /**
* Response for pack workflow sync operation * Response for pack workflow sync operation

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Response for pack workflow validation operation * Response for pack workflow validation operation
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { PaginationMeta } from "./PaginationMeta"; import type { PaginationMeta } from "./PaginationMeta";
/** /**
* Paginated response wrapper * Paginated response wrapper

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { EnforcementCondition } from './EnforcementCondition'; import type { EnforcementCondition } from './EnforcementCondition';
import type { EnforcementStatus } from './EnforcementStatus'; import type { EnforcementStatus } from './EnforcementStatus';
import type { i64 } from './i64'; import type { i64 } from './i64';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { PaginationMeta } from './PaginationMeta'; import type { PaginationMeta } from './PaginationMeta';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ExecutionStatus } from "./ExecutionStatus"; import type { ExecutionStatus } from "./ExecutionStatus";
import type { PaginationMeta } from "./PaginationMeta"; import type { PaginationMeta } from "./PaginationMeta";
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { InquiryStatus } from './InquiryStatus'; import type { InquiryStatus } from './InquiryStatus';
import type { PaginationMeta } from './PaginationMeta'; import type { PaginationMeta } from './PaginationMeta';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { OwnerType } from './OwnerType'; import type { OwnerType } from './OwnerType';
import type { PaginationMeta } from './PaginationMeta'; import type { PaginationMeta } from './PaginationMeta';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { PaginationMeta } from './PaginationMeta'; import type { PaginationMeta } from './PaginationMeta';
/** /**
* Paginated response wrapper * Paginated response wrapper

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { i64 } from './i64'; import type { i64 } from './i64';
import type { PaginationMeta } from './PaginationMeta'; import type { PaginationMeta } from './PaginationMeta';
/** /**

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { PaginationMeta } from './PaginationMeta'; import type { PaginationMeta } from './PaginationMeta';
/** /**
* Paginated response wrapper * Paginated response wrapper

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { PaginationMeta } from './PaginationMeta'; import type { PaginationMeta } from './PaginationMeta';
/** /**
* Paginated response wrapper * Paginated response wrapper

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { PaginationMeta } from './PaginationMeta'; import type { PaginationMeta } from './PaginationMeta';
/** /**
* Paginated response wrapper * Paginated response wrapper

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Pagination metadata * Pagination metadata
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Response DTO for queue statistics * Response DTO for queue statistics
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Refresh token request * Refresh token request
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Request DTO for registering a pack from local filesystem * Request DTO for registering a pack from local filesystem
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Register request * Register request
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Simplified sensor response (for list endpoints) * Simplified sensor response (for list endpoints)
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Success message response (for operations that don't return data) * Success message response (for operations that don't return data)
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { TestStatus } from './TestStatus'; import type { TestStatus } from './TestStatus';
/** /**
* Individual test case result * Individual test case result

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Test status enum * Test status enum
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { TestCaseResult } from './TestCaseResult'; import type { TestCaseResult } from './TestCaseResult';
/** /**
* Test suite result (collection of test cases) * Test suite result (collection of test cases)

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { UserInfo } from './UserInfo'; import type { UserInfo } from './UserInfo';
/** /**
* Token response * Token response

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Simplified trigger response (for list endpoints) * Simplified trigger response (for list endpoints)
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Request to update an existing key/secret * Request to update an existing key/secret
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* User information included in token response * User information included in token response
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { Value } from './Value'; import type { Value } from './Value';
/** /**
* Request body for webhook receiver endpoint * Request body for webhook receiver endpoint

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Response from webhook receiver endpoint * Response from webhook receiver endpoint
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Simplified workflow response (for list endpoints) * Simplified workflow response (for list endpoints)
*/ */

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
* Individual workflow sync result * Individual workflow sync result
*/ */

View File

@@ -1,5 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
export type i64 = number; export type i64 = number;

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ChangePasswordRequest } from '../models/ChangePasswordRequest'; import type { ChangePasswordRequest } from '../models/ChangePasswordRequest';
import type { LoginRequest } from '../models/LoginRequest'; import type { LoginRequest } from '../models/LoginRequest';
import type { RefreshTokenRequest } from '../models/RefreshTokenRequest'; import type { RefreshTokenRequest } from '../models/RefreshTokenRequest';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ApiResponse_EnforcementResponse } from '../models/ApiResponse_EnforcementResponse'; import type { ApiResponse_EnforcementResponse } from '../models/ApiResponse_EnforcementResponse';
import type { EnforcementStatus } from '../models/EnforcementStatus'; import type { EnforcementStatus } from '../models/EnforcementStatus';
import type { i64 } from '../models/i64'; import type { i64 } from '../models/i64';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ApiResponse_EventResponse } from '../models/ApiResponse_EventResponse'; import type { ApiResponse_EventResponse } from '../models/ApiResponse_EventResponse';
import type { i64 } from '../models/i64'; import type { i64 } from '../models/i64';
import type { PaginatedResponse_EventSummary } from '../models/PaginatedResponse_EventSummary'; import type { PaginatedResponse_EventSummary } from '../models/PaginatedResponse_EventSummary';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ApiResponse_InquiryResponse } from '../models/ApiResponse_InquiryResponse'; import type { ApiResponse_InquiryResponse } from '../models/ApiResponse_InquiryResponse';
import type { CreateInquiryRequest } from '../models/CreateInquiryRequest'; import type { CreateInquiryRequest } from '../models/CreateInquiryRequest';
import type { i64 } from '../models/i64'; import type { i64 } from '../models/i64';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ApiResponse_RuleResponse } from '../models/ApiResponse_RuleResponse'; import type { ApiResponse_RuleResponse } from '../models/ApiResponse_RuleResponse';
import type { CreateRuleRequest } from '../models/CreateRuleRequest'; import type { CreateRuleRequest } from '../models/CreateRuleRequest';
import type { PaginatedResponse_RuleSummary } from '../models/PaginatedResponse_RuleSummary'; import type { PaginatedResponse_RuleSummary } from '../models/PaginatedResponse_RuleSummary';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { CreateKeyRequest } from '../models/CreateKeyRequest'; import type { CreateKeyRequest } from '../models/CreateKeyRequest';
import type { i64 } from '../models/i64'; import type { i64 } from '../models/i64';
import type { OwnerType } from '../models/OwnerType'; import type { OwnerType } from '../models/OwnerType';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ApiResponse_SensorResponse } from '../models/ApiResponse_SensorResponse'; import type { ApiResponse_SensorResponse } from '../models/ApiResponse_SensorResponse';
import type { CreateSensorRequest } from '../models/CreateSensorRequest'; import type { CreateSensorRequest } from '../models/CreateSensorRequest';
import type { PaginatedResponse_SensorSummary } from '../models/PaginatedResponse_SensorSummary'; import type { PaginatedResponse_SensorSummary } from '../models/PaginatedResponse_SensorSummary';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { ApiResponse_TriggerResponse } from '../models/ApiResponse_TriggerResponse'; import type { ApiResponse_TriggerResponse } from '../models/ApiResponse_TriggerResponse';
import type { CreateTriggerRequest } from '../models/CreateTriggerRequest'; import type { CreateTriggerRequest } from '../models/CreateTriggerRequest';
import type { PaginatedResponse_TriggerSummary } from '../models/PaginatedResponse_TriggerSummary'; import type { PaginatedResponse_TriggerSummary } from '../models/PaginatedResponse_TriggerSummary';

View File

@@ -1,7 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */ /* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
import type { TriggerResponse } from '../models/TriggerResponse'; import type { TriggerResponse } from '../models/TriggerResponse';
import type { WebhookReceiverRequest } from '../models/WebhookReceiverRequest'; import type { WebhookReceiverRequest } from '../models/WebhookReceiverRequest';
import type { WebhookReceiverResponse } from '../models/WebhookReceiverResponse'; import type { WebhookReceiverResponse } from '../models/WebhookReceiverResponse';

View File

@@ -550,29 +550,34 @@ export default function AnalyticsDashboard({
hours, hours,
onHoursChange, onHoursChange,
}: AnalyticsDashboardProps) { }: 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(() => { const executionBuckets = useMemo(() => {
if (!data?.execution_throughput) return []; if (!executionThroughput) return [];
const agg = aggregateByBucket(data.execution_throughput); const agg = aggregateByBucket(executionThroughput);
return Array.from(agg.entries()) return Array.from(agg.entries())
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([bucket, v]) => ({ bucket, value: v.total })); .map(([bucket, v]) => ({ bucket, value: v.total }));
}, [data?.execution_throughput]); }, [executionThroughput]);
const eventBuckets = useMemo(() => { const eventBuckets = useMemo(() => {
if (!data?.event_volume) return []; if (!eventVolume) return [];
const agg = aggregateByBucket(data.event_volume); const agg = aggregateByBucket(eventVolume);
return Array.from(agg.entries()) return Array.from(agg.entries())
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([bucket, v]) => ({ bucket, value: v.total })); .map(([bucket, v]) => ({ bucket, value: v.total }));
}, [data?.event_volume]); }, [eventVolume]);
const enforcementBuckets = useMemo(() => { const enforcementBuckets = useMemo(() => {
if (!data?.enforcement_volume) return []; if (!enforcementVolume) return [];
const agg = aggregateByBucket(data.enforcement_volume); const agg = aggregateByBucket(enforcementVolume);
return Array.from(agg.entries()) return Array.from(agg.entries())
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([bucket, v]) => ({ bucket, value: v.total })); .map(([bucket, v]) => ({ bucket, value: v.total }));
}, [data?.enforcement_volume]); }, [enforcementVolume]);
const totalExecutions = useMemo( const totalExecutions = useMemo(
() => executionBuckets.reduce((s, b) => s + b.value, 0), () => executionBuckets.reduce((s, b) => s + b.value, 0),

View File

@@ -1,5 +1,17 @@
import { AlertCircle, ShieldAlert } from "lucide-react"; 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 { interface ErrorDisplayProps {
error: Error | unknown; error: Error | unknown;
title?: string; title?: string;
@@ -21,30 +33,39 @@ export default function ErrorDisplay({
showRetry = false, showRetry = false,
onRetry, onRetry,
}: ErrorDisplayProps) { }: ErrorDisplayProps) {
const asAxios = (err: unknown): AxiosLikeError | null => {
if (err && typeof err === "object") return err as AxiosLikeError;
return null;
};
// Type guard for axios errors // Type guard for axios errors
const isAxiosError = (err: any): boolean => { const isAxiosError = (err: unknown): boolean => {
return err?.response?.status !== undefined; const e = asAxios(err);
return e?.response?.status !== undefined;
}; };
// Check if this is a 403 (Forbidden) error // Check if this is a 403 (Forbidden) error
const is403Error = (err: any): boolean => { const is403Error = (err: unknown): boolean => {
return ( const e = asAxios(err);
err?.response?.status === 403 || return e?.response?.status === 403 || e?.isAuthorizationError === true;
err?.isAuthorizationError === true
);
}; };
// Check if this is a 401 (Unauthorized) error // Check if this is a 401 (Unauthorized) error
const is401Error = (err: any): boolean => { const is401Error = (err: unknown): boolean => {
return err?.response?.status === 401; const e = asAxios(err);
return e?.response?.status === 401;
}; };
// Extract error message // Extract error message
const getErrorMessage = (err: any): string => { const getErrorMessage = (err: unknown): string => {
if (err?.response?.data?.message) { const e = asAxios(err);
return err.response.data.message; 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 err.message;
} }
return "An unexpected error occurred"; return "An unexpected error occurred";
@@ -67,8 +88,8 @@ export default function ErrorDisplay({
role or permissions do not allow this action. role or permissions do not allow this action.
</p> </p>
<p className="mt-2 text-sm text-amber-700"> <p className="mt-2 text-sm text-amber-700">
If you believe you should have access, please contact your If you believe you should have access, please contact your system
system administrator. administrator.
</p> </p>
</div> </div>
</div> </div>
@@ -110,12 +131,10 @@ export default function ErrorDisplay({
<h3 className="text-lg font-semibold text-red-900"> <h3 className="text-lg font-semibold text-red-900">
{title || "Error"} {title || "Error"}
</h3> </h3>
<p className="mt-2 text-sm text-red-800"> <p className="mt-2 text-sm text-red-800">{getErrorMessage(error)}</p>
{getErrorMessage(error)} {isAxiosError(error) && asAxios(error)?.response?.status && (
</p>
{isAxiosError(error) && (error as any)?.response?.status && (
<p className="mt-1 text-xs text-red-600"> <p className="mt-1 text-xs text-red-600">
Status Code: {(error as any).response.status} Status Code: {asAxios(error)?.response?.status}
</p> </p>
)} )}
{showRetry && onRetry && ( {showRetry && onRetry && (

View File

@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { OpenAPI } from "@/api"; import { OpenAPI } from "@/api";
import type { ActionResponse } from "@/api";
import { Play, X } from "lucide-react"; import { Play, X } from "lucide-react";
import ParamSchemaForm, { import ParamSchemaForm, {
validateParamSchema, validateParamSchema,
@@ -8,10 +9,13 @@ import ParamSchemaForm, {
type ParamSchema, type ParamSchema,
} from "@/components/common/ParamSchemaForm"; } from "@/components/common/ParamSchemaForm";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JsonValue = any;
interface ExecuteActionModalProps { interface ExecuteActionModalProps {
action: any; action: ActionResponse;
onClose: () => void; onClose: () => void;
initialParameters?: Record<string, any>; initialParameters?: Record<string, JsonValue>;
} }
/** /**
@@ -32,9 +36,9 @@ export default function ExecuteActionModal({
const paramProperties = extractProperties(paramSchema); const paramProperties = extractProperties(paramSchema);
// If initialParameters are provided, use them (stripping out any keys not in the schema) // If initialParameters are provided, use them (stripping out any keys not in the schema)
const buildInitialValues = (): Record<string, any> => { const buildInitialValues = (): Record<string, JsonValue> => {
if (!initialParameters) return {}; if (!initialParameters) return {};
const values: Record<string, any> = {}; const values: Record<string, JsonValue> = {};
// Include all initial parameters - even those not in the schema // Include all initial parameters - even those not in the schema
// so users can see exactly what was run before // so users can see exactly what was run before
for (const [key, value] of Object.entries(initialParameters)) { for (const [key, value] of Object.entries(initialParameters)) {
@@ -52,7 +56,7 @@ export default function ExecuteActionModal({
}; };
const [parameters, setParameters] = const [parameters, setParameters] =
useState<Record<string, any>>(buildInitialValues); useState<Record<string, JsonValue>>(buildInitialValues);
const [paramErrors, setParamErrors] = useState<Record<string, string>>({}); const [paramErrors, setParamErrors] = useState<Record<string, string>>({});
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>( const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>(
[{ key: "", value: "" }], [{ key: "", value: "" }],
@@ -60,12 +64,12 @@ export default function ExecuteActionModal({
const executeAction = useMutation({ const executeAction = useMutation({
mutationFn: async (params: { mutationFn: async (params: {
parameters: Record<string, any>; parameters: Record<string, JsonValue>;
envVars: Array<{ key: string; value: string }>; envVars: Array<{ key: string; value: string }>;
}) => { }) => {
const token = const token =
typeof OpenAPI.TOKEN === "function" typeof OpenAPI.TOKEN === "function"
? await OpenAPI.TOKEN({} as any) ? await OpenAPI.TOKEN({} as Parameters<typeof OpenAPI.TOKEN>[0])
: OpenAPI.TOKEN; : OpenAPI.TOKEN;
const response = await fetch( const response = await fetch(

View File

@@ -8,9 +8,13 @@ import type { ParamSchema } from "./ParamSchemaForm";
export type { ParamSchema }; export type { ParamSchema };
import { extractProperties } from "./ParamSchemaForm"; 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 { interface ParamSchemaDisplayProps {
schema: ParamSchema; schema: ParamSchema;
values: Record<string, any>; values: Record<string, JsonValue>;
className?: string; className?: string;
emptyMessage?: string; emptyMessage?: string;
} }
@@ -53,7 +57,7 @@ export default function ParamSchemaDisplay({
* Returns both the formatted value and whether it should be displayed inline * Returns both the formatted value and whether it should be displayed inline
*/ */
const formatValue = ( const formatValue = (
value: any, value: JsonValue,
type?: string, type?: string,
): { element: React.JSX.Element; isInline: boolean } => { ): { element: React.JSX.Element; isInline: boolean } => {
if (value === undefined || value === null) { if (value === undefined || value === null) {

View File

@@ -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"; 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. * StackStorm-style parameter schema format.
* Parameters are defined as a flat map of parameter name to definition, * Parameters are defined as a flat map of parameter name to definition,
@@ -14,7 +19,7 @@ import { useState, useEffect } from "react";
export interface ParamSchemaProperty { export interface ParamSchemaProperty {
type?: "string" | "number" | "integer" | "boolean" | "array" | "object"; type?: "string" | "number" | "integer" | "boolean" | "array" | "object";
description?: string; description?: string;
default?: any; default?: JsonValue;
enum?: string[]; enum?: string[];
minimum?: number; minimum?: number;
maximum?: number; maximum?: number;
@@ -23,7 +28,7 @@ export interface ParamSchemaProperty {
secret?: boolean; secret?: boolean;
required?: boolean; required?: boolean;
position?: number; position?: number;
items?: any; items?: Record<string, unknown>;
} }
export interface ParamSchema { export interface ParamSchema {
@@ -40,7 +45,7 @@ export interface ParamSchema {
* { param_name: { type, description, required, secret, ... }, ... } * { param_name: { type, description, required, secret, ... }, ... }
*/ */
export function extractProperties( export function extractProperties(
schema: ParamSchema | any, schema: ParamSchema | Record<string, unknown> | null | undefined,
): Record<string, ParamSchemaProperty> { ): Record<string, ParamSchemaProperty> {
if (!schema || typeof schema !== "object") return {}; if (!schema || typeof schema !== "object") return {};
// StackStorm-style flat format: { param_name: { type, description, required, ... }, ... } // StackStorm-style flat format: { param_name: { type, description, required, ... }, ... }
@@ -56,8 +61,8 @@ export function extractProperties(
interface ParamSchemaFormProps { interface ParamSchemaFormProps {
schema: ParamSchema; schema: ParamSchema;
values: Record<string, any>; values: Record<string, JsonValue>;
onChange: (values: Record<string, any>) => void; onChange: (values: Record<string, JsonValue>) => void;
errors?: Record<string, string>; errors?: Record<string, string>;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
@@ -79,7 +84,7 @@ interface ParamSchemaFormProps {
/** /**
* Check if a string value contains a template expression ({{ ... }}) * 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); 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 * Non-string values (booleans, numbers, objects, arrays) are JSON-stringified
* so the user can edit them as text. * 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 (value === undefined || value === null) return "";
if (typeof value === "string") return value; if (typeof value === "string") return value;
return JSON.stringify(value); return JSON.stringify(value);
@@ -99,7 +104,7 @@ function valueToString(value: any): string {
* Template expressions are always kept as strings. * Template expressions are always kept as strings.
* Plain values are coerced to the schema type when possible. * 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 ""; if (raw === "") return "";
// Template expressions stay as strings - resolved server-side // Template expressions stay as strings - resolved server-side
if (isTemplateExpression(raw)) return raw; if (isTemplateExpression(raw)) return raw;
@@ -164,19 +169,20 @@ export default function ParamSchemaForm({
} }
return acc; return acc;
}, },
{ ...values } as Record<string, any>, { ...values } as Record<string, JsonValue>,
); );
// Only update if there are new defaults // Only update if there are new defaults
if (JSON.stringify(initialValues) !== JSON.stringify(values)) { if (JSON.stringify(initialValues) !== JSON.stringify(values)) {
onChange(initialValues); onChange(initialValues);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schema]); // Only run when schema changes }, [schema]); // Only run when schema changes
/** /**
* Handle input change for a specific field * Handle input change for a specific field
*/ */
const handleInputChange = (key: string, value: any) => { const handleInputChange = (key: string, value: JsonValue) => {
const newValues = { ...values, [key]: value }; const newValues = { ...values, [key]: value };
onChange(newValues); onChange(newValues);
@@ -200,7 +206,10 @@ export default function ParamSchemaForm({
/** /**
* Get a placeholder hint for template-mode inputs * 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"; const type = param?.type || "string";
switch (type) { switch (type) {
case "boolean": case "boolean":
@@ -225,7 +234,10 @@ export default function ParamSchemaForm({
/** /**
* Render a template-mode text input for any parameter type * 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 type = param?.type || "string";
const rawValue = values[key] ?? param?.default ?? ""; const rawValue = values[key] ?? param?.default ?? "";
const isDisabled = disabled; const isDisabled = disabled;
@@ -264,7 +276,7 @@ export default function ParamSchemaForm({
/** /**
* Render input field based on parameter type (standard mode) * 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 type = param?.type || "string";
const value = values[key] ?? param?.default ?? ""; const value = values[key] ?? param?.default ?? "";
const isDisabled = disabled; 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" 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"
> >
<option value="">Select...</option> <option value="">Select...</option>
{param.enum.map((option: any) => ( {param.enum.map((option: string) => (
<option key={option} value={option}> <option key={option} value={option}>
{option} {option}
</option> </option>
@@ -416,7 +428,10 @@ export default function ParamSchemaForm({
/** /**
* Render type hint badge and additional context for template-mode fields * 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 type = param?.type || "string";
const hints: string[] = []; const hints: string[] = [];
@@ -537,7 +552,7 @@ export default function ParamSchemaForm({
*/ */
export function validateParamSchema( export function validateParamSchema(
schema: ParamSchema, schema: ParamSchema,
values: Record<string, any>, values: Record<string, JsonValue>,
allowTemplates: boolean = false, allowTemplates: boolean = false,
): Record<string, string> { ): Record<string, string> {
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};

View File

@@ -1,6 +1,25 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { Plus, Trash2, ChevronDown, ChevronRight, Code } from "lucide-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<string, SchemaPropertyDef>;
interface SchemaProperty { interface SchemaProperty {
name: string; name: string;
type: string; type: string;
@@ -17,8 +36,8 @@ interface SchemaProperty {
} }
interface SchemaBuilderProps { interface SchemaBuilderProps {
value: Record<string, any>; value: FlatSchema;
onChange: (schema: Record<string, any>) => void; onChange: (schema: FlatSchema) => void;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
error?: string; error?: string;
@@ -58,24 +77,23 @@ export default function SchemaBuilder({
if (!value || typeof value !== "object") return; if (!value || typeof value !== "object") return;
const props: SchemaProperty[] = []; 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)) { if (propDef && typeof propDef === "object" && !Array.isArray(propDef)) {
const def = propDef as SchemaPropertyDef;
props.push({ props.push({
name, name,
type: propDef.type || "string", type: def.type || "string",
description: propDef.description || "", description: def.description || "",
required: propDef.required === true, required: def.required === true,
secret: propDef.secret === true, secret: def.secret === true,
default: default:
propDef.default !== undefined def.default !== undefined ? JSON.stringify(def.default) : undefined,
? JSON.stringify(propDef.default) minimum: def.minimum,
: undefined, maximum: def.maximum,
minimum: propDef.minimum, minLength: def.minLength,
maximum: propDef.maximum, maxLength: def.maxLength,
minLength: propDef.minLength, pattern: def.pattern,
maxLength: propDef.maxLength, enum: def.enum,
pattern: propDef.pattern,
enum: propDef.enum,
}); });
} }
}); });
@@ -83,26 +101,19 @@ export default function SchemaBuilder({
if (props.length > 0) { if (props.length > 0) {
setProperties(props); 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 // Build StackStorm-style flat parameter schema
const buildSchema = (): Record<string, any> => { const buildSchema = useCallback((): FlatSchema => {
if (properties.length === 0) { if (properties.length === 0) {
return {}; return {};
} }
const schema: Record<string, any> = {}; const schema: FlatSchema = {};
properties.forEach((prop) => { properties.forEach((prop) => {
const propSchema: Record<string, any> = { const propSchema: SchemaPropertyDef = {
type: prop.type, type: prop.type,
}; };
@@ -143,7 +154,15 @@ export default function SchemaBuilder({
}); });
return schema; 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[]) => { const handlePropertiesChange = (newProperties: SchemaProperty[]) => {
setProperties(newProperties); setProperties(newProperties);
@@ -152,17 +171,15 @@ export default function SchemaBuilder({
}; };
// Build StackStorm-style flat parameter schema from properties array // Build StackStorm-style flat parameter schema from properties array
const buildSchemaFromProperties = ( const buildSchemaFromProperties = (props: SchemaProperty[]): FlatSchema => {
props: SchemaProperty[],
): Record<string, any> => {
if (props.length === 0) { if (props.length === 0) {
return {}; return {};
} }
const schema: Record<string, any> = {}; const schema: FlatSchema = {};
props.forEach((prop) => { props.forEach((prop) => {
const propSchema: Record<string, any> = { const propSchema: SchemaPropertyDef = {
type: prop.type, type: prop.type,
}; };
@@ -266,31 +283,32 @@ export default function SchemaBuilder({
// Expects StackStorm-style flat format: { param_name: { type, required, secret, ... }, ... } // Expects StackStorm-style flat format: { param_name: { type, required, secret, ... }, ... }
const props: SchemaProperty[] = []; 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)) { if (propDef && typeof propDef === "object" && !Array.isArray(propDef)) {
const def = propDef as SchemaPropertyDef;
props.push({ props.push({
name, name,
type: propDef.type || "string", type: def.type || "string",
description: propDef.description || "", description: def.description || "",
required: propDef.required === true, required: def.required === true,
secret: propDef.secret === true, secret: def.secret === true,
default: default:
propDef.default !== undefined def.default !== undefined
? JSON.stringify(propDef.default) ? JSON.stringify(def.default)
: undefined, : undefined,
minimum: propDef.minimum, minimum: def.minimum,
maximum: propDef.maximum, maximum: def.maximum,
minLength: propDef.minLength, minLength: def.minLength,
maxLength: propDef.maxLength, maxLength: def.maxLength,
pattern: propDef.pattern, pattern: def.pattern,
enum: propDef.enum, enum: def.enum,
}); });
} }
}); });
setProperties(props); setProperties(props);
} catch (e: any) { } catch (e: unknown) {
setRawJsonError(e.message); setRawJsonError(e instanceof Error ? e.message : "Invalid JSON");
} }
}; };

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useCreatePack, useUpdatePack } from "@/hooks/usePacks"; import { useCreatePack, useUpdatePack } from "@/hooks/usePacks";
import type { PackResponse } from "@/api"; import type { PackResponse } from "@/api";
@@ -7,6 +7,24 @@ import SchemaBuilder from "@/components/common/SchemaBuilder";
import ParamSchemaForm from "@/components/common/ParamSchemaForm"; import ParamSchemaForm from "@/components/common/ParamSchemaForm";
import { RotateCcw } from "lucide-react"; 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<string, SchemaPropertyDef>;
interface PackFormProps { interface PackFormProps {
pack?: PackResponse; pack?: PackResponse;
onSuccess?: () => void; onSuccess?: () => void;
@@ -31,9 +49,10 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
const [isStandard, setIsStandard] = useState(pack?.is_standard ?? false); const [isStandard, setIsStandard] = useState(pack?.is_standard ?? false);
const [configValues, setConfigValues] = const [configValues, setConfigValues] =
useState<Record<string, any>>(initialConfig); useState<Record<string, JsonValue>>(initialConfig);
const [confSchema, setConfSchema] = const [confSchema, setConfSchema] = useState<FlatSchema>(
useState<Record<string, any>>(initialConfSchema); initialConfSchema as FlatSchema,
);
const [meta, setMeta] = useState( const [meta, setMeta] = useState(
pack?.meta ? JSON.stringify(pack.meta, null, 2) : "{}", pack?.meta ? JSON.stringify(pack.meta, null, 2) : "{}",
); );
@@ -49,14 +68,22 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
typeof confSchema === "object" && typeof confSchema === "object" &&
Object.keys(confSchema).length > 0; 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) // 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(() => { useEffect(() => {
// Only sync when confSchema actually changed
if (prevConfSchemaRef.current === confSchema) return;
prevConfSchemaRef.current = confSchema;
if (!isStandard && hasSchemaProperties) { if (!isStandard && hasSchemaProperties) {
// Get current schema property names (flat format: keys are parameter names) // Get current schema property names (flat format: keys are parameter names)
const schemaKeys = Object.keys(confSchema); const schemaKeys = Object.keys(confSchema);
// Create new config with only keys that exist in schema // Create new config with only keys that exist in schema
const syncedConfig: Record<string, any> = {}; const syncedConfig: Record<string, JsonValue> = {};
schemaKeys.forEach((key) => { schemaKeys.forEach((key) => {
if (configValues[key] !== undefined) { if (configValues[key] !== undefined) {
// Preserve existing value // Preserve existing value
@@ -77,7 +104,8 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
setConfigValues(syncedConfig); setConfigValues(syncedConfig);
} }
} }
}, [confSchema, isStandard]); }, [confSchema, isStandard, hasSchemaProperties, configValues]);
/* eslint-enable react-hooks/set-state-in-effect */
const validateForm = (): boolean => { const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
@@ -111,7 +139,7 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
if (meta.trim()) { if (meta.trim()) {
try { try {
JSON.parse(meta); JSON.parse(meta);
} catch (e) { } catch {
newErrors.meta = "Invalid JSON format"; newErrors.meta = "Invalid JSON format";
} }
} }
@@ -179,12 +207,14 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
onSuccess(); 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({ setErrors({
submit: submit: axiosErr?.response?.data?.message || errMsg,
error.response?.data?.message ||
error.message ||
"Failed to save pack",
}); });
} }
}; };
@@ -203,7 +233,7 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
}; };
const insertSchemaExample = (type: "api" | "database" | "webhook") => { const insertSchemaExample = (type: "api" | "database" | "webhook") => {
let example: Record<string, any>; let example: FlatSchema;
switch (type) { switch (type) {
case "api": case "api":
example = { example = {
@@ -280,8 +310,8 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
setConfSchema(example); setConfSchema(example);
// Immediately sync config values with schema defaults // Immediately sync config values with schema defaults
const syncedConfig: Record<string, any> = {}; const syncedConfig: Record<string, JsonValue> = {};
Object.entries(example).forEach(([key, propDef]: [string, any]) => { Object.entries(example).forEach(([key, propDef]) => {
if (propDef.default !== undefined) { if (propDef.default !== undefined) {
syncedConfig[key] = propDef.default; syncedConfig[key] = propDef.default;
} }

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { usePacks } from "@/hooks/usePacks"; import { usePacks } from "@/hooks/usePacks";
import { useTriggers, useTrigger } from "@/hooks/useTriggers"; import { useTriggers, useTrigger } from "@/hooks/useTriggers";
@@ -9,9 +9,17 @@ import ParamSchemaForm, {
type ParamSchema, type ParamSchema,
} from "@/components/common/ParamSchemaForm"; } from "@/components/common/ParamSchemaForm";
import SearchableSelect from "@/components/common/SearchableSelect"; 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"; import { labelToRef, extractLocalRef, combineRefs } from "@/lib/format-utils";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JsonValue = any;
interface RuleFormProps { interface RuleFormProps {
rule?: RuleResponse; rule?: RuleResponse;
onSuccess?: () => void; onSuccess?: () => void;
@@ -35,11 +43,11 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
rule?.conditions ? JSON.stringify(rule.conditions, null, 2) : "", rule?.conditions ? JSON.stringify(rule.conditions, null, 2) : "",
); );
const [triggerParameters, setTriggerParameters] = useState< const [triggerParameters, setTriggerParameters] = useState<
Record<string, any> Record<string, JsonValue>
>(rule?.trigger_params || {}); >(rule?.trigger_params || {});
const [actionParameters, setActionParameters] = useState<Record<string, any>>( const [actionParameters, setActionParameters] = useState<
rule?.action_params || {}, Record<string, JsonValue>
); >(rule?.action_params || {});
const [enabled, setEnabled] = useState(rule?.enabled ?? true); const [enabled, setEnabled] = useState(rule?.enabled ?? true);
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const [triggerParamErrors, setTriggerParamErrors] = useState< const [triggerParamErrors, setTriggerParamErrors] = useState<
@@ -51,7 +59,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
// Data fetching // Data fetching
const { data: packsData } = usePacks({ pageSize: 1000 }); 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); 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 // Get selected trigger and action refs for detail fetching
const selectedTriggerSummary = triggers.find((t) => t.id === triggerId); 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 // Fetch full trigger details (including param_schema) when a trigger is selected
const { data: triggerDetailsData } = useTrigger( const { data: triggerDetailsData } = useTrigger(
@@ -81,15 +91,18 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
// Extract param schemas from full details // Extract param schemas from full details
const triggerParamSchema: ParamSchema = const triggerParamSchema: ParamSchema =
((selectedTrigger as any)?.param_schema as ParamSchema) || {}; ((selectedTrigger as TriggerResponse | undefined)
?.param_schema as ParamSchema) || {};
const actionParamSchema: ParamSchema = const actionParamSchema: ParamSchema =
((selectedAction as any)?.param_schema as ParamSchema) || {}; ((selectedAction as ActionResponse | undefined)
?.param_schema as ParamSchema) || {};
// Mutations // Mutations
const createRule = useCreateRule(); const createRule = useCreateRule();
const updateRule = useUpdateRule(); const updateRule = useUpdateRule();
// Reset triggers, actions, and parameters when pack changes // Reset triggers, actions, and parameters when pack changes
/* eslint-disable react-hooks/set-state-in-effect -- intentional dependent-state reset */
useEffect(() => { useEffect(() => {
if (!isEditing) { if (!isEditing) {
setTriggerId(0); setTriggerId(0);
@@ -98,20 +111,25 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
setActionParameters({}); setActionParameters({});
} }
}, [packId, isEditing]); }, [packId, isEditing]);
/* eslint-enable react-hooks/set-state-in-effect */
// Reset trigger parameters when trigger changes // Reset trigger parameters when trigger changes
/* eslint-disable react-hooks/set-state-in-effect -- intentional dependent-state reset */
useEffect(() => { useEffect(() => {
if (!isEditing) { if (!isEditing) {
setTriggerParameters({}); setTriggerParameters({});
} }
}, [triggerId, isEditing]); }, [triggerId, isEditing]);
/* eslint-enable react-hooks/set-state-in-effect */
// Reset action parameters when action changes // Reset action parameters when action changes
/* eslint-disable react-hooks/set-state-in-effect -- intentional dependent-state reset */
useEffect(() => { useEffect(() => {
if (!isEditing) { if (!isEditing) {
setActionParameters({}); setActionParameters({});
} }
}, [actionId, isEditing]); }, [actionId, isEditing]);
/* eslint-enable react-hooks/set-state-in-effect */
const validateForm = (): boolean => { const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
@@ -144,7 +162,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
if (conditions.trim()) { if (conditions.trim()) {
try { try {
JSON.parse(conditions); JSON.parse(conditions);
} catch (e) { } catch {
newErrors.conditions = "Invalid JSON format"; 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 // Combine pack ref and local ref to create full ref
const fullRef = combineRefs(selectedPackData?.ref || "", localRef.trim()); const fullRef = combineRefs(selectedPackData?.ref || "", localRef.trim());
const formData: any = { const formData: Record<string, JsonValue> = {
pack_ref: selectedPackData?.ref || "", pack_ref: selectedPackData?.ref || "",
ref: fullRef, ref: fullRef,
label: label.trim(), label: label.trim(),
@@ -267,7 +285,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
id="pack" id="pack"
value={packId} value={packId}
onChange={(v) => setPackId(Number(v))} onChange={(v) => setPackId(Number(v))}
options={packs.map((pack: any) => ({ options={packs.map((pack) => ({
value: pack.id, value: pack.id,
label: `${pack.label} (${pack.version})`, label: `${pack.label} (${pack.version})`,
}))} }))}
@@ -408,7 +426,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
id="trigger" id="trigger"
value={triggerId} value={triggerId}
onChange={(v) => setTriggerId(Number(v))} onChange={(v) => setTriggerId(Number(v))}
options={triggers.map((trigger: any) => ({ options={triggers.map((trigger) => ({
value: trigger.id, value: trigger.id,
label: `${trigger.ref} - ${trigger.label}`, label: `${trigger.ref} - ${trigger.label}`,
}))} }))}
@@ -494,7 +512,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
id="action" id="action"
value={actionId} value={actionId}
onChange={(v) => setActionId(Number(v))} onChange={(v) => setActionId(Number(v))}
options={actions.map((action: any) => ({ options={actions.map((action) => ({
value: action.id, value: action.id,
label: `${action.ref} - ${action.label}`, label: `${action.ref} - ${action.label}`,
}))} }))}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { usePacks } from "@/hooks/usePacks"; import { usePacks } from "@/hooks/usePacks";
@@ -11,9 +11,14 @@ import {
import SchemaBuilder from "@/components/common/SchemaBuilder"; import SchemaBuilder from "@/components/common/SchemaBuilder";
import SearchableSelect from "@/components/common/SearchableSelect"; import SearchableSelect from "@/components/common/SearchableSelect";
import { WebhooksService } from "@/api"; 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<string, any>;
interface TriggerFormProps { interface TriggerFormProps {
initialData?: any; initialData?: TriggerResponse;
isEditing?: boolean; isEditing?: boolean;
} }
@@ -31,14 +36,14 @@ export default function TriggerForm({
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [webhookEnabled, setWebhookEnabled] = useState(false); const [webhookEnabled, setWebhookEnabled] = useState(false);
const [enabled, setEnabled] = useState(true); const [enabled, setEnabled] = useState(true);
const [paramSchema, setParamSchema] = useState<Record<string, any>>({}); const [paramSchema, setParamSchema] = useState<FlatSchema>({});
const [outSchema, setOutSchema] = useState<Record<string, any>>({}); const [outSchema, setOutSchema] = useState<FlatSchema>({});
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
// Fetch packs // Fetch packs
const { data: packsData } = usePacks({ page: 1, pageSize: 100 }); const { data: packsData } = usePacks({ page: 1, pageSize: 100 });
const packs = packsData?.data || []; const packs = useMemo(() => packsData?.data || [], [packsData?.data]);
const selectedPack = packs.find((p: any) => p.id === packId); const selectedPack = packs.find((p: PackSummary) => p.id === packId);
// Mutations // Mutations
const createTrigger = useCreateTrigger(); const createTrigger = useCreateTrigger();
@@ -56,7 +61,9 @@ export default function TriggerForm({
if (isEditing) { if (isEditing) {
// Find pack by pack_ref // 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) { if (pack) {
setPackId(pack.id); setPackId(pack.id);
} }
@@ -96,7 +103,7 @@ export default function TriggerForm({
} }
try { try {
const selectedPackData = packs.find((p: any) => p.id === packId); const selectedPackData = packs.find((p: PackSummary) => p.id === packId);
if (!selectedPackData) { if (!selectedPackData) {
throw new Error("Selected pack not found"); throw new Error("Selected pack not found");
} }
@@ -166,13 +173,15 @@ export default function TriggerForm({
} }
navigate("/triggers"); navigate("/triggers");
} catch (error: any) { } catch (error: unknown) {
console.error("Error submitting trigger:", error); 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({ setErrors({
submit: submit: axiosErr?.response?.data?.message || errMsg,
error.response?.data?.message ||
error.message ||
"Failed to save trigger",
}); });
} }
}; };
@@ -211,7 +220,7 @@ export default function TriggerForm({
id="pack" id="pack"
value={packId} value={packId}
onChange={(v) => setPackId(Number(v))} onChange={(v) => setPackId(Number(v))}
options={packs.map((pack: any) => ({ options={packs.map((pack: PackSummary) => ({
value: pack.id, value: pack.id,
label: `${pack.label} (${pack.version})`, label: `${pack.label} (${pack.version})`,
}))} }))}

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components -- exporting useAuth hook alongside AuthProvider is standard React pattern */
import { import {
createContext, createContext,
useContext, useContext,

View File

@@ -1,7 +1,9 @@
/* eslint-disable react-refresh/only-export-components -- exporting hooks alongside WebSocketProvider is standard React pattern */
import { import {
createContext, createContext,
useContext, useContext,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
useCallback, useCallback,
@@ -306,36 +308,33 @@ export function useEntityNotifications(
) { ) {
const { connected, subscribe, unsubscribe } = useWebSocketContext(); 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); const handlerRef = useRef(onNotification);
// Stable reference to the wrapper function (created once, never changes)
const stableHandlerRef = useRef<NotificationHandler | null>(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) // Update ref when handler changes (but don't cause re-subscription)
useEffect(() => { useEffect(() => {
handlerRef.current = onNotification; handlerRef.current = onNotification;
}, [onNotification]); }, [onNotification]);
// Create a stable wrapper function once via useMemo (no ref access during render)
const stableHandler = useMemo<NotificationHandler>(
() => (notification) => {
handlerRef.current(notification);
},
[], // intentionally empty — handlerRef is stable
);
useEffect(() => { useEffect(() => {
if (!connected || !enabled) return; if (!connected || !enabled) return;
const filter = `entity_type:${entityType}`; const filter = `entity_type:${entityType}`;
const stableHandler = stableHandlerRef.current!;
subscribe(filter, stableHandler); subscribe(filter, stableHandler);
return () => { return () => {
unsubscribe(filter, stableHandler); unsubscribe(filter, stableHandler);
}; };
}, [connected, enabled, entityType, subscribe, unsubscribe]); }, [connected, enabled, entityType, subscribe, unsubscribe, stableHandler]);
return { connected }; return { connected };
} }

View File

@@ -16,6 +16,26 @@ interface UseArtifactStreamOptions {
enabled?: boolean; 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. * Hook to subscribe to real-time artifact updates via WebSocket.
* *
@@ -42,8 +62,8 @@ export function useArtifactStream(options: UseArtifactStreamOptions = {}) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const handleNotification = useCallback( const handleNotification = useCallback(
(notification: any) => { (notification: ArtifactNotification) => {
const payload = notification.payload as any; const payload = notification.payload;
// If we're filtering by execution ID, only process matching artifacts // If we're filtering by execution ID, only process matching artifacts
if (executionId && payload?.execution !== executionId) { if (executionId && payload?.execution !== executionId) {
@@ -71,11 +91,11 @@ export function useArtifactStream(options: UseArtifactStreamOptions = {}) {
if (payload?.type === "progress" && payload?.progress_percent != null) { if (payload?.type === "progress" && payload?.progress_percent != null) {
queryClient.setQueryData( queryClient.setQueryData(
["artifact_progress", artifactExecution], ["artifact_progress", artifactExecution],
(old: any) => ({ (old: ArtifactProgressSummary | undefined) => ({
...old, ...old,
artifactId, artifactId,
name: payload.name, name: payload.name ?? null,
percent: payload.progress_percent, percent: payload.progress_percent as number,
message: payload.progress_message ?? null, message: payload.progress_message ?? null,
entries: payload.progress_entries ?? 0, entries: payload.progress_entries ?? 0,
timestamp: notification.timestamp, timestamp: notification.timestamp,

View File

@@ -1,6 +1,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useEntityNotifications } from "@/contexts/WebSocketContext"; import { useEntityNotifications } from "@/contexts/WebSocketContext";
import type { EnforcementSummary } from "@/api";
interface UseEnforcementStreamOptions { interface UseEnforcementStreamOptions {
/** /**
@@ -16,11 +17,47 @@ interface UseEnforcementStreamOptions {
enabled?: boolean; enabled?: boolean;
} }
/** Shape of data coming from WebSocket notifications for enforcements */
interface EnforcementNotification {
entity_id: number;
entity_type: string;
notification_type: string;
payload: Partial<EnforcementSummary> & Record<string, unknown>;
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 * Check if an enforcement matches the given query parameters
* Only checks fields that are reliably present in WebSocket payloads * Only checks fields that are reliably present in WebSocket payloads
*/ */
function enforcementMatchesParams(enforcement: any, params: any): boolean { function enforcementMatchesParams(
enforcement: Partial<EnforcementSummary>,
params: EnforcementQueryParams | undefined,
): boolean {
if (!params) return true; if (!params) return true;
// Check status filter // 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 * Check if query params include filters not present in WebSocket payloads
*/ */
function hasUnsupportedFilters(params: any): boolean { function hasUnsupportedFilters(
if (!params) return false; // eslint-disable-next-line @typescript-eslint/no-unused-vars
_params: EnforcementQueryParams | undefined,
): boolean {
// Currently all enforcement filters are supported in WebSocket payloads // Currently all enforcement filters are supported in WebSocket payloads
return false; return false;
} }
@@ -81,19 +120,20 @@ export function useEnforcementStream(
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const handleNotification = useCallback( const handleNotification = useCallback(
(notification: any) => { (notification: EnforcementNotification) => {
// Filter by enforcement ID if specified // Filter by enforcement ID if specified
if (enforcementId && notification.entity_id !== enforcementId) { if (enforcementId && notification.entity_id !== enforcementId) {
return; return;
} }
// Extract enforcement data from notification payload (flat structure) // Extract enforcement data from notification payload (flat structure)
const enforcementData = notification.payload as any; const enforcementData =
notification.payload as Partial<EnforcementSummary>;
// Update specific enforcement query if it exists // Update specific enforcement query if it exists
queryClient.setQueryData( queryClient.setQueryData(
["enforcements", notification.entity_id], ["enforcements", notification.entity_id],
(old: any) => { (old: EnforcementDetailCache | undefined) => {
if (!old) return old; if (!old) return old;
return { return {
...old, ...old,
@@ -108,21 +148,24 @@ export function useEnforcementStream(
// Update enforcement list queries by modifying existing data // Update enforcement list queries by modifying existing data
// We need to iterate manually to access query keys for filtering // We need to iterate manually to access query keys for filtering
const queries = queryClient const queries = queryClient
.getQueriesData({ queryKey: ["enforcements"], exact: false }) .getQueriesData<EnforcementListCache>({
.filter(([, data]) => data && Array.isArray((data as any)?.data)); queryKey: ["enforcements"],
exact: false,
})
.filter(([, data]) => data && Array.isArray(data?.data));
queries.forEach(([queryKey, oldData]) => { queries.forEach(([queryKey, oldData]) => {
// Extract query params from the query key (format: ["enforcements", params]) // 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 // Check if enforcement already exists in the list
const existingIndex = old.data.findIndex( 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) { if (existingIndex >= 0) {
// Always update existing enforcement in the list // Always update existing enforcement in the list
updatedData = [...old.data]; updatedData = [...old.data];
@@ -144,7 +187,10 @@ export function useEnforcementStream(
// Only add new enforcement if it matches the query parameters // Only add new enforcement if it matches the query parameters
if (enforcementMatchesParams(enforcementData, queryParams)) { if (enforcementMatchesParams(enforcementData, queryParams)) {
// Add to beginning and cap at 50 items to prevent performance issues // 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 { } else {
// Don't modify the list if the new enforcement doesn't match the query // Don't modify the list if the new enforcement doesn't match the query
return; return;

View File

@@ -1,6 +1,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useEntityNotifications } from "@/contexts/WebSocketContext"; import { useEntityNotifications } from "@/contexts/WebSocketContext";
import type { ExecutionSummary } from "@/api";
interface UseExecutionStreamOptions { interface UseExecutionStreamOptions {
/** /**
@@ -28,11 +29,57 @@ const NOTIFICATION_META_FIELDS = [
"action_id", "action_id",
] as const; ] 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<ExecutionSummary> {
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 * Strip notification-only metadata fields from the payload so cached data
* matches the shape returned by the API (ExecutionSummary / ExecutionResponse). * matches the shape returned by the API (ExecutionSummary / ExecutionResponse).
*/ */
function stripNotificationMeta(payload: any): any { function stripNotificationMeta(
payload: ExecutionNotificationPayload,
): Partial<ExecutionSummary> {
if (!payload || typeof payload !== "object") return payload; if (!payload || typeof payload !== "object") return payload;
const cleaned = { ...payload }; const cleaned = { ...payload };
for (const key of NOTIFICATION_META_FIELDS) { 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. * Check if an execution matches the given query parameters.
* Only checks fields that are reliably present in WebSocket payloads. * Only checks fields that are reliably present in WebSocket payloads.
*/ */
function executionMatchesParams(execution: any, params: any): boolean { function executionMatchesParams(
execution: Partial<ExecutionSummary> & { parent?: number | null },
params: ExecutionQueryParams | undefined,
): boolean {
if (!params) return true; if (!params) return true;
// Check topLevelOnly filter — child executions (with a parent) must not // 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. * 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; if (!params) return false;
return !!(params.ruleRef || params.triggerRef); return !!(params.ruleRef || params.triggerRef);
} }
@@ -123,7 +175,7 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const handleNotification = useCallback( const handleNotification = useCallback(
(notification: any) => { (notification: ExecutionNotification) => {
// Filter by execution ID if specified // Filter by execution ID if specified
if (executionId && notification.entity_id !== executionId) { if (executionId && notification.entity_id !== executionId) {
return; return;
@@ -131,14 +183,14 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) {
// Extract execution data from notification payload (flat structure). // Extract execution data from notification payload (flat structure).
// Keep raw payload for old_status inspection, but use cleaned data for cache. // 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 oldStatus: string | undefined = rawPayload?.old_status;
const executionData = stripNotificationMeta(rawPayload); const executionData = stripNotificationMeta(rawPayload);
// Update specific execution query if it exists // Update specific execution query if it exists
queryClient.setQueryData( queryClient.setQueryData(
["executions", notification.entity_id], ["executions", notification.entity_id],
(old: any) => { (old: ExecutionDetailCache | undefined) => {
if (!old) return old; if (!old) return old;
return { return {
...old, ...old,
@@ -153,35 +205,38 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) {
// Update execution list queries by modifying existing data. // Update execution list queries by modifying existing data.
// We need to iterate manually to access query keys for filtering. // We need to iterate manually to access query keys for filtering.
const queries = queryClient const queries = queryClient
.getQueriesData({ queryKey: ["executions"], exact: false }) .getQueriesData<ExecutionListCache>({
.filter(([, data]) => data && Array.isArray((data as any)?.data)); queryKey: ["executions"],
exact: false,
})
.filter(([, data]) => data && Array.isArray(data?.data));
queries.forEach(([queryKey, oldData]) => { queries.forEach(([queryKey, oldData]) => {
// Extract query params from the query key (format: ["executions", params]) // 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 // Child execution queries (keyed by { parent: id }) fetch all pages
// and must not be capped — the timeline DAG needs every child. // 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 // Check if execution already exists in the list
const existingIndex = old.data.findIndex( 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 // Merge the updated fields to determine if the execution matches the query
const mergedExecution = const mergedExecution =
existingIndex >= 0 existingIndex >= 0
? { ...old.data[existingIndex], ...executionData } ? { ...old.data[existingIndex], ...executionData }
: executionData; : (executionData as ExecutionSummary);
const matchesQuery = executionMatchesParams( const matchesQuery = executionMatchesParams(
mergedExecution, mergedExecution,
queryParams, queryParams,
); );
let updatedData; let updatedData: ExecutionSummary[];
let totalItemsDelta = 0; let totalItemsDelta = 0;
if (existingIndex >= 0) { if (existingIndex >= 0) {
@@ -189,12 +244,13 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) {
if (matchesQuery) { if (matchesQuery) {
// Still matches — update in place, no total_items change // Still matches — update in place, no total_items change
updatedData = [...old.data]; updatedData = [...old.data];
updatedData[existingIndex] = mergedExecution; updatedData[existingIndex] = {
...updatedData[existingIndex],
...executionData,
};
} else { } else {
// No longer matches the query filter — remove it // No longer matches the query filter — remove it
updatedData = old.data.filter( updatedData = old.data.filter((_, i) => i !== existingIndex);
(_: any, i: number) => i !== existingIndex,
);
totalItemsDelta = -1; totalItemsDelta = -1;
} }
} else { } else {
@@ -229,8 +285,8 @@ export function useExecutionStream(options: UseExecutionStreamOptions = {}) {
return; return;
} }
updatedData = isChildQuery updatedData = isChildQuery
? [...old.data, executionData] ? [...old.data, executionData as ExecutionSummary]
: [executionData, ...old.data].slice(0, 50); : [executionData as ExecutionSummary, ...old.data].slice(0, 50);
totalItemsDelta = 1; totalItemsDelta = 1;
} else { } else {
// No boundary crossing: either both match (execution was // 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); // Add to the list. Child queries keep all items (no cap);
// other lists cap at 50 to prevent unbounded growth. // other lists cap at 50 to prevent unbounded growth.
updatedData = isChildQuery updatedData = isChildQuery
? [...old.data, executionData] ? [...old.data, executionData as ExecutionSummary]
: [executionData, ...old.data].slice(0, 50); : [executionData as ExecutionSummary, ...old.data].slice(0, 50);
totalItemsDelta = 1; totalItemsDelta = 1;
} else { } else {
return; return;

View File

@@ -1,5 +1,6 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { SensorsService } from "@/api"; import { SensorsService } from "@/api";
import type { CreateSensorRequest, UpdateSensorRequest } from "@/api";
interface SensorsQueryParams { interface SensorsQueryParams {
page?: number; page?: number;
@@ -69,7 +70,7 @@ export function useCreateSensor() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (data: any) => { mutationFn: async (data: CreateSensorRequest) => {
return await SensorsService.createSensor({ requestBody: data }); return await SensorsService.createSensor({ requestBody: data });
}, },
onSuccess: () => { onSuccess: () => {
@@ -83,7 +84,13 @@ export function useUpdateSensor() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ 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 }); return await SensorsService.updateSensor({ ref, requestBody: data });
}, },
onSuccess: (_, variables) => { onSuccess: (_, variables) => {

View File

@@ -1,5 +1,6 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { TriggersService } from "@/api"; import { TriggersService } from "@/api";
import type { CreateTriggerRequest, UpdateTriggerRequest } from "@/api";
interface TriggersQueryParams { interface TriggersQueryParams {
page?: number; page?: number;
@@ -69,7 +70,7 @@ export function useCreateTrigger() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (data: any) => { mutationFn: async (data: CreateTriggerRequest) => {
return await TriggersService.createTrigger({ requestBody: data }); return await TriggersService.createTrigger({ requestBody: data });
}, },
onSuccess: () => { onSuccess: () => {
@@ -83,7 +84,13 @@ export function useUpdateTrigger() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ 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 }); return await TriggersService.updateTrigger({ ref, requestBody: data });
}, },
onSuccess: (_, variables) => { onSuccess: (_, variables) => {

View File

@@ -1,13 +1,22 @@
import { OpenAPI } from "../api"; import { OpenAPI } from "../api";
import { apiClient } from "./api-client"; import { apiClient } from "./api-client";
declare global {
interface Window {
__ATTUNE_CONFIG__?: {
API_BASE_URL: string;
WITH_CREDENTIALS: boolean;
};
}
}
// Configure the OpenAPI client // Configure the OpenAPI client
// Use empty string to make requests relative to current origin (uses Vite proxy) // Use empty string to make requests relative to current origin (uses Vite proxy)
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ""; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "";
// API configuration (silent - check window.__ATTUNE_CONFIG__ for debug info) // API configuration (silent - check window.__ATTUNE_CONFIG__ for debug info)
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
(window as any).__ATTUNE_CONFIG__ = { window.__ATTUNE_CONFIG__ = {
API_BASE_URL, API_BASE_URL,
WITH_CREDENTIALS: true, WITH_CREDENTIALS: true,
}; };

View File

@@ -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 * 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 true if token expires within threshold seconds (default 5 minutes)
return timeUntilExpiry <= thresholdSeconds; return timeUntilExpiry <= thresholdSeconds;
} catch (error) { } catch {
console.error("Failed to parse JWT:", error);
return true; return true;
} }
} }
@@ -84,8 +83,7 @@ export function isTokenExpired(token: string): boolean {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
return exp <= now; return exp <= now;
} catch (error) { } catch {
console.error("Failed to parse JWT:", error);
return true; return true;
} }
} }
@@ -114,7 +112,7 @@ async function attemptTokenRefresh(): Promise<boolean> {
} }
return true; return true;
} catch (error) { } catch {
console.error( console.error(
"Token refresh failed, clearing session and redirecting to login", "Token refresh failed, clearing session and redirecting to login",
); );
@@ -193,7 +191,9 @@ export function configureAxiosDefaults(): void {
axios.interceptors.response.use( axios.interceptors.response.use(
(response) => response, (response) => response,
async (error) => { async (error) => {
const originalRequest = error.config as any; const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
// Handle 401 Unauthorized — token expired or invalid // Handle 401 Unauthorized — token expired or invalid
if (error.response?.status === 401 && !originalRequest._retry) { if (error.response?.status === 401 && !originalRequest._retry) {
@@ -215,13 +215,12 @@ export function configureAxiosDefaults(): void {
// Handle 403 Forbidden — valid token but insufficient permissions // Handle 403 Forbidden — valid token but insufficient permissions
if (error.response?.status === 403) { if (error.response?.status === 403) {
const enhancedError = error as any; error.isAuthorizationError = true;
enhancedError.isAuthorizationError = true;
console.warn( console.warn(
"Access forbidden - insufficient permissions for this resource", "Access forbidden - insufficient permissions for this resource",
); );
return Promise.reject(enhancedError); return Promise.reject(error);
} }
return Promise.reject(error); return Promise.reject(error);

View File

@@ -1,9 +1,15 @@
import { QueryClient } from "@tanstack/react-query"; import { QueryClient } from "@tanstack/react-query";
interface HttpError extends Error {
response?: {
status: number;
};
}
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
retry: (failureCount, error: any) => { retry: (failureCount, error: HttpError) => {
// Don't retry on 401 (handled by interceptor) or 403 (permission denied) // Don't retry on 401 (handled by interceptor) or 403 (permission denied)
if ( if (
error?.response?.status === 401 || error?.response?.status === 401 ||

View File

@@ -2,6 +2,8 @@ import { Link, useParams, useNavigate } from "react-router-dom";
import { useActions, useAction, useDeleteAction } from "@/hooks/useActions"; import { useActions, useAction, useDeleteAction } from "@/hooks/useActions";
import { useExecutions } from "@/hooks/useExecutions"; import { useExecutions } from "@/hooks/useExecutions";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import type { ActionSummary, ExecutionSummary } from "@/api";
import type { ParamSchemaProperty } from "@/components/common/ParamSchemaForm";
import { import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
@@ -20,7 +22,7 @@ export default function ActionsPage() {
const { ref } = useParams<{ ref?: string }>(); const { ref } = useParams<{ ref?: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { data, isLoading, error } = useActions(); const { data, isLoading, error } = useActions();
const actions = data?.data || []; const actions = useMemo(() => data?.data || [], [data?.data]);
const [collapsedPacks, setCollapsedPacks] = useState<Set<string>>(new Set()); const [collapsedPacks, setCollapsedPacks] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@@ -28,7 +30,7 @@ export default function ActionsPage() {
const filteredActions = useMemo(() => { const filteredActions = useMemo(() => {
if (!searchQuery.trim()) return actions; if (!searchQuery.trim()) return actions;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return actions.filter((action: any) => { return actions.filter((action: ActionSummary) => {
return ( return (
action.label?.toLowerCase().includes(query) || action.label?.toLowerCase().includes(query) ||
action.ref?.toLowerCase().includes(query) || action.ref?.toLowerCase().includes(query) ||
@@ -40,8 +42,8 @@ export default function ActionsPage() {
// Group filtered actions by pack // Group filtered actions by pack
const actionsByPack = useMemo(() => { const actionsByPack = useMemo(() => {
const grouped = new Map<string, any[]>(); const grouped = new Map<string, ActionSummary[]>();
filteredActions.forEach((action: any) => { filteredActions.forEach((action: ActionSummary) => {
const packRef = action.pack_ref; const packRef = action.pack_ref;
if (!grouped.has(packRef)) { if (!grouped.has(packRef)) {
grouped.set(packRef, []); grouped.set(packRef, []);
@@ -176,7 +178,7 @@ export default function ActionsPage() {
{/* Actions List */} {/* Actions List */}
{!isCollapsed && ( {!isCollapsed && (
<div className="p-1"> <div className="p-1">
{packActions.map((action: any) => ( {packActions.map((action: ActionSummary) => (
<Link <Link
key={action.id} key={action.id}
to={`/actions/${action.ref}`} to={`/actions/${action.ref}`}
@@ -448,54 +450,58 @@ function ActionDetail({ actionRef }: { actionRef: string }) {
Parameters Parameters
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
{paramEntries.map(([key, param]: [string, any]) => ( {paramEntries.map(
<div ([key, param]: [string, ParamSchemaProperty]) => (
key={key} <div
className="border border-gray-200 rounded p-3" key={key}
> className="border border-gray-200 rounded p-3"
<div className="flex items-start justify-between"> >
<div className="flex-1"> <div className="flex items-start justify-between">
<div className="flex items-center gap-2"> <div className="flex-1">
<span className="font-mono font-semibold text-sm"> <div className="flex items-center gap-2">
{key} <span className="font-mono font-semibold text-sm">
</span> {key}
{param?.required && (
<span className="text-xs px-2 py-0.5 bg-red-100 text-red-700 rounded">
Required
</span> </span>
)} {param?.required && (
{param?.secret && ( <span className="text-xs px-2 py-0.5 bg-red-100 text-red-700 rounded">
<span className="text-xs px-2 py-0.5 bg-yellow-100 text-yellow-700 rounded"> Required
Secret </span>
)}
{param?.secret && (
<span className="text-xs px-2 py-0.5 bg-yellow-100 text-yellow-700 rounded">
Secret
</span>
)}
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-700 rounded">
{param?.type || "any"}
</span> </span>
</div>
{param?.description && (
<p className="text-sm text-gray-600 mt-1">
{param.description}
</p>
)}
{param?.default !== undefined && (
<p className="text-xs text-gray-500 mt-1">
Default:{" "}
<code className="bg-gray-100 px-1 rounded">
{JSON.stringify(param.default)}
</code>
</p>
)}
{param?.enum && param.enum.length > 0 && (
<p className="text-xs text-gray-500 mt-1">
Values:{" "}
{param.enum
.map((v: string) => `"${v}"`)
.join(", ")}
</p>
)} )}
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-700 rounded">
{param?.type || "any"}
</span>
</div> </div>
{param?.description && (
<p className="text-sm text-gray-600 mt-1">
{param.description}
</p>
)}
{param?.default !== undefined && (
<p className="text-xs text-gray-500 mt-1">
Default:{" "}
<code className="bg-gray-100 px-1 rounded">
{JSON.stringify(param.default)}
</code>
</p>
)}
{param?.enum && param.enum.length > 0 && (
<p className="text-xs text-gray-500 mt-1">
Values:{" "}
{param.enum.map((v: any) => `"${v}"`).join(", ")}
</p>
)}
</div> </div>
</div> </div>
</div> ),
))} )}
</div> </div>
</div> </div>
)} )}
@@ -532,7 +538,7 @@ function ActionDetail({ actionRef }: { actionRef: string }) {
</p> </p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{executions.map((execution: any) => ( {executions.map((execution: ExecutionSummary) => (
<Link <Link
key={execution.id} key={execution.id}
to={`/executions/${execution.id}`} to={`/executions/${execution.id}`}

View File

@@ -2,6 +2,22 @@ import React, { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
interface LocationState {
from?: {
pathname: string;
};
}
interface LoginError {
response?: {
status: number;
data?: {
message?: string;
};
};
message?: string;
}
export default function LoginPage() { export default function LoginPage() {
const [login, setLogin] = useState(""); const [login, setLogin] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -13,7 +29,8 @@ export default function LoginPage() {
// Check for redirect path from session storage (set by axios interceptor on 401) // Check for redirect path from session storage (set by axios interceptor on 401)
const redirectPath = sessionStorage.getItem("redirect_after_login"); const redirectPath = sessionStorage.getItem("redirect_after_login");
const from = redirectPath || (location.state as any)?.from?.pathname || "/"; const from =
redirectPath || (location.state as LocationState)?.from?.pathname || "/";
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -27,16 +44,16 @@ export default function LoginPage() {
sessionStorage.removeItem("redirect_after_login"); sessionStorage.removeItem("redirect_after_login");
navigate(from, { replace: true }); navigate(from, { replace: true });
} catch (err: any) { } catch (err: unknown) {
console.error("Login error:", err); const loginErr = err as LoginError;
console.error("Full error object:", JSON.stringify(err, null, 2)); console.error("Login error:", loginErr);
if (err.response) { if (loginErr.response) {
console.error("Response status:", err.response.status); console.error("Response status:", loginErr.response.status);
console.error("Response data:", err.response.data); console.error("Response data:", loginErr.response.data);
} }
const errorMessage = const errorMessage =
err.response?.data?.message || loginErr.response?.data?.message ||
err.message || loginErr.message ||
"Login failed. Please check your credentials."; "Login failed. Please check your credentials.";
setError(errorMessage); setError(errorMessage);
// Don't navigate on error - stay on login page // Don't navigate on error - stay on login page

View File

@@ -2,6 +2,7 @@ import { Link, useSearchParams } from "react-router-dom";
import { useEnforcements } from "@/hooks/useEvents"; import { useEnforcements } from "@/hooks/useEvents";
import { useEnforcementStream } from "@/hooks/useEnforcementStream"; import { useEnforcementStream } from "@/hooks/useEnforcementStream";
import { EnforcementStatus } from "@/api"; import { EnforcementStatus } from "@/api";
import type { EnforcementSummary } from "@/api";
import { useState, useMemo, memo, useCallback, useEffect } from "react"; import { useState, useMemo, memo, useCallback, useEffect } from "react";
import { Search, X } from "lucide-react"; import { Search, X } from "lucide-react";
import MultiSelect from "@/components/common/MultiSelect"; import MultiSelect from "@/components/common/MultiSelect";
@@ -99,7 +100,7 @@ const EnforcementsResultsTable = memo(
pageSize, pageSize,
total, total,
}: { }: {
enforcements: any[]; enforcements: EnforcementSummary[];
isLoading: boolean; isLoading: boolean;
isFetching: boolean; isFetching: boolean;
error: Error | null; error: Error | null;
@@ -195,7 +196,7 @@ const EnforcementsResultsTable = memo(
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{enforcements.map((enforcement: any) => ( {enforcements.map((enforcement: EnforcementSummary) => (
<tr key={enforcement.id} className="hover:bg-gray-50"> <tr key={enforcement.id} className="hover:bg-gray-50">
<td className="px-6 py-4 font-mono text-sm"> <td className="px-6 py-4 font-mono text-sm">
<Link <Link
@@ -364,7 +365,13 @@ export default function EnforcementsPage() {
// --- Build query params from debounced state --- // --- Build query params from debounced state ---
const queryParams = useMemo(() => { const queryParams = useMemo(() => {
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.trigger) params.triggerRef = debouncedFilters.trigger;
if (debouncedFilters.event) { if (debouncedFilters.event) {
const eventId = parseInt(debouncedFilters.event, 10); 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) // Filter by rule_ref (client-side since API doesn't support it)
if (debouncedFilters.rule) { if (debouncedFilters.rule) {
filtered = filtered.filter((enf: any) => filtered = filtered.filter((enf: EnforcementSummary) =>
enf.rule_ref enf.rule_ref
.toLowerCase() ?.toLowerCase()
.includes(debouncedFilters.rule.toLowerCase()), .includes(debouncedFilters.rule.toLowerCase()),
); );
} }
// If multiple statuses selected, filter client-side // If multiple statuses selected, filter client-side
if (debouncedStatuses.length > 1) { if (debouncedStatuses.length > 1) {
filtered = filtered.filter((enf: any) => filtered = filtered.filter((enf: EnforcementSummary) =>
debouncedStatuses.includes(enf.status), debouncedStatuses.includes(enf.status),
); );
} }

View File

@@ -310,7 +310,12 @@ export default function EventsPage() {
// --- Build query params from debounced state --- // --- Build query params from debounced state ---
const queryParams = useMemo(() => { 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.trigger) params.triggerRef = debouncedFilters.trigger;
if (debouncedFilters.rule) params.ruleRef = debouncedFilters.rule; if (debouncedFilters.rule) params.ruleRef = debouncedFilters.rule;
return params; return params;
@@ -320,19 +325,21 @@ export default function EventsPage() {
const handleEventNotification = useCallback( const handleEventNotification = useCallback(
(notification: Notification) => { (notification: Notification) => {
if (notification.notification_type === "event_created") { if (notification.notification_type === "event_created") {
const payload = notification.payload as any; const payload = notification.payload as Partial<EventSummary> & {
payload?: unknown;
};
const newEvent: EventSummary = { const newEvent: EventSummary = {
id: payload.id, id: payload.id ?? 0,
trigger: payload.trigger, trigger: payload.trigger ?? 0,
trigger_ref: payload.trigger_ref, trigger_ref: payload.trigger_ref ?? "",
rule: payload.rule, rule: payload.rule,
rule_ref: payload.rule_ref, rule_ref: payload.rule_ref,
source: payload.source, source: payload.source,
source_ref: payload.source_ref, source_ref: payload.source_ref,
has_payload: has_payload:
payload.payload !== null && payload.payload !== undefined, payload.payload !== null && payload.payload !== undefined,
created: payload.created, created: payload.created ?? new Date().toISOString(),
}; };
// Augment autocomplete suggestions with new refs from notification // Augment autocomplete suggestions with new refs from notification
@@ -357,44 +364,54 @@ export default function EventsPage() {
}; };
}); });
queryClient.setQueryData(["events", queryParams], (oldData: any) => { queryClient.setQueryData(
if (!oldData) return oldData; ["events", queryParams],
(
oldData:
| {
data: EventSummary[];
pagination?: { total_items?: number };
}
| undefined,
) => {
if (!oldData) return oldData;
// Check if filtering and event matches filter // Check if filtering and event matches filter
if ( if (
debouncedFilters.trigger && debouncedFilters.trigger &&
newEvent.trigger_ref !== debouncedFilters.trigger newEvent.trigger_ref !== debouncedFilters.trigger
) { ) {
return oldData; return oldData;
} }
if ( if (
debouncedFilters.rule && debouncedFilters.rule &&
newEvent.rule_ref !== debouncedFilters.rule newEvent.rule_ref !== debouncedFilters.rule
) { ) {
return oldData; return oldData;
} }
// Add new event to the beginning of the list if on first page // Add new event to the beginning of the list if on first page
if (page === 1) { 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 { return {
...oldData, ...oldData,
data: [newEvent, ...oldData.data].slice(0, pageSize),
pagination: { pagination: {
...oldData.pagination, ...oldData.pagination,
total_items: (oldData.pagination?.total_items || 0) + 1, 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], [queryClient, queryParams, page, pageSize, debouncedFilters],

View File

@@ -2,6 +2,7 @@ import { Link, useSearchParams } from "react-router-dom";
import { useExecutions } from "@/hooks/useExecutions"; import { useExecutions } from "@/hooks/useExecutions";
import { useExecutionStream } from "@/hooks/useExecutionStream"; import { useExecutionStream } from "@/hooks/useExecutionStream";
import { ExecutionStatus } from "@/api"; import { ExecutionStatus } from "@/api";
import type { ExecutionSummary } from "@/api";
import { useState, useMemo, memo, useCallback, useEffect } from "react"; import { useState, useMemo, memo, useCallback, useEffect } from "react";
import { Search, X, List, GitBranch } from "lucide-react"; import { Search, X, List, GitBranch } from "lucide-react";
import MultiSelect from "@/components/common/MultiSelect"; import MultiSelect from "@/components/common/MultiSelect";
@@ -96,7 +97,7 @@ const ExecutionsResultsTable = memo(
selectedExecutionId, selectedExecutionId,
onSelectExecution, onSelectExecution,
}: { }: {
executions: any[]; executions: ExecutionSummary[];
isLoading: boolean; isLoading: boolean;
isFetching: boolean; isFetching: boolean;
error: Error | null; error: Error | null;
@@ -191,7 +192,7 @@ const ExecutionsResultsTable = memo(
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{executions.map((exec: any) => ( {executions.map((exec: ExecutionSummary) => (
<tr <tr
key={exec.id} key={exec.id}
data-execution-id={exec.id} data-execution-id={exec.id}
@@ -361,7 +362,17 @@ export default function ExecutionsPage() {
// --- Build query params from debounced state --- // --- Build query params from debounced state ---
const queryParams = useMemo(() => { const queryParams = useMemo(() => {
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.pack) params.packName = debouncedFilters.pack;
if (debouncedFilters.rule) params.ruleRef = debouncedFilters.rule; if (debouncedFilters.rule) params.ruleRef = debouncedFilters.rule;
if (debouncedFilters.action) params.actionRef = debouncedFilters.action; 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) // Client-side filtering for multiple status selection (when > 1 selected)
const filteredExecutions = useMemo(() => { const filteredExecutions = useMemo(() => {
if (debouncedStatuses.length <= 1) return executions; if (debouncedStatuses.length <= 1) return executions;
return executions.filter((exec: any) => return executions.filter((exec: ExecutionSummary) =>
debouncedStatuses.includes(exec.status), debouncedStatuses.includes(exec.status),
); );
}, [executions, debouncedStatuses]); }, [executions, debouncedStatuses]);
@@ -500,7 +511,9 @@ export default function ExecutionsPage() {
return nextId; return nextId;
} }
const currentIndex = list.findIndex((ex: any) => ex.id === prevId); const currentIndex = list.findIndex(
(ex: ExecutionSummary) => ex.id === prevId,
);
if (currentIndex === -1) { if (currentIndex === -1) {
const nextId = list[0].id; const nextId = list[0].id;
requestAnimationFrame(() => { requestAnimationFrame(() => {

View File

@@ -22,7 +22,8 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
const createKeyMutation = useCreateKey(); const createKeyMutation = useCreateKey();
// Determine if encryption is allowed based on format // 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 // Auto-disable encryption for non-encryptable formats
const isEncrypted = canEncrypt && encrypted; const isEncrypted = canEncrypt && encrypted;
@@ -33,12 +34,13 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
// Validate ref format // Validate ref format
if (!/^[a-zA-Z0-9_.-]+$/.test(ref)) { 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; return;
} }
// Validate value based on format // Validate value based on format
let validatedValue = value;
try { try {
if (format === "json") { if (format === "json") {
JSON.parse(value); JSON.parse(value);
@@ -70,14 +72,14 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
await createKeyMutation.mutateAsync({ await createKeyMutation.mutateAsync({
ref, ref,
name, name,
value: validatedValue, value,
encrypted: isEncrypted, encrypted: isEncrypted,
owner_type: ownerType, owner_type: ownerType,
owner: owner || undefined, owner: owner || undefined,
}); });
onClose(); onClose();
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Failed to create key"); setError(err instanceof Error ? err.message : "Failed to create key");
} }
}; };
@@ -102,7 +104,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
)} )}
<div> <div>
<label htmlFor="ref" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="ref"
className="block text-sm font-medium text-gray-700 mb-1"
>
Reference <span className="text-red-500">*</span> Reference <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -120,7 +125,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
</div> </div>
<div> <div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Name <span className="text-red-500">*</span> Name <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -136,7 +144,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
</div> </div>
<div> <div>
<label htmlFor="format" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="format"
className="block text-sm font-medium text-gray-700 mb-1"
>
Value Format <span className="text-red-500">*</span> Value Format <span className="text-red-500">*</span>
</label> </label>
<select <select
@@ -160,7 +171,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
</div> </div>
<div> <div>
<label htmlFor="value" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="value"
className="block text-sm font-medium text-gray-700 mb-1"
>
Value <span className="text-red-500">*</span> Value <span className="text-red-500">*</span>
</label> </label>
<textarea <textarea
@@ -193,13 +207,19 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
disabled={!canEncrypt} disabled={!canEncrypt}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded disabled:opacity-50" className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded disabled:opacity-50"
/> />
<label htmlFor="encrypted" className="ml-2 block text-sm text-gray-900"> <label
htmlFor="encrypted"
className="ml-2 block text-sm text-gray-900"
>
Encrypt value (recommended for secrets) Encrypt value (recommended for secrets)
</label> </label>
</div> </div>
<div> <div>
<label htmlFor="ownerType" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="ownerType"
className="block text-sm font-medium text-gray-700 mb-1"
>
Scope <span className="text-red-500">*</span> Scope <span className="text-red-500">*</span>
</label> </label>
<select <select
@@ -218,7 +238,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
{ownerType !== OwnerType.SYSTEM && ( {ownerType !== OwnerType.SYSTEM && (
<div> <div>
<label htmlFor="owner" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="owner"
className="block text-sm font-medium text-gray-700 mb-1"
>
Owner Identifier Owner Identifier
</label> </label>
<input <input

View File

@@ -19,6 +19,7 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
const updateKeyMutation = useUpdateKey(); const updateKeyMutation = useUpdateKey();
/* eslint-disable react-hooks/set-state-in-effect -- sync local form state from fetched key data */
useEffect(() => { useEffect(() => {
if (key) { if (key) {
setName(key.name); setName(key.name);
@@ -26,6 +27,7 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
setEncrypted(key.encrypted); setEncrypted(key.encrypted);
} }
}, [key]); }, [key]);
/* eslint-enable react-hooks/set-state-in-effect */
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -41,8 +43,8 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
}, },
}); });
onClose(); onClose();
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Failed to update key"); setError(err instanceof Error ? err.message : "Failed to update key");
} }
}; };
@@ -77,7 +79,9 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-6 border-b border-gray-200"> <div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-2xl font-bold text-gray-900">Edit Key: {keyRef}</h2> <h2 className="text-2xl font-bold text-gray-900">
Edit Key: {keyRef}
</h2>
<button <button
onClick={onClose} onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors" className="text-gray-400 hover:text-gray-600 transition-colors"
@@ -95,7 +99,9 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 space-y-2"> <div className="bg-gray-50 border border-gray-200 rounded-lg p-4 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-500">Reference:</span> <span className="text-sm font-medium text-gray-500">
Reference:
</span>
<span className="text-sm font-mono text-gray-900">{key.ref}</span> <span className="text-sm font-mono text-gray-900">{key.ref}</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -104,14 +110,19 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
</div> </div>
{key.owner && ( {key.owner && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-500">Owner:</span> <span className="text-sm font-medium text-gray-500">
Owner:
</span>
<span className="text-sm text-gray-900">{key.owner}</span> <span className="text-sm text-gray-900">{key.owner}</span>
</div> </div>
)} )}
</div> </div>
<div> <div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Name <span className="text-red-500">*</span> Name <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -125,7 +136,10 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
</div> </div>
<div> <div>
<label htmlFor="value" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="value"
className="block text-sm font-medium text-gray-700 mb-1"
>
Value <span className="text-red-500">*</span> Value <span className="text-red-500">*</span>
</label> </label>
<div className="relative"> <div className="relative">
@@ -145,7 +159,11 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
className="absolute right-2 top-2 text-gray-400 hover:text-gray-600" className="absolute right-2 top-2 text-gray-400 hover:text-gray-600"
title={showValue ? "Hide value" : "Show value"} title={showValue ? "Hide value" : "Show value"}
> >
{showValue ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} {showValue ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</button> </button>
</div> </div>
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">
@@ -163,7 +181,10 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
onChange={(e) => setEncrypted(e.target.checked)} onChange={(e) => setEncrypted(e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/> />
<label htmlFor="encrypted" className="ml-2 block text-sm text-gray-900"> <label
htmlFor="encrypted"
className="ml-2 block text-sm text-gray-900"
>
Encrypt value (recommended for secrets) Encrypt value (recommended for secrets)
</label> </label>
</div> </div>

View File

@@ -2,6 +2,7 @@ import { Link, useParams } from "react-router-dom";
import { usePacks, usePack, useDeletePack } from "@/hooks/usePacks"; import { usePacks, usePack, useDeletePack } from "@/hooks/usePacks";
import { usePackActions } from "@/hooks/useActions"; import { usePackActions } from "@/hooks/useActions";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import type { PackSummary, PackResponse, ActionSummary } from "@/api";
import { import {
Search, Search,
X, X,
@@ -12,10 +13,13 @@ import {
Settings, Settings,
} from "lucide-react"; } from "lucide-react";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JsonValue = any;
export default function PacksPage() { export default function PacksPage() {
const { ref } = useParams<{ ref?: string }>(); const { ref } = useParams<{ ref?: string }>();
const { data, isLoading, error } = usePacks(); const { data, isLoading, error } = usePacks();
const packs = data?.data || []; const packs = useMemo(() => data?.data || [], [data?.data]);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [showPackMenu, setShowPackMenu] = useState(false); const [showPackMenu, setShowPackMenu] = useState(false);
@@ -23,7 +27,7 @@ export default function PacksPage() {
const filteredPacks = useMemo(() => { const filteredPacks = useMemo(() => {
if (!searchQuery.trim()) return packs; if (!searchQuery.trim()) return packs;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return packs.filter((pack: any) => { return packs.filter((pack: PackSummary) => {
return ( return (
pack.label?.toLowerCase().includes(query) || pack.label?.toLowerCase().includes(query) ||
pack.ref?.toLowerCase().includes(query) || pack.ref?.toLowerCase().includes(query) ||
@@ -193,7 +197,7 @@ export default function PacksPage() {
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{filteredPacks.map((pack: any) => ( {filteredPacks.map((pack: PackSummary) => (
<Link <Link
key={pack.id} key={pack.id}
to={`/packs/${pack.ref}`} to={`/packs/${pack.ref}`}
@@ -405,7 +409,7 @@ function PackDetail({ packRef }: { packRef: string }) {
Actions ({packActions.length}) Actions ({packActions.length})
</h2> </h2>
<div className="space-y-2"> <div className="space-y-2">
{packActions.map((action: any) => ( {packActions.map((action: ActionSummary) => (
<Link <Link
key={action.id} key={action.id}
to={`/actions/${action.ref}`} to={`/actions/${action.ref}`}
@@ -485,7 +489,7 @@ function PackDetail({ packRef }: { packRef: string }) {
} }
// Helper component to display pack configuration // Helper component to display pack configuration
function PackConfiguration({ pack }: { pack: any }) { function PackConfiguration({ pack }: { pack: PackResponse | undefined }) {
if (!pack) return null; if (!pack) return null;
const confSchema = pack.conf_schema || {}; const confSchema = pack.conf_schema || {};
@@ -504,57 +508,60 @@ function PackConfiguration({ pack }: { pack: any }) {
<h2 className="text-xl font-semibold">Configuration</h2> <h2 className="text-xl font-semibold">Configuration</h2>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{Object.entries(properties).map(([key, schema]: [string, any]) => { {Object.entries(properties).map(
const value = config[key]; ([key, schema]: [string, JsonValue]) => {
const hasValue = value !== undefined && value !== null; const value = config[key];
const displayValue = hasValue ? value : schema.default; const hasValue = value !== undefined && value !== null;
const isUsingDefault = !hasValue && schema.default !== undefined; const displayValue = hasValue ? value : schema.default;
const isUsingDefault = !hasValue && schema.default !== undefined;
return ( return (
<div <div
key={key} key={key}
className="border-b border-gray-200 pb-4 last:border-0 last:pb-0" className="border-b border-gray-200 pb-4 last:border-0 last:pb-0"
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<dt className="text-sm font-medium text-gray-900 font-mono"> <dt className="text-sm font-medium text-gray-900 font-mono">
{key} {key}
</dt> </dt>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700"> <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
{schema.type || "any"} {schema.type || "any"}
</span>
{isUsingDefault && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
default
</span> </span>
{isUsingDefault && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
default
</span>
)}
</div>
{schema.description && (
<p className="mt-1 text-sm text-gray-600">
{schema.description}
</p>
)} )}
</div> </div>
{schema.description && ( <dd className="ml-4 text-sm text-right">
<p className="mt-1 text-sm text-gray-600"> <ConfigValue value={displayValue} type={schema.type} />
{schema.description} </dd>
</div>
{schema.minimum !== undefined &&
schema.maximum !== undefined && (
<p className="mt-1 text-xs text-gray-500">
Range: {schema.minimum} - {schema.maximum}
</p> </p>
)} )}
</div>
<dd className="ml-4 text-sm text-right">
<ConfigValue value={displayValue} type={schema.type} />
</dd>
</div> </div>
{schema.minimum !== undefined && schema.maximum !== undefined && ( );
<p className="mt-1 text-xs text-gray-500"> },
Range: {schema.minimum} - {schema.maximum} )}
</p>
)}
</div>
);
})}
</div> </div>
</div> </div>
); );
} }
// Helper component to render config values based on type // Helper component to render config values based on type
function ConfigValue({ value, type }: { value: any; type?: string }) { function ConfigValue({ value, type }: { value: JsonValue; type?: string }) {
if (value === undefined || value === null) { if (value === undefined || value === null) {
return <span className="text-gray-400 italic">not set</span>; return <span className="text-gray-400 italic">not set</span>;
} }

View File

@@ -9,6 +9,7 @@ import {
import { useTrigger } from "@/hooks/useTriggers"; import { useTrigger } from "@/hooks/useTriggers";
import { useAction } from "@/hooks/useActions"; import { useAction } from "@/hooks/useActions";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import type { RuleSummary } from "@/api";
import { ChevronDown, ChevronRight, Search, X } from "lucide-react"; import { ChevronDown, ChevronRight, Search, X } from "lucide-react";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
import ParamSchemaDisplay, { import ParamSchemaDisplay, {
@@ -18,7 +19,7 @@ import ParamSchemaDisplay, {
export default function RulesPage() { export default function RulesPage() {
const { ref } = useParams<{ ref?: string }>(); const { ref } = useParams<{ ref?: string }>();
const { data, isLoading, error } = useRules({}); const { data, isLoading, error } = useRules({});
const rules = data?.data || []; const rules = useMemo(() => data?.data || [], [data?.data]);
const [collapsedPacks, setCollapsedPacks] = useState<Set<string>>(new Set()); const [collapsedPacks, setCollapsedPacks] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@@ -26,7 +27,7 @@ export default function RulesPage() {
const filteredRules = useMemo(() => { const filteredRules = useMemo(() => {
if (!searchQuery.trim()) return rules; if (!searchQuery.trim()) return rules;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return rules.filter((rule: any) => { return rules.filter((rule: RuleSummary) => {
return ( return (
rule.label?.toLowerCase().includes(query) || rule.label?.toLowerCase().includes(query) ||
rule.ref?.toLowerCase().includes(query) || rule.ref?.toLowerCase().includes(query) ||
@@ -40,8 +41,8 @@ export default function RulesPage() {
// Group filtered rules by pack // Group filtered rules by pack
const rulesByPack = useMemo(() => { const rulesByPack = useMemo(() => {
const grouped = new Map<string, any[]>(); const grouped = new Map<string, RuleSummary[]>();
filteredRules.forEach((rule: any) => { filteredRules.forEach((rule: RuleSummary) => {
const packRef = rule.pack_ref || "unknown"; const packRef = rule.pack_ref || "unknown";
if (!grouped.has(packRef)) { if (!grouped.has(packRef)) {
grouped.set(packRef, []); grouped.set(packRef, []);
@@ -179,7 +180,7 @@ export default function RulesPage() {
{/* Rules List */} {/* Rules List */}
{!isCollapsed && ( {!isCollapsed && (
<div className="p-1"> <div className="p-1">
{packRules.map((rule: any) => ( {packRules.map((rule: RuleSummary) => (
<Link <Link
key={rule.id} key={rule.id}
to={`/rules/${rule.ref}`} to={`/rules/${rule.ref}`}
@@ -269,9 +270,9 @@ function RuleDetail({ ruleRef }: { ruleRef: string }) {
const { data: actionData } = useAction(rule?.data?.action_ref || ""); const { data: actionData } = useAction(rule?.data?.action_ref || "");
const triggerParamSchema: ParamSchema = const triggerParamSchema: ParamSchema =
(triggerData?.data as any)?.param_schema || {}; (triggerData?.data as { param_schema?: ParamSchema })?.param_schema || {};
const actionParamSchema: ParamSchema = const actionParamSchema: ParamSchema =
(actionData?.data as any)?.param_schema || {}; (actionData?.data as { param_schema?: ParamSchema })?.param_schema || {};
const handleToggleEnabled = async () => { const handleToggleEnabled = async () => {
if (!rule?.data) return; if (!rule?.data) return;

Some files were not shown because too many files have changed in this diff Show More