mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: API for Tags (#237)
This commit is contained in:
parent
4a1c3aa43d
commit
f10c3be457
10 changed files with 121 additions and 15 deletions
5
.changeset/six-lemons-compete.md
Normal file
5
.changeset/six-lemons-compete.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/api': patch
|
||||
---
|
||||
|
||||
Add tags to Dashboards and LogViews
|
||||
|
|
@ -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),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue