Skip to content

Commit 7925642

Browse files
authored
feat: Allow to update team name (#688)
![Screenshot 2025-03-17 at 7 48 10 PM](https://github.com/user-attachments/assets/4bba3c92-e472-45b7-b7f5-003443e96a12) ![Screenshot 2025-03-17 at 7 48 16 PM](https://github.com/user-attachments/assets/4dec057b-0908-4ddf-a534-68f8dbc339ea)
1 parent 636b085 commit 7925642

File tree

5 files changed

+142
-3
lines changed

5 files changed

+142
-3
lines changed

.changeset/gentle-lamps-drop.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@hyperdx/api': patch
3+
'@hyperdx/app': patch
4+
---
5+
6+
feat: Allow to update team name

packages/api/src/controllers/team.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export function rotateTeamApiKey(teamId: ObjectId) {
6161
return Team.findByIdAndUpdate(teamId, { apiKey: uuidv4() }, { new: true });
6262
}
6363

64+
export function setTeamName(teamId: ObjectId, name: string) {
65+
return Team.findByIdAndUpdate(teamId, { name }, { new: true });
66+
}
67+
6468
export async function getTags(teamId: ObjectId) {
6569
const [dashboardTags, logViewTags] = await Promise.all([
6670
Dashboard.aggregate([

packages/api/src/routers/api/team.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { z } from 'zod';
66
import { validateRequest } from 'zod-express-middleware';
77

88
import * as config from '@/config';
9-
import { getTags, getTeam, rotateTeamApiKey } from '@/controllers/team';
9+
import {
10+
getTags,
11+
getTeam,
12+
rotateTeamApiKey,
13+
setTeamName,
14+
} from '@/controllers/team';
1015
import {
1116
deleteTeamMember,
1217
findUserByEmail,
@@ -78,6 +83,28 @@ router.patch('/apiKey', async (req, res, next) => {
7883
}
7984
});
8085

86+
router.patch(
87+
'/name',
88+
validateRequest({
89+
body: z.object({
90+
name: z.string().min(1).max(100),
91+
}),
92+
}),
93+
async (req, res, next) => {
94+
try {
95+
const teamId = req.user?.team;
96+
if (teamId == null) {
97+
throw new Error(`User ${req.user?._id} not associated with a team`);
98+
}
99+
const { name } = req.body;
100+
const team = await setTeamName(teamId, name);
101+
res.json({ name: team?.name });
102+
} catch (e) {
103+
next(e);
104+
}
105+
},
106+
);
107+
81108
router.post(
82109
'/invitation',
83110
validateRequest({

packages/app/src/TeamPage.tsx

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useState } from 'react';
1+
import { FormEventHandler, useCallback, useState } from 'react';
22
import Head from 'next/head';
33
import {
44
Button,
@@ -166,6 +166,7 @@ export default function TeamPage() {
166166
const deleteTeamInvitation = api.useDeleteTeamInvitation();
167167
const saveWebhook = api.useSaveWebhook();
168168
const deleteWebhook = api.useDeleteWebhook();
169+
const setTeamName = api.useSetTeamName();
169170

170171
const hasAdminAccess = true;
171172

@@ -492,13 +493,43 @@ export default function TeamPage() {
492493
],
493494
});
494495

496+
const [isEditingTeamName, setIsEditingTeamName] = useState(false);
497+
const [editingTeamNameValue, setEditingTeamNameValue] = useState('');
498+
const handleSetTeamName = useCallback<FormEventHandler<HTMLFormElement>>(
499+
e => {
500+
e.stopPropagation();
501+
e.preventDefault();
502+
setTeamName.mutate(
503+
{ name: editingTeamNameValue },
504+
{
505+
onError: e => {
506+
notifications.show({
507+
color: 'red',
508+
message: 'Failed to update team name',
509+
});
510+
},
511+
onSuccess: () => {
512+
notifications.show({
513+
color: 'green',
514+
message: 'Updated team name',
515+
});
516+
refetchTeam();
517+
setIsEditingTeamName(false);
518+
setEditingTeamNameValue(team.name);
519+
},
520+
},
521+
);
522+
},
523+
[editingTeamNameValue, refetchTeam, setTeamName, team?.name],
524+
);
525+
495526
return (
496527
<div className="TeamPage">
497528
<Head>
498529
<title>My Team - HyperDX</title>
499530
</Head>
500531
<div className={styles.header}>
501-
<div>{team?.name || 'My team'}</div>
532+
<div>Team Settings</div>
502533
</div>
503534
<div>
504535
<Container>
@@ -509,6 +540,69 @@ export default function TeamPage() {
509540
)}
510541
{!isLoading && team != null && (
511542
<Stack my={20} gap="xl">
543+
<div className={styles.sectionHeader}>Team Name</div>
544+
<Card>
545+
{isEditingTeamName ? (
546+
<form onSubmit={handleSetTeamName}>
547+
<Group gap="xs">
548+
<TextInput
549+
size="xs"
550+
value={editingTeamNameValue}
551+
onChange={e => {
552+
setEditingTeamNameValue(e.target.value);
553+
}}
554+
placeholder="My Team"
555+
miw={300}
556+
required
557+
min={1}
558+
max={100}
559+
autoFocus
560+
onKeyDown={e => {
561+
if (e.key === 'Escape') {
562+
setIsEditingTeamName(false);
563+
}
564+
}}
565+
/>
566+
<MButton
567+
type="submit"
568+
size="xs"
569+
variant="light"
570+
color="green"
571+
loading={setTeamName.isLoading}
572+
>
573+
Save
574+
</MButton>
575+
<MButton
576+
type="button"
577+
size="xs"
578+
variant="default"
579+
disabled={setTeamName.isLoading}
580+
onClick={() => setIsEditingTeamName(false)}
581+
>
582+
Cancel
583+
</MButton>
584+
</Group>
585+
</form>
586+
) : (
587+
<Group gap="lg">
588+
<div className="text-slate-300 fs-7">{team.name}</div>
589+
<MButton
590+
size="xs"
591+
variant="default"
592+
leftSection={
593+
<i className="bi bi-pencil text-slate-300" />
594+
}
595+
onClick={() => {
596+
setIsEditingTeamName(true);
597+
setEditingTeamNameValue(team.name);
598+
}}
599+
>
600+
Change
601+
</MButton>
602+
</Group>
603+
)}
604+
</Card>
605+
512606
<div className={styles.sectionHeader}>API Keys</div>
513607
<Card>
514608
<div className="mb-3 text-slate-300 fs-7">

packages/app/src/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,14 @@ const api = {
777777
hdxServer(`team/members`).json(),
778778
);
779779
},
780+
useSetTeamName() {
781+
return useMutation<any, HTTPError, { name: string }>(async ({ name }) =>
782+
hdxServer(`team/name`, {
783+
method: 'PATCH',
784+
json: { name },
785+
}).json(),
786+
);
787+
},
780788
useTags() {
781789
return useQuery<{ data: string[] }, HTTPError>(`team/tags`, () =>
782790
hdxServer(`team/tags`).json<{ data: string[] }>(),

0 commit comments

Comments
 (0)