Skip to content

Commit a2d879f

Browse files
atrakhConvex, Inc.
authored andcommitted
dashboard: show external vercel links for teams managed by vercel (#43068)
GitOrigin-RevId: 9325a61c63a75bb42c145c8ada69b9653570eff9
1 parent 170a3e8 commit a2d879f

File tree

8 files changed

+148
-65
lines changed

8 files changed

+148
-65
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ExternalLinkIcon } from "@radix-ui/react-icons";
2+
import { Button } from "@ui/Button";
3+
import { TeamResponse } from "generatedApi";
4+
import VercelLogo from "logos/vercel.svg";
5+
6+
export function OpenInVercel({ team }: { team: TeamResponse }) {
7+
if (team.managedBy !== "vercel" || !team.managedByUrl) {
8+
return null;
9+
}
10+
11+
return (
12+
<Button
13+
href={team.managedByUrl}
14+
target="_blank"
15+
size="sm"
16+
variant="neutral"
17+
icon={<VercelLogo className="size-3 fill-[#EDEDED]" />}
18+
className="bg-[#1A1A1A] text-[#EDEDED] hover:bg-[#1A1A1A] hover:opacity-40"
19+
tip="This team is managed by Vercel. Visit the Vercel dashboard to manage your team and create new projects."
20+
>
21+
Open in Vercel
22+
<ExternalLinkIcon className="ml-1 size-3" />
23+
</Button>
24+
);
25+
}

npm-packages/dashboard/src/components/header/ProjectSelector/ProjectMenuOptions.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { SelectorItem } from "elements/SelectorItem";
88
import { useDeploymentUris } from "hooks/useDeploymentUris";
99
import { useLastViewedDeploymentForProject } from "hooks/useLastViewed";
1010
import { InfiniteScrollList } from "dashboard-common/src/elements/InfiniteScrollList";
11+
import { OpenInVercel } from "components/OpenInVercel";
12+
import startCase from "lodash/startCase";
1113

1214
const PROJECT_SELECTOR_ITEM_SIZE = 44;
1315

@@ -97,18 +99,27 @@ export function ProjectMenuOptions({
9799
}
98100
/>
99101
</div>
100-
<Button
101-
inline
102-
onClick={() => {
103-
onCreateProjectClick(team);
104-
close();
105-
}}
106-
icon={<PlusIcon aria-hidden="true" />}
107-
className="w-full"
108-
size="sm"
109-
>
110-
Create Project
111-
</Button>
102+
<div className="flex w-full gap-2 p-2">
103+
<Button
104+
inline
105+
onClick={() => {
106+
onCreateProjectClick(team);
107+
close();
108+
}}
109+
icon={<PlusIcon aria-hidden="true" />}
110+
className="grow"
111+
size="sm"
112+
disabled={!!team.managedBy}
113+
tip={
114+
team.managedBy
115+
? `This team is managed by ${startCase(team.managedBy)}. You can create new projects through the ${startCase(team.managedBy)} dashboard.`
116+
: ""
117+
}
118+
>
119+
Create Project
120+
</Button>
121+
<OpenInVercel team={team} />
122+
</div>
112123
</>
113124
);
114125
}

