diff --git a/.env.example b/.env.example index bc70c47..18b0564 100644 --- a/.env.example +++ b/.env.example @@ -3,5 +3,5 @@ PORT=8080 API_KEY_TOKEN=token MONGODB_URL=mongodb://db:27017 MONGODB_DB_NAME=net-db -API_BASE_URL=db-api:8082 +API_BASE_URL=http://db-api:8082 CLIENT_URL=client:5173 diff --git a/docker-compose.yaml b/docker-compose.yaml index a23b111..cd323e4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -44,7 +44,7 @@ services: dockerfile: ../db-api/dev.Dockerfile command: npm run watch-node ports: - - '8082:3000' + - '8082:8082' env_file: ../db-api/.env volumes: - '../db-api:/usr/src/node-app' diff --git a/package.json b/package.json index ea53491..0000b0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dashboard-creator-server", - "version": "0.1.0", + "version": "1.0.1", "description": "", "main": "src/server.ts", "engines": { diff --git a/src/components/dashboard/dashboard.controller.ts b/src/components/dashboard/dashboard.controller.ts index b1773a9..0e9cef0 100644 --- a/src/components/dashboard/dashboard.controller.ts +++ b/src/components/dashboard/dashboard.controller.ts @@ -14,8 +14,14 @@ import { updateElementInDashboard, addFilter, getFilter, + getFilterByName, deleteFilter, updateFilter, + getElementByQueryId, + addLayoutItem, + getLayoutItems, + updateLayoutItem, + deleteLayoutItem, } from '@components/dashboard/dashboard.service'; import { IDashboard, @@ -25,7 +31,12 @@ import { IDashboardElement, IDashboardElementCustomQuery, } from '@components/dashboard/dashboardElement/dashboardElement.interface'; -import { IDashboardFilter } from '@components/dashboard/dashboardFilter/dashboardFilter.interface'; +import { + IDashboardFilterValue, + IDashboardFilter, + IDashboardFilterDependent, + IDashboardFilterDynamic, +} from '@components/dashboard/dashboardFilter/dashboardFilter.interface'; const { API_BASE_URL } = process.env; @@ -33,6 +44,7 @@ const createDashboard = async (req: Request, res: Response) => { try { const dashboardData = req.body as IWriteDashboard; const createdDashboard = await create(dashboardData); + logger.debug(`log dashboardData, ${dashboardData}`); res.status(httpStatus.CREATED).send({ message: 'Dashboard Created', @@ -42,6 +54,7 @@ const createDashboard = async (req: Request, res: Response) => { res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ message: err.message }); } }; +// const getAllDashboards = async (req: Request, res: Response) => { try { @@ -57,6 +70,40 @@ const getAllDashboards = async (req: Request, res: Response) => { const readDashboard = async (req: Request, res: Response) => { try { const dashboard = await read(req.params.id); + for (const filter of dashboard.filters) { + if ('queryId' in filter) { + const dynamicFilter = filter as + | IDashboardFilterDynamic + | IDashboardFilterDependent; + const filterValues = []; + if ('params' in dynamicFilter && dynamicFilter.params.length > 0) { + for (const param of dynamicFilter.params) { + filterValues.push({ name: param, value: [' '] }); + } + } + const queryPayload = prepareFilterQueryPayload( + dashboard, + filter as IDashboardFilterDynamic | IDashboardFilterDependent, + filterValues, + ); + logger.info( + `getFilterData queryPayload: ${JSON.stringify(queryPayload)}`, + ); + const url = `${API_BASE_URL}/execute-query`; + const response = await axios.post(url, queryPayload); + const data = await response.data; + logger.info(`getFilterData response: ${JSON.stringify(data)}`); + // Update options field if necessary + if (data && data.data && Array.isArray(data.data)) { + // Update options field if necessary + filter.options = data.data.map((item: any) => ({ + label: item.value, + value: item.value, + })); + } + } + } + res .status(httpStatus.OK) .send({ message: 'Dashboard Read', output: dashboard }); @@ -101,6 +148,8 @@ const addDashboardElement = async (req: Request, res: Response) => { try { const { dashboardId } = req.params; const elementData = req.body as IDashboardElement; + logger.debug(`elementData ${JSON.stringify(elementData)}`); + logger.debug(`dashboardId ${dashboardId}`); const newElement = await addElement(dashboardId, elementData); @@ -154,16 +203,23 @@ const updateDashboardElement = async (req: Request, res: Response) => { const prepareQueryPayload = ( dashboard: IDashboard, element: IDashboardElementCustomQuery, - filterValues: object[], + filterValues: IDashboardFilterValue[], ) => { const queryPayload: any = { - id: element.queryId, // Assuming queryId corresponds to the ID field in the payload + id: element.queryId, parameters: { values: [], identifiers: [], }, }; + // Extracting filter names from the dashboard + const dashboardFilterNames = dashboard.filters.map((filter) => filter.name); + // Filtering filterValues array based on dashboard filter names + const filteredFilterValues = filterValues.filter((filter) => + dashboardFilterNames.includes(filter.name), + ); + const extractDateRange = (period: string) => { let min_date: string; let max_date: string; @@ -228,7 +284,17 @@ const prepareQueryPayload = ( return { min_date, max_date }; }; - filterValues.forEach((filter: any) => { + // add default 'space' param for each missing (hidden) param + dashboardFilterNames.forEach((filterName) => { + if (!filteredFilterValues.some((filter) => filter.name === filterName)) { + queryPayload.parameters.values.push({ + name: filterName, + value: [' '], + }); + } + }); + + filteredFilterValues.forEach((filter: any) => { if (filter.name === 'period') { const { min_date, max_date } = extractDateRange(filter.value); queryPayload.parameters.values.push({ @@ -255,14 +321,19 @@ const getDashboardElementData = async (req: Request, res: Response) => { try { const { dashboardId, elementId } = req.params; const filterValues = req.body.filters; - - const dashboard = await read(dashboardId); + const includeHiddenFilters = true; + const dashboard = await read(dashboardId, includeHiddenFilters); const element = (await getElement( dashboardId, elementId, )) as IDashboardElementCustomQuery; const queryPayload = prepareQueryPayload(dashboard, element, filterValues); const url = `${API_BASE_URL}/execute-query`; + logger.info( + `sending queryPayload: ${JSON.stringify( + queryPayload, + )} to ${url} with POST`, + ); const response = await axios.post(url, queryPayload); res.status(httpStatus.OK).send({ @@ -298,7 +369,22 @@ const getDashboardFilter = async (req: Request, res: Response) => { res.status(httpStatus.OK).json(filter); } catch (err) { - console.error(`Error retrieving dashboard filter: ${err.message}`); + logger.error(`Error retrieving dashboard filter: ${err.message}`); + res + .status(err.status || httpStatus.INTERNAL_SERVER_ERROR) + .json({ message: err.message }); + } +}; + +const getDashboardFilterByName = async (req: Request, res: Response) => { + try { + const { dashboardId, filterName } = req.params; + + const filter = await getFilterByName(dashboardId, filterName); + + res.status(httpStatus.OK).json(filter); + } catch (err) { + logger.error(`Error retrieving dashboard filter by name: ${err.message}`); res .status(err.status || httpStatus.INTERNAL_SERVER_ERROR) .json({ message: err.message }); @@ -329,23 +415,146 @@ const updateDashboardFilter = async (req: Request, res: Response) => { } }; +const prepareFilterQueryPayload = ( + dashboard: IDashboard, + filter: IDashboardFilterDynamic | IDashboardFilterDependent, + filterValues: IDashboardFilterValue[], +): object => { + const queryPayload: any = { + id: filter.queryId, + parameters: { + values: [], + identifiers: [], + }, + }; + // Extracting filter names from the filter + const filterNames = filter.params; + const filteredFilterValues = filterValues.filter((filterValue) => + filterNames.includes(filterValue.name), + ); + filteredFilterValues.forEach((filterValue: any) => { + queryPayload.parameters.values.push({ + name: filterValue.name, + value: filterValue.value, + }); + }); + return queryPayload; +}; + const getDashboardFilterData = async (req: Request, res: Response) => { try { - // const { dashboardId, filterId } = req.params; - // const filterValues = req.body.filters; - - // const dashboard = await read(dashboardId); - // const getFilter = (await getFilter( - // dashboardId, - // filterId, - // )) as IDashboardFilter; - // const queryPayload = prepareFilterQueryPayload(dashboard, filter, filterValues); - // const url = `${API_BASE_URL}/execute-query`; - // const response = await axios.post(url, queryPayload); + const { dashboardId, filterId } = req.params; + const filterValues = req.body.params; + + const dashboard = await read(dashboardId); + const filter = await getFilter(dashboardId, filterId); + // Check if the filter object has the 'queryId' property + if ('queryId' in filter) { + const queryPayload = prepareFilterQueryPayload( + dashboard, + filter as IDashboardFilterDynamic | IDashboardFilterDependent, + filterValues, + ); + logger.info( + `getFilterData queryPayload: ${JSON.stringify(queryPayload)}`, + ); + const url = `${API_BASE_URL}/execute-query`; + const response = await axios.post(url, queryPayload); + + res.status(httpStatus.OK).send({ + message: 'Dashboard Filter Data Retrieved', + output: response.data, + }); + } else { + throw new Error('Invalid filter type'); + } + } catch (err) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ message: err.message }); + } +}; + +const getDashboardElementByQueryId = async (req: Request, res: Response) => { + try { + const { dashboardId } = req.params; + const { queryId } = req.body; + + logger.info( + `Searching in dashboardId: ${dashboardId} for queryId: ${queryId}`, + ); + + const element = await getElementByQueryId(dashboardId, Number(queryId)); + + if (!element) { + return res + .status(httpStatus.NOT_FOUND) + .send({ message: 'Element not found' }); + } + + res + .status(httpStatus.OK) + .send({ message: 'Element Retrieved', output: element }); + } catch (err) { + logger.error(`Error retrieving element: %O`, err); + res + .status(err.statusCode || httpStatus.INTERNAL_SERVER_ERROR) + .send({ message: err.message }); + } +}; + +const addDashboardLayoutItem = async (req: Request, res: Response) => { + try { + const { dashboardId } = req.params; + const layoutItemData = req.body; + + await addLayoutItem(dashboardId, layoutItemData); + + res.status(httpStatus.CREATED).send({ + message: 'Layout Item Added to Dashboard', + }); + } catch (err) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ message: err.message }); + } +}; + +const getDashboardLayoutItems = async (req: Request, res: Response) => { + try { + const { dashboardId } = req.params; + + const layoutItems = await getLayoutItems(dashboardId); res.status(httpStatus.OK).send({ - message: 'Dashboard Element Data Retrieved', - // output: response.data, + message: 'Layout Items retrieved', + output: layoutItems, + }); + } catch (err) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ message: err.message }); + } +}; + +const updateDashboardLayoutItem = async (req: Request, res: Response) => { + try { + const { dashboardId } = req.params; + const layoutItemId = req.body.id; // Assuming the client sends the layout item's ID in the body + const layoutItemData = req.body; // The rest of the body is the layout item data + + await updateLayoutItem(dashboardId, layoutItemId, layoutItemData); + + res.status(httpStatus.OK).send({ + message: 'Layout Item Updated in Dashboard', + }); + } catch (err) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ message: err.message }); + } +}; + +const deleteDashboardLayoutItem = async (req: Request, res: Response) => { + try { + const { dashboardId, layoutItemId } = req.params; + + await deleteLayoutItem(dashboardId, layoutItemId); + + res.status(httpStatus.ACCEPTED).send({ + message: 'Layout Item Removed from Dashboard', }); } catch (err) { res.status(httpStatus.INTERNAL_SERVER_ERROR).send({ message: err.message }); @@ -365,7 +574,13 @@ export { getAllDashboards, addDashboardFilter, getDashboardFilter, + getDashboardFilterByName, deleteDashboardFilter, updateDashboardFilter, getDashboardFilterData, + getDashboardElementByQueryId, + addDashboardLayoutItem, + getDashboardLayoutItems, + updateDashboardLayoutItem, + deleteDashboardLayoutItem, }; diff --git a/src/components/dashboard/dashboard.interface.ts b/src/components/dashboard/dashboard.interface.ts index 7e0fc25..86a9e71 100644 --- a/src/components/dashboard/dashboard.interface.ts +++ b/src/components/dashboard/dashboard.interface.ts @@ -27,11 +27,11 @@ interface ITheme extends Document { bgColor: string; itemGridRadius: string; itemGridBgColor: string; - font: string, - textColor: string, - itemGridStroke: string, - chartGradient: boolean, - bottomTimeline: boolean + font: string; + textColor: string; + itemGridStroke: string; + chartGradient: boolean; + bottomTimeline: boolean; } interface IWriteDashboard extends Document { @@ -40,5 +40,4 @@ interface IWriteDashboard extends Document { layout?: ILayoutItem[]; } -export { IDashboard, ILayoutItem, ITheme, IWriteDashboard } - +export { IDashboard, ILayoutItem, ITheme, IWriteDashboard }; diff --git a/src/components/dashboard/dashboard.router.ts b/src/components/dashboard/dashboard.router.ts index 5befc52..7765451 100644 --- a/src/components/dashboard/dashboard.router.ts +++ b/src/components/dashboard/dashboard.router.ts @@ -13,9 +13,15 @@ import { getDashboardElementData, addDashboardFilter, getDashboardFilter, + getDashboardFilterByName, deleteDashboardFilter, updateDashboardFilter, getDashboardFilterData, + getDashboardElementByQueryId, + addDashboardLayoutItem, + getDashboardLayoutItems, + updateDashboardLayoutItem, + deleteDashboardLayoutItem, } from './dashboard.controller'; // Import any necessary validations for dashboard and dashboard elements @@ -38,15 +44,22 @@ router.put( '/dashboard/:dashboardId/element/:elementId', updateDashboardElement, ); - router.post( '/dashboard/:dashboardId/element/:elementId/exec', getDashboardElementData, ); +router.get( + '/dashboard/:dashboardId/element/query/:queryId', + getDashboardElementByQueryId, +); // Filters router.post('/dashboard/:dashboardId/filter', addDashboardFilter); router.get('/dashboard/:dashboardId/filter/:filterId', getDashboardFilter); +router.get( + '/dashboard/:dashboardId/filterByName/:filterName', + getDashboardFilterByName, +); router.delete( '/dashboard/:dashboardId/filter/:filterId', deleteDashboardFilter, @@ -58,4 +71,16 @@ router.post( getDashboardFilterData, ); +// Layouts +router.post('/dashboard/:dashboardId/layout', addDashboardLayoutItem); +router.get('/dashboard/:dashboardId/layout', getDashboardLayoutItems); +router.put( + '/dashboard/:dashboardId/layout/:layoutItemId', + updateDashboardLayoutItem, +); +router.delete( + '/dashboard/:dashboardId/layout/:layoutItemId', + deleteDashboardLayoutItem, +); + export default router; diff --git a/src/components/dashboard/dashboard.service.ts b/src/components/dashboard/dashboard.service.ts index 39401b6..9608a04 100644 --- a/src/components/dashboard/dashboard.service.ts +++ b/src/components/dashboard/dashboard.service.ts @@ -6,6 +6,7 @@ import * as dashboardElementService from '@components/dashboard/dashboardElement import * as dashboardFilterService from '@components/dashboard/dashboardFilter/dashboardFilter.service'; import { IDashboard, + ILayoutItem, IWriteDashboard, } from '@components/dashboard/dashboard.interface'; import { IDashboardElement } from '@components/dashboard/dashboardElement/dashboardElement.interface'; @@ -23,18 +24,32 @@ const create = async (dashboardData: IWriteDashboard): Promise => { return newDashboard; }; -const read = async (id: string): Promise => { +const read = async ( + id: string, + includeHiddenFilters = false as boolean, +): Promise => { try { - logger.debug('log id', id); logger.debug(`Fetching dashboard with id ${id}`); // Populate the elements field - const dashboard = await DashboardModel.findOne({ _id: id }) - .populate('elements') - .populate('filters') - .populate('theme'); + let dashboard: IDashboard; + if (includeHiddenFilters) { + dashboard = await DashboardModel.findOne({ _id: id }) + .populate('elements') + .populate('filters') + .populate('theme'); + } else { + dashboard = await DashboardModel.findOne({ _id: id }) + .populate('elements') + .populate({ + path: 'filters', + match: { type: { $ne: 'hidden' } }, // Exclude filters with type 'hidden' + }) + .populate('theme'); + } if (!dashboard) { throw new Error(`Dashboard with id ${id} not found`); } + return dashboard as IDashboard; } catch (error) { logger.error('Error in fetching dashboard:', error); @@ -46,7 +61,10 @@ const getAll = async (): Promise => { logger.debug(`Fetching all dashboards`); const dashboards = await DashboardModel.find({}) .populate('elements') - .populate('filters') + .populate({ + path: 'filters', + match: { type: { $ne: 'hidden' } }, // Exclude filters with type 'hidden' + }) .populate('theme'); return dashboards as IDashboard[]; }; @@ -127,8 +145,11 @@ const getElement = async ( throw new AppError(httpStatus.NOT_FOUND, 'Dashboard not found'); } + dashboard.elements.forEach((element) => { + logger.debug(`log element, ${element.id}`); + }); const element = dashboard.elements.find( - (element) => element.id.toString() === elementId, + (elt) => elt.id.toString() === elementId, ); if (!element) { @@ -268,14 +289,12 @@ const getFilter = async ( try { const dashboard = await ( await DashboardModel.findById(dashboardId) - ).populated('filters'); + ).populate('filters'); if (!dashboard) { throw new AppError(httpStatus.NOT_FOUND, 'Dashboard not found'); } - const filter = dashboard.filters.find( - (filter) => filter._id.toString() === filterId, - ); + const filter = dashboard.filters.find((x) => x._id.toString() === filterId); if (!filter) { throw new AppError(httpStatus.NOT_FOUND, 'Filter not found in dashboard'); } @@ -290,6 +309,38 @@ const getFilter = async ( } }; +const getFilterByName = async ( + dashboardId: string, + filterName: string, +): Promise => { + try { + const dashboard = await DashboardModel.findById(dashboardId).populate( + 'filters', + ); + if (!dashboard) { + throw new AppError(httpStatus.NOT_FOUND, 'Dashboard not found'); + } + + const filter = dashboard.filters.find( + (filterElement) => filterElement.name === filterName, + ); + if (!filter) { + throw new AppError( + httpStatus.NOT_FOUND, + `Filter '${filterName}' not found in dashboard`, + ); + } + + return filter; + } catch (err) { + throw new AppError( + httpStatus.INTERNAL_SERVER_ERROR, + `Error retrieving filter '${filterName}' from dashboard`, + err.message, + ); + } +}; + const deleteFilter = async ( dashboardId: string, filterId: string, @@ -344,7 +395,7 @@ const updateFilter = async ( if (!filterExists) { throw new AppError(httpStatus.NOT_FOUND, 'Filter not found in dashboard'); } - + logger.info(`FilterData: ${JSON.stringify(filterData)}`); await dashboardFilterService.updateFilter(filterId, filterData); logger.debug(`Filter updated in dashboard: %O`, filterData); return true; @@ -357,6 +408,145 @@ const updateFilter = async ( } }; +const getElementByQueryId = async (dashboardId: string, queryId: number) => { + try { + logger.info( + `Searching in dashboardId: ${dashboardId} for queryId: ${queryId}`, + ); + const dashboard = await DashboardModel.findById(dashboardId).populate({ + path: 'elements', + match: { type: 'basicQuery', queryId }, // Ensure to match elements of type basicQuery and the specific queryId + }); + + if (!dashboard) { + throw new AppError(httpStatus.NOT_FOUND, 'Dashboard not found'); + } + + // Since populate with match might return an empty array if no elements are found + if (dashboard.elements.length === 0) { + throw new AppError( + httpStatus.NOT_FOUND, + 'Element not found in dashboard', + ); + } + + const element = dashboard.elements[0]; // Assuming match returns at least one element + logger.debug(`Element read from dashboard ${dashboardId}: ${element}`); + return element; + } catch (err) { + logger.error(`Error retrieving element from dashboard: %O`, err.message); + throw new AppError( + httpStatus.INTERNAL_SERVER_ERROR, + 'Error retrieving element from dashboard', + err.message, + ); + } +}; + +const addLayoutItem = async ( + dashboardId: string, + layoutItem: ILayoutItem, +): Promise => { + try { + const dashboard = await DashboardModel.findOne({ _id: dashboardId }); + if (!dashboard) { + throw new AppError(httpStatus.NOT_FOUND, 'Dashboard not found'); + } + + dashboard.layout.push(layoutItem); + await dashboard.save(); + + logger.info(`Layout item added to dashboard: ${dashboardId}`); + } catch (err) { + logger.error(`Error adding layout item to dashboard: %O`, err.message); + throw new AppError( + httpStatus.INTERNAL_SERVER_ERROR, + 'Error adding layout item to dashboard', + err.message, + ); + } +}; + +const getLayoutItems = async (dashboardId: string): Promise => { + try { + const dashboard = await DashboardModel.findById(dashboardId); + if (!dashboard) { + throw new AppError(httpStatus.NOT_FOUND, 'Dashboard not found'); + } + + logger.info(`Retrieved layout items from dashboard: ${dashboardId}`); + return dashboard.layout; + } catch (err) { + logger.error( + `Error retrieving layout items from dashboard: %O`, + err.message, + ); + throw new AppError( + httpStatus.INTERNAL_SERVER_ERROR, + 'Error retrieving layout items from dashboard', + err.message, + ); + } +}; + +const updateLayoutItem = async ( + dashboardId: string, + layoutItemId: string, + layoutItemData: ILayoutItem, +): Promise => { + try { + const dashboard = await DashboardModel.findOne({ _id: dashboardId }); + if (!dashboard) { + throw new AppError(httpStatus.NOT_FOUND, 'Dashboard not found'); + } + + const layoutItemIndex = dashboard.layout.findIndex( + (item) => item.id === layoutItemId, + ); + if (layoutItemIndex === -1) { + throw new AppError(httpStatus.NOT_FOUND, 'Layout item not found'); + } + + dashboard.layout[layoutItemIndex] = layoutItemData; + await dashboard.save(); + + logger.info(`Layout item updated in dashboard: ${dashboardId}`); + } catch (err) { + logger.error(`Error updating layout item in dashboard: %O`, err.message); + throw new AppError( + httpStatus.INTERNAL_SERVER_ERROR, + 'Error updating layout item in dashboard', + err.message, + ); + } +}; + +const deleteLayoutItem = async ( + dashboardId: string, + layoutItemId: string, +): Promise => { + try { + const dashboard = await DashboardModel.findOne({ _id: dashboardId }); + if (!dashboard) { + throw new AppError(httpStatus.NOT_FOUND, 'Dashboard not found'); + } + + dashboard.layout = dashboard.layout.filter( + (item) => item.id !== layoutItemId, + ); + await dashboard.save(); + + logger.info(`Layout item deleted from dashboard: ${dashboardId}`); + } catch (err) { + logger.error(`Error deleting layout item from dashboard: %O`, err.message); + throw new AppError( + httpStatus.INTERNAL_SERVER_ERROR, + 'Error deleting layout item from dashboard', + err.message, + ); + } +}; + export { create, read, @@ -371,4 +561,10 @@ export { getFilter, deleteFilter, updateFilter, + getElementByQueryId, + getFilterByName, + addLayoutItem, + getLayoutItems, + updateLayoutItem, + deleteLayoutItem, }; diff --git a/src/components/dashboard/dashboardElement/dashboardElement.interface.ts b/src/components/dashboard/dashboardElement/dashboardElement.interface.ts index 3499988..a6c07fc 100644 --- a/src/components/dashboard/dashboardElement/dashboardElement.interface.ts +++ b/src/components/dashboard/dashboardElement/dashboardElement.interface.ts @@ -2,32 +2,41 @@ export interface IDashboardElement { _id: string; id: string; title: string; - type: "button" | "text" | "basicQuery" | "customQuery"; + type: 'button' | 'text' | 'basicQuery' | 'customQuery'; } export interface IDashboardElementText extends IDashboardElement { - type: "text"; + type: 'text'; text: string; } export interface IDashboardElementButton extends IDashboardElement { - type: "button"; + type: 'button'; text: string; link: string; } export interface IDashboardElementVis extends IDashboardElement { - visType: "areaChart" | "barChart" | "stackedBarChart" | "multiAreaChart" | "lineChart" | "multiLineChart" | "table" + visType: + | 'areaChart' + | 'barChart' + | 'stackedBarChart' + | 'multiAreaChart' + | 'lineChart' + | 'multiLineChart' + | 'table' + | 'singleValue' + | 'pieChart'; } export interface IDashboardElementBasicQuery extends IDashboardElementVis { - type: "basicQuery"; + type: 'basicQuery'; dimension: string; differential?: string; measures: string[]; } export interface IDashboardElementCustomQuery extends IDashboardElementVis { - type: "customQuery"; + type: 'customQuery'; queryId: number; } diff --git a/src/components/dashboard/dashboardElement/dashboardElement.model.ts b/src/components/dashboard/dashboardElement/dashboardElement.model.ts index 22701c2..8a60ad4 100644 --- a/src/components/dashboard/dashboardElement/dashboardElement.model.ts +++ b/src/components/dashboard/dashboardElement/dashboardElement.model.ts @@ -13,35 +13,57 @@ const dashboardElementSchema = new mongoose.Schema( title: { type: String, required: true }, type: { type: String, required: true }, }, - { discriminatorKey: 'type' } + { discriminatorKey: 'type' }, ); const dashboardElementTextSchema = new mongoose.Schema({ text: { type: String, required: true }, }); -const dashboardElementButtonSchema = new mongoose.Schema({ - text: { type: String, required: true }, - link: { type: String, required: true }, -}); +const dashboardElementButtonSchema = + new mongoose.Schema({ + text: { type: String, required: true }, + link: { type: String, required: true }, + }); -const dashboardElementBasicQuerySchema = new mongoose.Schema({ - dimension: { type: String }, - differential: { type: String }, - measures: [{ type: String }], - visType: { type: String, required: true }, -}); +const dashboardElementBasicQuerySchema = + new mongoose.Schema({ + dimension: { type: String }, + differential: { type: String }, + measures: [{ type: String }], + visType: { type: String, required: true }, + }); -const dashboardElementCustomQuerySchema = new mongoose.Schema({ - queryId: { type: Number, required: true }, - visType: { type: String, required: true }, -}); +const dashboardElementCustomQuerySchema = + new mongoose.Schema({ + queryId: { type: Number, required: true }, + visType: { type: String, required: true }, + }); -const DashboardElementModel = mongoose.model('DashboardElement', dashboardElementSchema); -const DashboardElementButtonModel = DashboardElementModel.discriminator('button', dashboardElementButtonSchema); -const DashboardElementTextModel = DashboardElementModel.discriminator('text', dashboardElementTextSchema); -const DashboardElementBasicQueryModel = DashboardElementModel.discriminator('basicQuery', dashboardElementBasicQuerySchema); -const DashboardElementCustomQueryModel = DashboardElementModel.discriminator('customQuery', dashboardElementCustomQuerySchema); +const DashboardElementModel = mongoose.model( + 'DashboardElement', + dashboardElementSchema, +); +const DashboardElementButtonModel = + DashboardElementModel.discriminator( + 'button', + dashboardElementButtonSchema, + ); +const DashboardElementTextModel = + DashboardElementModel.discriminator( + 'text', + dashboardElementTextSchema, + ); +const DashboardElementBasicQueryModel = + DashboardElementModel.discriminator( + 'basicQuery', + dashboardElementBasicQuerySchema, + ); +const DashboardElementCustomQueryModel = + DashboardElementModel.discriminator( + 'customQuery', + dashboardElementCustomQuerySchema, + ); export { DashboardElementModel, @@ -50,4 +72,3 @@ export { DashboardElementBasicQueryModel, DashboardElementCustomQueryModel, }; - diff --git a/src/components/dashboard/dashboardElement/dashboardElement.service.ts b/src/components/dashboard/dashboardElement/dashboardElement.service.ts index bc9b1d4..e95e284 100644 --- a/src/components/dashboard/dashboardElement/dashboardElement.service.ts +++ b/src/components/dashboard/dashboardElement/dashboardElement.service.ts @@ -1,6 +1,7 @@ import httpStatus from 'http-status'; import AppError from '@core/utils/appError'; import logger from '@core/utils/logger'; +import { v4 as uuidv4 } from 'uuid'; import { DashboardElementModel, DashboardElementButtonModel, @@ -17,7 +18,7 @@ import { } from './dashboardElement.interface'; const createDashboardElement = async ( - elementData: IDashboardElement + elementData: IDashboardElement, ): Promise => { try { let newElement: any; @@ -25,28 +26,28 @@ const createDashboardElement = async ( switch (elementData.type) { case 'button': newElement = await DashboardElementButtonModel.create( - elementData as IDashboardElementButton + elementData as IDashboardElementButton, ); break; case 'text': newElement = await DashboardElementTextModel.create( - elementData as IDashboardElementText + elementData as IDashboardElementText, ); break; case 'basicQuery': newElement = await DashboardElementBasicQueryModel.create( - elementData as IDashboardElementBasicQuery + elementData as IDashboardElementBasicQuery, ); break; case 'customQuery': newElement = await DashboardElementCustomQueryModel.create( - elementData as IDashboardElementCustomQuery + elementData as IDashboardElementCustomQuery, ); break; default: throw new AppError( httpStatus.BAD_REQUEST, - `Invalid dashboard element type: ${elementData.type}` + `Invalid dashboard element type: ${elementData.type}`, ); } @@ -56,16 +57,16 @@ const createDashboardElement = async ( logger.error(`DashboardElement create error: %O`, err.message); throw new AppError( httpStatus.BAD_REQUEST, - 'Dashboard element was not created!' + 'Dashboard element was not created!', ); } }; const getDashboardElement = async ( - id: string + id: string, ): Promise => { logger.debug(`Fetching DashboardElement with id ${id}`); - const element = await DashboardElementModel.findOne({id}).lean(); + const element = await DashboardElementModel.findOne({ id }).lean(); if (!element) { return null; @@ -83,21 +84,20 @@ const getDashboardElement = async ( default: throw new AppError( httpStatus.INTERNAL_SERVER_ERROR, - `Unexpected dashboard element type: ${element.type}` + `Unexpected dashboard element type: ${element.type}`, ); } }; - const updateDashboardElement = async ( elementId: string, - elementData: IDashboardElement + elementData: IDashboardElement, ): Promise => { try { if (!elementData || !elementId) { throw new AppError( httpStatus.BAD_REQUEST, - 'Invalid element data or missing element ID' + 'Invalid element data or missing element ID', ); } @@ -111,34 +111,34 @@ const updateDashboardElement = async ( await DashboardElementButtonModel.findByIdAndUpdate( elementId, elementData as IDashboardElementButton, - { new: true } + { new: true }, ); break; case 'text': await DashboardElementTextModel.findByIdAndUpdate( elementId, elementData as IDashboardElementText, - { new: true } + { new: true }, ); break; case 'basicQuery': await DashboardElementBasicQueryModel.findByIdAndUpdate( elementId, elementData as IDashboardElementBasicQuery, - { new: true } + { new: true }, ); break; case 'customQuery': await DashboardElementCustomQueryModel.findByIdAndUpdate( elementId, elementData as IDashboardElementCustomQuery, - { new: true } + { new: true }, ); break; default: throw new AppError( httpStatus.BAD_REQUEST, - `Invalid dashboard element type: ${elementData.type}` + `Invalid dashboard element type: ${elementData.type}`, ); } diff --git a/src/components/dashboard/dashboardFilter/dashboardFilter.interface.ts b/src/components/dashboard/dashboardFilter/dashboardFilter.interface.ts index c990992..d15c1ac 100644 --- a/src/components/dashboard/dashboardFilter/dashboardFilter.interface.ts +++ b/src/components/dashboard/dashboardFilter/dashboardFilter.interface.ts @@ -1,33 +1,49 @@ import { Document } from 'mongoose'; +interface IDashboardFilterOptions { + label: string; + value: any; +} +interface IDashboardFilterValue { + name: string; + value: any; +} + interface IDashboardFilter extends Document { name: string; - options: string[] | null; - type: "static" | "dynamic" | "dependent"; - component: "datePicker" | "select" | "multiselect" | "checkbox" | "radio"; + title: string; + options: IDashboardFilterOptions[] | null; + type: 'static' | 'dynamic' | 'dependent' | 'hidden'; + component: 'datePicker' | 'select' | 'multiselect' | 'checkbox' | 'radio'; defaultValue: any; } -interface IDashboardFilterStatic extends IDashboardFilter{ - type: "static" +interface IDashboardFilterStatic extends IDashboardFilter { + type: 'static'; } -interface IDashboardFilterDynamic extends IDashboardFilter{ - type: "dynamic" +interface IDashboardFilterHidden extends IDashboardFilter { + type: 'hidden'; +} + +interface IDashboardFilterDynamic extends IDashboardFilter { + type: 'dynamic'; queryId: number; params: string[]; } -interface IDashboardFilterDependent extends IDashboardFilter{ - type: "dependent" +interface IDashboardFilterDependent extends IDashboardFilter { + type: 'dependent'; queryId: number; params: string[]; reactsTo: string[]; // dependent Filter _id } export { - IDashboardFilter, - IDashboardFilterStatic, - IDashboardFilterDynamic, - IDashboardFilterDependent, -} + IDashboardFilterValue, + IDashboardFilter, + IDashboardFilterStatic, + IDashboardFilterHidden, + IDashboardFilterDynamic, + IDashboardFilterDependent, +}; diff --git a/src/components/dashboard/dashboardFilter/dashboardFilter.model.ts b/src/components/dashboard/dashboardFilter/dashboardFilter.model.ts index d7520f2..5b21958 100644 --- a/src/components/dashboard/dashboardFilter/dashboardFilter.model.ts +++ b/src/components/dashboard/dashboardFilter/dashboardFilter.model.ts @@ -2,20 +2,36 @@ import mongoose from 'mongoose'; import { IDashboardFilter, IDashboardFilterStatic, + IDashboardFilterHidden, IDashboardFilterDynamic, IDashboardFilterDependent, } from './dashboardFilter.interface'; +const optionsSchema = new mongoose.Schema({ + label: { type: String, required: true }, + value: { type: String, required: true }, +}); + const filterSchema = new mongoose.Schema({ name: { type: String, required: true }, - options: [{ type: String }], // For simplicity, treating options as array of strings - type: { type: String, enum: ["static", "dynamic", "dependent"], required: true }, - component: { type: String, enum: ["date_picker", "select", "multiselect", "checkbox", "radio"], required: true }, + title: { type: String, required: true }, + options: [optionsSchema], + type: { + type: String, + enum: ['static', 'hidden', 'dynamic', 'dependent'], + required: true, + }, + component: { + type: String, + enum: ['date_picker', 'select', 'multiselect', 'checkbox', 'radio'], + required: true, + }, defaultValue: { type: mongoose.Schema.Types.Mixed, required: true }, }); -const staticFilterSchema = new mongoose.Schema({ -}); +const staticFilterSchema = new mongoose.Schema({}); + +const hiddenFilterSchema = new mongoose.Schema({}); const dynamicFilterSchema = new mongoose.Schema({ queryId: { type: Number, required: true }, @@ -28,14 +44,35 @@ const dependentFilterSchema = new mongoose.Schema({ reactsTo: [{ type: mongoose.Schema.Types.ObjectId, ref: 'DashboardFilter' }], }); -const DashboardFilterModel = mongoose.model('DashboardFilter', filterSchema); -const DashboardFilterStaticModel = DashboardFilterModel.discriminator('static', staticFilterSchema); -const DashboardFilterDynamicModel = DashboardFilterModel.discriminator('dynamic', dynamicFilterSchema); -const DashboardFilterDependentModel = DashboardFilterModel.discriminator('dependent', dependentFilterSchema); +const DashboardFilterModel = mongoose.model( + 'DashboardFilter', + filterSchema, +); +const DashboardFilterStaticModel = + DashboardFilterModel.discriminator( + 'static', + staticFilterSchema, + ); +const DashboardFilterHiddenModel = + DashboardFilterModel.discriminator( + 'hidden', + hiddenFilterSchema, + ); +const DashboardFilterDynamicModel = + DashboardFilterModel.discriminator( + 'dynamic', + dynamicFilterSchema, + ); +const DashboardFilterDependentModel = + DashboardFilterModel.discriminator( + 'dependent', + dependentFilterSchema, + ); -export { - DashboardFilterModel, - DashboardFilterStaticModel, - DashboardFilterDynamicModel, - DashboardFilterDependentModel +export { + DashboardFilterModel, + DashboardFilterStaticModel, + DashboardFilterHiddenModel, + DashboardFilterDynamicModel, + DashboardFilterDependentModel, }; diff --git a/src/components/dashboard/dashboardFilter/dashboardFilter.service.ts b/src/components/dashboard/dashboardFilter/dashboardFilter.service.ts index 0744f33..055925f 100644 --- a/src/components/dashboard/dashboardFilter/dashboardFilter.service.ts +++ b/src/components/dashboard/dashboardFilter/dashboardFilter.service.ts @@ -6,43 +6,66 @@ import logger from '@core/utils/logger'; import { DashboardFilterModel, DashboardFilterStaticModel, + DashboardFilterHiddenModel, DashboardFilterDynamicModel, DashboardFilterDependentModel, } from './dashboardFilter.model'; import { IDashboardFilter, IDashboardFilterStatic, + IDashboardFilterHidden, IDashboardFilterDynamic, IDashboardFilterDependent, } from './dashboardFilter.interface'; -const createFilter = async (filterData: IDashboardFilter): Promise => { +const createFilter = async ( + filterData: IDashboardFilter, +): Promise => { try { let newFilter: any; switch (filterData.type) { case 'static': - newFilter = await DashboardFilterStaticModel.create(filterData as IDashboardFilterStatic); + newFilter = await DashboardFilterStaticModel.create( + filterData as IDashboardFilterStatic, + ); + break; + case 'hidden': + newFilter = await DashboardFilterHiddenModel.create( + filterData as IDashboardFilterHidden, + ); break; case 'dynamic': - newFilter = await DashboardFilterDynamicModel.create(filterData as IDashboardFilterDynamic); + newFilter = await DashboardFilterDynamicModel.create( + filterData as IDashboardFilterDynamic, + ); break; case 'dependent': - newFilter = await DashboardFilterDependentModel.create(filterData as IDashboardFilterDependent); + newFilter = await DashboardFilterDependentModel.create( + filterData as IDashboardFilterDependent, + ); break; default: - throw new AppError(httpStatus.BAD_REQUEST, `Invalid dashboard filter type: ${filterData.type}`); + throw new AppError( + httpStatus.BAD_REQUEST, + `Invalid dashboard filter type: ${filterData.type}`, + ); } logger.debug(`DashboardFilter created: %O`, newFilter); return newFilter; } catch (err) { logger.error(`DashboardFilter create error: %O`, err.message); - throw new AppError(httpStatus.BAD_REQUEST, 'Dashboard filter was not created!'); + throw new AppError( + httpStatus.BAD_REQUEST, + 'Dashboard filter was not created!', + ); } }; -const getFilter = async (filterId: string): Promise => { +const getFilter = async ( + filterId: string, +): Promise => { logger.debug(`Fetching DashboardFilter with id ${filterId}`); const filter = await DashboardFilterModel.findById(filterId).lean(); @@ -53,19 +76,33 @@ const getFilter = async (filterId: string): Promise => switch (filter.type) { case 'static': return filter as IDashboardFilterStatic; + case 'hidden': + return filter as IDashboardFilterHidden; case 'dynamic': return filter as IDashboardFilterDynamic; case 'dependent': - return filter as IDashboardFilterDependent; + const dependentFilter = await DashboardFilterModel.findById(filterId) + .populate('reactsTo') + .lean(); + return dependentFilter as IDashboardFilterDependent; default: - throw new AppError(httpStatus.INTERNAL_SERVER_ERROR, `Unexpected dashboard filter type: ${filter.type}`); + throw new AppError( + httpStatus.INTERNAL_SERVER_ERROR, + `Unexpected dashboard filter type: ${filter.type}`, + ); } }; -const updateFilter = async (filterId: string, filterData: IDashboardFilter): Promise => { +const updateFilter = async ( + filterId: string, + filterData: IDashboardFilter, +): Promise => { try { if (!filterData || !filterId) { - throw new AppError(httpStatus.BAD_REQUEST, 'Invalid filter data or missing filter ID'); + throw new AppError( + httpStatus.BAD_REQUEST, + 'Invalid filter data or missing filter ID', + ); } const existingFilter = await DashboardFilterModel.findById(filterId); @@ -75,16 +112,38 @@ const updateFilter = async (filterId: string, filterData: IDashboardFilter): Pro switch (filterData.type) { case 'static': - await DashboardFilterStaticModel.findByIdAndUpdate(filterId, filterData as IDashboardFilterStatic, { new: true }); + await DashboardFilterStaticModel.findByIdAndUpdate( + filterId, + filterData as IDashboardFilterStatic, + { new: true }, + ); + break; + case 'hidden': + await DashboardFilterHiddenModel.findByIdAndUpdate( + filterId, + filterData as IDashboardFilterHidden, + { new: true }, + ); break; case 'dynamic': - await DashboardFilterDynamicModel.findByIdAndUpdate(filterId, filterData as IDashboardFilterDynamic, { new: true }); + await DashboardFilterDynamicModel.findByIdAndUpdate( + filterId, + filterData as IDashboardFilterDynamic, + { new: true }, + ); break; case 'dependent': - await DashboardFilterDependentModel.findByIdAndUpdate(filterId, filterData as IDashboardFilterDependent, { new: true }); + await DashboardFilterDependentModel.findByIdAndUpdate( + filterId, + filterData as IDashboardFilterDependent, + { new: true }, + ); break; default: - throw new AppError(httpStatus.BAD_REQUEST, `Invalid dashboard filter type: ${filterData.type}`); + throw new AppError( + httpStatus.BAD_REQUEST, + `Invalid dashboard filter type: ${filterData.type}`, + ); } logger.debug(`DashboardFilter updated`); @@ -101,9 +160,4 @@ const deleteFilter = async (filterId: string): Promise => { return true; }; -export { - createFilter, - getFilter, - updateFilter, - deleteFilter -}; +export { createFilter, getFilter, updateFilter, deleteFilter }; diff --git a/src/swagger/dashboard-swagger.json b/src/swagger/dashboard-swagger.json index 1593ac6..cb37c46 100644 --- a/src/swagger/dashboard-swagger.json +++ b/src/swagger/dashboard-swagger.json @@ -624,7 +624,7 @@ }, "type": { "type": "string", - "enum": ["static", "dynamic", "dependent"] + "enum": ["static", "dynamic", "dependent", "hidden"] }, "component": { "type": "string", @@ -650,7 +650,7 @@ }, "type": { "type": "string", - "enum": ["static", "dynamic", "dependent"] + "enum": ["static", "dynamic", "dependent", "hidden"] }, "component": { "type": "string",