Skip to content

feat(webhooks): make admin webhooks view-only via enable_actions flag#1754

Open
Shreyag02 wants to merge 1 commit into
mainfrom
feature/webhooks-view-only
Open

feat(webhooks): make admin webhooks view-only via enable_actions flag#1754
Shreyag02 wants to merge 1 commit into
mainfrom
feature/webhooks-view-only

Conversation

@Shreyag02

Copy link
Copy Markdown
Contributor

Summary

Makes the admin console Webhooks page view-only by default. All write actions (create, update, delete) are now gated behind a single config flag, so full CRUD can be restored later by flipping one value — no code changes or redeploy of the app bundle required.

This addresses the decision to hide webhook editing in the admin UI rather than enable delete.

Changes

  • Added a single enable_actions flag that gates all webhook write actions together (New Webhook button, create/update drawers, and the row Action menu with Update + Delete).
  • Replaced the previous delete-only enable_delete gate on WebhooksView with the broader enableActions prop.
  • Wired the flag through the app config and the Go server's /configs response; defaults to false (view-only).
  • Kept enable_delete on both frontend and backend for backward compatibility — only enable_actions is read now.
  • No components were removed, so the change is fully reversible.

Technical Details

  • SDK (web/sdk/admin/views/webhooks/webhooks/): WebhooksView now takes enableActions. When false, the New Webhook button, create/update drawers, and the Action column are not rendered (the column is simply omitted from the table).
  • App (WebhooksPage.tsx): reads config?.webhooks?.enable_actions ?? false. The ?? false guard means an older backend (or missing config) safely falls back to view-only.
  • Config (constants.ts, configs.dev.json): added enable_actions (default false) alongside enable_delete.
  • Go server (pkg/server/config.go, server.go): added EnableActions to WebhooksConfig and the /configs response.
  • To re-enable full CRUD: set ui.webhooks.enable_actions: true (env FRONTIER_SERVICE_UI_WEBHOOKS_ENABLE_ACTIONS=true) in the deployment.

Test Plan

  • Manual testing completed — verified view-only by default (no write actions visible) and full CRUD restored when enable_actions: true.
  • Build and type checking passes — admin tsc + Vite production build, SDK build, and go build / go vet all clean.

SQL Safety (if your PR touches *_repository.go or goqu.*)

N/A

@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jul 13, 2026 5:15pm

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added a webhook actions setting for administrators.
    • When enabled, administrators can create, update, and delete webhooks.
    • When disabled, webhook action controls and creation options are hidden.
    • The setting is disabled by default for safer configuration.

Walkthrough

Changes

Webhook action gating

Layer / File(s) Summary
Server configuration and response contract
pkg/server/config.go, pkg/server/server.go
Adds the EnableActions setting and exposes it as enable_actions in the /configs response.
Admin configuration propagation
web/apps/admin/configs.dev.json, web/apps/admin/src/utils/constants.ts, web/apps/admin/src/pages/webhooks/WebhooksPage.tsx
Adds the admin configuration field and passes its value to WebhooksView.
Webhook write-control rendering
web/sdk/admin/views/webhooks/webhooks/*
Uses enableActions to conditionally render the Action column, New Webhook button, and create/update panels.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: paansinghcoder, rohilsurana, rohanchkrabrty, ravisuhag, amangit07

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0445ed10-2956-4c1c-b75a-211d48b2d3bf

📥 Commits

Reviewing files that changed from the base of the PR and between cd6b070 and 68b0e3d.

📒 Files selected for processing (7)
  • pkg/server/config.go
  • pkg/server/server.go
  • web/apps/admin/configs.dev.json
  • web/apps/admin/src/pages/webhooks/WebhooksPage.tsx
  • web/apps/admin/src/utils/constants.ts
  • web/sdk/admin/views/webhooks/webhooks/columns.tsx
  • web/sdk/admin/views/webhooks/webhooks/index.tsx

Comment on lines +30 to +88
const actionColumn: DataTableColumnDef<Webhook, unknown> = {
header: "Action",
accessorKey: "id",
classNames: { cell: styles.actionColumn, header: styles.actionColumn },
cell: ({ getValue, row }) => {
const ActionCell = () => {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const webhookId = getValue() as string;
const webhook = row.original;

return (
<>
{/* @ts-ignore */}
<Menu style={{ padding: "0 !important" }}>
<Menu.Trigger
render={<DotsVerticalIcon style={{ cursor: "pointer" }} />}
/>
<Menu.Content>
<Menu.Group style={{ padding: 0 }}>
<Menu.Item style={{ padding: 0 }}>
<Flex
style={{ padding: "12px" }}
gap={3}
data-test-id="admin-webhook-update-btn"
onClick={() => openEditPage(webhookId)}
>
<UpdateIcon />
Update
</Flex>
</Menu.Item>
<Menu.Item style={{ padding: 0 }}>
<Flex
className={styles.deleteMenuItem}
gap={3}
data-test-id="admin-webhook-delete-btn"
onClick={() => setIsDeleteDialogOpen(true)}
>
<TrashIcon />
Delete
</Flex>
</Menu.Item>
</Menu.Group>
</Menu.Content>
</Menu>

