diff --git a/apps/console/src/pages/organizations/compliance-page/overview/_components/CompliancePageSlackConnectionCard.tsx b/apps/console/src/pages/organizations/compliance-page/overview/_components/CompliancePageSlackConnectionCard.tsx new file mode 100644 index 000000000..9478e963a --- /dev/null +++ b/apps/console/src/pages/organizations/compliance-page/overview/_components/CompliancePageSlackConnectionCard.tsx @@ -0,0 +1,108 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { sprintf } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { Badge, Button, Card, Slack } from "@probo/ui"; +import { useFragment } from "react-relay"; +import { graphql } from "relay-runtime"; + +import type { CompliancePageSlackConnectionCardFragment$key } from "#/__generated__/core/CompliancePageSlackConnectionCardFragment.graphql"; + +const fragment = graphql` + fragment CompliancePageSlackConnectionCardFragment on SlackConnection { + id + channel + channelId + createdAt + canDelete: permission(action: "core:connector:delete") + } +`; + +interface Props { + slackConnectionKey: CompliancePageSlackConnectionCardFragment$key; + canConnect: boolean; + buildConnectionUrl: (connectorId: string) => string; + onDisconnect: (slackConnectionId: string) => void; +} + +export function CompliancePageSlackConnectionCard(props: Props) { + const { slackConnectionKey, canConnect, buildConnectionUrl, onDisconnect } = props; + const { __, dateTimeFormat } = useTranslate(); + + const slackConnection = useFragment(fragment, slackConnectionKey); + + // A channel is only set once the incoming-webhook scope is granted, so its + // absence means the connector isn't usable for the compliance page yet. + const isConfigured = Boolean(slackConnection.channelId); + + return ( + +
+ +
+
+

Slack

+

+ {isConfigured + ? ( + <> + {sprintf( + __("Connected on %s"), + dateTimeFormat(slackConnection.createdAt), + )} + {slackConnection.channel && ( + <> + {" • "} + {sprintf(__("Channel: %s"), slackConnection.channel)} + + )} + + ) + : ( + __("Slack is connected, but no channel is set up for the compliance page yet.") + )} +

+
+
+ {isConfigured + ? ( + + {__("Connected")} + + ) + : ( + + {__("Channel not configured")} + + )} + {canConnect && ( + + )} + {slackConnection.canDelete && ( + + )} +
+
+ ); +} diff --git a/apps/console/src/pages/organizations/compliance-page/overview/_components/CompliancePageSlackSection.tsx b/apps/console/src/pages/organizations/compliance-page/overview/_components/CompliancePageSlackSection.tsx index 8bf54bb17..950994181 100644 --- a/apps/console/src/pages/organizations/compliance-page/overview/_components/CompliancePageSlackSection.tsx +++ b/apps/console/src/pages/organizations/compliance-page/overview/_components/CompliancePageSlackSection.tsx @@ -12,9 +12,8 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -import { sprintf } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; -import { Badge, Button, Card, Slack, useConfirm } from "@probo/ui"; +import { Button, Card, Slack, useConfirm } from "@probo/ui"; import { useFragment, useMutation } from "react-relay"; import { graphql } from "relay-runtime"; @@ -22,6 +21,8 @@ import type { CompliancePageSlackSectionDeleteMutation } from "#/__generated__/c import type { CompliancePageSlackSectionFragment$key } from "#/__generated__/core/CompliancePageSlackSectionFragment.graphql"; import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { CompliancePageSlackConnectionCard } from "./CompliancePageSlackConnectionCard"; + const fragment = graphql` fragment CompliancePageSlackSectionFragment on Organization { canConnectSlack: permission(action: "core:connector:initiate") @@ -31,9 +32,7 @@ const fragment = graphql` edges { node { id - channel - createdAt - canDelete: permission(action: "core:connector:delete") + ...CompliancePageSlackConnectionCardFragment } } } @@ -55,13 +54,14 @@ export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageS const { fragmentRef } = props; const organizationId = useOrganizationId(); - const { __, dateTimeFormat } = useTranslate(); + const { __ } = useTranslate(); const confirm = useConfirm(); const organization = useFragment(fragment, fragmentRef); const [deleteSlackConnection] = useMutation(deleteMutation); const connectionId = organization.slackConnections.__id; + const scopes = organization.slackOAuth2Scopes; const handleDisconnect = (slackConnectionId: string) => { confirm( @@ -87,48 +87,23 @@ export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageS ); }; + // Passing connector_id reconnects in place (union of scopes), letting users + // pick a channel without disconnecting first. + const buildConnectionUrl = (connectorId?: string) => + getSlackConnectionUrl(organizationId, scopes, connectorId); + return (

{__("Integrations")}

{organization.slackConnections.edges.map(({ node: slackConnection }) => ( - -
- -
-
-

Slack

-

- {sprintf( - __("Connected on %s"), - dateTimeFormat(slackConnection.createdAt), - )} - {slackConnection.channel && ( - <> - {" • "} - {sprintf(__("Channel: %s"), slackConnection.channel)} - - )} -

-
-
- - {__("Connected")} - - {slackConnection.canDelete && ( - - )} -
-
+ slackConnectionKey={slackConnection} + canConnect={organization.canConnectSlack} + buildConnectionUrl={connectorId => buildConnectionUrl(connectorId)} + onDisconnect={handleDisconnect} + /> ))} {organization.canConnectSlack && organization.slackConnections.edges.length === 0 && (
@@ -156,7 +131,11 @@ export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageS ); } -function getSlackConnectionUrl(organizationId: string, scopes: readonly string[]): string { +function getSlackConnectionUrl( + organizationId: string, + scopes: readonly string[], + connectorId?: string, +): string { const baseUrl = import.meta.env.VITE_API_URL || window.location.origin; const url = new URL("/api/console/v1/connectors/initiate", baseUrl); url.searchParams.append("organization_id", organizationId); @@ -164,6 +143,9 @@ function getSlackConnectionUrl(organizationId: string, scopes: readonly string[] for (const scope of scopes) { url.searchParams.append("scope", scope); } + if (connectorId) { + url.searchParams.append("connector_id", connectorId); + } const redirectUrl = `/organizations/${organizationId}/compliance-page`; url.searchParams.append("continue", redirectUrl); return url.toString();