feat: API for Tags (#237)

This commit is contained in:
Shorpo 2024-01-18 22:34:00 -07:00 committed by GitHub
parent 4a1c3aa43d
commit f10c3be457
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 121 additions and 15 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/api': patch
---
Add tags to Dashboards and LogViews

View file

@ -1,6 +1,8 @@
import { v4 as uuidv4 } from 'uuid';
import type { ObjectId } from '@/models';
import Dashboard from '@/models/dashboard';
import LogView from '@/models/logView';
import Team from '@/models/team';
export async function isTeamExisting() {
@ -31,3 +33,25 @@ export function getTeamByApiKey(apiKey: string) {
export function rotateTeamApiKey(teamId: ObjectId) {
return Team.findByIdAndUpdate(teamId, { apiKey: uuidv4() }, { new: true });
}
export async function getTags(teamId: ObjectId) {
const [dashboardTags, logViewTags] = await Promise.all([
Dashboard.aggregate([
{ $match: { team: teamId } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags' } },
]),
LogView.aggregate([
{ $match: { team: teamId } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags' } },
]),
]);
return [
...new Set([
...dashboardTags.map(t => t._id),
...logViewTags.map(t => t._id),
]),
];
}

View file

@ -78,6 +78,7 @@ export interface IDashboard {
query: string;
team: ObjectId;
charts: Chart[];
tags: string[];
}
const DashboardSchema = new Schema<IDashboard>(
@ -89,6 +90,10 @@ const DashboardSchema = new Schema<IDashboard>(
query: String,
team: { type: mongoose.Schema.Types.ObjectId, ref: 'Team' },
charts: { type: mongoose.Schema.Types.Mixed, required: true },
tags: {
type: [String],
default: [],
},
},
{
timestamps: true,

View file

@ -8,6 +8,7 @@ export interface ILogView {
name: string;
query: string;
team: ObjectId;
tags: string[];
}
const LogViewSchema = new Schema<ILogView>(
@ -22,6 +23,10 @@ const LogViewSchema = new Schema<ILogView>(
},
team: { type: mongoose.Schema.Types.ObjectId, ref: 'Team' },
creator: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
tags: {
type: [String],
default: [],
},
},
{
timestamps: true,

View file

@ -45,4 +45,35 @@ Object {
}
`);
});
it('GET /team/tags - no tags', async () => {
const { agent } = await getLoggedInAgent(server);
const resp = await agent.get('/team/tags').expect(200);
expect(resp.body.data).toMatchInlineSnapshot(`Array []`);
});
it('GET /team/tags', async () => {
const { agent } = await getLoggedInAgent(server);
await agent
.post('/dashboards')
.send({
name: 'Test',
charts: [],
query: '',
tags: ['test', 'test'], // make sure we dedupe
})
.expect(200);
await agent
.post('/log-views')
.send({
name: 'Test',
query: '',
tags: ['test2'],
})
.expect(200);
const resp = await agent.get('/team/tags').expect(200);
expect(resp.body.data).toStrictEqual(['test', 'test2']);
});
});

View file

@ -1,5 +1,5 @@
import express from 'express';
import { differenceBy, groupBy } from 'lodash';
import { differenceBy, groupBy, uniq } from 'lodash';
import { z } from 'zod';
import { validateRequest } from 'zod-express-middleware';
@ -60,6 +60,9 @@ const zChart = z.object({
),
});
// TODO: Move common zod schemas to a common file?
const zTags = z.array(z.string().max(32)).max(50).optional();
router.get('/', async (req, res, next) => {
try {
const teamId = req.user?.team;
@ -97,6 +100,7 @@ router.post(
name: z.string(),
charts: z.array(zChart),
query: z.string(),
tags: zTags,
}),
}),
async (req, res, next) => {
@ -106,15 +110,16 @@ router.post(
return res.sendStatus(403);
}
const { name, charts, query } = req.body ?? {};
const { name, charts, query, tags } = req.body ?? {};
// Create new dashboard from name and charts
const newDashboard = await new Dashboard({
name,
charts,
query,
tags: tags && uniq(tags),
team: teamId,
}).save();
res.json({
data: newDashboard,
});
@ -131,6 +136,7 @@ router.put(
name: z.string(),
charts: z.array(zChart),
query: z.string(),
tags: zTags,
}),
}),
async (req, res, next) => {
@ -144,7 +150,8 @@ router.put(
return res.sendStatus(400);
}
const { name, charts, query } = req.body ?? {};
const { name, charts, query, tags } = req.body ?? {};
// Update dashboard from name and charts
const oldDashboard = await Dashboard.findById(dashboardId);
const updatedDashboard = await Dashboard.findByIdAndUpdate(
@ -153,6 +160,7 @@ router.put(
name,
charts,
query,
tags: tags && uniq(tags),
},
{ new: true },
);
@ -170,7 +178,6 @@ router.put(
chartId: { $in: deletedChartIds },
});
}
res.json({
data: updatedDashboard,
});

View file

@ -1,4 +1,5 @@
import express from 'express';
import { uniq } from 'lodash';
import Alert from '@/models/alert';
import LogView from '@/models/logView';
@ -9,7 +10,7 @@ router.post('/', async (req, res, next) => {
try {
const teamId = req.user?.team;
const userId = req.user?._id;
const { query, name } = req.body;
const { query, name, tags } = req.body;
if (teamId == null) {
return res.sendStatus(403);
}
@ -18,11 +19,11 @@ router.post('/', async (req, res, next) => {
}
const logView = await new LogView({
name,
tags: tags && uniq(tags),
query: `${query}`,
team: teamId,
creator: userId,
}).save();
res.json({
data: logView,
});
@ -64,7 +65,7 @@ router.patch('/:id', async (req, res, next) => {
try {
const teamId = req.user?.team;
const { id: logViewId } = req.params;
const { query } = req.body;
const { query, tags } = req.body;
if (teamId == null) {
return res.sendStatus(403);
}
@ -76,6 +77,7 @@ router.patch('/:id', async (req, res, next) => {
logViewId,
{
query,
tags: tags && uniq(tags),
},
{ new: true },
);

View file

@ -5,7 +5,7 @@ import pick from 'lodash/pick';
import { serializeError } from 'serialize-error';
import * as config from '@/config';
import { getTeam, rotateTeamApiKey } from '@/controllers/team';
import { getTags, getTeam, rotateTeamApiKey } from '@/controllers/team';
import { findUserByEmail, findUsersByTeam } from '@/controllers/user';
import TeamInvite from '@/models/teamInvite';
import logger from '@/utils/logger';
@ -144,4 +144,18 @@ router.patch('/apiKey', async (req, res, next) => {
}
});
router.get('/tags', async (req, res, next) => {
try {
const teamId = req.user?.team;
if (teamId == null) {
throw new Error(`User ${req.user?._id} not associated with a team`);
}
const tags = await getTags(teamId);
return res.json({ data: tags });
} catch (e) {
next(e);
}
});
export default router;

View file

@ -576,11 +576,11 @@ const api = {
return useMutation<
any,
HTTPError,
{ name: string; query: string; charts: any[] }
>(async ({ name, charts, query }) =>
{ name: string; query: string; charts: any[]; tags?: string[] }
>(async ({ name, charts, query, tags }) =>
server(`dashboards`, {
method: 'POST',
json: { name, charts, query },
json: { name, charts, query, tags },
}).json(),
);
},
@ -588,11 +588,17 @@ const api = {
return useMutation<
any,
HTTPError,
{ id: string; name: string; query: string; charts: any[] }
>(async ({ id, name, charts, query }) =>
{
id: string;
name: string;
query: string;
charts: any[];
tags?: string[];
}
>(async ({ id, name, charts, query, tags }) =>
server(`dashboards/${id}`, {
method: 'PUT',
json: { name, charts, query },
json: { name, charts, query, tags },
}).json(),
);
},
@ -655,6 +661,11 @@ const api = {
retry: 1,
});
},
useTags() {
return useQuery<{ data: string[] }, HTTPError>(`team/tags`, () =>
server(`team/tags`).json<{ data: string[] }>(),
);
},
useSaveWebhook() {
return useMutation<
any,

View file

@ -41,6 +41,7 @@ export type LogView = {
name: string;
query: string;
alerts?: Alert[];
tags: string[];
};
export type Dashboard = {
@ -51,6 +52,7 @@ export type Dashboard = {
charts: Chart[];
alerts?: Alert[];
query?: string;
tags: string[];
};
export type AlertType = 'presence' | 'absence';