<DeleteWebhookDialog
isOpen={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
webhookId={webhookId}
webhookDescription={webhook.description}
deleteWebhookMutation={deleteWebhookMutation}
/>
</>
);
};

return <ActionCell />;
},
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

ActionCell defined inside cell causes state loss on re-renders.

ActionCell is declared as a function component inside the cell callback. Every table re-render (data refetch, sort, filter) creates a new function reference, so React unmounts and remounts the component — discarding isDeleteDialogOpen and the Menu's open state. The delete dialog and action menu will close unexpectedly mid-interaction.

Extract ActionCell outside getColumns so its identity is stable:

♻️ Proposed fix
+interface ActionCellProps {
+  getValue: () => unknown;
+  row: { original: Webhook };
+  openEditPage: (id: string) => void;
+  deleteWebhookMutation: ReturnType<typeof useMutation>;
+}
+
+function ActionCell({
+  getValue,
+  row,
+  openEditPage,
+  deleteWebhookMutation,
+}: ActionCellProps) {
+  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+  const webhookId = getValue() as string;
+  const webhook = row.original;
+
+  return (
+    <>
+      {/* `@ts-ignore` */}
+      <Menu style={{ padding: "0 !important" }}>
+        <Menu.Trigger
+          render={<DotsVerticalIcon style={{ cursor: "pointer" }} />}
+        />
+        <Menu.Content>
+          <Menu.Group style={{ padding: 0 }}>
+            <Menu.Item style={{ padding: 0 }}>
+              <Flex
+                style={{ padding: "12px" }}
+                gap={3}
+                data-test-id="admin-webhook-update-btn"
+                onClick={() => openEditPage(webhookId)}
+              >
+                <UpdateIcon />
+                Update
+              </Flex>
+            </Menu.Item>
+            <Menu.Item style={{ padding: 0 }}>
+              <Flex
+                className={styles.deleteMenuItem}
+                gap={3}
+                data-test-id="admin-webhook-delete-btn"
+                onClick={() => setIsDeleteDialogOpen(true)}
+              >
+                <TrashIcon />
+                Delete
+              </Flex>
+            </Menu.Item>
+          </Menu.Group>
+        </Menu.Content>
+      </Menu>
+
+      <DeleteWebhookDialog
+        isOpen={isDeleteDialogOpen}
+        onOpenChange={setIsDeleteDialogOpen}
+        webhookId={webhookId}
+        webhookDescription={webhook.description}
+        deleteWebhookMutation={deleteWebhookMutation}
+      />
+    </>
+  );
+}
+
 export const getColumns: (
   opt: getColumnsOptions,
 ) => DataTableColumnDef<Webhook, unknown>[] = ({ openEditPage, deleteWebhookMutation, enableActions }) => {
   const actionColumn: DataTableColumnDef<Webhook, unknown> = {
     header: "Action",
     accessorKey: "id",
     classNames: { cell: styles.actionColumn, header: styles.actionColumn },
-    cell: ({ getValue, row }) => {
-      const ActionCell = () => {
-        const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
-        const webhookId = getValue() as string;
-        const webhook = row.original;
-
-        return (
-          <>
-            {/* `@ts-ignore` */}
-            <Menu style={{ padding: "0 !important" }}>
-              <Menu.Trigger
-                render={<DotsVerticalIcon style={{ cursor: "pointer" }} />}
-              />
-              <Menu.Content>
-                <Menu.Group style={{ padding: 0 }}>
-                  <Menu.Item style={{ padding: 0 }}>
-                    <Flex
-                      style={{ padding: "12px" }}
-                      gap={3}
-                      data-test-id="admin-webhook-update-btn"
-                      onClick={() => openEditPage(webhookId)}
-                    >
-                      <UpdateIcon />
-                      Update
-                    </Flex>
-                  </Menu.Item>
-                  <Menu.Item style={{ padding: 0 }}>
-                    <Flex
-                      className={styles.deleteMenuItem}
-                      gap={3}
-                      data-test-id="admin-webhook-delete-btn"
-                      onClick={() => setIsDeleteDialogOpen(true)}
-                    >
-                      <TrashIcon />
-                      Delete
-                    </Flex>
-                  </Menu.Item>
-                </Menu.Group>
-              </Menu.Content>
-            </Menu>
-
-            <DeleteWebhookDialog
-              isOpen={isDeleteDialogOpen}
-              onOpenChange={setIsDeleteDialogOpen}
-              webhookId={webhookId}
-              webhookDescription={webhook.description}
-              deleteWebhookMutation={deleteWebhookMutation}
-            />
-          </>
-        );
-      };
-
-      return <ActionCell />;
-    },
+    cell: ({ getValue, row }) => (
+      <ActionCell
+        getValue={getValue}
+        row={row}
+        openEditPage={openEditPage}
+        deleteWebhookMutation={deleteWebhookMutation}
+      />
+    ),
   };

   return [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const actionColumn: DataTableColumnDef<Webhook, unknown> = {
header: "Action",
accessorKey: "id",
classNames: { cell: styles.actionColumn, header: styles.actionColumn },
cell: ({ getValue, row }) => {
const ActionCell = () => {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const webhookId = getValue() as string;
const webhook = row.original;
return (
<>
{/* @ts-ignore */}
<Menu style={{ padding: "0 !important" }}>
<Menu.Trigger
render={<DotsVerticalIcon style={{ cursor: "pointer" }} />}
/>
<Menu.Content>
<Menu.Group style={{ padding: 0 }}>
<Menu.Item style={{ padding: 0 }}>
<Flex
style={{ padding: "12px" }}
gap={3}
data-test-id="admin-webhook-update-btn"
onClick={() => openEditPage(webhookId)}
>
<UpdateIcon />
Update
</Flex>
</Menu.Item>
<Menu.Item style={{ padding: 0 }}>
<Flex
className={styles.deleteMenuItem}
gap={3}
data-test-id="admin-webhook-delete-btn"
onClick={() => setIsDeleteDialogOpen(true)}
>
<TrashIcon />
Delete
</Flex>
</Menu.Item>
</Menu.Group>
</Menu.Content>
</Menu>
<DeleteWebhookDialog
isOpen={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
webhookId={webhookId}
webhookDescription={webhook.description}
deleteWebhookMutation={deleteWebhookMutation}
/>
</>
);
};
return <ActionCell />;
},
};
interface ActionCellProps {
getValue: () => unknown;
row: { original: Webhook };
openEditPage: (id: string) => void;
deleteWebhookMutation: ReturnType<typeof useMutation>;
}
function ActionCell({
getValue,
row,
openEditPage,
deleteWebhookMutation,
}: ActionCellProps) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const webhookId = getValue() as string;
const webhook = row.original;
return (
<>
{/* `@ts-ignore` */}
<Menu style={{ padding: "0 !important" }}>
<Menu.Trigger
render={<DotsVerticalIcon style={{ cursor: "pointer" }} />}
/>
<Menu.Content>
<Menu.Group style={{ padding: 0 }}>
<Menu.Item style={{ padding: 0 }}>
<Flex
style={{ padding: "12px" }}
gap={3}
data-test-id="admin-webhook-update-btn"
onClick={() => openEditPage(webhookId)}
>
<UpdateIcon />
Update
</Flex>
</Menu.Item>
<Menu.Item style={{ padding: 0 }}>
<Flex
className={styles.deleteMenuItem}
gap={3}
data-test-id="admin-webhook-delete-btn"
onClick={() => setIsDeleteDialogOpen(true)}
>
<TrashIcon />
Delete
</Flex>
</Menu.Item>
</Menu.Group>
</Menu.Content>
</Menu>
<DeleteWebhookDialog
isOpen={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
webhookId={webhookId}
webhookDescription={webhook.description}
deleteWebhookMutation={deleteWebhookMutation}
/>
</>
);
}
export const getColumns: (
opt: getColumnsOptions,
) => DataTableColumnDef<Webhook, unknown>[] = ({
openEditPage,
deleteWebhookMutation,
enableActions,
}) => {
const actionColumn: DataTableColumnDef<Webhook, unknown> = {
header: "Action",
accessorKey: "id",
classNames: { cell: styles.actionColumn, header: styles.actionColumn },
cell: ({ getValue, row }) => (
<ActionCell
getValue={getValue}
row={row}
openEditPage={openEditPage}
deleteWebhookMutation={deleteWebhookMutation}
/>
),
};

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 29269745070

Coverage decreased (-0.002%) to 44.874%

Details

  • Coverage decreased (-0.002%) from the base build.
  • Patch coverage: 2 uncovered changes across 1 file (0 of 2 lines covered, 0.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
pkg/server/server.go 2 0 0.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 37645
Covered Lines: 16893
Line Coverage: 44.87%
Coverage Strength: 12.54 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants