diff --git a/web/src/components/layout/sidebar.tsx b/web/src/components/layout/sidebar.tsx index de0aa0eb..bb136182 100644 --- a/web/src/components/layout/sidebar.tsx +++ b/web/src/components/layout/sidebar.tsx @@ -1,3 +1,4 @@ +import { ProjectFormDialog } from '@/components/projects/project-form-dialog.js'; import { Select, SelectContent, @@ -20,6 +21,7 @@ import { ChevronDown, ChevronRight, FolderGit2, + Plus, Settings, Users, Zap, @@ -174,6 +176,7 @@ export function Sidebar({ user }: SidebarProps) { const currentPath = routerState.location.pathname; const { data: projects } = useQuery(trpc.projects.list.queryOptions()); + const [createDialogOpen, setCreateDialogOpen] = useState(false); return (
@@ -186,8 +189,18 @@ export function Sidebar({ user }: SidebarProps) { -
- Projects +
+ + Projects + +
{projects && projects.length > 0 ? ( @@ -195,21 +208,17 @@ export function Sidebar({ user }: SidebarProps) { )) ) : ( - setCreateDialogOpen(true)} + className="block px-3 py-2 text-sm text-muted-foreground hover:text-foreground text-left" > + Create a project - + )}
- + + diff --git a/web/src/components/projects/integration-form.tsx b/web/src/components/projects/integration-form.tsx index bd1e4167..df4be0cb 100644 --- a/web/src/components/projects/integration-form.tsx +++ b/web/src/components/projects/integration-form.tsx @@ -6,10 +6,10 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { AlertCircle, AlertTriangle, - ChevronDown, - ChevronRight, + Check, + Clipboard, ExternalLink, - KeyRound, + Info, Loader2, RefreshCw, Trash2, @@ -97,6 +97,26 @@ function GitHubCredentialSlots({ projectId }: { projectId: string }) { // GitHub Webhook Management // ============================================================================ +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + const handleCopy = async () => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return ( + + ); +} + function GitHubWebhookSection({ projectId }: { projectId: string }) { const queryClient = useQueryClient(); @@ -104,14 +124,6 @@ function GitHubWebhookSection({ projectId }: { projectId: string }) { API_URL || (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); - const [adminTokensOpen, setAdminTokensOpen] = useState(false); - const [oneTimeGithubToken, setOneTimeGithubToken] = useState(''); - - const buildOneTimeTokens = () => { - if (oneTimeGithubToken) return { github: oneTimeGithubToken }; - return undefined; - }; - const webhooksQuery = useQuery(trpc.webhooks.list.queryOptions({ projectId })); const createGithubWebhookMutation = useMutation({ @@ -120,10 +132,8 @@ function GitHubWebhookSection({ projectId }: { projectId: string }) { projectId, callbackBaseUrl, githubOnly: true, - oneTimeTokens: buildOneTimeTokens(), }), onSuccess: () => { - setOneTimeGithubToken(''); queryClient.invalidateQueries({ queryKey: trpc.webhooks.list.queryOptions({ projectId }).queryKey, }); @@ -136,7 +146,6 @@ function GitHubWebhookSection({ projectId }: { projectId: string }) { projectId, callbackBaseUrl: deleteCallbackBaseUrl, githubOnly: true, - oneTimeTokens: buildOneTimeTokens(), }), onSuccess: () => { queryClient.invalidateQueries({ @@ -151,6 +160,24 @@ function GitHubWebhookSection({ projectId }: { projectId: string }) { active: w.active, })); + const webhookCallbackUrl = callbackBaseUrl + ? `${callbackBaseUrl}/github/webhook` + : '/github/webhook'; + const githubCurlCommand = [ + 'curl -X POST "https://api.github.com/repos///hooks" \\', + ' -H "Authorization: Bearer " \\', + ' -H "Content-Type: application/json" \\', + " -d '{", + ' "name": "web",', + ' "active": true,', + ' "events": ["push", "pull_request", "check_suite", "pull_request_review"],', + ' "config": {', + ` "url": "${webhookCallbackUrl}",`, + ' "content_type": "json"', + ' }', + " }'", + ].join('\n'); + return (
@@ -244,42 +271,26 @@ function GitHubWebhookSection({ projectId }: { projectId: string }) { )}
- {/* One-time admin credentials */} -
- - {adminTokensOpen && ( -
-

- Provide a token with elevated permissions for webhook management. This is used once - and never saved. -

-
- - setOneTimeGithubToken(e.target.value)} - placeholder="ghp_... — used once, not saved" - type="password" - className="h-8 text-sm" - /> -
+ {/* curl instructions for manual GitHub webhook creation */} +
+
+ +

+ Manual webhook creation (if the button above doesn't work) +

+
+

+ Use the following curl command to create the GitHub webhook manually. Requires a token + with admin:repo_hook scope. +

+
+
+
- )} +
+						{githubCurlCommand}
+					
+
); diff --git a/web/src/components/projects/pm-wizard-common-steps.tsx b/web/src/components/projects/pm-wizard-common-steps.tsx index 71bac5e7..ed755e1b 100644 --- a/web/src/components/projects/pm-wizard-common-steps.tsx +++ b/web/src/components/projects/pm-wizard-common-steps.tsx @@ -2,20 +2,20 @@ * Provider-agnostic step renderer components for PMWizard: * WebhookStep and SaveStep. */ -import { Input } from '@/components/ui/input.js'; import { Label } from '@/components/ui/label.js'; import type { UseMutationResult } from '@tanstack/react-query'; import { AlertCircle, AlertTriangle, - ChevronDown, - ChevronRight, + Check, + Clipboard, ExternalLink, - KeyRound, + Info, Loader2, RefreshCw, Trash2, } from 'lucide-react'; +import { useState } from 'react'; import type { WizardState } from './pm-wizard-state.js'; // ============================================================================ @@ -36,22 +36,31 @@ interface WebhooksQueryProps { refetch: () => void; } -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: webhook step UI with provider-specific admin credential fields +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + const handleCopy = async () => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return ( + + ); +} + export function WebhookStep({ state, webhooksQuery, activeWebhooks, callbackBaseUrl, - adminTokensOpen, - setAdminTokensOpen, - oneTimeTrelloApiKey, - setOneTimeTrelloApiKey, - oneTimeTrelloToken, - setOneTimeTrelloToken, - oneTimeJiraEmail, - setOneTimeJiraEmail, - oneTimeJiraApiToken, - setOneTimeJiraApiToken, createWebhookMutation, deleteWebhookMutation, }: { @@ -59,19 +68,48 @@ export function WebhookStep({ webhooksQuery: WebhooksQueryProps; activeWebhooks: ActiveWebhook[]; callbackBaseUrl: string; - adminTokensOpen: boolean; - setAdminTokensOpen: (open: boolean | ((prev: boolean) => boolean)) => void; - oneTimeTrelloApiKey: string; - setOneTimeTrelloApiKey: (v: string) => void; - oneTimeTrelloToken: string; - setOneTimeTrelloToken: (v: string) => void; - oneTimeJiraEmail: string; - setOneTimeJiraEmail: (v: string) => void; - oneTimeJiraApiToken: string; - setOneTimeJiraApiToken: (v: string) => void; createWebhookMutation: UseMutationResult; deleteWebhookMutation: UseMutationResult; }) { + const isTrello = state.provider === 'trello'; + const providerName = isTrello ? 'Trello' : 'JIRA'; + + // Build curl commands for manual webhook creation + const buildTrelloCurl = () => { + const boardId = state.trelloBoardId || ''; + const callbackUrl = callbackBaseUrl + ? `${callbackBaseUrl}/trello/webhook` + : '/trello/webhook'; + return `curl -X POST "https://api.trello.com/1/webhooks" \\ + -H "Content-Type: application/json" \\ + -d '{ + "key": "", + "token": "", + "callbackURL": "${callbackUrl}", + "idModel": "${boardId}", + "description": "CASCADE webhook" + }'`; + }; + + const buildJiraCurl = () => { + const baseUrl = state.jiraBaseUrl || ''; + const callbackUrl = callbackBaseUrl + ? `${callbackBaseUrl}/jira/webhook` + : '/jira/webhook'; + return `curl -X POST "${baseUrl}/rest/webhooks/1.0/webhook" \\ + -H "Content-Type: application/json" \\ + -u ":" \\ + -d '{ + "name": "CASCADE webhook", + "url": "${callbackUrl}", + "events": ["jira:issue_updated", "jira:issue_created"], + "filters": {}, + "excludeBody": false + }'`; + }; + + const curlCommand = isTrello ? buildTrelloCurl() : buildJiraCurl(); + return (
{/* Per-provider errors */} @@ -136,7 +174,7 @@ export function WebhookStep({ ) : (
- No {state.provider === 'trello' ? 'Trello' : 'JIRA'} webhooks configured for this project. + No {providerName} webhooks configured for this project.
)} @@ -173,76 +211,26 @@ export function WebhookStep({ )}
- {/* One-time admin credentials */} -
- - {adminTokensOpen && ( -
-

- Provide tokens with elevated permissions for webhook management. These are used once - and never saved. -

- {/* PM-provider-specific fields */} - {state.provider === 'trello' ? ( - <> -
- - setOneTimeTrelloApiKey(e.target.value)} - placeholder="One-time API key" - type="password" - className="h-8 text-sm" - /> -
-
- - setOneTimeTrelloToken(e.target.value)} - placeholder="One-time token" - type="password" - className="h-8 text-sm" - /> -
- - ) : ( - <> -
- - setOneTimeJiraEmail(e.target.value)} - placeholder="user@example.com" - className="h-8 text-sm" - /> -
-
- - setOneTimeJiraApiToken(e.target.value)} - placeholder="One-time API token" - type="password" - className="h-8 text-sm" - /> -
- - )} + {/* curl instructions for manual webhook creation */} +
+
+ +

+ Manual webhook creation (if the button above doesn't work) +

+
+

+ Use the following curl command to create the {providerName} webhook manually with your own + credentials: +

+
+
+
- )} +
+						{curlCommand}
+					
+
); @@ -261,48 +249,6 @@ export function SaveStep({ }) { return (
- {/* Summary */} -
-
- Provider - {state.provider === 'trello' ? 'Trello' : 'JIRA'} -
- {state.verificationResult && ( -
- Identity - {state.verificationResult.display} -
- )} -
- - {state.provider === 'trello' ? 'Board' : 'Project'} - - - {state.provider === 'trello' - ? state.trelloBoards.find((b) => b.id === state.trelloBoardId)?.name || - state.trelloBoardId - : state.jiraProjects.find((p) => p.key === state.jiraProjectKey)?.name || - state.jiraProjectKey} - -
-
- - {state.provider === 'trello' ? 'Lists mapped' : 'Statuses mapped'} - - - {state.provider === 'trello' - ? Object.keys(state.trelloListMappings).filter((k) => state.trelloListMappings[k]) - .length - : Object.keys(state.jiraStatusMappings).filter((k) => state.jiraStatusMappings[k]) - .length} - -
-
- -

- Trigger configuration is managed separately in the Agents tab. -

-
-
- - { - setId(e.target.value); - setIdManual(true); - }} - placeholder="my-project" - pattern="^[a-z0-9-]+$" - required - /> -

- Lowercase letters, numbers, and hyphens only. -

-
-
- - setRepo(e.target.value)} - placeholder="owner/repo" - /> -

Leave empty for email-only projects.

-
-
- - setBaseBranch(e.target.value)} - placeholder="main" - /> -
@@ -329,6 +355,52 @@ export function ProjectGeneralForm({ project }: { project: Project }) {
+ {/* Danger Zone */} + + + Danger Zone + + +
+
+

Delete this project

+

+ Permanently delete this project and all its integrations, credentials, and agent + configs. This action cannot be undone. +

+
+ +
+
+
+ + + + + Delete Project + + This will permanently delete {project.name} and all its + integrations, credential overrides, and agent configs. This action cannot be undone. + + + + Cancel + deleteMutation.mutate()} + className="bg-destructive text-white hover:bg-destructive/90" + > + {deleteMutation.isPending ? 'Deleting...' : 'Delete'} + + + + + {/* API Keys */}