Skip to content

feat: API for Tags #237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-lemons-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/api': patch
---

Add tags to Dashboards and LogViews
24 changes: 24 additions & 0 deletions packages/api/src/controllers/team.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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),
]),
];
}
5 changes: 5 additions & 0 deletions packages/api/src/models/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export interface IDashboard {
query: string;
team: ObjectId;
charts: Chart[];
tags: string[];
}

const DashboardSchema = new Schema<IDashboard>(
Expand All @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/models/logView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ILogView {
name: string;
query: string;
team: ObjectId;
tags: string[];
}

const LogViewSchema = new Schema<ILogView>(
Expand All @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions packages/api/src/routers/api/__tests__/team.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
17 changes: 12 additions & 5 deletions packages/api/src/routers/api/dashboards.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -97,6 +100,7 @@ router.post(
name: z.string(),
charts: z.array(zChart),
query: z.string(),
tags: zTags,
}),
}),
async (req, res, next) => {
Expand All @@ -106,15 +110,16 @@ router.post(
return res.sendStatus(403);
}

const { name, charts, query } = req.body ?? {};
const { name, charts, query, tags } = req.body ?? {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might want to assert the uniqueness of tagging values and also enforce the size limit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, added uniq(). by size limit do you mean array length or individual tag lengths?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think we should enforce the limit on both lengths tho (zod validator). like individual tag length 32 and array length 50

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added it for dashboards endpoint. since logViews doesn't have validators, think it'll be better to address it in a separate PR. will add a task


// Create new dashboard from name and charts
const newDashboard = await new Dashboard({
name,
charts,
query,
tags: tags && uniq(tags),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double check that we want to assign this undefined instead empty array ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, just want to skip updating this field if it's not passed. just in case we run old app and new api at the same time, to prevent data loss

team: teamId,
}).save();

res.json({
data: newDashboard,
});
Expand All @@ -131,6 +136,7 @@ router.put(
name: z.string(),
charts: z.array(zChart),
query: z.string(),
tags: zTags,
}),
}),
async (req, res, next) => {
Expand All @@ -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(
Expand All @@ -153,6 +160,7 @@ router.put(
name,
charts,
query,
tags: tags && uniq(tags),
},
{ new: true },
);
Expand All @@ -170,7 +178,6 @@ router.put(
chartId: { $in: deletedChartIds },
});
}

res.json({
data: updatedDashboard,
});
Expand Down
8 changes: 5 additions & 3 deletions packages/api/src/routers/api/logViews.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import express from 'express';
import { uniq } from 'lodash';

import Alert from '@/models/alert';
import LogView from '@/models/logView';
Expand All @@ -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);
}
Expand All @@ -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,
});
Expand Down Expand Up @@ -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);
}
Expand All @@ -76,6 +77,7 @@ router.patch('/:id', async (req, res, next) => {
logViewId,
{
query,
tags: tags && uniq(tags),
},
{ new: true },
);
Expand Down
16 changes: 15 additions & 1 deletion packages/api/src/routers/api/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
23 changes: 17 additions & 6 deletions packages/app/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,23 +576,29 @@ 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(),
);
},
useUpdateDashboard() {
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(),
);
},
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type LogView = {
name: string;
query: string;
alerts?: Alert[];
tags: string[];
};

export type Dashboard = {
Expand All @@ -51,6 +52,7 @@ export type Dashboard = {
charts: Chart[];
alerts?: Alert[];
query?: string;
tags: string[];
};

export type AlertType = 'presence' | 'absence';
Expand Down