From 2d7fc8d056564db19f04ecc8b5f627c35fc72401 Mon Sep 17 00:00:00 2001 From: salah-72 Date: Wed, 8 Jul 2026 12:49:26 +0300 Subject: [PATCH 1/8] feat(db): add Dashboard model to schema --- prisma/schema.prisma | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index adb5e1e..52cb9e5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -39,6 +39,7 @@ model Analyst { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt refreshTokens RefreshToken[] + dashboards Dashboard[] } model RefreshToken { @@ -137,3 +138,20 @@ model Alert { source String? mitre String[] } + +model Dashboard { + id String @id @default(uuid()) + title String + description String? + tags String[] + ownerId String + owner Analyst @relation(fields: [ownerId], references: [id], onDelete: Cascade) + panels Json @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + + @@index([ownerId]) + @@index([createdAt]) + @@index([title]) +} \ No newline at end of file From f730caf29180b88cdbf10d870f390f8d0739fd98 Mon Sep 17 00:00:00 2001 From: salah-72 Date: Wed, 8 Jul 2026 14:52:31 +0300 Subject: [PATCH 2/8] feat(dashboard): add create dashboard endpoint --- .../migration.sql | 25 +++++++++++++++++++ .../controllers/dashboard.controller.ts | 18 +++++++++++++ .../Dashboards/routes/dashboard.routes.ts | 13 ++++++++++ .../Dashboards/services/dashboard.service.ts | 17 +++++++++++++ src/modules/Dashboards/types/types.ts | 15 +++++++++++ .../validation/dashboard.validation.ts | 15 +++++++++++ src/routes/index.ts | 4 ++- 7 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20260708113434_add_dashboard_model/migration.sql create mode 100644 src/modules/Dashboards/controllers/dashboard.controller.ts create mode 100644 src/modules/Dashboards/routes/dashboard.routes.ts create mode 100644 src/modules/Dashboards/services/dashboard.service.ts create mode 100644 src/modules/Dashboards/types/types.ts create mode 100644 src/modules/Dashboards/validation/dashboard.validation.ts diff --git a/prisma/migrations/20260708113434_add_dashboard_model/migration.sql b/prisma/migrations/20260708113434_add_dashboard_model/migration.sql new file mode 100644 index 0000000..7a47c74 --- /dev/null +++ b/prisma/migrations/20260708113434_add_dashboard_model/migration.sql @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE "Dashboard" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "tags" TEXT[], + "ownerId" TEXT NOT NULL, + "panels" JSONB NOT NULL DEFAULT '[]', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Dashboard_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Dashboard_ownerId_idx" ON "Dashboard"("ownerId"); + +-- CreateIndex +CREATE INDEX "Dashboard_createdAt_idx" ON "Dashboard"("createdAt"); + +-- CreateIndex +CREATE INDEX "Dashboard_title_idx" ON "Dashboard"("title"); + +-- AddForeignKey +ALTER TABLE "Dashboard" ADD CONSTRAINT "Dashboard_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "Analyst"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/modules/Dashboards/controllers/dashboard.controller.ts b/src/modules/Dashboards/controllers/dashboard.controller.ts new file mode 100644 index 0000000..ab61aef --- /dev/null +++ b/src/modules/Dashboards/controllers/dashboard.controller.ts @@ -0,0 +1,18 @@ +import { Request, Response } from 'express'; +import catchAsync from '../../../common/utils/catchAsync'; +import { STATUS_CODE } from '../../../common/constants/responseCode'; +import { STATUS } from '../../../common/constants/responseStatus'; +import { createDashboardData } from '../types/types'; +import { createDashboardService } from '../services/dashboard.service'; +import { IRequest } from '../../../common/interfaces/types'; + +export const createDashboard = catchAsync(async (req: IRequest, res: Response) => { + const data: createDashboardData = req.body as createDashboardData; + const dashboard = await createDashboardService(req.user!.id, data); + + res.status(STATUS_CODE.CREATED).json({ + status: STATUS.SUCCESS, + data: dashboard, + message: 'Dashboard created successfully', + }); +}); diff --git a/src/modules/Dashboards/routes/dashboard.routes.ts b/src/modules/Dashboards/routes/dashboard.routes.ts new file mode 100644 index 0000000..8522d99 --- /dev/null +++ b/src/modules/Dashboards/routes/dashboard.routes.ts @@ -0,0 +1,13 @@ +import Router from 'express'; +import { createDashboard } from '../controllers/dashboard.controller'; +import { authenticate } from '../../../common/middlewares'; +import validationMiddleware from '../../../common/middlewares/validation.middleware'; +import { createDashboardValidation } from '../validation/dashboard.validation'; + +const router = Router(); + +router.use(authenticate); + +router.post('/', validationMiddleware({ body: createDashboardValidation }), createDashboard); + +export default router; diff --git a/src/modules/Dashboards/services/dashboard.service.ts b/src/modules/Dashboards/services/dashboard.service.ts new file mode 100644 index 0000000..2e58511 --- /dev/null +++ b/src/modules/Dashboards/services/dashboard.service.ts @@ -0,0 +1,17 @@ +import logger from '../../../common/utils/logger'; +import { prisma } from '../../../config/postgres'; +import { createDashboardData } from '../types/types'; + +export const createDashboardService = async (ownerId: string, data: createDashboardData) => { + const dashboard = await prisma.dashboard.create({ + data: { + title: data.title, + description: data.description, + tags: data.tags ?? [], + panels: data.panels ?? [], + ownerId, + }, + }); + logger.info(`dashboard created successfully: title ${dashboard.title}`); + return dashboard; +}; diff --git a/src/modules/Dashboards/types/types.ts b/src/modules/Dashboards/types/types.ts new file mode 100644 index 0000000..31a4aef --- /dev/null +++ b/src/modules/Dashboards/types/types.ts @@ -0,0 +1,15 @@ +export type PanelType = 'metric' | 'histogram' | 'breakdown'; + +export interface DashboardPanelDefinition { + id: string; + type: PanelType; + title: string; + spec: Record; +} + +export interface createDashboardData { + title: string; + description?: string; + tags?: string[]; + panels?: any; +} diff --git a/src/modules/Dashboards/validation/dashboard.validation.ts b/src/modules/Dashboards/validation/dashboard.validation.ts new file mode 100644 index 0000000..8812185 --- /dev/null +++ b/src/modules/Dashboards/validation/dashboard.validation.ts @@ -0,0 +1,15 @@ +import * as z from 'zod'; + +const panelValidation = z.object({ + id: z.string().min(1), + type: z.enum(['metric', 'histogram', 'breakdown']), + title: z.string().min(1).max(150), + spec: z.record(z.string(), z.any()), +}); + +export const createDashboardValidation = z.object({ + title: z.string().min(3).max(150).trim(), + description: z.string().max(500).trim().optional(), + tags: z.array(z.string().trim()).optional(), + panels: z.array(panelValidation).optional(), +}); diff --git a/src/routes/index.ts b/src/routes/index.ts index b220355..2389054 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -5,6 +5,8 @@ import deviceRoutes from '../modules/device/routes/device.routes'; import ruleRoutes from '../modules/Rule/routes/rule.routes'; import serviceRouter from '../modules/service/routes/service.routes'; import alertRoutes from '../modules/Alerts/routes/alert.routes'; +import dashboardRoutes from '../modules/Dashboards/routes/dashboard.routes'; + const router = express.Router(); router.use('/auth', authRoutes); @@ -13,5 +15,5 @@ router.use('/devices', deviceRoutes); router.use('/rules', ruleRoutes); router.use('/services', serviceRouter); router.use('/alerts', alertRoutes); - +router.use('/dashboards', dashboardRoutes); export default router; From ee22dc7e072a46157ba8cbe97b4a622aaed857de Mon Sep 17 00:00:00 2001 From: salah-72 Date: Wed, 8 Jul 2026 15:04:42 +0300 Subject: [PATCH 3/8] feat(dashboard): add update dashboard endpoint --- .../controllers/dashboard.controller.ts | 16 ++++++++++++++-- .../Dashboards/routes/dashboard.routes.ts | 14 +++++++++++--- .../Dashboards/services/dashboard.service.ts | 16 +++++++++++++++- src/modules/Dashboards/types/types.ts | 2 ++ .../validation/dashboard.validation.ts | 6 ++++++ 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/modules/Dashboards/controllers/dashboard.controller.ts b/src/modules/Dashboards/controllers/dashboard.controller.ts index ab61aef..2317a2a 100644 --- a/src/modules/Dashboards/controllers/dashboard.controller.ts +++ b/src/modules/Dashboards/controllers/dashboard.controller.ts @@ -2,8 +2,8 @@ import { Request, Response } from 'express'; import catchAsync from '../../../common/utils/catchAsync'; import { STATUS_CODE } from '../../../common/constants/responseCode'; import { STATUS } from '../../../common/constants/responseStatus'; -import { createDashboardData } from '../types/types'; -import { createDashboardService } from '../services/dashboard.service'; +import { createDashboardData, updateDashboardData } from '../types/types'; +import { createDashboardService, updateDashboardService } from '../services/dashboard.service'; import { IRequest } from '../../../common/interfaces/types'; export const createDashboard = catchAsync(async (req: IRequest, res: Response) => { @@ -16,3 +16,15 @@ export const createDashboard = catchAsync(async (req: IRequest, res: Response) = message: 'Dashboard created successfully', }); }); + +export const updateDashboard = catchAsync(async (req: Request, res: Response) => { + const id = req.params.id as string; + const data: updateDashboardData = req.body as updateDashboardData; + const dashboard = await updateDashboardService(id, data); + + res.status(STATUS_CODE.SUCCESS).json({ + status: STATUS.SUCCESS, + data: dashboard, + message: 'Dashboard updated successfully', + }); +}); diff --git a/src/modules/Dashboards/routes/dashboard.routes.ts b/src/modules/Dashboards/routes/dashboard.routes.ts index 8522d99..cab400b 100644 --- a/src/modules/Dashboards/routes/dashboard.routes.ts +++ b/src/modules/Dashboards/routes/dashboard.routes.ts @@ -1,13 +1,21 @@ import Router from 'express'; -import { createDashboard } from '../controllers/dashboard.controller'; +import { createDashboard, updateDashboard } from '../controllers/dashboard.controller'; import { authenticate } from '../../../common/middlewares'; import validationMiddleware from '../../../common/middlewares/validation.middleware'; -import { createDashboardValidation } from '../validation/dashboard.validation'; +import { + createDashboardValidation, + updateDashboardValidation, + getDashboardValidation, +} from '../validation/dashboard.validation'; const router = Router(); router.use(authenticate); router.post('/', validationMiddleware({ body: createDashboardValidation }), createDashboard); - +router.patch( + '/:id', + validationMiddleware({ params: getDashboardValidation, body: updateDashboardValidation }), + updateDashboard, +); export default router; diff --git a/src/modules/Dashboards/services/dashboard.service.ts b/src/modules/Dashboards/services/dashboard.service.ts index 2e58511..f446d1a 100644 --- a/src/modules/Dashboards/services/dashboard.service.ts +++ b/src/modules/Dashboards/services/dashboard.service.ts @@ -1,6 +1,7 @@ import logger from '../../../common/utils/logger'; import { prisma } from '../../../config/postgres'; -import { createDashboardData } from '../types/types'; +import { createDashboardData, updateDashboardData } from '../types/types'; +import ApiErrorHandler from '../../../common/utils/ApiErrorHandler'; export const createDashboardService = async (ownerId: string, data: createDashboardData) => { const dashboard = await prisma.dashboard.create({ @@ -15,3 +16,16 @@ export const createDashboardService = async (ownerId: string, data: createDashbo logger.info(`dashboard created successfully: title ${dashboard.title}`); return dashboard; }; + +export const updateDashboardService = async (id: string, data: updateDashboardData) => { + const existing = await prisma.dashboard.findUnique({ where: { id } }); + if (!existing) throw new ApiErrorHandler(404, 'Dashboard Not Found'); + + const dashboard = await prisma.dashboard.update({ + where: { id }, + data: { ...data }, + }); + + logger.info(`dashboard updated successfully: id ${id}`); + return dashboard; +}; diff --git a/src/modules/Dashboards/types/types.ts b/src/modules/Dashboards/types/types.ts index 31a4aef..f5a85a6 100644 --- a/src/modules/Dashboards/types/types.ts +++ b/src/modules/Dashboards/types/types.ts @@ -13,3 +13,5 @@ export interface createDashboardData { tags?: string[]; panels?: any; } + +export type updateDashboardData = Partial; diff --git a/src/modules/Dashboards/validation/dashboard.validation.ts b/src/modules/Dashboards/validation/dashboard.validation.ts index 8812185..966edf4 100644 --- a/src/modules/Dashboards/validation/dashboard.validation.ts +++ b/src/modules/Dashboards/validation/dashboard.validation.ts @@ -13,3 +13,9 @@ export const createDashboardValidation = z.object({ tags: z.array(z.string().trim()).optional(), panels: z.array(panelValidation).optional(), }); + +export const updateDashboardValidation = createDashboardValidation.partial(); + +export const getDashboardValidation = z.object({ + id: z.uuid(), +}); From 8529f39f4a37df62546c95a3fd9b53f4ae22eeab Mon Sep 17 00:00:00 2001 From: salah-72 Date: Wed, 8 Jul 2026 17:48:02 +0300 Subject: [PATCH 4/8] feat(dashboard): add delete dashboard endpoint --- .../Dashboards/controllers/dashboard.controller.ts | 13 ++++++++++++- src/modules/Dashboards/routes/dashboard.routes.ts | 8 +++++++- .../Dashboards/services/dashboard.service.ts | 9 +++++---- .../Dashboards/validation/dashboard.validation.ts | 4 ++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/modules/Dashboards/controllers/dashboard.controller.ts b/src/modules/Dashboards/controllers/dashboard.controller.ts index 2317a2a..3cd1161 100644 --- a/src/modules/Dashboards/controllers/dashboard.controller.ts +++ b/src/modules/Dashboards/controllers/dashboard.controller.ts @@ -3,7 +3,11 @@ import catchAsync from '../../../common/utils/catchAsync'; import { STATUS_CODE } from '../../../common/constants/responseCode'; import { STATUS } from '../../../common/constants/responseStatus'; import { createDashboardData, updateDashboardData } from '../types/types'; -import { createDashboardService, updateDashboardService } from '../services/dashboard.service'; +import { + createDashboardService, + updateDashboardService, + deleteDashboardService, +} from '../services/dashboard.service'; import { IRequest } from '../../../common/interfaces/types'; export const createDashboard = catchAsync(async (req: IRequest, res: Response) => { @@ -28,3 +32,10 @@ export const updateDashboard = catchAsync(async (req: Request, res: Response) => message: 'Dashboard updated successfully', }); }); + +export const deleteDashboard = catchAsync(async (req: Request, res: Response) => { + const id = req.params.id as string; + await deleteDashboardService(id); + + res.status(STATUS_CODE.NO_CONTENT).send(); +}); diff --git a/src/modules/Dashboards/routes/dashboard.routes.ts b/src/modules/Dashboards/routes/dashboard.routes.ts index cab400b..2354c0b 100644 --- a/src/modules/Dashboards/routes/dashboard.routes.ts +++ b/src/modules/Dashboards/routes/dashboard.routes.ts @@ -1,11 +1,16 @@ import Router from 'express'; -import { createDashboard, updateDashboard } from '../controllers/dashboard.controller'; +import { + createDashboard, + updateDashboard, + deleteDashboard, +} from '../controllers/dashboard.controller'; import { authenticate } from '../../../common/middlewares'; import validationMiddleware from '../../../common/middlewares/validation.middleware'; import { createDashboardValidation, updateDashboardValidation, getDashboardValidation, + deleteDashboardValidation, } from '../validation/dashboard.validation'; const router = Router(); @@ -18,4 +23,5 @@ router.patch( validationMiddleware({ params: getDashboardValidation, body: updateDashboardValidation }), updateDashboard, ); +router.delete('/:id', validationMiddleware({ params: deleteDashboardValidation }), deleteDashboard); export default router; diff --git a/src/modules/Dashboards/services/dashboard.service.ts b/src/modules/Dashboards/services/dashboard.service.ts index f446d1a..bf589e8 100644 --- a/src/modules/Dashboards/services/dashboard.service.ts +++ b/src/modules/Dashboards/services/dashboard.service.ts @@ -1,7 +1,6 @@ import logger from '../../../common/utils/logger'; import { prisma } from '../../../config/postgres'; import { createDashboardData, updateDashboardData } from '../types/types'; -import ApiErrorHandler from '../../../common/utils/ApiErrorHandler'; export const createDashboardService = async (ownerId: string, data: createDashboardData) => { const dashboard = await prisma.dashboard.create({ @@ -18,9 +17,6 @@ export const createDashboardService = async (ownerId: string, data: createDashbo }; export const updateDashboardService = async (id: string, data: updateDashboardData) => { - const existing = await prisma.dashboard.findUnique({ where: { id } }); - if (!existing) throw new ApiErrorHandler(404, 'Dashboard Not Found'); - const dashboard = await prisma.dashboard.update({ where: { id }, data: { ...data }, @@ -29,3 +25,8 @@ export const updateDashboardService = async (id: string, data: updateDashboardDa logger.info(`dashboard updated successfully: id ${id}`); return dashboard; }; + +export const deleteDashboardService = async (id: string) => { + await prisma.dashboard.delete({ where: { id } }); + logger.info(`dashboard deleted successfully: id ${id}`); +}; diff --git a/src/modules/Dashboards/validation/dashboard.validation.ts b/src/modules/Dashboards/validation/dashboard.validation.ts index 966edf4..3a0aa34 100644 --- a/src/modules/Dashboards/validation/dashboard.validation.ts +++ b/src/modules/Dashboards/validation/dashboard.validation.ts @@ -19,3 +19,7 @@ export const updateDashboardValidation = createDashboardValidation.partial(); export const getDashboardValidation = z.object({ id: z.uuid(), }); + +export const deleteDashboardValidation = z.object({ + id: z.uuid(), +}); From 2b9fa2312fbc545e856db8b8e6ad138e725c817b Mon Sep 17 00:00:00 2001 From: salah-72 Date: Wed, 8 Jul 2026 17:54:34 +0300 Subject: [PATCH 5/8] feat(dashboard): add Get dashboard endpoint --- .../Dashboards/controllers/dashboard.controller.ts | 12 ++++++++++++ src/modules/Dashboards/routes/dashboard.routes.ts | 4 ++++ src/modules/Dashboards/services/dashboard.service.ts | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/src/modules/Dashboards/controllers/dashboard.controller.ts b/src/modules/Dashboards/controllers/dashboard.controller.ts index 3cd1161..dbc965a 100644 --- a/src/modules/Dashboards/controllers/dashboard.controller.ts +++ b/src/modules/Dashboards/controllers/dashboard.controller.ts @@ -7,6 +7,7 @@ import { createDashboardService, updateDashboardService, deleteDashboardService, + getDashboardService, } from '../services/dashboard.service'; import { IRequest } from '../../../common/interfaces/types'; @@ -39,3 +40,14 @@ export const deleteDashboard = catchAsync(async (req: Request, res: Response) => res.status(STATUS_CODE.NO_CONTENT).send(); }); + +export const getDashboardById = catchAsync(async (req: Request, res: Response) => { + const id = req.params.id as string; + const dashboard = await getDashboardService(id); + + res.status(STATUS_CODE.SUCCESS).json({ + status: STATUS.SUCCESS, + data: dashboard, + message: 'Dashboard retrieved successfully', + }); +}); diff --git a/src/modules/Dashboards/routes/dashboard.routes.ts b/src/modules/Dashboards/routes/dashboard.routes.ts index 2354c0b..6af8c83 100644 --- a/src/modules/Dashboards/routes/dashboard.routes.ts +++ b/src/modules/Dashboards/routes/dashboard.routes.ts @@ -3,6 +3,7 @@ import { createDashboard, updateDashboard, deleteDashboard, + getDashboardById, } from '../controllers/dashboard.controller'; import { authenticate } from '../../../common/middlewares'; import validationMiddleware from '../../../common/middlewares/validation.middleware'; @@ -18,6 +19,9 @@ const router = Router(); router.use(authenticate); router.post('/', validationMiddleware({ body: createDashboardValidation }), createDashboard); + +router.get('/:id', validationMiddleware({ params: getDashboardValidation }), getDashboardById); + router.patch( '/:id', validationMiddleware({ params: getDashboardValidation, body: updateDashboardValidation }), diff --git a/src/modules/Dashboards/services/dashboard.service.ts b/src/modules/Dashboards/services/dashboard.service.ts index bf589e8..085cd2b 100644 --- a/src/modules/Dashboards/services/dashboard.service.ts +++ b/src/modules/Dashboards/services/dashboard.service.ts @@ -1,3 +1,4 @@ +import ApiErrorHandler from '../../../common/utils/ApiErrorHandler'; import logger from '../../../common/utils/logger'; import { prisma } from '../../../config/postgres'; import { createDashboardData, updateDashboardData } from '../types/types'; @@ -30,3 +31,11 @@ export const deleteDashboardService = async (id: string) => { await prisma.dashboard.delete({ where: { id } }); logger.info(`dashboard deleted successfully: id ${id}`); }; + +export const getDashboardService = async (id: string) => { + const dashboard = await prisma.dashboard.findUnique({ where: { id } }); + if (!dashboard) throw new ApiErrorHandler(404, 'Dashboard Not Found'); + + logger.info(`dashboard retrieved successfully: id ${id}`); + return dashboard; +}; From 3780a67e85a385b65f34113b8e9bbedb99ec47cb Mon Sep 17 00:00:00 2001 From: salah-72 Date: Wed, 8 Jul 2026 18:26:03 +0300 Subject: [PATCH 6/8] feat(dashboard): add Get all dashboards endpoint --- .../controllers/dashboard.controller.ts | 14 ++++++- .../Dashboards/routes/dashboard.routes.ts | 4 ++ .../Dashboards/services/dashboard.service.ts | 36 ++++++++++++++++- .../Dashboards/services/dashboard.utils.ts | 40 +++++++++++++++++++ src/modules/Dashboards/types/types.ts | 6 +++ .../validation/dashboard.validation.ts | 6 +++ 6 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/modules/Dashboards/services/dashboard.utils.ts diff --git a/src/modules/Dashboards/controllers/dashboard.controller.ts b/src/modules/Dashboards/controllers/dashboard.controller.ts index dbc965a..97e7ff6 100644 --- a/src/modules/Dashboards/controllers/dashboard.controller.ts +++ b/src/modules/Dashboards/controllers/dashboard.controller.ts @@ -2,12 +2,13 @@ import { Request, Response } from 'express'; import catchAsync from '../../../common/utils/catchAsync'; import { STATUS_CODE } from '../../../common/constants/responseCode'; import { STATUS } from '../../../common/constants/responseStatus'; -import { createDashboardData, updateDashboardData } from '../types/types'; +import { createDashboardData, ListDashboardsQuery, updateDashboardData } from '../types/types'; import { createDashboardService, updateDashboardService, deleteDashboardService, getDashboardService, + getAllDashboardsService, } from '../services/dashboard.service'; import { IRequest } from '../../../common/interfaces/types'; @@ -51,3 +52,14 @@ export const getDashboardById = catchAsync(async (req: Request, res: Response) = message: 'Dashboard retrieved successfully', }); }); + +export const getAllDashboards = catchAsync(async (req: Request, res: Response) => { + const query: ListDashboardsQuery = req.query as unknown as ListDashboardsQuery; + const dashboards = await getAllDashboardsService(query); + + res.status(STATUS_CODE.SUCCESS).json({ + status: STATUS.SUCCESS, + data: dashboards.data, + meta: dashboards.meta, + }); +}); diff --git a/src/modules/Dashboards/routes/dashboard.routes.ts b/src/modules/Dashboards/routes/dashboard.routes.ts index 6af8c83..08218c3 100644 --- a/src/modules/Dashboards/routes/dashboard.routes.ts +++ b/src/modules/Dashboards/routes/dashboard.routes.ts @@ -4,6 +4,7 @@ import { updateDashboard, deleteDashboard, getDashboardById, + getAllDashboards, } from '../controllers/dashboard.controller'; import { authenticate } from '../../../common/middlewares'; import validationMiddleware from '../../../common/middlewares/validation.middleware'; @@ -12,12 +13,15 @@ import { updateDashboardValidation, getDashboardValidation, deleteDashboardValidation, + queryDashboardsValidation, } from '../validation/dashboard.validation'; const router = Router(); router.use(authenticate); +router.get('/', validationMiddleware({ query: queryDashboardsValidation }), getAllDashboards); + router.post('/', validationMiddleware({ body: createDashboardValidation }), createDashboard); router.get('/:id', validationMiddleware({ params: getDashboardValidation }), getDashboardById); diff --git a/src/modules/Dashboards/services/dashboard.service.ts b/src/modules/Dashboards/services/dashboard.service.ts index 085cd2b..ae89f12 100644 --- a/src/modules/Dashboards/services/dashboard.service.ts +++ b/src/modules/Dashboards/services/dashboard.service.ts @@ -1,7 +1,9 @@ import ApiErrorHandler from '../../../common/utils/ApiErrorHandler'; +import { paginate } from '../../../common/utils/ApiFeatures.utils'; import logger from '../../../common/utils/logger'; import { prisma } from '../../../config/postgres'; -import { createDashboardData, updateDashboardData } from '../types/types'; +import { createDashboardData, ListDashboardsQuery, updateDashboardData } from '../types/types'; +import { buildDashboardFilter } from './dashboard.utils'; export const createDashboardService = async (ownerId: string, data: createDashboardData) => { const dashboard = await prisma.dashboard.create({ @@ -39,3 +41,35 @@ export const getDashboardService = async (id: string) => { logger.info(`dashboard retrieved successfully: id ${id}`); return dashboard; }; + +export const getAllDashboardsService = async (query: ListDashboardsQuery) => { + const where = buildDashboardFilter({ search: query.search }); + + const page = query.page || 1; + const limit = query.limit || 10; + const skip = (page - 1) * limit; + + const [data, total] = await Promise.all([ + prisma.dashboard.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + }), + prisma.dashboard.count({ where }), + ]); + + const totalPages = Math.ceil(total / limit); + + return { + data, + meta: { + total, + page, + limit, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + }, + }; +}; diff --git a/src/modules/Dashboards/services/dashboard.utils.ts b/src/modules/Dashboards/services/dashboard.utils.ts new file mode 100644 index 0000000..2dcb94a --- /dev/null +++ b/src/modules/Dashboards/services/dashboard.utils.ts @@ -0,0 +1,40 @@ +import { Prisma } from '@prisma/client'; + +export const buildDashboardFilter = (query: { + search?: string; + ownerId?: string; + createdAt?: string; +}): Prisma.DashboardWhereInput => { + const { search, ownerId, createdAt } = query; + const where: Prisma.DashboardWhereInput = {}; + + if (ownerId) where.ownerId = ownerId; + + if (search) { + where.OR = [ + { + title: { + contains: search, + mode: 'insensitive', + }, + }, + { + description: { + contains: search, + mode: 'insensitive', + }, + }, + ]; + } + + if (createdAt) { + if (createdAt.includes('to')) { + const [start, end] = createdAt.split('to').map((d) => new Date(d.trim())); + where.createdAt = { gte: start, lte: end }; + } else { + where.createdAt = { gte: new Date(createdAt) }; + } + } + + return where; +}; diff --git a/src/modules/Dashboards/types/types.ts b/src/modules/Dashboards/types/types.ts index f5a85a6..f966ddf 100644 --- a/src/modules/Dashboards/types/types.ts +++ b/src/modules/Dashboards/types/types.ts @@ -15,3 +15,9 @@ export interface createDashboardData { } export type updateDashboardData = Partial; + +export interface ListDashboardsQuery { + search?: string; + page: number; + limit: number; +} diff --git a/src/modules/Dashboards/validation/dashboard.validation.ts b/src/modules/Dashboards/validation/dashboard.validation.ts index 3a0aa34..afd6a74 100644 --- a/src/modules/Dashboards/validation/dashboard.validation.ts +++ b/src/modules/Dashboards/validation/dashboard.validation.ts @@ -23,3 +23,9 @@ export const getDashboardValidation = z.object({ export const deleteDashboardValidation = z.object({ id: z.uuid(), }); + +export const queryDashboardsValidation = z.object({ + search: z.string().min(1).optional(), + page: z.coerce.number().int().min(1).default(1).optional(), + limit: z.coerce.number().int().min(1).max(100).default(10).optional(), +}); From a1d8d8ee9d7fdaf26f21eca8bc750f6877e8a6d1 Mon Sep 17 00:00:00 2001 From: salah-72 Date: Thu, 9 Jul 2026 19:18:19 +0300 Subject: [PATCH 7/8] feat(dashboard): add panel data endpoint with ES aggregations --- .../controllers/dashboard.controller.ts | 20 ++- .../Dashboards/routes/dashboard.routes.ts | 8 +- .../Dashboards/services/dashboard.service.ts | 162 +++++++++++++++++- src/modules/Dashboards/types/types.ts | 68 +++++++- .../validation/dashboard.validation.ts | 48 +++++- 5 files changed, 292 insertions(+), 14 deletions(-) diff --git a/src/modules/Dashboards/controllers/dashboard.controller.ts b/src/modules/Dashboards/controllers/dashboard.controller.ts index 97e7ff6..9c3da5a 100644 --- a/src/modules/Dashboards/controllers/dashboard.controller.ts +++ b/src/modules/Dashboards/controllers/dashboard.controller.ts @@ -2,13 +2,19 @@ import { Request, Response } from 'express'; import catchAsync from '../../../common/utils/catchAsync'; import { STATUS_CODE } from '../../../common/constants/responseCode'; import { STATUS } from '../../../common/constants/responseStatus'; -import { createDashboardData, ListDashboardsQuery, updateDashboardData } from '../types/types'; +import { + createDashboardData, + DashboardDataQuery, + ListDashboardsQuery, + updateDashboardData, +} from '../types/types'; import { createDashboardService, updateDashboardService, deleteDashboardService, getDashboardService, getAllDashboardsService, + getDashboardDataService, } from '../services/dashboard.service'; import { IRequest } from '../../../common/interfaces/types'; @@ -63,3 +69,15 @@ export const getAllDashboards = catchAsync(async (req: Request, res: Response) = meta: dashboards.meta, }); }); + +export const getDashboardData = catchAsync(async (req: Request, res: Response) => { + const id = req.params.id as string; + const query: DashboardDataQuery = req.query as unknown as DashboardDataQuery; + const data = await getDashboardDataService(id, query); + + res.status(STATUS_CODE.SUCCESS).json({ + status: STATUS.SUCCESS, + data, + message: 'Dashboard data retrieved successfully', + }); +}); diff --git a/src/modules/Dashboards/routes/dashboard.routes.ts b/src/modules/Dashboards/routes/dashboard.routes.ts index 08218c3..cad0f29 100644 --- a/src/modules/Dashboards/routes/dashboard.routes.ts +++ b/src/modules/Dashboards/routes/dashboard.routes.ts @@ -5,6 +5,7 @@ import { deleteDashboard, getDashboardById, getAllDashboards, + getDashboardData, } from '../controllers/dashboard.controller'; import { authenticate } from '../../../common/middlewares'; import validationMiddleware from '../../../common/middlewares/validation.middleware'; @@ -14,6 +15,7 @@ import { getDashboardValidation, deleteDashboardValidation, queryDashboardsValidation, + dashboardDataQueryValidation, } from '../validation/dashboard.validation'; const router = Router(); @@ -21,7 +23,11 @@ const router = Router(); router.use(authenticate); router.get('/', validationMiddleware({ query: queryDashboardsValidation }), getAllDashboards); - +router.get( + '/:id/data', + validationMiddleware({ params: getDashboardValidation, query: dashboardDataQueryValidation }), + getDashboardData, +); router.post('/', validationMiddleware({ body: createDashboardValidation }), createDashboard); router.get('/:id', validationMiddleware({ params: getDashboardValidation }), getDashboardById); diff --git a/src/modules/Dashboards/services/dashboard.service.ts b/src/modules/Dashboards/services/dashboard.service.ts index ae89f12..f46a481 100644 --- a/src/modules/Dashboards/services/dashboard.service.ts +++ b/src/modules/Dashboards/services/dashboard.service.ts @@ -1,8 +1,22 @@ import ApiErrorHandler from '../../../common/utils/ApiErrorHandler'; -import { paginate } from '../../../common/utils/ApiFeatures.utils'; import logger from '../../../common/utils/logger'; +import { Prisma } from '@prisma/client'; import { prisma } from '../../../config/postgres'; -import { createDashboardData, ListDashboardsQuery, updateDashboardData } from '../types/types'; +import { elasticClient } from '../../../config/elasticsearch'; +import { + createDashboardData, + ListDashboardsQuery, + updateDashboardData, + DashboardDataQuery, + DashboardPanelDefinition, + MetricPanelDefinition, + HistogramPanelDefinition, + BreakdownPanelDefinition, + DashboardPanelsData, + PanelStat, + HistogramPoint, + BreakdownItem, +} from '../types/types'; import { buildDashboardFilter } from './dashboard.utils'; export const createDashboardService = async (ownerId: string, data: createDashboardData) => { @@ -11,7 +25,7 @@ export const createDashboardService = async (ownerId: string, data: createDashbo title: data.title, description: data.description, tags: data.tags ?? [], - panels: data.panels ?? [], + panels: data.panels ? (data.panels as unknown as Prisma.InputJsonValue) : undefined, ownerId, }, }); @@ -22,7 +36,10 @@ export const createDashboardService = async (ownerId: string, data: createDashbo export const updateDashboardService = async (id: string, data: updateDashboardData) => { const dashboard = await prisma.dashboard.update({ where: { id }, - data: { ...data }, + data: { + ...data, + panels: data.panels ? (data.panels as unknown as Prisma.InputJsonValue) : undefined, + }, }); logger.info(`dashboard updated successfully: id ${id}`); @@ -73,3 +90,140 @@ export const getAllDashboardsService = async (query: ListDashboardsQuery) => { }, }; }; + +const DEFAULT_RANGE_HOURS = 24; + +const buildTimeRange = (query: DashboardDataQuery) => { + const to = query.to ? new Date(query.to) : new Date(); + const from = query.from + ? new Date(query.from) + : new Date(to.getTime() - DEFAULT_RANGE_HOURS * 60 * 60 * 1000); + return { gte: from.toISOString(), lte: to.toISOString() }; +}; + +const buildFilterClauses = (filter?: Record) => + filter ? Object.entries(filter).map(([field, value]) => ({ term: { [field]: value } })) : []; + +const buildBoolQuery = ( + timeRange: { gte: string; lte: string }, + filter?: Record, +) => ({ + bool: { + filter: [{ range: { '@timestamp': timeRange } }, ...buildFilterClauses(filter)], + }, +}); + +const runMetricPanel = async ( + panel: MetricPanelDefinition, + timeRange: { gte: string; lte: string }, +): Promise => { + const { spec } = panel; + const query = buildBoolQuery(timeRange, spec.filter); + + if (spec.aggType === 'count') { + const res = await elasticClient.count({ index: spec.index, query }); + return { label: panel.title, value: res.count }; + } + + if (spec.aggType === 'cardinality') { + if (!spec.field) throw new Error(`Panel "${panel.title}": cardinality metric requires "field"`); + const res = await elasticClient.search({ + index: spec.index, + size: 0, + query, + aggs: { unique: { cardinality: { field: spec.field } } }, + }); + const value = (res.aggregations as Record)?.unique?.value ?? 0; + return { label: panel.title, value }; + } + + const [totalRes, numeratorRes] = await Promise.all([ + elasticClient.count({ index: spec.index, query }), + elasticClient.count({ + index: spec.index, + query: buildBoolQuery(timeRange, { ...spec.filter, ...spec.numeratorFilter }), + }), + ]); + const total = totalRes.count; + const numerator = numeratorRes.count; + const value = total > 0 ? `${((numerator / total) * 100).toFixed(1)}%` : '0.0%'; + return { label: panel.title, value }; +}; + +const runHistogramPanel = async ( + panel: HistogramPanelDefinition, + timeRange: { gte: string; lte: string }, +): Promise => { + const { spec } = panel; + const res = await elasticClient.search({ + index: spec.index, + size: 0, + query: buildBoolQuery(timeRange, spec.filter), + aggs: { + over_time: { date_histogram: { field: '@timestamp', fixed_interval: spec.interval ?? '1h' } }, + }, + }); + + const buckets = ((res.aggregations as Record)?.over_time?.buckets ?? []) as { + key: number; + doc_count: number; + }[]; + + return buckets.map((b) => ({ + time: new Date(b.key).toISOString().slice(11, 16), + count: b.doc_count, + })); +}; + +const runBreakdownPanel = async ( + panel: BreakdownPanelDefinition, + timeRange: { gte: string; lte: string }, +): Promise => { + const { spec } = panel; + const res = await elasticClient.search({ + index: spec.index, + size: 0, + query: buildBoolQuery(timeRange, spec.filter), + aggs: { by_field: { terms: { field: spec.field, size: spec.size ?? 5 } } }, + }); + + const buckets = ((res.aggregations as Record)?.by_field?.buckets ?? []) as { + key: string; + doc_count: number; + }[]; + + return buckets.map((b) => ({ label: String(b.key), value: b.doc_count })); +}; + +export const getDashboardDataService = async ( + id: string, + query: DashboardDataQuery, +): Promise => { + const dashboard = await prisma.dashboard.findUnique({ where: { id } }); + if (!dashboard) throw new ApiErrorHandler(404, 'Dashboard Not Found'); + + const panels = (dashboard.panels as unknown as DashboardPanelDefinition[]) ?? []; + const timeRange = buildTimeRange(query); + + const stats: PanelStat[] = []; + let histogram: HistogramPoint[] = []; + let breakdown: BreakdownItem[] = []; + + await Promise.all( + panels.map(async (panel) => { + try { + if (panel.type === 'metric') { + stats.push(await runMetricPanel(panel, timeRange)); + } else if (panel.type === 'histogram') { + histogram = await runHistogramPanel(panel, timeRange); + } else if (panel.type === 'breakdown') { + breakdown = await runBreakdownPanel(panel, timeRange); + } + } catch (error) { + logger.error(`Failed to compute panel "${panel.title}" for dashboard ${id}`, { error }); + } + }), + ); + + return { stats, histogram, breakdown }; +}; diff --git a/src/modules/Dashboards/types/types.ts b/src/modules/Dashboards/types/types.ts index f966ddf..b66d713 100644 --- a/src/modules/Dashboards/types/types.ts +++ b/src/modules/Dashboards/types/types.ts @@ -1,17 +1,49 @@ -export type PanelType = 'metric' | 'histogram' | 'breakdown'; +export interface MetricPanelDefinition { + id: string; + type: 'metric'; + title: string; + spec: { + index: string; + aggType: 'count' | 'cardinality' | 'ratio'; + field?: string; + filter?: Record; + numeratorFilter?: Record; + }; +} -export interface DashboardPanelDefinition { +export interface HistogramPanelDefinition { id: string; - type: PanelType; + type: 'histogram'; title: string; - spec: Record; + spec: { + index: string; + interval?: string; + filter?: Record; + }; } +export interface BreakdownPanelDefinition { + id: string; + type: 'breakdown'; + title: string; + spec: { + index: string; + field: string; + size?: number; + filter?: Record; + }; +} + +export type DashboardPanelDefinition = + | MetricPanelDefinition + | HistogramPanelDefinition + | BreakdownPanelDefinition; + export interface createDashboardData { title: string; description?: string; tags?: string[]; - panels?: any; + panels?: DashboardPanelDefinition[]; } export type updateDashboardData = Partial; @@ -21,3 +53,29 @@ export interface ListDashboardsQuery { page: number; limit: number; } + +export interface DashboardDataQuery { + from?: string; + to?: string; +} + +export interface PanelStat { + label: string; + value: string | number; +} + +export interface HistogramPoint { + time: string; + count: number; +} + +export interface BreakdownItem { + label: string; + value: number; +} + +export interface DashboardPanelsData { + stats: PanelStat[]; + histogram: HistogramPoint[]; + breakdown: BreakdownItem[]; +} diff --git a/src/modules/Dashboards/validation/dashboard.validation.ts b/src/modules/Dashboards/validation/dashboard.validation.ts index afd6a74..4f18e6c 100644 --- a/src/modules/Dashboards/validation/dashboard.validation.ts +++ b/src/modules/Dashboards/validation/dashboard.validation.ts @@ -1,12 +1,49 @@ import * as z from 'zod'; -const panelValidation = z.object({ +const filterValidation = z.record(z.string(), z.string()).optional(); + +const metricPanelValidation = z.object({ + id: z.string().min(1), + type: z.literal('metric'), + title: z.string().min(1).max(150), + spec: z.object({ + index: z.string().min(1), + aggType: z.enum(['count', 'cardinality', 'ratio']), + field: z.string().optional(), + filter: filterValidation, + numeratorFilter: filterValidation, + }), +}); + +const histogramPanelValidation = z.object({ + id: z.string().min(1), + type: z.literal('histogram'), + title: z.string().min(1).max(150), + spec: z.object({ + index: z.string().min(1), + interval: z.string().optional(), + filter: filterValidation, + }), +}); + +const breakdownPanelValidation = z.object({ id: z.string().min(1), - type: z.enum(['metric', 'histogram', 'breakdown']), + type: z.literal('breakdown'), title: z.string().min(1).max(150), - spec: z.record(z.string(), z.any()), + spec: z.object({ + index: z.string().min(1), + field: z.string().min(1), + size: z.number().int().min(1).max(50).optional(), + filter: filterValidation, + }), }); +const panelValidation = z.discriminatedUnion('type', [ + metricPanelValidation, + histogramPanelValidation, + breakdownPanelValidation, +]); + export const createDashboardValidation = z.object({ title: z.string().min(3).max(150).trim(), description: z.string().max(500).trim().optional(), @@ -29,3 +66,8 @@ export const queryDashboardsValidation = z.object({ page: z.coerce.number().int().min(1).default(1).optional(), limit: z.coerce.number().int().min(1).max(100).default(10).optional(), }); + +export const dashboardDataQueryValidation = z.object({ + from: z.string().datetime().optional(), + to: z.string().datetime().optional(), +}); From 77ef8ed8386c9e39156fa2b68e7f4ff4328beeb7 Mon Sep 17 00:00:00 2001 From: salah-72 Date: Fri, 10 Jul 2026 12:59:11 +0300 Subject: [PATCH 8/8] docs(swagger): add swagger for dashboard --- src/docs/openapi.docs.ts | 116 +++++ src/docs/swagger.config.ts | 4 + .../Dashboards/routes/dashboard.routes.ts | 402 +++++++++++++++++- 3 files changed, 520 insertions(+), 2 deletions(-) diff --git a/src/docs/openapi.docs.ts b/src/docs/openapi.docs.ts index 7cd08f5..cc0ce33 100644 --- a/src/docs/openapi.docs.ts +++ b/src/docs/openapi.docs.ts @@ -716,4 +716,120 @@ * 404: { $ref: '#/components/responses/NotFound' } */ +/** + * @swagger + * components: + * schemas: + * DashboardPanel: + * type: object + * required: [id, type, title, spec] + * properties: + * id: { type: string, example: p1 } + * type: { type: string, enum: [metric, histogram, breakdown] } + * title: { type: string, example: Total events } + * spec: + * type: object + * description: "metric: { index, aggType: count|cardinality|ratio, field?, filter?, numeratorFilter? } | histogram: { index, interval?, filter? } | breakdown: { index, field, size?, filter? }" + * example: { index: "logs-auditbeat.auditd-*", aggType: cardinality, field: host.name } + * Dashboard: + * type: object + * properties: + * id: { type: string, format: uuid } + * title: { type: string, example: Auditd Overview } + * description: { type: string, nullable: true, example: Kernel audit events } + * tags: { type: array, items: { type: string }, example: [auditd] } + * ownerId: { type: string, format: uuid } + * panels: { type: array, items: { $ref: '#/components/schemas/DashboardPanel' } } + * createdAt: { type: string, format: date-time } + * updatedAt: { type: string, format: date-time } + * DashboardListItem: + * type: object + * properties: + * id: { type: string, format: uuid } + * title: { type: string } + * desc: { type: string } + * tags: { type: array, items: { type: string } } + * DashboardInput: + * type: object + * required: [title] + * properties: + * title: { type: string, example: Auditd Overview } + * description: { type: string, example: Kernel audit events } + * tags: { type: array, items: { type: string }, example: [auditd] } + * panels: { type: array, items: { $ref: '#/components/schemas/DashboardPanel' } } + */ + +/** + * @swagger + * /api/v1/dashboards: + * get: + * tags: [Dashboards] + * summary: List dashboards + * security: [{ bearerAuth: [] }] + * parameters: + * - { in: query, name: search, schema: { type: string } } + * - { in: query, name: page, schema: { type: integer, default: 1 } } + * - { in: query, name: limit, schema: { type: integer, default: 10 } } + * responses: + * 200: { description: Paginated dashboard list } + * 401: { $ref: '#/components/responses/Unauthorized' } + * post: + * tags: [Dashboards] + * summary: Create dashboard + * security: [{ bearerAuth: [] }] + * requestBody: + * required: true + * content: + * application/json: + * schema: { $ref: '#/components/schemas/DashboardInput' } + * responses: + * 201: { description: Dashboard created } + * 401: { $ref: '#/components/responses/Unauthorized' } + * /api/v1/dashboards/{id}: + * get: + * tags: [Dashboards] + * summary: Get dashboard definition + * security: [{ bearerAuth: [] }] + * parameters: + * - { in: path, name: id, required: true, schema: { type: string, format: uuid } } + * responses: + * 200: { description: Dashboard details } + * 404: { $ref: '#/components/responses/NotFound' } + * patch: + * tags: [Dashboards] + * summary: Update dashboard + * security: [{ bearerAuth: [] }] + * parameters: + * - { in: path, name: id, required: true, schema: { type: string, format: uuid } } + * requestBody: + * required: true + * content: + * application/json: + * schema: { $ref: '#/components/schemas/DashboardInput' } + * responses: + * 200: { description: Dashboard updated } + * 404: { $ref: '#/components/responses/NotFound' } + * delete: + * tags: [Dashboards] + * summary: Delete dashboard + * security: [{ bearerAuth: [] }] + * parameters: + * - { in: path, name: id, required: true, schema: { type: string, format: uuid } } + * responses: + * 204: { description: Dashboard deleted } + * 404: { $ref: '#/components/responses/NotFound' } + * /api/v1/dashboards/{id}/data: + * get: + * tags: [Dashboards] + * summary: Get live panel data over a time range + * security: [{ bearerAuth: [] }] + * parameters: + * - { in: path, name: id, required: true, schema: { type: string, format: uuid } } + * - { in: query, name: from, schema: { type: string, format: date-time } } + * - { in: query, name: to, schema: { type: string, format: date-time } } + * responses: + * 200: { description: Dashboard panel data } + * 404: { $ref: '#/components/responses/NotFound' } + */ + export {}; diff --git a/src/docs/swagger.config.ts b/src/docs/swagger.config.ts index 9ad8d1f..bbebc8b 100644 --- a/src/docs/swagger.config.ts +++ b/src/docs/swagger.config.ts @@ -86,6 +86,10 @@ const swaggerDefinition: SwaggerDefinition = { name: 'Alerts', description: 'Security alert queue management endpoints', }, + { + name: 'Dashboards', + description: 'Dashboard management endpoints', + }, ], }; diff --git a/src/modules/Dashboards/routes/dashboard.routes.ts b/src/modules/Dashboards/routes/dashboard.routes.ts index cad0f29..1bdc5f1 100644 --- a/src/modules/Dashboards/routes/dashboard.routes.ts +++ b/src/modules/Dashboards/routes/dashboard.routes.ts @@ -1,4 +1,4 @@ -import Router from 'express'; +import express from 'express'; import { createDashboard, updateDashboard, @@ -18,24 +18,422 @@ import { dashboardDataQueryValidation, } from '../validation/dashboard.validation'; -const router = Router(); +const router = express.Router(); router.use(authenticate); +/** + * @swagger + * tags: + * name: Dashboards + * description: Saved dashboards (panel definitions + live Elasticsearch data) + * + * components: + * schemas: + * DashboardPanel: + * type: object + * required: [id, type, title, spec] + * properties: + * id: + * type: string + * example: p1 + * type: + * type: string + * enum: [metric, histogram, breakdown] + * title: + * type: string + * example: Total events + * spec: + * type: object + * description: > + * Query/aggregation spec, shape depends on "type". + * metric: { index, aggType: count|cardinality|ratio, field?, filter?, numeratorFilter? } + * histogram: { index, interval?, filter? } + * breakdown: { index, field, size?, filter? } + * example: + * index: logs-auditbeat.auditd-* + * aggType: cardinality + * field: host.name + * Dashboard: + * type: object + * properties: + * id: + * type: string + * format: uuid + * title: + * type: string + * example: Auditd Overview + * description: + * type: string + * nullable: true + * example: Kernel audit events + * tags: + * type: array + * items: + * type: string + * example: [auditd] + * ownerId: + * type: string + * format: uuid + * panels: + * type: array + * items: + * $ref: '#/components/schemas/DashboardPanel' + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * DashboardListItem: + * type: object + * properties: + * id: + * type: string + * format: uuid + * title: + * type: string + * desc: + * type: string + * tags: + * type: array + * items: + * type: string + * PaginationMeta: + * type: object + * properties: + * total: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * totalPages: + * type: integer + * hasNextPage: + * type: boolean + * hasPreviousPage: + * type: boolean + */ + +/** + * @swagger + * /api/v1/dashboards: + * get: + * summary: List dashboards (metadata rows for the dashboard list page) + * tags: [Dashboards] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: search + * schema: + * type: string + * description: Search across title and description + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * responses: + * 200: + * description: Paginated list of dashboards + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * type: array + * items: + * $ref: '#/components/schemas/DashboardListItem' + * meta: + * $ref: '#/components/schemas/PaginationMeta' + * 401: + * description: Unauthorized + */ router.get('/', validationMiddleware({ query: queryDashboardsValidation }), getAllDashboards); + +/** + * @swagger + * /dashboards/{id}/data: + * get: + * summary: Get live panel data for a dashboard (metric cards, histogram, breakdown) + * description: > + * Runs each panel's Elasticsearch aggregation at request time over the given + * time range. Kept separate from the dashboard definition so changing the + * time range does not refetch the layout/panels config. + * tags: [Dashboards] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * format: uuid + * - in: query + * name: from + * schema: + * type: string + * format: date-time + * description: Start of time range (ISO 8601). Defaults to 24h before "to". + * - in: query + * name: to + * schema: + * type: string + * format: date-time + * description: End of time range (ISO 8601). Defaults to now. + * responses: + * 200: + * description: Dashboard panel data retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * type: object + * properties: + * stats: + * type: array + * items: + * type: object + * properties: + * label: + * type: string + * example: Total events + * value: + * oneOf: + * - type: string + * - type: number + * example: 128340 + * histogram: + * type: array + * items: + * type: object + * properties: + * time: + * type: string + * example: "14:00" + * count: + * type: integer + * example: 342 + * breakdown: + * type: array + * items: + * type: object + * properties: + * label: + * type: string + * example: success + * value: + * type: integer + * example: 87 + * message: + * type: string + * example: Dashboard data retrieved successfully + * 401: + * description: Unauthorized + * 404: + * description: Dashboard not found + */ + router.get( '/:id/data', validationMiddleware({ params: getDashboardValidation, query: dashboardDataQueryValidation }), getDashboardData, ); + +/** + * @swagger + * /dashboards: + * post: + * summary: Create a dashboard + * tags: [Dashboards] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [title] + * properties: + * title: + * type: string + * example: Auditd Overview + * description: + * type: string + * example: Kernel audit events + * tags: + * type: array + * items: + * type: string + * example: [auditd] + * panels: + * type: array + * items: + * $ref: '#/components/schemas/DashboardPanel' + * responses: + * 201: + * description: Dashboard created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * $ref: '#/components/schemas/Dashboard' + * message: + * type: string + * example: Dashboard created successfully + * 400: + * description: Validation error + * 401: + * description: Unauthorized + */ router.post('/', validationMiddleware({ body: createDashboardValidation }), createDashboard); +/** + * @swagger + * /dashboards/{id}: + * get: + * summary: Get a dashboard's full definition (metadata + panels config) + * tags: [Dashboards] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * format: uuid + * responses: + * 200: + * description: Dashboard retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * $ref: '#/components/schemas/Dashboard' + * message: + * type: string + * example: Dashboard retrieved successfully + * 401: + * description: Unauthorized + * 404: + * description: Dashboard not found + */ router.get('/:id', validationMiddleware({ params: getDashboardValidation }), getDashboardById); +/** + * @swagger + * /dashboards/{id}: + * patch: + * summary: Update a dashboard's metadata and/or panels + * tags: [Dashboards] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * format: uuid + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: + * type: string + * tags: + * type: array + * items: + * type: string + * panels: + * type: array + * items: + * $ref: '#/components/schemas/DashboardPanel' + * responses: + * 200: + * description: Dashboard updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * $ref: '#/components/schemas/Dashboard' + * message: + * type: string + * example: Dashboard updated successfully + * 400: + * description: Validation error + * 401: + * description: Unauthorized + * 404: + * description: Dashboard not found + */ router.patch( '/:id', validationMiddleware({ params: getDashboardValidation, body: updateDashboardValidation }), updateDashboard, ); + +/** + * @swagger + * /dashboards/{id}: + * delete: + * summary: Delete a dashboard + * tags: [Dashboards] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * format: uuid + * responses: + * 204: + * description: Dashboard deleted successfully (no content) + * 401: + * description: Unauthorized + * 404: + * description: Dashboard not found + */ router.delete('/:id', validationMiddleware({ params: deleteDashboardValidation }), deleteDashboard); export default router;