Skip to content

Commit 75ee695

Browse files
thomasballingerConvex, Inc.
authored andcommitted
Dashboard UI for WorkOS integration (#41941)
Team- and deployment-level UI for integrated WorkOS provisioning. GitOrigin-RevId: 52bda44d2f4ec89a8384171cccdf173481b5468b
1 parent 376f02a commit 75ee695

File tree

21 files changed

+758
-57
lines changed

21 files changed

+758
-57
lines changed

npm-packages/@convex-dev/design-system/src/Menu.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,18 +128,21 @@ export function MenuLink({
128128
disabled = false,
129129
selected = false,
130130
shortcut,
131+
target,
131132
}: {
132133
children: React.ReactChild | React.ReactChild[];
133134
href: string;
134135
disabled?: boolean;
135136
selected?: boolean;
136137
shortcut?: Key[];
138+
target?: "_blank";
137139
}) {
138140
return (
139141
<HeadlessMenu.Item disabled={disabled}>
140142
{({ active, close }) => (
141143
<a
142144
href={href}
145+
target={target}
143146
aria-disabled={disabled}
144147
onClick={disabled ? (e) => e.preventDefault() : () => close()}
145148
className={classNames(

npm-packages/dashboard-common/src/features/settings/components/integrations/IntegrationTitle.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@ export function IntegrationTitle({
1616
{logo}
1717

1818
<p className="text-sm font-semibold">
19-
{integrationKind.charAt(0).toUpperCase() + integrationKind.slice(1)}
19+
{integrationKind === "workos"
20+
? "WorkOS"
21+
: integrationKind.charAt(0).toUpperCase() + integrationKind.slice(1)}
2022
</p>
2123
<Tooltip tip={description}>
2224
<p className="max-w-fit rounded-sm border p-1 text-xs">
2325
{integrationKind === "sentry"
2426
? "Exception Reporting"
2527
: integrationKind === "airbyte" || integrationKind === "fivetran"
2628
? "Streaming Export"
27-
: "Log Stream"}
29+
: integrationKind === "workos"
30+
? "Authentication"
31+
: "Log Stream"}
2832
</p>
2933
</Tooltip>
3034
</div>

npm-packages/dashboard-common/src/features/settings/components/integrations/Integrations.tsx

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React, { useContext } from "react";
22
import { LocalDevCallout } from "@common/elements/LocalDevCallout";
3-
import { Callout } from "@ui/Callout";
4-
import { Button } from "@ui/Button";
53
import { Sheet } from "@ui/Sheet";
64
import {
5+
AuthIntegration,
76
EXC_INTEGRATIONS,
87
EXPORT_INTEGRATIONS,
98
ExceptionReportingIntegration,
@@ -23,14 +22,18 @@ export function Integrations({
2322
team,
2423
entitlements,
2524
integrations,
25+
workosData,
2626
}: {
2727
team: ReturnType<DeploymentInfo["useCurrentTeam"]>;
2828
entitlements: ReturnType<DeploymentInfo["useTeamEntitlements"]>;
2929
integrations: Doc<"_log_sinks">[];
30+
workosData: ReturnType<DeploymentInfo["useDeploymentWorkOSEnvironment"]>;
3031
}) {
31-
const { useCurrentDeployment, useHasProjectAdminPermissions } = useContext(
32-
DeploymentInfoContext,
33-
);
32+
const {
33+
useCurrentDeployment,
34+
useHasProjectAdminPermissions,
35+
workosIntegrationEnabled,
36+
} = useContext(DeploymentInfoContext);
3437
const deployment = useCurrentDeployment();
3538
const hasAdminPermissions = useHasProjectAdminPermissions(
3639
deployment?.projectId,
@@ -63,6 +66,16 @@ export function Integrations({
6366
return 0;
6467
});
6568

69+
const authIntegrations: AuthIntegration[] = workosIntegrationEnabled
70+
? [
71+
{
72+
kind: "workos",
73+
// Consider this integration to exist if a WorkOS enviroment has been provisioned
74+
existing: workosData?.environment ?? null,
75+
},
76+
]
77+
: [];
78+
6679
const exceptionReportingIntegrations: ExceptionReportingIntegration[] =
6780
EXC_INTEGRATIONS.map((kind) => {
6881
const existing = configuredIntegrationsMap[kind];
@@ -78,28 +91,6 @@ export function Integrations({
7891
return 0;
7992
});
8093

81-
// Show the proCallout if either of the entitlements aren't granted. Both are granted
82-
// with a pro account.
83-
const proCallout =
84-
logStreamingEntitlementGranted &&
85-
streamingExportEntitlementGranted ? null : (
86-
<Callout variant="upsell">
87-
<div className="flex w-fit flex-col gap-2">
88-
<p className="max-w-prose">
89-
Log Stream, Exception Reporting, and Streaming Export integrations
90-
are available on the Pro plan.
91-
</p>
92-
<Button
93-
href={`/${team?.slug}/settings/billing`}
94-
size="xs"
95-
className="w-fit"
96-
>
97-
Upgrade Now
98-
</Button>
99-
</div>
100-
</Callout>
101-
);
102-
10394
const devCallouts = [];
10495
if (!logStreamingEntitlementGranted) {
10596
devCallouts.push(
@@ -129,7 +120,6 @@ export function Integrations({
129120

130121
return (
131122
<div className="flex flex-col gap-4">
132-
{proCallout}
133123
<Sheet className="flex flex-col gap-4">
134124
<div className="flex flex-col gap-2">
135125
<h3>Integrations</h3>
@@ -147,20 +137,26 @@ export function Integrations({
147137
</div>
148138
</div>
149139
<div className="flex flex-col gap-2">
150-
{[...exceptionReportingIntegrations, ...logIntegrations]
140+
{[
141+
...authIntegrations,
142+
...exceptionReportingIntegrations,
143+
...logIntegrations,
144+
]
151145
.sort((a, b) =>
152146
a.existing !== null && b.existing === null ? -1 : 0,
153147
)
154148
.map((i) => (
155149
<PanelCard
156150
integration={i}
157151
unavailableReason={logIntegrationUnvaliableReason}
152+
teamSlug={team?.slug}
158153
/>
159154
))}
160155
{EXPORT_INTEGRATIONS.map((i) => (
161156
<PanelCard
162157
integration={{ kind: i }}
163158
unavailableReason={streamingExportIntegrationUnavailableReason}
159+
teamSlug={team?.slug}
164160
/>
165161
))}
166162
</div>

npm-packages/dashboard-common/src/features/settings/components/integrations/IntegrationsView.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,27 @@ import { useContext } from "react";
77
import { LoadingTransition } from "@ui/Loading";
88

99
export function IntegrationsView() {
10-
const { useCurrentTeam, useTeamEntitlements } = useContext(
11-
DeploymentInfoContext,
12-
);
10+
const {
11+
useCurrentTeam,
12+
useTeamEntitlements,
13+
useCurrentDeployment,
14+
useDeploymentWorkOSEnvironment,
15+
} = useContext(DeploymentInfoContext);
1316
const team = useCurrentTeam();
17+
const deployment = useCurrentDeployment();
1418
const entitlements = useTeamEntitlements(team?.id);
1519
const integrations = useQuery(udfs.listConfiguredSinks.default);
20+
const workosData = useDeploymentWorkOSEnvironment(deployment?.name);
1621

1722
return (
1823
<DeploymentSettingsLayout page="integrations">
1924
<LoadingTransition>
20-
{team && entitlements && integrations !== undefined && (
25+
{team && entitlements && integrations !== undefined && workosData && (
2126
<Integrations
2227
team={team}
2328
entitlements={entitlements}
2429
integrations={integrations}
30+
workosData={workosData}
2531
/>
2632
)}
2733
</LoadingTransition>

npm-packages/dashboard-common/src/features/settings/components/integrations/PanelCard.tsx

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { ExportIntegrationType } from "system-udfs/convex/_system/frontend/commo
33
import { ExternalLinkIcon } from "@radix-ui/react-icons";
44
import { Button } from "@ui/Button";
55
import { Modal } from "@ui/Modal";
6+
import { Tooltip } from "@ui/Tooltip";
7+
import Link from "next/link";
68
import {
79
IntegrationUnavailableReason,
810
LogIntegration,
911
ExceptionReportingIntegration,
12+
AuthIntegration,
1013
integrationToLogo,
1114
STREAMING_EXPORT_DESCRIPTION,
1215
LOG_STREAMS_DESCRIPTION,
16+
AUTHENTICATION_DESCRIPTION,
1317
} from "@common/lib/integrationHelpers";
1418
import { useState, ReactNode, useCallback } from "react";
1519
import { IntegrationTitle } from "./IntegrationTitle";
@@ -19,20 +23,44 @@ import { AxiomConfigurationForm } from "./AxiomConfigurationForm";
1923
import { DatadogConfigurationForm } from "./DatadogConfigurationForm";
2024
import { SentryConfigurationForm } from "./SentryConfigurationForm";
2125
import { WebhookConfigurationForm } from "./WebhookConfigurationForm";
26+
import { WorkOSConfigurationForm } from "./WorkOSConfigurationForm";
27+
import { WorkOSIntegrationStatus } from "./WorkOSIntegrationStatus";
28+
import { WorkOSIntegrationOverflowMenu } from "./WorkOSIntegrationOverflowMenu";
2229

2330
export type PanelCardProps = {
2431
className?: string;
2532
integration:
2633
| LogIntegration
2734
| ExceptionReportingIntegration
35+
| AuthIntegration
2836
| { kind: ExportIntegrationType };
2937
unavailableReason: IntegrationUnavailableReason | null;
38+
teamSlug?: string;
3039
};
3140

41+
function ProBadge({ teamSlug }: { teamSlug?: string }) {
42+
const badge = (
43+
<span className="cursor-pointer rounded-sm bg-util-accent px-1.5 py-0.5 text-xs font-semibold tracking-wider text-white uppercase">
44+
Pro
45+
</span>
46+
);
47+
48+
if (!teamSlug) {
49+
return <Tooltip tip="Only available on the Pro plan">{badge}</Tooltip>;
50+
}
51+
52+
return (
53+
<Tooltip tip="Only available on the Pro plan">
54+
<Link href={`/${teamSlug}/settings/billing`}>{badge}</Link>
55+
</Tooltip>
56+
);
57+
}
58+
3259
export function PanelCard({
3360
className,
3461
integration,
3562
unavailableReason,
63+
teamSlug,
3664
}: PanelCardProps) {
3765
const classes = classNames(
3866
"py-3 px-4",
@@ -60,6 +88,28 @@ export function PanelCard({
6088

6189
return (
6290
<div className={classes}>
91+
{integration.kind === "workos" && (
92+
<div className="flex flex-wrap items-center justify-between gap-2">
93+
{modalState.content}
94+
<IntegrationTitle
95+
logo={logo}
96+
integrationKind={integration.kind}
97+
description={AUTHENTICATION_DESCRIPTION}
98+
/>
99+
<div className="flex items-center gap-4">
100+
<WorkOSIntegrationStatus integration={integration} />
101+
<WorkOSIntegrationOverflowMenu
102+
integration={integration}
103+
onConfigure={() =>
104+
setModalState({
105+
showing: true,
106+
content: renderModal(integration, closeModal),
107+
})
108+
}
109+
/>
110+
</div>
111+
</div>
112+
)}
63113
{(integration.kind === "airbyte" || integration.kind === "fivetran") && (
64114
<div className="flex flex-wrap items-center justify-between gap-2">
65115
<IntegrationTitle
@@ -68,7 +118,9 @@ export function PanelCard({
68118
description={STREAMING_EXPORT_DESCRIPTION}
69119
/>
70120
<div className="ml-auto">
71-
{unavailableReason === null && (
121+
{unavailableReason === "MissingEntitlement" ? (
122+
<ProBadge teamSlug={teamSlug} />
123+
) : (
72124
<Button
73125
href={exportSetupLink(integration.kind)}
74126
target="_blank"
@@ -96,14 +148,15 @@ export function PanelCard({
96148
/>
97149
<div className="flex items-center gap-4">
98150
<IntegrationStatus integration={integration} />
99-
{unavailableReason === null && (
151+
{unavailableReason === "MissingEntitlement" ? (
152+
<ProBadge teamSlug={teamSlug} />
153+
) : (
100154
<IntegrationOverflowMenu
101155
integration={integration}
102156
onConfigure={() =>
103157
setModalState({
104158
showing: true,
105-
content:
106-
renderModal && renderModal(integration, closeModal),
159+
content: renderModal(integration, closeModal),
107160
})
108161
}
109162
/>
@@ -129,7 +182,7 @@ function exportSetupLink(kind: ExportIntegrationType): string {
129182
}
130183

131184
function renderModal(
132-
integration: LogIntegration | ExceptionReportingIntegration,
185+
integration: LogIntegration | ExceptionReportingIntegration | AuthIntegration,
133186
closeModal: () => void,
134187
) {
135188
switch (integration.kind) {
@@ -189,6 +242,12 @@ function renderModal(
189242
</div>
190243
</Modal>
191244
);
245+
case "workos":
246+
return (
247+
<Modal onClose={closeModal} title="WorkOS AuthKit Environment">
248+
<WorkOSConfigurationForm />
249+
</Modal>
250+
);
192251
default: {
193252
integration satisfies never;
194253
return null;

0 commit comments

Comments
 (0)