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

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

View File

@@ -550,29 +550,34 @@ export default function AnalyticsDashboard({
hours,
onHoursChange,
}: AnalyticsDashboardProps) {
// Extract sub-properties so useMemo deps match what the React Compiler infers
const executionThroughput = data?.execution_throughput;
const eventVolume = data?.event_volume;
const enforcementVolume = data?.enforcement_volume;
const executionBuckets = useMemo(() => {
if (!data?.execution_throughput) return [];
const agg = aggregateByBucket(data.execution_throughput);
if (!executionThroughput) return [];
const agg = aggregateByBucket(executionThroughput);
return Array.from(agg.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([bucket, v]) => ({ bucket, value: v.total }));
}, [data?.execution_throughput]);
}, [executionThroughput]);
const eventBuckets = useMemo(() => {
if (!data?.event_volume) return [];
const agg = aggregateByBucket(data.event_volume);
if (!eventVolume) return [];
const agg = aggregateByBucket(eventVolume);
return Array.from(agg.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([bucket, v]) => ({ bucket, value: v.total }));
}, [data?.event_volume]);
}, [eventVolume]);
const enforcementBuckets = useMemo(() => {
if (!data?.enforcement_volume) return [];
const agg = aggregateByBucket(data.enforcement_volume);
if (!enforcementVolume) return [];
const agg = aggregateByBucket(enforcementVolume);
return Array.from(agg.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([bucket, v]) => ({ bucket, value: v.total }));
}, [data?.enforcement_volume]);
}, [enforcementVolume]);
const totalExecutions = useMemo(
() => executionBuckets.reduce((s, b) => s + b.value, 0),

View File

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

View File

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

View File

@@ -8,9 +8,13 @@ import type { ParamSchema } from "./ParamSchemaForm";
export type { ParamSchema };
import { extractProperties } from "./ParamSchemaForm";
/** A JSON-compatible value that can appear in display data */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JsonValue = any;
interface ParamSchemaDisplayProps {
schema: ParamSchema;
values: Record<string, any>;
values: Record<string, JsonValue>;
className?: string;
emptyMessage?: string;
}
@@ -53,7 +57,7 @@ export default function ParamSchemaDisplay({
* Returns both the formatted value and whether it should be displayed inline
*/
const formatValue = (
value: any,
value: JsonValue,
type?: string,
): { element: React.JSX.Element; isInline: boolean } => {
if (value === undefined || value === null) {

View File

@@ -1,5 +1,10 @@
/* eslint-disable react-refresh/only-export-components -- extractProperties and validateParamSchema are shared utilities co-located with the form component */
import { useState, useEffect } from "react";
/** A JSON-compatible value that can appear in form data */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JsonValue = any;
/**
* StackStorm-style parameter schema format.
* Parameters are defined as a flat map of parameter name to definition,
@@ -14,7 +19,7 @@ import { useState, useEffect } from "react";
export interface ParamSchemaProperty {
type?: "string" | "number" | "integer" | "boolean" | "array" | "object";
description?: string;
default?: any;
default?: JsonValue;
enum?: string[];
minimum?: number;
maximum?: number;
@@ -23,7 +28,7 @@ export interface ParamSchemaProperty {
secret?: boolean;
required?: boolean;
position?: number;
items?: any;
items?: Record<string, unknown>;
}
export interface ParamSchema {
@@ -40,7 +45,7 @@ export interface ParamSchema {
* { param_name: { type, description, required, secret, ... }, ... }
*/
export function extractProperties(
schema: ParamSchema | any,
schema: ParamSchema | Record<string, unknown> | null | undefined,
): Record<string, ParamSchemaProperty> {
if (!schema || typeof schema !== "object") return {};
// StackStorm-style flat format: { param_name: { type, description, required, ... }, ... }
@@ -56,8 +61,8 @@ export function extractProperties(
interface ParamSchemaFormProps {
schema: ParamSchema;
values: Record<string, any>;
onChange: (values: Record<string, any>) => void;
values: Record<string, JsonValue>;
onChange: (values: Record<string, JsonValue>) => void;
errors?: Record<string, string>;
disabled?: boolean;
className?: string;
@@ -79,7 +84,7 @@ interface ParamSchemaFormProps {
/**
* Check if a string value contains a template expression ({{ ... }})
*/
function isTemplateExpression(value: any): boolean {
function isTemplateExpression(value: JsonValue): boolean {
return typeof value === "string" && /\{\{.*\}\}/.test(value);
}
@@ -88,7 +93,7 @@ function isTemplateExpression(value: any): boolean {
* Non-string values (booleans, numbers, objects, arrays) are JSON-stringified
* so the user can edit them as text.
*/
function valueToString(value: any): string {
function valueToString(value: JsonValue): string {
if (value === undefined || value === null) return "";
if (typeof value === "string") return value;
return JSON.stringify(value);
@@ -99,7 +104,7 @@ function valueToString(value: any): string {
* Template expressions are always kept as strings.
* Plain values are coerced to the schema type when possible.
*/
function parseTemplateValue(raw: string, type: string): any {
function parseTemplateValue(raw: string, type: string): JsonValue {
if (raw === "") return "";
// Template expressions stay as strings - resolved server-side
if (isTemplateExpression(raw)) return raw;
@@ -164,19 +169,20 @@ export default function ParamSchemaForm({
}
return acc;
},
{ ...values } as Record<string, any>,
{ ...values } as Record<string, JsonValue>,
);
// Only update if there are new defaults
if (JSON.stringify(initialValues) !== JSON.stringify(values)) {
onChange(initialValues);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schema]); // Only run when schema changes
/**
* Handle input change for a specific field
*/
const handleInputChange = (key: string, value: any) => {
const handleInputChange = (key: string, value: JsonValue) => {
const newValues = { ...values, [key]: value };
onChange(newValues);
@@ -200,7 +206,10 @@ export default function ParamSchemaForm({
/**
* Get a placeholder hint for template-mode inputs
*/
const getTemplatePlaceholder = (key: string, param: any): string => {
const getTemplatePlaceholder = (
key: string,
param: ParamSchemaProperty | undefined,
) => {
const type = param?.type || "string";
switch (type) {
case "boolean":
@@ -225,7 +234,10 @@ export default function ParamSchemaForm({
/**
* Render a template-mode text input for any parameter type
*/
const renderTemplateInput = (key: string, param: any) => {
const renderTemplateInput = (
key: string,
param: ParamSchemaProperty | undefined,
) => {
const type = param?.type || "string";
const rawValue = values[key] ?? param?.default ?? "";
const isDisabled = disabled;
@@ -264,7 +276,7 @@ export default function ParamSchemaForm({
/**
* Render input field based on parameter type (standard mode)
*/
const renderInput = (key: string, param: any) => {
const renderInput = (key: string, param: ParamSchemaProperty | undefined) => {
const type = param?.type || "string";
const value = values[key] ?? param?.default ?? "";
const isDisabled = disabled;
@@ -388,7 +400,7 @@ export default function ParamSchemaForm({
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
>
<option value="">Select...</option>
{param.enum.map((option: any) => (
{param.enum.map((option: string) => (
<option key={option} value={option}>
{option}
</option>
@@ -416,7 +428,10 @@ export default function ParamSchemaForm({
/**
* Render type hint badge and additional context for template-mode fields
*/
const renderTemplateHints = (_key: string, param: any) => {
const renderTemplateHints = (
_key: string,
param: ParamSchemaProperty | undefined,
) => {
const type = param?.type || "string";
const hints: string[] = [];
@@ -537,7 +552,7 @@ export default function ParamSchemaForm({
*/
export function validateParamSchema(
schema: ParamSchema,
values: Record<string, any>,
values: Record<string, JsonValue>,
allowTemplates: boolean = false,
): Record<string, string> {
const errors: Record<string, string> = {};

View File

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

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { useCreatePack, useUpdatePack } from "@/hooks/usePacks";
import type { PackResponse } from "@/api";
@@ -7,6 +7,24 @@ import SchemaBuilder from "@/components/common/SchemaBuilder";
import ParamSchemaForm from "@/components/common/ParamSchemaForm";
import { RotateCcw } from "lucide-react";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JsonValue = any;
/** A single property definition within a flat schema object */
interface SchemaPropertyDef {
type?: string;
description?: string;
required?: boolean;
secret?: boolean;
default?: JsonValue;
minimum?: number;
maximum?: number;
[key: string]: unknown;
}
/** The flat schema format: each key is a parameter name mapped to its definition */
type FlatSchema = Record<string, SchemaPropertyDef>;
interface PackFormProps {
pack?: PackResponse;
onSuccess?: () => void;
@@ -31,9 +49,10 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
const [isStandard, setIsStandard] = useState(pack?.is_standard ?? false);
const [configValues, setConfigValues] =
useState<Record<string, any>>(initialConfig);
const [confSchema, setConfSchema] =
useState<Record<string, any>>(initialConfSchema);
useState<Record<string, JsonValue>>(initialConfig);
const [confSchema, setConfSchema] = useState<FlatSchema>(
initialConfSchema as FlatSchema,
);
const [meta, setMeta] = useState(
pack?.meta ? JSON.stringify(pack.meta, null, 2) : "{}",
);
@@ -49,14 +68,22 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
typeof confSchema === "object" &&
Object.keys(confSchema).length > 0;
// Track previous confSchema to detect changes without re-running on every render
const prevConfSchemaRef = useRef(confSchema);
// Sync config values when schema changes (for ad-hoc packs only)
/* eslint-disable react-hooks/set-state-in-effect -- intentional sync of dependent state */
useEffect(() => {
// Only sync when confSchema actually changed
if (prevConfSchemaRef.current === confSchema) return;
prevConfSchemaRef.current = confSchema;
if (!isStandard && hasSchemaProperties) {
// Get current schema property names (flat format: keys are parameter names)
const schemaKeys = Object.keys(confSchema);
// Create new config with only keys that exist in schema
const syncedConfig: Record<string, any> = {};
const syncedConfig: Record<string, JsonValue> = {};
schemaKeys.forEach((key) => {
if (configValues[key] !== undefined) {
// Preserve existing value
@@ -77,7 +104,8 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
setConfigValues(syncedConfig);
}
}
}, [confSchema, isStandard]);
}, [confSchema, isStandard, hasSchemaProperties, configValues]);
/* eslint-enable react-hooks/set-state-in-effect */
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
@@ -111,7 +139,7 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
if (meta.trim()) {
try {
JSON.parse(meta);
} catch (e) {
} catch {
newErrors.meta = "Invalid JSON format";
}
}
@@ -179,12 +207,14 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
onSuccess();
}
}
} catch (error: any) {
} catch (error: unknown) {
const errMsg =
error instanceof Error ? error.message : "Failed to save pack";
const axiosErr = error as {
response?: { data?: { message?: string } };
};
setErrors({
submit:
error.response?.data?.message ||
error.message ||
"Failed to save pack",
submit: axiosErr?.response?.data?.message || errMsg,
});
}
};
@@ -203,7 +233,7 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
};
const insertSchemaExample = (type: "api" | "database" | "webhook") => {
let example: Record<string, any>;
let example: FlatSchema;
switch (type) {
case "api":
example = {
@@ -280,8 +310,8 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
setConfSchema(example);
// Immediately sync config values with schema defaults
const syncedConfig: Record<string, any> = {};
Object.entries(example).forEach(([key, propDef]: [string, any]) => {
const syncedConfig: Record<string, JsonValue> = {};
Object.entries(example).forEach(([key, propDef]) => {
if (propDef.default !== undefined) {
syncedConfig[key] = propDef.default;
}

View File

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

View File

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