Compare commits

..

1 Commits

Author SHA1 Message Date
Raunak Bhagat
8cda87c105 Minor fixes in sizes 2026-03-24 17:47:37 -07:00
13 changed files with 72 additions and 761 deletions

View File

@@ -91,8 +91,6 @@ class HookResponse(BaseModel):
# Nullable to match the DB column — endpoint_url is required on creation but
# future hook point types may not use an external endpoint (e.g. built-in handlers).
endpoint_url: str | None
# Partially-masked API key (e.g. "abcd••••••••wxyz"), or None if no key is set.
api_key_masked: str | None
fail_strategy: HookFailStrategy
timeout_seconds: float # always resolved — None from request is replaced with spec default before DB write
is_active: bool

View File

@@ -62,9 +62,6 @@ def _hook_to_response(hook: Hook, creator_email: str | None = None) -> HookRespo
name=hook.name,
hook_point=hook.hook_point,
endpoint_url=hook.endpoint_url,
api_key_masked=(
hook.api_key.get_value(apply_mask=True) if hook.api_key else None
),
fail_strategy=hook.fail_strategy,
timeout_seconds=hook.timeout_seconds,
is_active=hook.is_active,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 KiB

View File

@@ -12,7 +12,7 @@
.input-normal:focus:not(:active),
.input-normal:focus-within:not(:active) {
border-color: var(--border-05);
box-shadow: 0px 0px 0px 2px rgba(204, 204, 207, 1);
box-shadow: inset 0px 0px 0px 2px var(--background-tint-04);
}
.input-error {

View File

@@ -1,15 +0,0 @@
"use client";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { HookResponse } from "@/refresh-pages/admin/HooksPage/interfaces";
export function useHooks() {
const { data, isLoading, error, mutate } = useSWR<HookResponse[]>(
"/api/admin/hooks",
errorHandlingFetcher,
{ revalidateOnFocus: false }
);
return { hooks: data, isLoading, error, mutate };
}

View File

@@ -19,7 +19,7 @@ export const wrapperClasses: ClassNamesMap = {
export const innerClasses: ClassNamesMap = {
primary:
"text-text-04 placeholder:font-main-ui-body placeholder:text-text-02",
"text-text-04 placeholder:!font-secondary-body placeholder:text-text-02",
internal: null,
error: null,
disabled: "text-text-02",

View File

@@ -1,334 +0,0 @@
"use client";
import { useState } from "react";
import { Button } from "@opal/components";
import { Disabled } from "@opal/core";
import { SvgHookNodes } from "@opal/icons";
import Modal, { BasicModalFooter } from "@/refresh-components/Modal";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import InputSelect from "@/refresh-components/inputs/InputSelect";
import PasswordInputTypeIn from "@/refresh-components/inputs/PasswordInputTypeIn";
import Text from "@/refresh-components/texts/Text";
import { toast } from "@/hooks/useToast";
import { createHook, updateHook } from "@/refresh-pages/admin/HooksPage/svc";
import type {
HookFailStrategy,
HookPointMeta,
HookResponse,
} from "@/refresh-pages/admin/HooksPage/interfaces";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface HookFormModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** When provided, the modal is in edit mode for this hook. */
hook?: HookResponse;
/** When provided (create mode), the hook point is pre-selected and locked. */
spec?: HookPointMeta;
onSuccess: (hook: HookResponse) => void;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
interface FormState {
name: string;
endpoint_url: string;
api_key: string;
fail_strategy: HookFailStrategy;
timeout_seconds: string;
}
function buildInitialState(
hook: HookResponse | undefined,
spec: HookPointMeta | undefined
): FormState {
if (hook) {
return {
name: hook.name,
endpoint_url: hook.endpoint_url ?? "",
api_key: hook.api_key_masked ?? "",
fail_strategy: hook.fail_strategy,
timeout_seconds: String(hook.timeout_seconds),
};
}
return {
name: "",
endpoint_url: "",
api_key: "",
fail_strategy: spec?.default_fail_strategy ?? "soft",
timeout_seconds: spec ? String(spec.default_timeout_seconds) : "5",
};
}
const SOFT_DESCRIPTION =
"If the endpoint returns an error, Onyx logs it and continues the pipeline as normal, ignoring the hook result.";
// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------
interface FieldProps {
label: string;
required?: boolean;
description?: string;
children: React.ReactNode;
}
function Field({ label, required, description, children }: FieldProps) {
return (
<div className="flex flex-col gap-1 w-full">
<span className="font-main-ui-action text-text-04 px-[0.125rem]">
{label}
{required && <span className="text-status-error-05 ml-0.5">*</span>}
</span>
{children}
{description && (
<Text secondaryBody text03>
{description}
</Text>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export default function HookFormModal({
open,
onOpenChange,
hook,
spec,
onSuccess,
}: HookFormModalProps) {
const isEdit = !!hook;
const [form, setForm] = useState<FormState>(() =>
buildInitialState(hook, spec)
);
const [isSubmitting, setIsSubmitting] = useState(false);
function handleOpenChange(next: boolean) {
if (!next) {
setTimeout(() => {
setForm(buildInitialState(hook, spec));
setIsSubmitting(false);
}, 200);
}
onOpenChange(next);
}
function set<K extends keyof FormState>(key: K, value: FormState[K]) {
setForm((prev) => ({ ...prev, [key]: value }));
}
const timeoutNum = parseFloat(form.timeout_seconds);
const isValid =
form.name.trim().length > 0 &&
form.endpoint_url.trim().length > 0 &&
!isNaN(timeoutNum) &&
timeoutNum > 0;
async function handleSubmit() {
if (!isValid) return;
setIsSubmitting(true);
try {
let result: HookResponse;
if (isEdit && hook) {
const req: Record<string, unknown> = {};
if (form.name !== hook.name) req.name = form.name;
if (form.endpoint_url !== (hook.endpoint_url ?? ""))
req.endpoint_url = form.endpoint_url;
if (form.fail_strategy !== hook.fail_strategy)
req.fail_strategy = form.fail_strategy;
if (timeoutNum !== hook.timeout_seconds)
req.timeout_seconds = timeoutNum;
const maskedPlaceholder = hook.api_key_masked ?? "";
if (form.api_key !== maskedPlaceholder) {
req.api_key = form.api_key || null;
}
if (Object.keys(req).length === 0) {
handleOpenChange(false);
return;
}
result = await updateHook(hook.id, req);
} else {
const hookPoint = spec!.hook_point;
result = await createHook({
name: form.name,
hook_point: hookPoint,
endpoint_url: form.endpoint_url,
...(form.api_key ? { api_key: form.api_key } : {}),
fail_strategy: form.fail_strategy,
timeout_seconds: timeoutNum,
});
}
toast.success(isEdit ? "Hook updated." : "Hook created.");
onSuccess(result);
handleOpenChange(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : "Something went wrong.");
} finally {
setIsSubmitting(false);
}
}
const hookPointDisplayName = isEdit
? hook!.hook_point
: spec?.display_name ?? spec?.hook_point ?? "";
const hookPointDescription = isEdit ? undefined : spec?.description;
const docsUrl = isEdit ? undefined : spec?.docs_url;
const failStrategyDescription =
form.fail_strategy === "soft"
? SOFT_DESCRIPTION
: spec?.fail_hard_description ?? undefined;
return (
<Modal open={open} onOpenChange={handleOpenChange}>
<Modal.Content width="md" height="fit">
<Modal.Header
icon={SvgHookNodes}
title="Set Up Hook Extension"
description="Connect a external API endpoints to extend the hook point."
onClose={() => handleOpenChange(false)}
/>
<Modal.Body>
{/* Hook point section header */}
<div className="flex flex-row items-start justify-between gap-1">
<div className="flex flex-col flex-1 min-w-0">
<span className="font-main-ui-action text-text-04 px-[0.125rem]">
{hookPointDisplayName}
</span>
{hookPointDescription && (
<span className="font-secondary-body text-text-03 px-[0.125rem]">
{hookPointDescription}
</span>
)}
</div>
<div className="flex flex-col items-end shrink-0 gap-1">
<div className="flex items-center gap-1">
<SvgHookNodes
style={{ width: "1rem", height: "1rem" }}
className="text-text-03 shrink-0 p-0.5"
/>
<span className="font-secondary-body text-text-03">
Hook Point
</span>
</div>
{docsUrl && (
<a
href={docsUrl}
target="_blank"
rel="noopener noreferrer"
className="font-secondary-body text-text-03 underline"
>
Documentation
</a>
)}
</div>
</div>
<Field label="Display Name" required>
<InputTypeIn
value={form.name}
onChange={(e) => set("name", e.target.value)}
placeholder="Name your extension at this hook point"
variant={isSubmitting ? "disabled" : undefined}
/>
</Field>
<Field label="Fail Strategy" description={failStrategyDescription}>
<InputSelect
value={form.fail_strategy}
onValueChange={(v) => set("fail_strategy", v as HookFailStrategy)}
disabled={isSubmitting}
>
<InputSelect.Trigger placeholder="Select strategy" />
<InputSelect.Content>
<InputSelect.Item value="soft">
Log Error and Continue (Default)
</InputSelect.Item>
<InputSelect.Item value="hard">
Block Pipeline on Failure
</InputSelect.Item>
</InputSelect.Content>
</InputSelect>
</Field>
<Field
label="Timeout (seconds)"
required
description="Maximum time Onyx will wait for the endpoint to respond before applying the fail strategy."
>
<InputTypeIn
type="number"
value={form.timeout_seconds}
onChange={(e) => set("timeout_seconds", e.target.value)}
placeholder="5"
variant={isSubmitting ? "disabled" : undefined}
/>
</Field>
<Field
label="External API Endpoint URL"
required
description="Only connect to servers you trust. You are responsible for actions taken and data shared with this connection."
>
<InputTypeIn
value={form.endpoint_url}
onChange={(e) => set("endpoint_url", e.target.value)}
placeholder="https://your-api-endpoint.com"
variant={isSubmitting ? "disabled" : undefined}
/>
</Field>
<Field
label="API Key"
description="Onyx will use this key to authenticate with your API endpoint."
>
<PasswordInputTypeIn
value={form.api_key}
onChange={(e) => set("api_key", e.target.value)}
placeholder={
isEdit && hook?.api_key_masked
? "Leave blank to keep current key"
: undefined
}
disabled={isSubmitting}
/>
</Field>
</Modal.Body>
<Modal.Footer>
<BasicModalFooter
cancel={
<Disabled disabled={isSubmitting}>
<Button
prominence="tertiary"
onClick={() => handleOpenChange(false)}
>
Cancel
</Button>
</Disabled>
}
submit={
<Disabled disabled={isSubmitting || !isValid}>
<Button onClick={handleSubmit}>
{isEdit ? "Save" : "Connect"}
</Button>
</Disabled>
}
/>
</Modal.Footer>
</Modal.Content>
</Modal>
);
}

View File

@@ -3,34 +3,20 @@
import { useState, useEffect } from "react";
import { toast } from "@/hooks/useToast";
import { useHookSpecs } from "@/hooks/useHookSpecs";
import { useHooks } from "@/hooks/useHooks";
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
import { ContentAction } from "@opal/layouts";
import { Button } from "@opal/components";
import { Disabled } from "@opal/core";
import InputSearch from "@/refresh-components/inputs/InputSearch";
import Card from "@/refresh-components/cards/Card";
import Text from "@/refresh-components/texts/Text";
import {
SvgArrowExchange,
SvgBubbleText,
SvgCheckCircle,
SvgExternalLink,
SvgFileBroadcast,
SvgHookNodes,
SvgRefreshCw,
SvgSettings,
SvgXCircle,
} from "@opal/icons";
import { IconFunctionComponent } from "@opal/types";
import HookFormModal from "@/refresh-pages/admin/HooksPage/HookFormModal";
import type {
HookPointMeta,
HookResponse,
} from "@/refresh-pages/admin/HooksPage/interfaces";
import {
activateHook,
deactivateHook,
} from "@/refresh-pages/admin/HooksPage/svc";
const HOOK_POINT_ICONS: Record<string, IconFunctionComponent> = {
document_ingestion: SvgFileBroadcast,
@@ -41,152 +27,22 @@ function getHookPointIcon(hookPoint: string): IconFunctionComponent {
return HOOK_POINT_ICONS[hookPoint] ?? SvgHookNodes;
}
// ---------------------------------------------------------------------------
// Sub-component: connected hook card
// ---------------------------------------------------------------------------
interface ConnectedHookCardProps {
hook: HookResponse;
spec: HookPointMeta | undefined;
onEdit: () => void;
onDeleted: () => void;
onToggled: (updated: HookResponse) => void;
}
function ConnectedHookCard({
hook,
spec,
onEdit,
onDeleted: _onDeleted,
onToggled,
}: ConnectedHookCardProps) {
const [isBusy, setIsBusy] = useState(false);
async function handleToggle() {
setIsBusy(true);
try {
const updated = hook.is_active
? await deactivateHook(hook.id)
: await activateHook(hook.id);
onToggled(updated);
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to update hook status."
);
} finally {
setIsBusy(false);
}
}
const HookIcon = getHookPointIcon(hook.hook_point);
return (
<Card variant="primary" padding={0.5} gap={0}>
<div className="flex flex-row items-start w-full">
{/* Left: manually replicate ContentMd main-content layout so the docs
link sits as a natural third line with zero artificial gap. */}
<div className="flex flex-row flex-1 min-w-0 items-start gap-1 p-2">
<div className="shrink-0 p-0.5 text-text-04">
<HookIcon style={{ width: "1rem", height: "1rem" }} />
</div>
<div className="flex flex-col items-start min-w-0 flex-1">
<span
className="font-main-ui-action text-text-04"
style={{ height: "1.25rem" }}
>
{hook.name}
</span>
{/* matches opal-content-md-description: font-secondary-body px-[0.125rem] */}
<div className="font-secondary-body text-text-03">
{`Hook Point: ${spec?.display_name ?? hook.hook_point}`}
</div>
{spec?.docs_url && (
<div className="font-secondary-body text-text-03">
<a
href={spec.docs_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 w-fit"
>
<span className="underline">Documentation</span>
<SvgExternalLink size={12} className="shrink-0" />
</a>
</div>
)}
</div>
</div>
{/* Right: status + actions, top-aligned */}
<div className="flex flex-col items-end shrink-0">
<div className="flex items-center gap-1 p-2">
<span className="font-main-ui-action text-text-03">
{hook.is_active ? "Connected" : "Inactive"}
</span>
{hook.is_active ? (
<SvgCheckCircle size={16} className="text-status-success-05" />
) : (
<SvgXCircle size={16} className="text-text-03" />
)}
</div>
<div className="flex items-center gap-0.5 pl-2 pr-0.5">
<Disabled disabled={isBusy}>
<Button
prominence="tertiary"
size="md"
icon={SvgRefreshCw}
onClick={handleToggle}
tooltip={hook.is_active ? "Deactivate" : "Activate"}
aria-label={
hook.is_active ? "Deactivate hook" : "Activate hook"
}
/>
</Disabled>
<Disabled disabled={isBusy}>
<Button
prominence="tertiary"
size="md"
icon={SvgSettings}
onClick={onEdit}
aria-label="Configure hook"
/>
</Disabled>
</div>
</div>
</div>
</Card>
);
}
// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------
export default function HooksContent() {
const [search, setSearch] = useState("");
const [connectSpec, setConnectSpec] = useState<HookPointMeta | null>(null);
const [editHook, setEditHook] = useState<HookResponse | null>(null);
const { specs, isLoading: specsLoading, error: specsError } = useHookSpecs();
const {
hooks,
isLoading: hooksLoading,
error: hooksError,
mutate,
} = useHooks();
const { specs, isLoading, error } = useHookSpecs();
useEffect(() => {
if (specsError) toast.error("Failed to load hook specifications.");
}, [specsError]);
if (error) {
toast.error("Failed to load hook specifications.");
}
}, [error]);
useEffect(() => {
if (hooksError) toast.error("Failed to load hooks.");
}, [hooksError]);
if (specsLoading || hooksLoading) {
if (isLoading) {
return <SimpleLoader />;
}
if (specsError) {
if (error) {
return (
<Text text03 secondaryBody>
Failed to load hook specifications. Please refresh the page.
@@ -194,179 +50,68 @@ export default function HooksContent() {
);
}
const hooksByPoint: Record<string, HookResponse[]> = {};
for (const hook of hooks ?? []) {
(hooksByPoint[hook.hook_point] ??= []).push(hook);
}
const searchLower = search.toLowerCase();
// Connected hooks sorted alphabetically by hook name
const connectedHooks = (hooks ?? [])
.filter(
(hook) =>
!searchLower ||
hook.name.toLowerCase().includes(searchLower) ||
(hooksByPoint[hook.hook_point] &&
specs
?.find((s) => s.hook_point === hook.hook_point)
?.display_name.toLowerCase()
.includes(searchLower))
)
.sort((a, b) => a.name.localeCompare(b.name));
// Unconnected hook point specs sorted alphabetically
const unconnectedSpecs = (specs ?? [])
.filter(
(spec) =>
(hooksByPoint[spec.hook_point]?.length ?? 0) === 0 &&
(!searchLower ||
spec.display_name.toLowerCase().includes(searchLower) ||
spec.description.toLowerCase().includes(searchLower))
)
.sort((a, b) => a.display_name.localeCompare(b.display_name));
function handleHookSuccess(updated: HookResponse) {
mutate((prev) => {
if (!prev) return [updated];
const idx = prev.findIndex((h) => h.id === updated.id);
if (idx >= 0) {
const next = [...prev];
next[idx] = updated;
return next;
}
return [...prev, updated];
});
}
function handleHookDeleted(id: number) {
mutate((prev) => prev?.filter((h) => h.id !== id));
}
const connectSpec_ =
connectSpec ??
(editHook
? specs?.find((s) => s.hook_point === editHook.hook_point)
: undefined);
const filtered = (specs ?? []).filter(
(spec) =>
spec.display_name.toLowerCase().includes(search.toLowerCase()) ||
spec.description.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<div className="flex flex-col gap-6">
<InputSearch
placeholder="Search hooks..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="flex flex-col gap-6">
<InputSearch
placeholder="Search hooks..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="flex flex-col gap-2">
{connectedHooks.length === 0 && unconnectedSpecs.length === 0 ? (
<Text text03 secondaryBody>
{search
? "No hooks match your search."
: "No hook points are available."}
</Text>
) : (
<>
{connectedHooks.map((hook) => {
const spec = specs?.find(
(s) => s.hook_point === hook.hook_point
);
return (
<ConnectedHookCard
key={hook.id}
hook={hook}
spec={spec}
onEdit={() => setEditHook(hook)}
onDeleted={() => handleHookDeleted(hook.id)}
onToggled={handleHookSuccess}
/>
);
})}
{unconnectedSpecs.map((spec) => {
const UnconnectedIcon = getHookPointIcon(spec.hook_point);
return (
<Card
key={spec.hook_point}
variant="secondary"
padding={0.5}
gap={0}
<div className="flex flex-col gap-2">
{filtered.length === 0 ? (
<Text text03 secondaryBody>
{search
? "No hooks match your search."
: "No hook points are available."}
</Text>
) : (
filtered.map((spec) => (
<Card
key={spec.hook_point}
variant="secondary"
padding={0.5}
gap={0}
>
<ContentAction
icon={getHookPointIcon(spec.hook_point)}
title={spec.display_name}
description={spec.description}
sizePreset="main-content"
variant="section"
paddingVariant="fit"
rightChildren={
// TODO(Bo-Onyx): wire up Connect — open modal to create/edit hook
<Button prominence="tertiary" rightIcon={SvgArrowExchange}>
Connect
</Button>
}
/>
{spec.docs_url && (
<div className="pl-7 pt-1">
<a
href={spec.docs_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 w-fit text-text-03"
>
<div className="flex flex-row items-start w-full">
<div className="flex flex-row flex-1 min-w-0 items-start gap-1 p-2">
<div className="shrink-0 p-0.5 text-text-04">
<UnconnectedIcon
style={{ width: "1rem", height: "1rem" }}
/>
</div>
<div className="flex flex-col items-start min-w-0 flex-1">
<span
className="font-main-ui-action text-text-04"
style={{ height: "1.25rem" }}
>
{spec.display_name}
</span>
<div className="font-secondary-body text-text-03">
{spec.description}
</div>
{spec.docs_url && (
<div className="font-secondary-body text-text-03">
<a
href={spec.docs_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 w-fit"
>
<span className="underline">Documentation</span>
<SvgExternalLink
size={12}
className="shrink-0"
/>
</a>
</div>
)}
</div>
</div>
<div
className="flex items-center gap-1 p-2 cursor-pointer"
onClick={() => setConnectSpec(spec)}
>
<span className="font-main-ui-action text-text-03">
Connect
</span>
<SvgArrowExchange
size={16}
className="text-text-03 shrink-0"
/>
</div>
</div>
</Card>
);
})}
</>
)}
</div>
<Text as="span" secondaryBody text03 className="underline">
Documentation
</Text>
<SvgExternalLink size={16} className="text-text-02" />
</a>
</div>
)}
</Card>
))
)}
</div>
{/* Create modal */}
<HookFormModal
open={!!connectSpec}
onOpenChange={(open) => {
if (!open) setConnectSpec(null);
}}
spec={connectSpec ?? undefined}
onSuccess={handleHookSuccess}
/>
{/* Edit modal */}
<HookFormModal
open={!!editHook}
onOpenChange={(open) => {
if (!open) setEditHook(null);
}}
hook={editHook ?? undefined}
spec={connectSpec_ ?? undefined}
onSuccess={handleHookSuccess}
/>
</>
</div>
);
}

View File

@@ -18,8 +18,6 @@ export interface HookResponse {
name: string;
hook_point: HookPoint;
endpoint_url: string | null;
/** Partially-masked API key (e.g. "abcd••••••••wxyz"), or null if no key is set. */
api_key_masked: string | null;
fail_strategy: HookFailStrategy;
timeout_seconds: number;
is_active: boolean;

View File

@@ -1,81 +0,0 @@
import {
HookCreateRequest,
HookResponse,
HookUpdateRequest,
} from "@/refresh-pages/admin/HooksPage/interfaces";
async function parseErrorDetail(
res: Response,
fallback: string
): Promise<string> {
try {
const body = await res.json();
return body?.detail ?? fallback;
} catch {
return fallback;
}
}
export async function listHooks(): Promise<HookResponse[]> {
const res = await fetch("/api/admin/hooks");
if (!res.ok) {
throw new Error(await parseErrorDetail(res, "Failed to load hooks"));
}
return res.json();
}
export async function createHook(
req: HookCreateRequest
): Promise<HookResponse> {
const res = await fetch("/api/admin/hooks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
if (!res.ok) {
throw new Error(await parseErrorDetail(res, "Failed to create hook"));
}
return res.json();
}
export async function updateHook(
id: number,
req: HookUpdateRequest
): Promise<HookResponse> {
const res = await fetch(`/api/admin/hooks/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
if (!res.ok) {
throw new Error(await parseErrorDetail(res, "Failed to update hook"));
}
return res.json();
}
export async function deleteHook(id: number): Promise<void> {
const res = await fetch(`/api/admin/hooks/${id}`, { method: "DELETE" });
if (!res.ok) {
throw new Error(await parseErrorDetail(res, "Failed to delete hook"));
}
}
export async function activateHook(id: number): Promise<HookResponse> {
const res = await fetch(`/api/admin/hooks/${id}/activate`, {
method: "POST",
});
if (!res.ok) {
throw new Error(await parseErrorDetail(res, "Failed to activate hook"));
}
return res.json();
}
export async function deactivateHook(id: number): Promise<HookResponse> {
const res = await fetch(`/api/admin/hooks/${id}/deactivate`, {
method: "POST",
});
if (!res.ok) {
throw new Error(await parseErrorDetail(res, "Failed to deactivate hook"));
}
return res.json();
}

View File

@@ -256,6 +256,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
title="Featured"
sizePreset="main-ui"
variant="body"
widthVariant="fit"
/>
)}
<Content
@@ -264,6 +265,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
sizePreset="main-ui"
variant="body"
prominence="muted"
widthVariant="fit"
/>
{agent.is_public && (
<Content
@@ -272,6 +274,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
sizePreset="main-ui"
variant="body"
prominence="muted"
widthVariant="fit"
/>
)}
</Section>

View File

@@ -7,6 +7,7 @@ import {
SvgOrganization,
SvgShare,
SvgTag,
SvgUser,
SvgUsers,
SvgX,
} from "@opal/icons";
@@ -18,7 +19,6 @@ import InputComboBox from "@/refresh-components/inputs/InputComboBox/InputComboB
import * as InputLayouts from "@/layouts/input-layouts";
import SwitchField from "@/refresh-components/form/SwitchField";
import LineItem from "@/refresh-components/buttons/LineItem";
import { SvgUser } from "@opal/icons";
import { Section } from "@/layouts/general-layouts";
import Text from "@/refresh-components/texts/Text";
import useShareableUsers from "@/hooks/useShareableUsers";