Unified · Split
| @@ -37,6 +37,7 @@ import { IntegrationEditSheet } from "../components/metadata-edit-sheet"; · 36 unmodified lines | |||
| 37 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/tabs"; | 37 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/tabs"; |
| 38 | import { authMethodsFromDescriptors, type AuthMethod } from "../lib/auth-placements"; | 38 | import { authMethodsFromDescriptors, type AuthMethod } from "../lib/auth-placements"; |
| 39 | import { usePolicyActions } from "../hooks/use-policy-actions"; | 39 | import { usePolicyActions } from "../hooks/use-policy-actions"; |
| 40 | import { useCanCreateWorkspaceConnections } from "../multiplayer/use-admin-nav"; | ||
| 40 | import { useIntegrationPlugins, type IntegrationAccountHandoff } from "@executor-js/sdk/client"; | 41 | import { useIntegrationPlugins, type IntegrationAccountHandoff } from "@executor-js/sdk/client"; |
| 41 | import { Button } from "../components/button"; | 42 | import { Button } from "../components/button"; |
| 42 | import { Skeleton } from "../components/skeleton"; | 43 | import { Skeleton } from "../components/skeleton"; |
| @@ -84,9 +85,13 @@ export function IntegrationDetailPage(props: { · 41 unmodified lines | |||
| 84 | const connectionsResult = useAtomValue(connectionsAllAtom); | 85 | const connectionsResult = useAtomValue(connectionsAllAtom); |
| 85 | const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom); | 86 | const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom); |
| 86 | const refreshTools = useAtomRefresh(integrationToolsAllAtom(slug)); | 87 | const refreshTools = useAtomRefresh(integrationToolsAllAtom(slug)); |
| 87 | const doRemove = useAtomSet(removeIntegrationOptimistic, { mode: "promiseExit" }); | 88 | const doRemove = useAtomSet(removeIntegrationOptimistic, { |
| 89 | mode: "promiseExit", | ||
| 90 | }); | ||
| 88 | const doRefresh = useAtomSet(refreshConnection, { mode: "promiseExit" }); | 91 | const doRefresh = useAtomSet(refreshConnection, { mode: "promiseExit" }); |
| 89 | const doCheckHealth = useAtomSet(checkConnectionHealth, { mode: "promiseExit" }); | 92 | const doCheckHealth = useAtomSet(checkConnectionHealth, { |
| 93 | mode: "promiseExit", | ||
| 94 | }); | ||
| 90 | // Policies are owner-partitioned on write; the integration policy menu writes | 95 | // Policies are owner-partitioned on write; the integration policy menu writes |
| 91 | // Workspace (org) rules, preserving the prior default behavior. | 96 | // Workspace (org) rules, preserving the prior default behavior. |
| 92 | const policyActions = usePolicyActions("org"); | 97 | const policyActions = usePolicyActions("org"); |
| @@ -133,8 +138,12 @@ export function IntegrationDetailPage(props: { · 40 unmodified lines | |||
| 133 | useExecutorDocumentTitle(integrationData?.name || namespace); | 138 | useExecutorDocumentTitle(integrationData?.name || namespace); |
| 134 | const isBuiltInIntegration = namespace === "executor" || integrationData?.kind === "built-in"; | 139 | const isBuiltInIntegration = namespace === "executor" || integrationData?.kind === "built-in"; |
| 135 | const currentTab = isBuiltInIntegration ? "tools" : activeTab; | 140 | const currentTab = isBuiltInIntegration ? "tools" : activeTab; |
| 141 | // Integrations are workspace-owned; the server refuses catalog mutations | ||
| 142 | // (update/remove) from non-admin members, so hide the controls for them. | ||
| 143 | const canMutateIntegration = useCanCreateWorkspaceConnections(); | ||
| 144 | const canEdit = canMutateIntegration && !isBuiltInIntegration && integrationData !== null; | ||
| 136 | const canRefresh = integrationData?.canRefresh ?? false; | 145 | const canRefresh = integrationData?.canRefresh ?? false; |
| 137 | const canRemove = integrationData?.canRemove ?? false; | 146 | const canRemove = canMutateIntegration && (integrationData?.canRemove ?? false); |
| 138 | const urlAccountHandoff = useMemo<IntegrationAccountHandoff | null>(() => { | 147 | const urlAccountHandoff = useMemo<IntegrationAccountHandoff | null>(() => { |
| 139 | const search = new URLSearchParams(locationSearch); | 148 | const search = new URLSearchParams(locationSearch); |
| 140 | // The route-validated flag and the raw `addAccount=1` are the same request; | 149 | // The route-validated flag and the raw `addAccount=1` are the same request; |
| @@ -389,7 +398,11 @@ export function IntegrationDetailPage(props: { · 248 unmodified lines | |||
| 389 | if (connection.integration !== slug) continue; | 398 | if (connection.integration !== slug) continue; |
| 390 | connectionCount++; | 399 | connectionCount++; |
| 391 | const refreshExit = await doRefresh({ | 400 | const refreshExit = await doRefresh({ |
| 392 | params: { owner: connection.owner, integration: slug, name: connection.name }, | 401 | params: { |
| 402 | owner: connection.owner, | ||
| 403 | integration: slug, | ||
| 404 | name: connection.name, | ||
| 405 | }, | ||
| 393 | reactivityKeys: connectionWriteKeys, | 406 | reactivityKeys: connectionWriteKeys, |
| 394 | }); | 407 | }); |
| 395 | refreshExits.push(Exit.isSuccess(refreshExit)); | 408 | refreshExits.push(Exit.isSuccess(refreshExit)); |
| @@ -485,7 +498,7 @@ export function IntegrationDetailPage(props: { · 89 unmodified lines | |||
| 485 | </div> | 498 | </div> |
| 486 | 499 | ||
| 487 | <div className="flex shrink-0 items-center gap-2"> | 500 | <div className="flex shrink-0 items-center gap-2"> |
| 488 | {!confirmDelete && !isBuiltInIntegration && integrationData && ( | 501 | {!confirmDelete && canEdit && ( |
| 489 | <Button variant="outline" size="sm" onClick={() => setEditSheetOpen(true)}> | 502 | <Button variant="outline" size="sm" onClick={() => setEditSheetOpen(true)}> |
| 490 | Edit | 503 | Edit |
| 491 | </Button> | 504 | </Button> |