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:
@@ -2,6 +2,8 @@ import { Link, useParams, useNavigate } from "react-router-dom";
|
||||
import { useActions, useAction, useDeleteAction } from "@/hooks/useActions";
|
||||
import { useExecutions } from "@/hooks/useExecutions";
|
||||
import { useState, useMemo } from "react";
|
||||
import type { ActionSummary, ExecutionSummary } from "@/api";
|
||||
import type { ParamSchemaProperty } from "@/components/common/ParamSchemaForm";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@@ -20,7 +22,7 @@ export default function ActionsPage() {
|
||||
const { ref } = useParams<{ ref?: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading, error } = useActions();
|
||||
const actions = data?.data || [];
|
||||
const actions = useMemo(() => data?.data || [], [data?.data]);
|
||||
const [collapsedPacks, setCollapsedPacks] = useState<Set<string>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -28,7 +30,7 @@ export default function ActionsPage() {
|
||||
const filteredActions = useMemo(() => {
|
||||
if (!searchQuery.trim()) return actions;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return actions.filter((action: any) => {
|
||||
return actions.filter((action: ActionSummary) => {
|
||||
return (
|
||||
action.label?.toLowerCase().includes(query) ||
|
||||
action.ref?.toLowerCase().includes(query) ||
|
||||
@@ -40,8 +42,8 @@ export default function ActionsPage() {
|
||||
|
||||
// Group filtered actions by pack
|
||||
const actionsByPack = useMemo(() => {
|
||||
const grouped = new Map<string, any[]>();
|
||||
filteredActions.forEach((action: any) => {
|
||||
const grouped = new Map<string, ActionSummary[]>();
|
||||
filteredActions.forEach((action: ActionSummary) => {
|
||||
const packRef = action.pack_ref;
|
||||
if (!grouped.has(packRef)) {
|
||||
grouped.set(packRef, []);
|
||||
@@ -176,7 +178,7 @@ export default function ActionsPage() {
|
||||
{/* Actions List */}
|
||||
{!isCollapsed && (
|
||||
<div className="p-1">
|
||||
{packActions.map((action: any) => (
|
||||
{packActions.map((action: ActionSummary) => (
|
||||
<Link
|
||||
key={action.id}
|
||||
to={`/actions/${action.ref}`}
|
||||
@@ -448,54 +450,58 @@ function ActionDetail({ actionRef }: { actionRef: string }) {
|
||||
Parameters
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{paramEntries.map(([key, param]: [string, any]) => (
|
||||
<div
|
||||
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-center gap-2">
|
||||
<span className="font-mono font-semibold text-sm">
|
||||
{key}
|
||||
</span>
|
||||
{param?.required && (
|
||||
<span className="text-xs px-2 py-0.5 bg-red-100 text-red-700 rounded">
|
||||
Required
|
||||
{paramEntries.map(
|
||||
([key, param]: [string, ParamSchemaProperty]) => (
|
||||
<div
|
||||
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-center gap-2">
|
||||
<span className="font-mono font-semibold text-sm">
|
||||
{key}
|
||||
</span>
|
||||
)}
|
||||
{param?.secret && (
|
||||
<span className="text-xs px-2 py-0.5 bg-yellow-100 text-yellow-700 rounded">
|
||||
Secret
|
||||
{param?.required && (
|
||||
<span className="text-xs px-2 py-0.5 bg-red-100 text-red-700 rounded">
|
||||
Required
|
||||
</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>
|
||||
</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>
|
||||
{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>
|
||||
)}
|
||||
@@ -532,7 +538,7 @@ function ActionDetail({ actionRef }: { actionRef: string }) {
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{executions.map((execution: any) => (
|
||||
{executions.map((execution: ExecutionSummary) => (
|
||||
<Link
|
||||
key={execution.id}
|
||||
to={`/executions/${execution.id}`}
|
||||
|
||||
@@ -2,6 +2,22 @@ import React, { useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
|
||||
interface LocationState {
|
||||
from?: {
|
||||
pathname: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginError {
|
||||
response?: {
|
||||
status: number;
|
||||
data?: {
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const [login, setLogin] = 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)
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
@@ -27,16 +44,16 @@ export default function LoginPage() {
|
||||
sessionStorage.removeItem("redirect_after_login");
|
||||
|
||||
navigate(from, { replace: true });
|
||||
} catch (err: any) {
|
||||
console.error("Login error:", err);
|
||||
console.error("Full error object:", JSON.stringify(err, null, 2));
|
||||
if (err.response) {
|
||||
console.error("Response status:", err.response.status);
|
||||
console.error("Response data:", err.response.data);
|
||||
} catch (err: unknown) {
|
||||
const loginErr = err as LoginError;
|
||||
console.error("Login error:", loginErr);
|
||||
if (loginErr.response) {
|
||||
console.error("Response status:", loginErr.response.status);
|
||||
console.error("Response data:", loginErr.response.data);
|
||||
}
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
loginErr.response?.data?.message ||
|
||||
loginErr.message ||
|
||||
"Login failed. Please check your credentials.";
|
||||
setError(errorMessage);
|
||||
// Don't navigate on error - stay on login page
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useEnforcements } from "@/hooks/useEvents";
|
||||
import { useEnforcementStream } from "@/hooks/useEnforcementStream";
|
||||
import { EnforcementStatus } from "@/api";
|
||||
import type { EnforcementSummary } from "@/api";
|
||||
import { useState, useMemo, memo, useCallback, useEffect } from "react";
|
||||
import { Search, X } from "lucide-react";
|
||||
import MultiSelect from "@/components/common/MultiSelect";
|
||||
@@ -99,7 +100,7 @@ const EnforcementsResultsTable = memo(
|
||||
pageSize,
|
||||
total,
|
||||
}: {
|
||||
enforcements: any[];
|
||||
enforcements: EnforcementSummary[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
@@ -195,7 +196,7 @@ const EnforcementsResultsTable = memo(
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<td className="px-6 py-4 font-mono text-sm">
|
||||
<Link
|
||||
@@ -364,7 +365,13 @@ export default function EnforcementsPage() {
|
||||
|
||||
// --- Build query params from debounced state ---
|
||||
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.event) {
|
||||
const eventId = parseInt(debouncedFilters.event, 10);
|
||||
@@ -416,16 +423,16 @@ export default function EnforcementsPage() {
|
||||
|
||||
// Filter by rule_ref (client-side since API doesn't support it)
|
||||
if (debouncedFilters.rule) {
|
||||
filtered = filtered.filter((enf: any) =>
|
||||
filtered = filtered.filter((enf: EnforcementSummary) =>
|
||||
enf.rule_ref
|
||||
.toLowerCase()
|
||||
?.toLowerCase()
|
||||
.includes(debouncedFilters.rule.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
// If multiple statuses selected, filter client-side
|
||||
if (debouncedStatuses.length > 1) {
|
||||
filtered = filtered.filter((enf: any) =>
|
||||
filtered = filtered.filter((enf: EnforcementSummary) =>
|
||||
debouncedStatuses.includes(enf.status),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,7 +310,12 @@ export default function EventsPage() {
|
||||
|
||||
// --- Build query params from debounced state ---
|
||||
const queryParams = useMemo(() => {
|
||||
const params: any = { page, pageSize };
|
||||
const params: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
triggerRef?: string;
|
||||
ruleRef?: string;
|
||||
} = { page, pageSize };
|
||||
if (debouncedFilters.trigger) params.triggerRef = debouncedFilters.trigger;
|
||||
if (debouncedFilters.rule) params.ruleRef = debouncedFilters.rule;
|
||||
return params;
|
||||
@@ -320,19 +325,21 @@ export default function EventsPage() {
|
||||
const handleEventNotification = useCallback(
|
||||
(notification: Notification) => {
|
||||
if (notification.notification_type === "event_created") {
|
||||
const payload = notification.payload as any;
|
||||
const payload = notification.payload as Partial<EventSummary> & {
|
||||
payload?: unknown;
|
||||
};
|
||||
|
||||
const newEvent: EventSummary = {
|
||||
id: payload.id,
|
||||
trigger: payload.trigger,
|
||||
trigger_ref: payload.trigger_ref,
|
||||
id: payload.id ?? 0,
|
||||
trigger: payload.trigger ?? 0,
|
||||
trigger_ref: payload.trigger_ref ?? "",
|
||||
rule: payload.rule,
|
||||
rule_ref: payload.rule_ref,
|
||||
source: payload.source,
|
||||
source_ref: payload.source_ref,
|
||||
has_payload:
|
||||
payload.payload !== null && payload.payload !== undefined,
|
||||
created: payload.created,
|
||||
created: payload.created ?? new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Augment autocomplete suggestions with new refs from notification
|
||||
@@ -357,44 +364,54 @@ export default function EventsPage() {
|
||||
};
|
||||
});
|
||||
|
||||
queryClient.setQueryData(["events", queryParams], (oldData: any) => {
|
||||
if (!oldData) return oldData;
|
||||
queryClient.setQueryData(
|
||||
["events", queryParams],
|
||||
(
|
||||
oldData:
|
||||
| {
|
||||
data: EventSummary[];
|
||||
pagination?: { total_items?: number };
|
||||
}
|
||||
| undefined,
|
||||
) => {
|
||||
if (!oldData) return oldData;
|
||||
|
||||
// Check if filtering and event matches filter
|
||||
if (
|
||||
debouncedFilters.trigger &&
|
||||
newEvent.trigger_ref !== debouncedFilters.trigger
|
||||
) {
|
||||
return oldData;
|
||||
}
|
||||
if (
|
||||
debouncedFilters.rule &&
|
||||
newEvent.rule_ref !== debouncedFilters.rule
|
||||
) {
|
||||
return oldData;
|
||||
}
|
||||
// Check if filtering and event matches filter
|
||||
if (
|
||||
debouncedFilters.trigger &&
|
||||
newEvent.trigger_ref !== debouncedFilters.trigger
|
||||
) {
|
||||
return oldData;
|
||||
}
|
||||
if (
|
||||
debouncedFilters.rule &&
|
||||
newEvent.rule_ref !== debouncedFilters.rule
|
||||
) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
// Add new event to the beginning of the list if on first page
|
||||
if (page === 1) {
|
||||
// Add new event to the beginning of the list if on first page
|
||||
if (page === 1) {
|
||||
return {
|
||||
...oldData,
|
||||
data: [newEvent, ...oldData.data].slice(0, pageSize),
|
||||
pagination: {
|
||||
...oldData.pagination,
|
||||
total_items: (oldData.pagination?.total_items || 0) + 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// For other pages, just update the total count
|
||||
return {
|
||||
...oldData,
|
||||
data: [newEvent, ...oldData.data].slice(0, pageSize),
|
||||
pagination: {
|
||||
...oldData.pagination,
|
||||
total_items: (oldData.pagination?.total_items || 0) + 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// For other pages, just update the total count
|
||||
return {
|
||||
...oldData,
|
||||
pagination: {
|
||||
...oldData.pagination,
|
||||
total_items: (oldData.pagination?.total_items || 0) + 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
[queryClient, queryParams, page, pageSize, debouncedFilters],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useExecutions } from "@/hooks/useExecutions";
|
||||
import { useExecutionStream } from "@/hooks/useExecutionStream";
|
||||
import { ExecutionStatus } from "@/api";
|
||||
import type { ExecutionSummary } from "@/api";
|
||||
import { useState, useMemo, memo, useCallback, useEffect } from "react";
|
||||
import { Search, X, List, GitBranch } from "lucide-react";
|
||||
import MultiSelect from "@/components/common/MultiSelect";
|
||||
@@ -96,7 +97,7 @@ const ExecutionsResultsTable = memo(
|
||||
selectedExecutionId,
|
||||
onSelectExecution,
|
||||
}: {
|
||||
executions: any[];
|
||||
executions: ExecutionSummary[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
@@ -191,7 +192,7 @@ const ExecutionsResultsTable = memo(
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{executions.map((exec: any) => (
|
||||
{executions.map((exec: ExecutionSummary) => (
|
||||
<tr
|
||||
key={exec.id}
|
||||
data-execution-id={exec.id}
|
||||
@@ -361,7 +362,17 @@ export default function ExecutionsPage() {
|
||||
|
||||
// --- Build query params from debounced state ---
|
||||
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.rule) params.ruleRef = debouncedFilters.rule;
|
||||
if (debouncedFilters.action) params.actionRef = debouncedFilters.action;
|
||||
@@ -429,7 +440,7 @@ export default function ExecutionsPage() {
|
||||
// Client-side filtering for multiple status selection (when > 1 selected)
|
||||
const filteredExecutions = useMemo(() => {
|
||||
if (debouncedStatuses.length <= 1) return executions;
|
||||
return executions.filter((exec: any) =>
|
||||
return executions.filter((exec: ExecutionSummary) =>
|
||||
debouncedStatuses.includes(exec.status),
|
||||
);
|
||||
}, [executions, debouncedStatuses]);
|
||||
@@ -500,7 +511,9 @@ export default function ExecutionsPage() {
|
||||
return nextId;
|
||||
}
|
||||
|
||||
const currentIndex = list.findIndex((ex: any) => ex.id === prevId);
|
||||
const currentIndex = list.findIndex(
|
||||
(ex: ExecutionSummary) => ex.id === prevId,
|
||||
);
|
||||
if (currentIndex === -1) {
|
||||
const nextId = list[0].id;
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
@@ -22,7 +22,8 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
const createKeyMutation = useCreateKey();
|
||||
|
||||
// Determine if encryption is allowed based on format
|
||||
const canEncrypt = format === "text" || format === "json" || format === "yaml";
|
||||
const canEncrypt =
|
||||
format === "text" || format === "json" || format === "yaml";
|
||||
|
||||
// Auto-disable encryption for non-encryptable formats
|
||||
const isEncrypted = canEncrypt && encrypted;
|
||||
@@ -33,12 +34,13 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
|
||||
// Validate ref format
|
||||
if (!/^[a-zA-Z0-9_.-]+$/.test(ref)) {
|
||||
setError("Reference must contain only letters, numbers, underscores, hyphens, and dots");
|
||||
setError(
|
||||
"Reference must contain only letters, numbers, underscores, hyphens, and dots",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate value based on format
|
||||
let validatedValue = value;
|
||||
try {
|
||||
if (format === "json") {
|
||||
JSON.parse(value);
|
||||
@@ -70,14 +72,14 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
await createKeyMutation.mutateAsync({
|
||||
ref,
|
||||
name,
|
||||
value: validatedValue,
|
||||
value,
|
||||
encrypted: isEncrypted,
|
||||
owner_type: ownerType,
|
||||
owner: owner || undefined,
|
||||
});
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to create key");
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "Failed to create key");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,7 +104,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
)}
|
||||
|
||||
<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>
|
||||
</label>
|
||||
<input
|
||||
@@ -120,7 +125,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
</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>
|
||||
</label>
|
||||
<input
|
||||
@@ -136,7 +144,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
</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>
|
||||
</label>
|
||||
<select
|
||||
@@ -160,7 +171,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
</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>
|
||||
</label>
|
||||
<textarea
|
||||
@@ -193,13 +207,19 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
disabled={!canEncrypt}
|
||||
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)
|
||||
</label>
|
||||
</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>
|
||||
</label>
|
||||
<select
|
||||
@@ -218,7 +238,10 @@ export default function KeyCreateModal({ onClose }: KeyCreateModalProps) {
|
||||
|
||||
{ownerType !== OwnerType.SYSTEM && (
|
||||
<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
|
||||
</label>
|
||||
<input
|
||||
|
||||
@@ -19,6 +19,7 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
|
||||
|
||||
const updateKeyMutation = useUpdateKey();
|
||||
|
||||
/* eslint-disable react-hooks/set-state-in-effect -- sync local form state from fetched key data */
|
||||
useEffect(() => {
|
||||
if (key) {
|
||||
setName(key.name);
|
||||
@@ -26,6 +27,7 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
|
||||
setEncrypted(key.encrypted);
|
||||
}
|
||||
}, [key]);
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -41,8 +43,8 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
|
||||
},
|
||||
});
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to update key");
|
||||
} catch (err: unknown) {
|
||||
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="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">
|
||||
<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
|
||||
onClick={onClose}
|
||||
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="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>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -104,14 +110,19 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
|
||||
</div>
|
||||
{key.owner && (
|
||||
<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>
|
||||
</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>
|
||||
</label>
|
||||
<input
|
||||
@@ -125,7 +136,10 @@ export default function KeyEditModal({ keyRef, onClose }: KeyEditModalProps) {
|
||||
</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>
|
||||
</label>
|
||||
<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"
|
||||
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>
|
||||
</div>
|
||||
<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)}
|
||||
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)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Link, useParams } from "react-router-dom";
|
||||
import { usePacks, usePack, useDeletePack } from "@/hooks/usePacks";
|
||||
import { usePackActions } from "@/hooks/useActions";
|
||||
import { useState, useMemo } from "react";
|
||||
import type { PackSummary, PackResponse, ActionSummary } from "@/api";
|
||||
import {
|
||||
Search,
|
||||
X,
|
||||
@@ -12,10 +13,13 @@ import {
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type JsonValue = any;
|
||||
|
||||
export default function PacksPage() {
|
||||
const { ref } = useParams<{ ref?: string }>();
|
||||
const { data, isLoading, error } = usePacks();
|
||||
const packs = data?.data || [];
|
||||
const packs = useMemo(() => data?.data || [], [data?.data]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showPackMenu, setShowPackMenu] = useState(false);
|
||||
|
||||
@@ -23,7 +27,7 @@ export default function PacksPage() {
|
||||
const filteredPacks = useMemo(() => {
|
||||
if (!searchQuery.trim()) return packs;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return packs.filter((pack: any) => {
|
||||
return packs.filter((pack: PackSummary) => {
|
||||
return (
|
||||
pack.label?.toLowerCase().includes(query) ||
|
||||
pack.ref?.toLowerCase().includes(query) ||
|
||||
@@ -193,7 +197,7 @@ export default function PacksPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{filteredPacks.map((pack: any) => (
|
||||
{filteredPacks.map((pack: PackSummary) => (
|
||||
<Link
|
||||
key={pack.id}
|
||||
to={`/packs/${pack.ref}`}
|
||||
@@ -405,7 +409,7 @@ function PackDetail({ packRef }: { packRef: string }) {
|
||||
Actions ({packActions.length})
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{packActions.map((action: any) => (
|
||||
{packActions.map((action: ActionSummary) => (
|
||||
<Link
|
||||
key={action.id}
|
||||
to={`/actions/${action.ref}`}
|
||||
@@ -485,7 +489,7 @@ function PackDetail({ packRef }: { packRef: string }) {
|
||||
}
|
||||
|
||||
// Helper component to display pack configuration
|
||||
function PackConfiguration({ pack }: { pack: any }) {
|
||||
function PackConfiguration({ pack }: { pack: PackResponse | undefined }) {
|
||||
if (!pack) return null;
|
||||
|
||||
const confSchema = pack.conf_schema || {};
|
||||
@@ -504,57 +508,60 @@ function PackConfiguration({ pack }: { pack: any }) {
|
||||
<h2 className="text-xl font-semibold">Configuration</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(properties).map(([key, schema]: [string, any]) => {
|
||||
const value = config[key];
|
||||
const hasValue = value !== undefined && value !== null;
|
||||
const displayValue = hasValue ? value : schema.default;
|
||||
const isUsingDefault = !hasValue && schema.default !== undefined;
|
||||
{Object.entries(properties).map(
|
||||
([key, schema]: [string, JsonValue]) => {
|
||||
const value = config[key];
|
||||
const hasValue = value !== undefined && value !== null;
|
||||
const displayValue = hasValue ? value : schema.default;
|
||||
const isUsingDefault = !hasValue && schema.default !== undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="border-b border-gray-200 pb-4 last:border-0 last:pb-0"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<dt className="text-sm font-medium text-gray-900 font-mono">
|
||||
{key}
|
||||
</dt>
|
||||
<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"}
|
||||
</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
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="border-b border-gray-200 pb-4 last:border-0 last:pb-0"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<dt className="text-sm font-medium text-gray-900 font-mono">
|
||||
{key}
|
||||
</dt>
|
||||
<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"}
|
||||
</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>
|
||||
{schema.description && (
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
{schema.description}
|
||||
<dd className="ml-4 text-sm text-right">
|
||||
<ConfigValue value={displayValue} type={schema.type} />
|
||||
</dd>
|
||||
</div>
|
||||
{schema.minimum !== undefined &&
|
||||
schema.maximum !== undefined && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Range: {schema.minimum} - {schema.maximum}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<dd className="ml-4 text-sm text-right">
|
||||
<ConfigValue value={displayValue} type={schema.type} />
|
||||
</dd>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return <span className="text-gray-400 italic">not set</span>;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { useTrigger } from "@/hooks/useTriggers";
|
||||
import { useAction } from "@/hooks/useActions";
|
||||
import { useState, useMemo } from "react";
|
||||
import type { RuleSummary } from "@/api";
|
||||
import { ChevronDown, ChevronRight, Search, X } from "lucide-react";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import ParamSchemaDisplay, {
|
||||
@@ -18,7 +19,7 @@ import ParamSchemaDisplay, {
|
||||
export default function RulesPage() {
|
||||
const { ref } = useParams<{ ref?: string }>();
|
||||
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 [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -26,7 +27,7 @@ export default function RulesPage() {
|
||||
const filteredRules = useMemo(() => {
|
||||
if (!searchQuery.trim()) return rules;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return rules.filter((rule: any) => {
|
||||
return rules.filter((rule: RuleSummary) => {
|
||||
return (
|
||||
rule.label?.toLowerCase().includes(query) ||
|
||||
rule.ref?.toLowerCase().includes(query) ||
|
||||
@@ -40,8 +41,8 @@ export default function RulesPage() {
|
||||
|
||||
// Group filtered rules by pack
|
||||
const rulesByPack = useMemo(() => {
|
||||
const grouped = new Map<string, any[]>();
|
||||
filteredRules.forEach((rule: any) => {
|
||||
const grouped = new Map<string, RuleSummary[]>();
|
||||
filteredRules.forEach((rule: RuleSummary) => {
|
||||
const packRef = rule.pack_ref || "unknown";
|
||||
if (!grouped.has(packRef)) {
|
||||
grouped.set(packRef, []);
|
||||
@@ -179,7 +180,7 @@ export default function RulesPage() {
|
||||
{/* Rules List */}
|
||||
{!isCollapsed && (
|
||||
<div className="p-1">
|
||||
{packRules.map((rule: any) => (
|
||||
{packRules.map((rule: RuleSummary) => (
|
||||
<Link
|
||||
key={rule.id}
|
||||
to={`/rules/${rule.ref}`}
|
||||
@@ -269,9 +270,9 @@ function RuleDetail({ ruleRef }: { ruleRef: string }) {
|
||||
const { data: actionData } = useAction(rule?.data?.action_ref || "");
|
||||
|
||||
const triggerParamSchema: ParamSchema =
|
||||
(triggerData?.data as any)?.param_schema || {};
|
||||
(triggerData?.data as { param_schema?: ParamSchema })?.param_schema || {};
|
||||
const actionParamSchema: ParamSchema =
|
||||
(actionData?.data as any)?.param_schema || {};
|
||||
(actionData?.data as { param_schema?: ParamSchema })?.param_schema || {};
|
||||
|
||||
const handleToggleEnabled = async () => {
|
||||
if (!rule?.data) return;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useSensors, useSensor, useDeleteSensor } from "@/hooks/useSensors";
|
||||
import { useState, useMemo } from "react";
|
||||
import type { SensorSummary } from "@/api";
|
||||
import { ChevronDown, ChevronRight, Search, X } from "lucide-react";
|
||||
|
||||
export default function SensorsPage() {
|
||||
const { ref } = useParams<{ ref?: string }>();
|
||||
const { data, isLoading, error } = useSensors({});
|
||||
const sensors = data?.data || [];
|
||||
const sensors = useMemo(() => data?.data || [], [data?.data]);
|
||||
const [collapsedPacks, setCollapsedPacks] = useState<Set<string>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -14,7 +15,7 @@ export default function SensorsPage() {
|
||||
const filteredSensors = useMemo(() => {
|
||||
if (!searchQuery.trim()) return sensors;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return sensors.filter((sensor: any) => {
|
||||
return sensors.filter((sensor: SensorSummary) => {
|
||||
return (
|
||||
sensor.label?.toLowerCase().includes(query) ||
|
||||
sensor.ref?.toLowerCase().includes(query) ||
|
||||
@@ -26,8 +27,8 @@ export default function SensorsPage() {
|
||||
|
||||
// Group filtered sensors by pack
|
||||
const sensorsByPack = useMemo(() => {
|
||||
const grouped = new Map<string, any[]>();
|
||||
filteredSensors.forEach((sensor: any) => {
|
||||
const grouped = new Map<string, SensorSummary[]>();
|
||||
filteredSensors.forEach((sensor: SensorSummary) => {
|
||||
const packRef = sensor.pack_ref || "unknown";
|
||||
if (!grouped.has(packRef)) {
|
||||
grouped.set(packRef, []);
|
||||
@@ -152,7 +153,7 @@ export default function SensorsPage() {
|
||||
{/* Sensors List */}
|
||||
{!isCollapsed && (
|
||||
<div className="p-1">
|
||||
{packSensors.map((sensor: any) => (
|
||||
{packSensors.map((sensor: SensorSummary) => (
|
||||
<Link
|
||||
key={sensor.id}
|
||||
to={`/sensors/${sensor.ref}`}
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
useDisableTrigger,
|
||||
} from "@/hooks/useTriggers";
|
||||
import { useState, useMemo } from "react";
|
||||
import { extractProperties } from "@/components/common/ParamSchemaForm";
|
||||
import type { TriggerSummary } from "@/api";
|
||||
import {
|
||||
extractProperties,
|
||||
type ParamSchemaProperty,
|
||||
} from "@/components/common/ParamSchemaForm";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@@ -23,7 +27,7 @@ import { useAuth } from "@/contexts/AuthContext";
|
||||
export default function TriggersPage() {
|
||||
const { ref } = useParams<{ ref?: string }>();
|
||||
const { data, isLoading, error } = useTriggers({});
|
||||
const triggers = data?.data || [];
|
||||
const triggers = useMemo(() => data?.data || [], [data?.data]);
|
||||
const [collapsedPacks, setCollapsedPacks] = useState<Set<string>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -31,7 +35,7 @@ export default function TriggersPage() {
|
||||
const filteredTriggers = useMemo(() => {
|
||||
if (!searchQuery.trim()) return triggers;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return triggers.filter((trigger: any) => {
|
||||
return triggers.filter((trigger: TriggerSummary) => {
|
||||
return (
|
||||
trigger.label?.toLowerCase().includes(query) ||
|
||||
trigger.ref?.toLowerCase().includes(query) ||
|
||||
@@ -43,8 +47,8 @@ export default function TriggersPage() {
|
||||
|
||||
// Group filtered triggers by pack
|
||||
const triggersByPack = useMemo(() => {
|
||||
const grouped = new Map<string, any[]>();
|
||||
filteredTriggers.forEach((trigger: any) => {
|
||||
const grouped = new Map<string, TriggerSummary[]>();
|
||||
filteredTriggers.forEach((trigger: TriggerSummary) => {
|
||||
const packRef = trigger.pack_ref || "unknown";
|
||||
if (!grouped.has(packRef)) {
|
||||
grouped.set(packRef, []);
|
||||
@@ -178,7 +182,7 @@ export default function TriggersPage() {
|
||||
{/* Triggers List */}
|
||||
{!isCollapsed && (
|
||||
<div className="p-1">
|
||||
{packTriggers.map((trigger: any) => (
|
||||
{packTriggers.map((trigger: TriggerSummary) => (
|
||||
<Link
|
||||
key={trigger.id}
|
||||
to={`/triggers/${trigger.ref}`}
|
||||
@@ -484,7 +488,65 @@ function TriggerDetail({ triggerRef }: { triggerRef: string }) {
|
||||
Parameters
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{paramEntries.map(([key, param]: [string, any]) => (
|
||||
{paramEntries.map(
|
||||
([key, param]: [string, ParamSchemaProperty]) => (
|
||||
<div
|
||||
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-center gap-2">
|
||||
<span className="font-mono font-semibold text-sm">
|
||||
{key}
|
||||
</span>
|
||||
{param?.required && (
|
||||
<span className="text-xs px-2 py-0.5 bg-red-100 text-red-700 rounded">
|
||||
Required
|
||||
</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>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payload Schema Card */}
|
||||
{outEntries.length > 0 && (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-2">Payload Schema</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Schema of the event payload generated when this trigger fires.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{outEntries.map(
|
||||
([key, param]: [string, ParamSchemaProperty]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="border border-gray-200 rounded p-3"
|
||||
@@ -500,11 +562,6 @@ function TriggerDetail({ triggerRef }: { triggerRef: string }) {
|
||||
Required
|
||||
</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>
|
||||
@@ -525,54 +582,8 @@ function TriggerDetail({ triggerRef }: { triggerRef: string }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payload Schema Card */}
|
||||
{outEntries.length > 0 && (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-2">Payload Schema</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Schema of the event payload generated when this trigger fires.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{outEntries.map(([key, param]: [string, any]) => (
|
||||
<div 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-center gap-2">
|
||||
<span className="font-mono font-semibold text-sm">
|
||||
{key}
|
||||
</span>
|
||||
{param?.required && (
|
||||
<span className="text-xs px-2 py-0.5 bg-red-100 text-red-700 rounded">
|
||||
Required
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-700 rounded">
|
||||
{param?.type || "any"}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user