npm-packages/dashboard/src/components/header/ProjectSelector/ProjectSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ function ProjectSelectorPanel({
151151
<CaretSortIcon
152152
className={cn(
153153
"text-content-primary",
154-
"min-h-[1rem] min-w-[1rem] rounded-full group-hover:bg-background-tertiary",
154+
"min-h-[1rem] min-w-[1rem] rounded-full",
155155
)}
156156
/>
157157
</Button>

npm-packages/dashboard/src/components/teamSettings/TeamMembers.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EnvelopeClosedIcon } from "@radix-ui/react-icons";
22
import { Callout } from "@ui/Callout";
33
import { Loading } from "@ui/Loading";
4+
import { Sheet } from "@ui/Sheet";
45
import { useTeamMembers, useTeamEntitlements } from "api/teams";
56
import { useTeamInvites } from "api/invitations";
67
import { useIsCurrentMemberTeamAdmin } from "api/roles";
@@ -9,6 +10,7 @@ import { TeamResponse } from "generatedApi";
910
import startCase from "lodash/startCase";
1011

1112
import { captureMessage } from "@sentry/nextjs";
13+
import { OpenInVercel } from "components/OpenInVercel";
1214
import { InviteMemberForm } from "./InviteMemberForm";
1315
import { TeamMemberList } from "./TeamMemberList";
1416

@@ -31,14 +33,15 @@ export function TeamMembers({ team }: { team: TeamResponse }) {
3133
);
3234
} else if (team.managedBy) {
3335
inviteMembers = (
34-
<Callout>
35-
<div className="flex flex-col gap-2 p-2">
36+
<Sheet>
37+
<div className="flex items-center justify-between gap-4">
3638
<div>
3739
This team is managed by {startCase(team.managedBy)}.{" "}
3840
{joinInstructionsForTeamManagedBy(team.managedBy)}
3941
</div>
42+
<OpenInVercel team={team} />
4043
</div>
41-
</Callout>
44+
</Sheet>
4245
);
4346
} else if (canAddMembers) {
4447
// Show invite form if you can add members.
@@ -103,7 +106,7 @@ export function TeamMembers({ team }: { team: TeamResponse }) {
103106
function joinInstructionsForTeamManagedBy(managedBy: string) {
104107
switch (managedBy) {
105108
case "vercel":
106-
return 'Your team members may join the team by clicking "Open in Convex" when viewing the Convex integration in their Vercel dashboard.';
109+
return 'Your Vercel team members may join this Convex team by clicking "Open in Convex" when viewing the Convex integration in their Vercel dashboard.';
107110
default:
108111
captureMessage(`Unknown team managed by: ${managedBy}`, "error");
109112
return "";

npm-packages/dashboard/src/components/teamSettings/TeamSSO.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { cn } from "@ui/cn";
2424
import { useProfileEmails } from "api/profile";
2525
import Link from "next/link";
2626
import { LoadingTransition } from "@ui/Loading";
27+
import { OpenInVercel } from "components/OpenInVercel";
28+
import startCase from "lodash/startCase";
2729

2830
export function TeamSSO({ team }: { team: TeamResponse }) {
2931
const hasAdminPermissions = useIsCurrentMemberTeamAdmin();
@@ -119,7 +121,17 @@ export function TeamSSO({ team }: { team: TeamResponse }) {
119121

120122
{entitlements && !ssoEnabled && (
121123
<Callout variant="upsell">
122-
SSO is not available on your plan. Upgrade your plan to use SSO.
124+
{team.managedBy ? (
125+
<div className="flex w-full items-center justify-between gap-4">
126+
<div>
127+
SSO is not available for teams managed by{" "}
128+
{startCase(team.managedBy)}.
129+
</div>
130+
<OpenInVercel team={team} />
131+
</div>
132+
) : (
133+
"SSO is not available on your plan. Upgrade your plan to use SSO."
134+
)}
123135
</Callout>
124136
)}
125137

npm-packages/dashboard/src/components/teamSettings/TeamSettings.tsx

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Button } from "@ui/Button";
1313
import { ConfirmationDialog } from "@ui/ConfirmationDialog";
1414
import { useState } from "react";
1515
import startCase from "lodash/startCase";
16-
import { Callout } from "@ui/Callout";
16+
import { OpenInVercel } from "components/OpenInVercel";
1717
import { TeamForm } from "./TeamForm";
1818

1919
export function TeamSettings({ team }: { team: TeamResponse }) {
@@ -36,9 +36,13 @@ export function TeamSettings({ team }: { team: TeamResponse }) {
3636
<Sheet>
3737
<h3 className="mb-4">Delete Team</h3>
3838
<p className="mb-4">
39-
Permanently delete this team. To delete your team, you must first
40-
remove all team members and delete all projects associated with the
41-
team.
39+
Permanently deletes this team.{" "}
40+
{!team.managedBy && (
41+
<>
42+
To delete your team, you must first remove all team members and
43+
delete all projects associated with the team.
44+
</>
45+
)}
4246
</p>
4347
{subscription && (
4448
<p className="mb-4">
@@ -48,39 +52,44 @@ export function TeamSettings({ team }: { team: TeamResponse }) {
4852
</p>
4953
)}
5054
{team.managedBy && (
51-
<Callout className="mb-4">
52-
This team is managed by {startCase(team.managedBy)}. You must delete
53-
the integration in {startCase(team.managedBy)} before you can delete
54-
this team.
55-
</Callout>
55+
<div className="flex items-center justify-between gap-4">
56+
<div>
57+
This team is managed by {startCase(team.managedBy)}. You may
58+
delete this Convex team by deleting your Convex integration in{" "}
59+
{startCase(team.managedBy)}.
60+
</div>
61+
<OpenInVercel team={team} />
62+
</div>
63+
)}
64+
{!team.managedBy && (
65+
<Button
66+
variant="danger"
67+
onClick={() => setShowDeleteTeamModal(true)}
68+
disabled={
69+
!!team.managedBy ||
70+
!hasAdminPermissions ||
71+
!teams ||
72+
teams.length === 1 ||
73+
!teamMembers ||
74+
teamMembers.length > 1 ||
75+
!projects ||
76+
projects.length > 0
77+
}
78+
tip={
79+
!hasAdminPermissions
80+
? "You do not have permission to delete this team."
81+
: teams && teams.length === 1
82+
? "You cannot delete your last team."
83+
: teamMembers && teamMembers.length > 1
84+
? "You must remove all other team members before deleting the team."
85+
: projects && projects.length > 0
86+
? "You must delete all projects before deleting the team."
87+
: undefined
88+
}
89+
>
90+
Delete Team
91+
</Button>
5692
)}
57-
<Button
58-
variant="danger"
59-
onClick={() => setShowDeleteTeamModal(true)}
60-
disabled={
61-
!!team.managedBy ||
62-
!hasAdminPermissions ||
63-
!teams ||
64-
teams.length === 1 ||
65-
!teamMembers ||
66-
teamMembers.length > 1 ||
67-
!projects ||
68-
projects.length > 0
69-
}
70-
tip={
71-
!hasAdminPermissions
72-
? "You do not have permission to delete this team."
73-
: teams && teams.length === 1
74-
? "You cannot delete your last team."
75-
: teamMembers && teamMembers.length > 1
76-
? "You must remove all other team members before deleting the team."
77-
: projects && projects.length > 0
78-
? "You must delete all projects before deleting the team."
79-
: undefined
80-
}
81-
>
82-
Delete Team
83-
</Button>
8493
{showDeleteTeamModal && (
8594
<ConfirmationDialog
8695
onClose={() => setShowDeleteTeamModal(false)}

npm-packages/dashboard/src/pages/t/[team]/index.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useProjects } from "api/projects";
1313
import { useCurrentTeam, useTeamEntitlements } from "api/teams";
1414
import { useTeamOrbSubscription } from "api/billing";
1515
import { useReferralState } from "api/referrals";
16-
import { ProjectDetails } from "generatedApi";
16+
import { ProjectDetails, TeamResponse } from "generatedApi";
1717
import Link from "next/link";
1818
import { ReferralsBanner } from "components/referral/ReferralsBanner";
1919
import { DocsGrid } from "components/projects/DocsGrid";
@@ -25,6 +25,7 @@ import { cn } from "@ui/cn";
2525
import { PaginationControls } from "elements/PaginationControls";
2626
import { usePagination } from "hooks/usePagination";
2727
import { EmptySection } from "@common/elements/EmptySection";
28+
import { OpenInVercel } from "components/OpenInVercel";
2829

2930
export { getServerSideProps } from "lib/ssr";
3031

@@ -92,7 +93,7 @@ export default withAuthenticatedPage(() => {
9293
/>
9394
)}
9495

95-
<ProjectGrid projects={nonDemoProjects} />
96+
<ProjectGrid projects={nonDemoProjects} team={team} />
9697
</div>
9798
)}
9899
</div>
@@ -103,7 +104,13 @@ export default withAuthenticatedPage(() => {
103104
);
104105
});
105106

106-
function ProjectGrid({ projects }: { projects: ProjectDetails[] }) {
107+
function ProjectGrid({
108+
team,
109+
projects,
110+
}: {
111+
team: TeamResponse;
112+
projects: ProjectDetails[];
113+
}) {
107114
const [createProjectModal, showCreateProjectModal] = useCreateProjectModal();
108115
const [showAsList, setShowAsList] = useGlobalLocalStorage(
109116
"showProjectsAsList",
@@ -160,14 +167,17 @@ function ProjectGrid({ projects }: { projects: ProjectDetails[] }) {
160167
type="search"
161168
id="Search projects"
162169
/>
163-
<Button
164-
onClick={() => showCreateProjectModal()}
165-
variant="neutral"
166-
size="sm"
167-
icon={<PlusIcon />}
168-
>
169-
Create Project
170-
</Button>
170+
{!team.managedBy && (
171+
<Button
172+
onClick={() => showCreateProjectModal()}
173+
variant="neutral"
174+
size="sm"
175+
icon={<PlusIcon />}
176+
>
177+
Create Project
178+
</Button>
179+
)}
180+
<OpenInVercel team={team} />
171181
{paginatedProjects.length > 0 && (
172182
<Button
173183
href="https://docs.convex.dev/tutorial"

npm-packages/dashboard/src/pages/t/[team]/settings/billing.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { useProfile } from "api/profile";
1919
import { ChevronLeftIcon } from "@radix-ui/react-icons";
2020
import { Loading } from "@ui/Loading";
2121
import { planNameMap } from "components/billing/planCards/PlanCard";
22+
import { OpenInVercel } from "components/OpenInVercel";
23+
import startCase from "lodash/startCase";
2224

2325
export { getServerSideProps } from "lib/ssr";
2426

@@ -72,6 +74,17 @@ function Billing({ team }: { team: TeamResponse }) {
7274
)}
7375
<h2>Billing</h2>
7476
</div>
77+
{team.managedBy && (
78+
<Callout className="mx-6 mb-4" variant="upsell">
79+
<div className="flex w-full items-center justify-between gap-4">
80+
<div>
81+
This team is managed by {startCase(team.managedBy)}. You must
82+
manage billing through the {startCase(team.managedBy)} dashboard.
83+
</div>
84+
<OpenInVercel team={team} />
85+
</div>
86+
</Callout>
87+
)}
7588
<ErrorBoundary fallback={BillingErrorFallback}>
7689
<div className="relative min-h-0 flex-1 overflow-x-hidden">
7790
{!isOrbSubLoading && orbSub !== undefined ? (

0 commit comments

Comments
 (0)