Skip to content

Commit f10c3be

Browse files
authored
feat: API for Tags (#237)
1 parent 4a1c3aa commit f10c3be

File tree

10 files changed

+121
-15
lines changed

10 files changed

+121
-15
lines changed

.changeset/six-lemons-compete.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperdx/api': patch
3+
---
4+
5+
Add tags to Dashboards and LogViews

packages/api/src/controllers/team.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { v4 as uuidv4 } from 'uuid';
22

33
import type { ObjectId } from '@/models';
4+
import Dashboard from '@/models/dashboard';
5+
import LogView from '@/models/logView';
46
import Team from '@/models/team';
57

68
export async function isTeamExisting() {
@@ -31,3 +33,25 @@ export function getTeamByApiKey(apiKey: string) {
3133
export function rotateTeamApiKey(teamId: ObjectId) {
3234
return Team.findByIdAndUpdate(teamId, { apiKey: uuidv4() }, { new: true });
3335
}
36+
37+
export async function getTags(teamId: ObjectId) {
38+
const [dashboardTags, logViewTags] = await Promise.all([
39+
Dashboard.aggregate([
40+
{ $match: { team: teamId } },
41+
{ $unwind: '$tags' },
42+
{ $group: { _id: '$tags' } },
43+
]),
44+
LogView.aggregate([
45+
{ $match: { team: teamId } },
46+
{ $unwind: '$tags' },
47+
{ $group: { _id: '$tags' } },
48+
]),
49+
]);
50+
51+
return [
52+
...new Set([
53+
...dashboardTags.map(t => t._id),
54+
...logViewTags.map(t => t._id),
55+
]),
56+
];
57+
}

packages/api/src/models/dashboard.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export interface IDashboard {
7878
query: string;
7979
team: ObjectId;
8080
charts: Chart[];
81+
tags: string[];
8182
}
8283

8384
const DashboardSchema = new Schema<IDashboard>(
@@ -89,6 +90,10 @@ const DashboardSchema = new Schema<IDashboard>(
8990
query: String,
9091
team: { type: mongoose.Schema.Types.ObjectId, ref: 'Team' },
9192
charts: { type: mongoose.Schema.Types.Mixed, required: true },
93+
tags: {
94+
type: [String],
95+
default: [],
96+
},
9297
},
9398
{
9499
timestamps: true,

packages/api/src/models/logView.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ILogView {
88
name: string;
99
query: string;
1010
team: ObjectId;
11+
tags: string[];
1112
}
1213

1314
const LogViewSchema = new Schema<ILogView>(
@@ -22,6 +23,10 @@ const LogViewSchema = new Schema<ILogView>(
2223
},
2324
team: { type: mongoose.Schema.Types.ObjectId, ref: 'Team' },
2425
creator: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
26+
tags: {
27+
type: [String],
28+
default: [],
29+
},
2530
},
2631
{
2732
timestamps: true,

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,35 @@ Object {
4545
}
4646
`);
4747
});
48+
49+
it('GET /team/tags - no tags', async () => {
50+
const { agent } = await getLoggedInAgent(server);
51+
52+
const resp = await agent.get('/team/tags').expect(200);
53+
54+
expect(resp.body.data).toMatchInlineSnapshot(`Array []`);
55+
});
56+
57+
it('GET /team/tags', async () => {
58+
const { agent } = await getLoggedInAgent(server);
59+
await agent
60+
.post('/dashboards')
61+
.send({
62+
name: 'Test',
63+
charts: [],
64+
query: '',
65+
tags: ['test', 'test'], // make sure we dedupe
66+
})
67+
.expect(200);
68+
await agent
69+
.post('/log-views')
70+
.send({
71+
name: 'Test',
72+
query: '',
73+
tags: ['test2'],
74+
})
75+
.expect(200);
76+
const resp = await agent.get('/team/tags').expect(200);
77+
expect(resp.body.data).toStrictEqual(['test', 'test2']);
78+
});
4879
});

packages/api/src/routers/api/dashboards.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import express from 'express';
2-
import { differenceBy, groupBy } from 'lodash';
2+
import { differenceBy, groupBy, uniq } from 'lodash';
33
import { z } from 'zod';
44
import { validateRequest } from 'zod-express-middleware';
55

@@ -60,6 +60,9 @@ const zChart = z.object({
6060
),
6161
});
6262

63+
// TODO: Move common zod schemas to a common file?
64+
const zTags = z.array(z.string().max(32)).max(50).optional();
65+
6366
router.get('/', async (req, res, next) => {
6467
try {
6568
const teamId = req.user?.team;
@@ -97,6 +100,7 @@ router.post(
97100
name: z.string(),
98101
charts: z.array(zChart),
99102
query: z.string(),
103+
tags: zTags,
100104
}),
101105
}),
102106
async (req, res, next) => {
@@ -106,15 +110,16 @@ router.post(
106110
return res.sendStatus(403);
107111
}
108112

109-
const { name, charts, query } = req.body ?? {};
113+
const { name, charts, query, tags } = req.body ?? {};
114+
110115
// Create new dashboard from name and charts
111116
const newDashboard = await new Dashboard({
112117
name,
113118
charts,
114119
query,
120+
tags: tags && uniq(tags),
115121
team: teamId,
116122
}).save();
117-
118123
res.json({
119124
data: newDashboard,
120125
});
@@ -131,6 +136,7 @@ router.put(
131136
name: z.string(),
132137
charts: z.array(zChart),
133138
query: z.string(),
139+
tags: zTags,
134140
}),
135141
}),
136142
async (req, res, next) => {
@@ -144,7 +150,8 @@ router.put(
144150
return res.sendStatus(400);
145151
}
146152

147-
const { name, charts, query } = req.body ?? {};
153+
const { name, charts, query, tags } = req.body ?? {};
154+
148155
// Update dashboard from name and charts
149156
const oldDashboard = await Dashboard.findById(dashboardId);
150157
const updatedDashboard = await Dashboard.findByIdAndUpdate(
@@ -153,6 +160,7 @@ router.put(
153160
name,
154161
charts,
155162
query,
163+
tags: tags && uniq(tags),
156164
},
157165
{ new: true },
158166
);
@@ -170,7 +178,6 @@ router.put(
170178
chartId: { $in: deletedChartIds },
171179
});
172180
}
173-
174181
res.json({
175182
data: updatedDashboard,
176183
});

packages/api/src/routers/api/logViews.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import express from 'express';
2+
import { uniq } from 'lodash';
23

34
import Alert from '@/models/alert';
45
import LogView from '@/models/logView';
@@ -9,7 +10,7 @@ router.post('/', async (req, res, next) => {
910
try {
1011
const teamId = req.user?.team;
1112
const userId = req.user?._id;
12-
const { query, name } = req.body;
13+
const { query, name, tags } = req.body;
1314
if (teamId == null) {
1415
return res.sendStatus(403);
1516
}
@@ -18,11 +19,11 @@ router.post('/', async (req, res, next) => {
1819
}
1920
const logView = await new LogView({
2021
name,
22+
tags: tags && uniq(tags),
2123
query: `${query}`,
2224
team: teamId,
2325
creator: userId,
2426
}).save();
25-
2627
res.json({
2728
data: logView,
2829
});
@@ -64,7 +65,7 @@ router.patch('/:id', async (req, res, next) => {
6465
try {
6566
const teamId = req.user?.team;
6667
const { id: logViewId } = req.params;
67-
const { query } = req.body;
68+
const { query, tags } = req.body;
6869
if (teamId == null) {
6970
return res.sendStatus(403);
7071
}
@@ -76,6 +77,7 @@ router.patch('/:id', async (req, res, next) => {
7677
logViewId,
7778
{
7879
query,
80+
tags: tags && uniq(tags),
7981
},
8082
{ new: true },
8183
);

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import pick from 'lodash/pick';
55
import { serializeError } from 'serialize-error';
66

77
import * as config from '@/config';
8-
import { getTeam, rotateTeamApiKey } from '@/controllers/team';
8+
import { getTags, getTeam, rotateTeamApiKey } from '@/controllers/team';
99
import { findUserByEmail, findUsersByTeam } from '@/controllers/user';
1010
import TeamInvite from '@/models/teamInvite';
1111
import logger from '@/utils/logger';
@@ -144,4 +144,18 @@ router.patch('/apiKey', async (req, res, next) => {
144144
}
145145
});
146146

147+
router.get('/tags', async (req, res, next) => {
148+
try {
149+
const teamId = req.user?.team;
150+
if (teamId == null) {
151+
throw new Error(`User ${req.user?._id} not associated with a team`);
152+
}
153+
154+
const tags = await getTags(teamId);
155+
return res.json({ data: tags });
156+
} catch (e) {
157+
next(e);
158+
}
159+
});
160+
147161
export default router;

packages/app/src/api.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -576,23 +576,29 @@ const api = {
576576
return useMutation<
577577
any,
578578
HTTPError,
579-
{ name: string; query: string; charts: any[] }
580-
>(async ({ name, charts, query }) =>
579+
{ name: string; query: string; charts: any[]; tags?: string[] }
580+
>(async ({ name, charts, query, tags }) =>
581581
server(`dashboards`, {
582582
method: 'POST',
583-
json: { name, charts, query },
583+
json: { name, charts, query, tags },
584584
}).json(),
585585
);
586586
},
587587
useUpdateDashboard() {
588588
return useMutation<
589589
any,
590590
HTTPError,
591-
{ id: string; name: string; query: string; charts: any[] }
592-
>(async ({ id, name, charts, query }) =>
591+
{
592+
id: string;
593+
name: string;
594+
query: string;
595+
charts: any[];
596+
tags?: string[];
597+
}
598+
>(async ({ id, name, charts, query, tags }) =>
593599
server(`dashboards/${id}`, {
594600
method: 'PUT',
595-
json: { name, charts, query },
601+
json: { name, charts, query, tags },
596602
}).json(),
597603
);
598604
},
@@ -655,6 +661,11 @@ const api = {
655661
retry: 1,
656662
});
657663
},
664+
useTags() {
665+
return useQuery<{ data: string[] }, HTTPError>(`team/tags`, () =>
666+
server(`team/tags`).json<{ data: string[] }>(),
667+
);
668+
},
658669
useSaveWebhook() {
659670
return useMutation<
660671
any,

packages/app/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export type LogView = {
4141
name: string;
4242
query: string;
4343
alerts?: Alert[];
44+
tags: string[];
4445
};
4546

4647
export type Dashboard = {
@@ -51,6 +52,7 @@ export type Dashboard = {
5152
charts: Chart[];
5253
alerts?: Alert[];
5354
query?: string;
55+
tags: string[];
5456
};
5557

5658
export type AlertType = 'presence' | 'absence';

0 commit comments

Comments
 (0)