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
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:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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";
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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> = {};
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`,
|
||||||
}))}
|
}))}
|
||||||
|
|||||||
@@ -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})`,
|
||||||
}))}
|
}))}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 ||
|
||||||
|
|||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user