Skip to content

Commit a20d4e9

Browse files
atrakhConvex, Inc.
authored andcommitted
dashboard: add authorized team applications page (#39117)
GitOrigin-RevId: 8cb16dd9330bcfc9fea0482805397085aa018ed1
1 parent 81d3759 commit a20d4e9

File tree

7 files changed

+170
-33
lines changed

7 files changed

+170
-33
lines changed

npm-packages/dashboard/src/api/accessTokens.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ export function useTeamAccessTokens(teamId?: number) {
1313
return accessTokens;
1414
}
1515

16+
export function useTeamAppAccessTokens(teamId?: number) {
17+
const { data: accessTokens } = useBBQuery({
18+
path: "/teams/{team_id}/app_access_tokens",
19+
pathParams: {
20+
team_id: teamId?.toString() || "",
21+
},
22+
});
23+
24+
return accessTokens;
25+
}
26+
1627
export function useInstanceAccessTokens(deploymentName?: string) {
1728
const { data: accessTokens } = useBBQuery({
1829
path: "/instances/{deployment_name}/access_tokens",

npm-packages/dashboard/src/components/projectSettings/AuthorizedApplications.tsx

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,36 @@
1-
import { AppAccessTokenResponse, ProjectDetails } from "generatedApi";
2-
1+
import { AppAccessTokenResponse } from "generatedApi";
32
import { Sheet } from "@ui/Sheet";
4-
import {
5-
useDeleteAppAccessTokenByName,
6-
useProjectAppAccessTokens,
7-
} from "api/accessTokens";
83
import { LoadingTransition } from "@ui/Loading";
94
import { TimestampDistance } from "@common/elements/TimestampDistance";
105
import { Button } from "@ui/Button";
116
import { Cross2Icon } from "@radix-ui/react-icons";
12-
import { useState } from "react";
7+
import React, { useState } from "react";
138
import { ConfirmationDialog } from "@ui/ConfirmationDialog";
149

1510
export function AuthorizedApplications({
16-
project,
11+
accessTokens,
12+
explainer,
13+
onRevoke,
1714
}: {
18-
project: ProjectDetails;
15+
accessTokens: AppAccessTokenResponse[] | undefined;
16+
explainer: React.ReactNode;
17+
onRevoke: (token: AppAccessTokenResponse) => Promise<void>;
1918
}) {
20-
const projectAccessTokens = useProjectAppAccessTokens(project.id);
21-
2219
return (
2320
<Sheet>
2421
<h3 className="mb-2">Authorized Applications</h3>
25-
<p className="text-sm text-content-primary">
26-
These 3rd-party applications have been authorized to access this project
27-
on your behalf.
28-
</p>
29-
<p className="mt-1 mb-2 text-sm text-content-primary">
30-
You cannot see applications that other members of your team have
31-
authorized.
32-
</p>
22+
{explainer}
3323
<LoadingTransition
3424
loadingProps={{ fullHeight: false, className: "h-14 w-full" }}
3525
>
36-
{projectAccessTokens !== undefined && (
26+
{accessTokens !== undefined && (
3727
<div className="flex w-full flex-col gap-2">
38-
{projectAccessTokens.length ? (
39-
projectAccessTokens.map((token, idx) => (
28+
{accessTokens.length ? (
29+
accessTokens.map((token, idx) => (
4030
<AuthorizedApplicationListItem
4131
key={idx}
4232
token={token}
43-
project={project}
33+
onRevoke={onRevoke}
4434
/>
4535
))
4636
) : (
@@ -55,16 +45,15 @@ export function AuthorizedApplications({
5545
);
5646
}
5747

58-
function AuthorizedApplicationListItem({
59-
project,
48+
export function AuthorizedApplicationListItem({
6049
token,
50+
onRevoke,
6151
}: {
62-
project: ProjectDetails;
6352
token: AppAccessTokenResponse;
53+
onRevoke: (token: AppAccessTokenResponse) => Promise<void>;
6454
}) {
6555
const [showConfirmation, setShowConfirmation] = useState(false);
6656
const [isDeleting, setIsDeleting] = useState(false);
67-
const deleteAppAccessTokenByName = useDeleteAppAccessTokenByName(project.id);
6857
return (
6958
<div className="flex w-full flex-col">
7059
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
@@ -103,7 +92,7 @@ function AuthorizedApplicationListItem({
10392
onConfirm={async () => {
10493
setIsDeleting(true);
10594
try {
106-
await deleteAppAccessTokenByName({ name: token.name });
95+
await onRevoke(token);
10796
} finally {
10897
setIsDeleting(false);
10998
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export function AuditLogToolbar({
9898
[(option) => option.label.toLowerCase()],
9999
),
100100
]}
101+
optionsWidth="fit"
101102
allowCustomValue
102103
selectedOption={selectedMember}
103104
setSelectedOption={(o) =>

npm-packages/dashboard/src/hooks/useLaunchDarkly.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const flagDefaults: {
1616
commandPaletteDeleteProjects: boolean;
1717
multipleUserIdentities: boolean;
1818
changePrimaryIdentity: boolean;
19+
showTeamOauthTokens: boolean;
1920
} = {
2021
oauthProviderConfiguration: {},
2122
enableIndexFilters: false,
@@ -24,6 +25,7 @@ const flagDefaults: {
2425
commandPaletteDeleteProjects: false,
2526
multipleUserIdentities: false,
2627
changePrimaryIdentity: false,
28+
showTeamOauthTokens: false,
2729
};
2830

2931
function kebabCaseKeys(object: typeof flagDefaults) {

npm-packages/dashboard/src/layouts/TeamSettingsLayout.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ export function TeamSettingsLayout({
2121
| "usage"
2222
| "audit-log"
2323
| "access-tokens"
24-
| "referrals";
24+
| "referrals"
25+
| "authorized-applications";
2526
Component: React.FunctionComponent<{ team: Team }>;
2627
title: string;
2728
}) {
2829
const selectedTeam = useCurrentTeam();
29-
const { referralsPage } = useLaunchDarkly();
30+
const { referralsPage, showTeamOauthTokens } = useLaunchDarkly();
3031

3132
const auditLogsEnabled = useTeamEntitlements(
3233
selectedTeam?.id,
@@ -38,6 +39,7 @@ export function TeamSettingsLayout({
3839
"billing",
3940
"usage",
4041
...(referralsPage ? ["referrals"] : []),
42+
...(showTeamOauthTokens ? ["authorized-applications"] : []),
4143
];
4244

4345
return (
@@ -59,7 +61,7 @@ export function TeamSettingsLayout({
5961
<aside
6062
className={classNames(
6163
"flex sm:flex-col gap-1",
62-
"min-w-40 sm:w-fit",
64+
"min-w-52 sm:w-fit",
6365
"min-h-fit",
6466
"px-3 py-2",
6567
"overflow-x-auto scrollbar-none",

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

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {
1313
useCreateTeamAccessToken,
1414
useInstanceAccessTokens,
1515
useProjectAccessTokens,
16+
useProjectAppAccessTokens,
17+
useDeleteAppAccessTokenByName,
1618
} from "api/accessTokens";
1719
import { useHasProjectAdminPermissions } from "api/roles";
1820
import { useRouter } from "next/router";
1921
import { useState, useEffect, useMemo } from "react";
2022
import { ProjectForm } from "components/projects/ProjectForm";
21-
import { TrashIcon } from "@radix-ui/react-icons";
23+
import { TrashIcon, InfoCircledIcon } from "@radix-ui/react-icons";
2224
import {
2325
LostAccessCommand,
2426
LostAccessDescription,
@@ -40,6 +42,8 @@ import { CustomDomains } from "components/projectSettings/CustomDomains";
4042
import { TransferProject } from "components/projects/TransferProject";
4143
import { cn } from "@ui/cn";
4244
import { AuthorizedApplications } from "components/projectSettings/AuthorizedApplications";
45+
import { Tooltip } from "@ui/Tooltip";
46+
import { useLaunchDarkly } from "hooks/useLaunchDarkly";
4347

4448
const SECTION_IDS = {
4549
projectForm: "project-form",
@@ -191,6 +195,55 @@ function ProjectSettings() {
191195
const hasAdminPermissions = useHasProjectAdminPermissions(project?.id);
192196
const router = useRouter();
193197

198+
const projectAppAccessTokens = useProjectAppAccessTokens(project?.id);
199+
const deleteAppAccessTokenByName = useDeleteAppAccessTokenByName(
200+
project?.id!,
201+
);
202+
203+
const { showTeamOauthTokens } = useLaunchDarkly();
204+
205+
const authorizedAppsExplainer = (
206+
<>
207+
<p className="text-sm text-content-primary">
208+
These 3rd-party applications have been authorized to access this project
209+
on your behalf.
210+
</p>
211+
<div className="mt-2 mb-2 text-sm text-content-primary">
212+
<span className="font-semibold">
213+
What can authorized applications do?
214+
</span>
215+
<ul className="mt-1 list-disc pl-4">
216+
<li>Create new projects</li>
217+
<li>Create new deployments</li>
218+
<li>
219+
<span className="flex items-center gap-1">
220+
Read and write data in any deployment in this project
221+
<Tooltip tip="Write access to Production deployments will depend on your team-level and project-level roles.">
222+
<InfoCircledIcon />
223+
</Tooltip>
224+
</span>
225+
</li>
226+
</ul>
227+
</div>
228+
<p className="mt-1 mb-2 text-sm text-content-primary">
229+
You cannot see applications that other members of your team have
230+
authorized.
231+
</p>
232+
{team && showTeamOauthTokens && (
233+
<p className="mt-1 mb-2 text-xs text-content-secondary">
234+
There may also be <b>team-wide authorized applications</b> that can
235+
access all projects in this team. You can view them in{" "}
236+
<Link
237+
href={`/t/${team.slug}/settings/authorized-applications`}
238+
className="text-content-link hover:underline"
239+
>
240+
Team Settings
241+
</Link>{" "}
242+
</p>
243+
)}
244+
</>
245+
);
246+
194247
useEffect(() => {
195248
// Handle initial scroll based on hash
196249
if (typeof window !== "undefined" && window.location.hash) {
@@ -273,7 +326,13 @@ function ProjectSettings() {
273326
)}
274327
{project && (
275328
<div id={SECTION_IDS.authorizedApplications}>
276-
<AuthorizedApplications project={project} />
329+
<AuthorizedApplications
330+
accessTokens={projectAppAccessTokens}
331+
explainer={authorizedAppsExplainer}
332+
onRevoke={async (token) => {
333+
await deleteAppAccessTokenByName({ name: token.name });
334+
}}
335+
/>
277336
</div>
278337
)}
279338
<div id={SECTION_IDS.envVars}>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { TeamSettingsLayout } from "layouts/TeamSettingsLayout";
2+
import { withAuthenticatedPage } from "lib/withAuthenticatedPage";
3+
import {
4+
useTeamAppAccessTokens,
5+
useDeleteTeamAccessToken,
6+
} from "api/accessTokens";
7+
import { useCurrentTeam } from "api/teams";
8+
import { AppAccessTokenResponse, Team } from "generatedApi";
9+
import { AuthorizedApplications } from "components/projectSettings/AuthorizedApplications";
10+
import { InfoCircledIcon } from "@radix-ui/react-icons";
11+
import { Tooltip } from "@ui/Tooltip";
12+
import React from "react";
13+
14+
function TeamAuthorizedApplicationsPage({ team }: { team: Team }) {
15+
const teamAccessTokens = useTeamAppAccessTokens(team.id);
16+
const deleteTeamAccessToken = useDeleteTeamAccessToken(team.id);
17+
18+
const explainer = (
19+
<>
20+
<p className="text-sm text-content-primary">
21+
These 3rd-party applications have been authorized to access this team on
22+
your behalf.
23+
</p>
24+
<div className="mt-2 mb-2 text-sm text-content-primary">
25+
<span className="font-semibold">
26+
What can authorized applications do?
27+
</span>
28+
<ul className="mt-1 list-disc pl-4">
29+
<li>Create new projects</li>
30+
<li>Create new deployments</li>
31+
<li>
32+
<span className="flex items-center gap-1">
33+
Read and write data in all projects
34+
<Tooltip tip="Write access to Production deployments will depend on your team-level and project-level roles.">
35+
<InfoCircledIcon />
36+
</Tooltip>
37+
</span>
38+
</li>
39+
</ul>
40+
</div>
41+
<p className="mt-1 mb-2 text-sm text-content-primary">
42+
You cannot see applications that other members of your team have
43+
authorized.
44+
</p>
45+
<p className="mt-1 mb-2 text-xs text-content-secondary">
46+
You can view authorized applications for each project in the respective
47+
Settings page for each project.
48+
</p>
49+
</>
50+
);
51+
52+
return (
53+
<AuthorizedApplications
54+
accessTokens={teamAccessTokens}
55+
explainer={explainer}
56+
onRevoke={async (token: AppAccessTokenResponse) => {
57+
await deleteTeamAccessToken({ name: token.name } as any);
58+
}}
59+
/>
60+
);
61+
}
62+
63+
export default withAuthenticatedPage(() => {
64+
const team = useCurrentTeam();
65+
if (!team) return null;
66+
return (
67+
<TeamSettingsLayout
68+
page="authorized-applications"
69+
Component={() => <TeamAuthorizedApplicationsPage team={team} />}
70+
title="Authorized Applications"
71+
/>
72+
);
73+
});

0 commit comments

Comments
 (0)