Skip to content

Commit 66445af

Browse files
authored
refactor: Organize team page with tabs and refactor into separate components (#1878)
## Summary Refactors team page into various components. Adds tabbed interface to keep things organized. ### Screenshots or video <img width="1540" height="994" alt="image" src="https://github.com/user-attachments/assets/7103726d-ce95-4622-b51c-c47a98289b01" /> <img width="1424" height="672" alt="image" src="https://github.com/user-attachments/assets/7c2f28be-b87d-4754-bb3d-bcf196051562" /> <img width="1516" height="591" alt="image" src="https://github.com/user-attachments/assets/55b74fc2-32fa-4b3d-8df0-310f5420a53a" /> <img width="1262" height="800" alt="image" src="https://github.com/user-attachments/assets/93ed5b60-3166-4c3c-869f-6c7548759887" /> ### How to test locally or on Vercel Needs to be tested locally. Navigate to the team page. ### References N/A
1 parent ce85064 commit 66445af

File tree

11 files changed

+961
-645
lines changed

11 files changed

+961
-645
lines changed

packages/app/src/TeamPage.tsx

Lines changed: 227 additions & 630 deletions
Large diffs are not rendered by default.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { useState } from 'react';
2+
import { CopyToClipboard } from 'react-copy-to-clipboard';
3+
import { Box, Button, Card, Divider, Group, Modal, Text } from '@mantine/core';
4+
import { notifications } from '@mantine/notifications';
5+
import { IconCheck, IconClipboard } from '@tabler/icons-react';
6+
7+
import api from '@/api';
8+
9+
function APIKeyCopyButton({
10+
value,
11+
dataTestId,
12+
}: {
13+
value: string;
14+
dataTestId?: string;
15+
}) {
16+
const [copied, setCopied] = useState(false);
17+
18+
return (
19+
<CopyToClipboard text={value}>
20+
<Button
21+
onClick={() => setCopied(true)}
22+
variant={copied ? 'light' : 'default'}
23+
color="gray"
24+
rightSection={
25+
<Group wrap="nowrap" gap={4} ms="xs">
26+
{copied ? <IconCheck size={14} /> : <IconClipboard size={14} />}
27+
{copied ? 'Copied!' : 'Copy'}
28+
</Group>
29+
}
30+
>
31+
<div data-test-id={dataTestId} className="text-wrap text-break">
32+
{value}
33+
</div>
34+
</Button>
35+
</CopyToClipboard>
36+
);
37+
}
38+
39+
export default function ApiKeysSection() {
40+
const { data: team, refetch: refetchTeam } = api.useTeam();
41+
const { data: me, isLoading: isLoadingMe } = api.useMe();
42+
const rotateTeamApiKey = api.useRotateTeamApiKey();
43+
const hasAdminAccess = true;
44+
const [
45+
rotateApiKeyConfirmationModalShow,
46+
setRotateApiKeyConfirmationModalShow,
47+
] = useState(false);
48+
49+
const rotateTeamApiKeyAction = () => {
50+
rotateTeamApiKey.mutate(undefined, {
51+
onSuccess: () => {
52+
notifications.show({
53+
color: 'green',
54+
message: 'Revoked old API key and generated new key.',
55+
});
56+
refetchTeam();
57+
},
58+
onError: e => {
59+
notifications.show({
60+
color: 'red',
61+
message: e.message,
62+
autoClose: 5000,
63+
});
64+
},
65+
});
66+
};
67+
68+
const onConfirmUpdateTeamApiKey = () => {
69+
rotateTeamApiKeyAction();
70+
setRotateApiKeyConfirmationModalShow(false);
71+
};
72+
73+
return (
74+
<Box id="api_keys" data-testid="api-keys-section">
75+
<Text size="md">API Keys</Text>
76+
<Divider my="md" />
77+
<Card mb="md">
78+
<Text mb="md">Ingestion API Key</Text>
79+
<Group gap="xs">
80+
{team?.apiKey && (
81+
<APIKeyCopyButton value={team.apiKey} dataTestId="api-key" />
82+
)}
83+
{hasAdminAccess && (
84+
<Button
85+
data-testid="rotate-api-key-button"
86+
variant="danger"
87+
onClick={() => setRotateApiKeyConfirmationModalShow(true)}
88+
>
89+
Rotate API Key
90+
</Button>
91+
)}
92+
</Group>
93+
<Modal
94+
aria-labelledby="contained-modal-title-vcenter"
95+
centered
96+
onClose={() => setRotateApiKeyConfirmationModalShow(false)}
97+
opened={rotateApiKeyConfirmationModalShow}
98+
size="lg"
99+
title={
100+
<Text size="xl">
101+
<b>Rotate API Key</b>
102+
</Text>
103+
}
104+
>
105+
<Modal.Body>
106+
<Text size="md">
107+
Rotating the API key will invalidate your existing API key and
108+
generate a new one for you. This action is <b>not reversible</b>.
109+
</Text>
110+
<Group justify="end">
111+
<Button
112+
data-testid="rotate-api-key-cancel"
113+
variant="secondary"
114+
className="mt-2 px-4 ms-2 float-end"
115+
size="sm"
116+
onClick={() => setRotateApiKeyConfirmationModalShow(false)}
117+
>
118+
Cancel
119+
</Button>
120+
<Button
121+
data-testid="rotate-api-key-confirm"
122+
variant="danger"
123+
className="mt-2 px-4 float-end"
124+
size="sm"
125+
onClick={onConfirmUpdateTeamApiKey}
126+
>
127+
Confirm
128+
</Button>
129+
</Group>
130+
</Modal.Body>
131+
</Modal>
132+
</Card>
133+
{!isLoadingMe && me != null && (
134+
<Card>
135+
<Card.Section p="md">
136+
<Text mb="md">Personal API Access Key</Text>
137+
<APIKeyCopyButton value={me.accessKey} dataTestId="api-key" />
138+
</Card.Section>
139+
</Card>
140+
)}
141+
</Box>
142+
);
143+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useState } from 'react';
2+
import { Box, Button, Card, Divider, Flex, Stack, Text } from '@mantine/core';
3+
import { IconPencil, IconX } from '@tabler/icons-react';
4+
5+
import { ConnectionForm } from '@/components/ConnectionForm';
6+
import { IS_CLICKHOUSE_BUILD, IS_LOCAL_MODE } from '@/config';
7+
import { useConnections } from '@/connection';
8+
9+
export default function ConnectionsSection() {
10+
const { data: connections } = useConnections();
11+
const [editedConnectionId, setEditedConnectionId] = useState<string | null>(
12+
null,
13+
);
14+
const [isCreatingConnection, setIsCreatingConnection] = useState(false);
15+
16+
return (
17+
<Box id="connections" data-testid="connections-section">
18+
<Text size="md">Connections</Text>
19+
<Divider my="md" />
20+
<Card>
21+
<Stack mb="md">
22+
{connections?.map(connection => (
23+
<Box key={connection.id}>
24+
<Flex justify="space-between" align="flex-start">
25+
<Stack gap="xs">
26+
<Text fw={500} size="lg">
27+
{connection.name}
28+
</Text>
29+
<Text size="sm" c="dimmed">
30+
<b>Host:</b> {connection.host}
31+
</Text>
32+
<Text size="sm" c="dimmed">
33+
<b>Username:</b> {connection.username}
34+
</Text>
35+
<Text size="sm" c="dimmed">
36+
<b>Password:</b> [Configured]
37+
</Text>
38+
</Stack>
39+
{editedConnectionId !== connection.id ? (
40+
<Button
41+
variant="subtle"
42+
onClick={() => setEditedConnectionId(connection.id)}
43+
size="sm"
44+
>
45+
<IconPencil size={14} className="me-2" /> Edit
46+
</Button>
47+
) : (
48+
<Button
49+
variant="subtle"
50+
onClick={() => setEditedConnectionId(null)}
51+
size="sm"
52+
>
53+
<IconX size={14} className="me-2" /> Cancel
54+
</Button>
55+
)}
56+
</Flex>
57+
{editedConnectionId === connection.id && (
58+
<ConnectionForm
59+
connection={connection}
60+
isNew={false}
61+
onSave={() => {
62+
setEditedConnectionId(null);
63+
}}
64+
showCancelButton={false}
65+
showDeleteButton
66+
/>
67+
)}
68+
<Divider my="md" />
69+
</Box>
70+
))}
71+
</Stack>
72+
{!isCreatingConnection &&
73+
(IS_LOCAL_MODE ? (connections?.length ?? 0) < 1 : true) && (
74+
<Button
75+
data-testid="add-connection-button"
76+
variant="primary"
77+
onClick={() => setIsCreatingConnection(true)}
78+
>
79+
Add Connection
80+
</Button>
81+
)}
82+
{isCreatingConnection && (
83+
<Stack gap="md">
84+
<ConnectionForm
85+
connection={{
86+
id: 'new',
87+
name: 'My New Connection',
88+
host: IS_CLICKHOUSE_BUILD
89+
? window.location.origin
90+
: 'http://localhost:8123',
91+
username: 'default',
92+
password: '',
93+
}}
94+
isNew={true}
95+
onSave={() => setIsCreatingConnection(false)}
96+
onClose={() => setIsCreatingConnection(false)}
97+
showCancelButton
98+
/>
99+
</Stack>
100+
)}
101+
</Card>
102+
</Box>
103+
);
104+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Box, Card, Divider, Stack, Text } from '@mantine/core';
2+
3+
import WebhooksSection from './WebhooksSection';
4+
5+
export default function IntegrationsSection() {
6+
return (
7+
<Box id="integrations" data-testid="integrations-section">
8+
<Text size="md">Integrations</Text>
9+
<Divider my="md" />
10+
<Card>
11+
<Stack gap="md">
12+
<WebhooksSection />
13+
</Stack>
14+
</Card>
15+
</Box>
16+
);
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Box, Card, Divider, Text } from '@mantine/core';
2+
3+
export default function SecurityPoliciesSection({
4+
allowedAuthMethods,
5+
}: {
6+
allowedAuthMethods: string[];
7+
}) {
8+
return (
9+
<Box id="security-policies">
10+
<Text size="md">Security Policies</Text>
11+
<Divider my="md" />
12+
<Card>
13+
<Text size="sm" c="dimmed">
14+
Team members can only authenticate via{' '}
15+
<span className="text-capitalize fw-bold">
16+
{allowedAuthMethods.join(', ')}
17+
</span>
18+
</Text>
19+
</Card>
20+
</Box>
21+
);
22+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Box, Divider, Text } from '@mantine/core';
2+
3+
import { SourcesList } from '@/components/Sources/SourcesList';
4+
5+
export default function SourcesSection() {
6+
return (
7+
<Box id="sources" data-testid="sources-section">
8+
<Text size="md">Sources</Text>
9+
<Divider my="md" />
10+
<SourcesList
11+
withBorder={false}
12+
variant="default"
13+
showEmptyState={false}
14+
/>
15+
</Box>
16+
);
17+
}

packages/app/src/components/TeamSettings/TeamMembersSection.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,8 @@ export default function TeamMembersSection() {
206206

207207
return (
208208
<Box id="team_members" data-testid="team-members-section">
209-
<Text size="md">Team</Text>
209+
<Text size="md">Team Members</Text>
210210
<Divider my="md" />
211-
212211
<Card>
213212
<Card.Section withBorder py="sm" px="lg">
214213
<Group align="center" justify="space-between">

0 commit comments

Comments
 (0)