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