Skip to content

Commit 2fc56b3

Browse files
authored
Merge pull request #4311 from Countly/SER-690
[SER-690] Delete dashboard widgets if a related Drill query is deleted
2 parents 741459f + de64749 commit 2fc56b3

File tree

2 files changed

+233
-0
lines changed

2 files changed

+233
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
var plugins = require('../../../../plugins/pluginManager');
2+
const dashboard = require('../../../../plugins/dashboards/api/parts/dashboards.js');
3+
4+
async function recheckFunnelWidgets(countlyDb) {
5+
console.log("Detecting deleted data for funnels...");
6+
7+
const widgets = await countlyDb.collection('widgets').find({ widget_type: 'funnels', funnel_type: { $exists: true, $ne: [] } }, { funnel_type: 1 }).toArray();
8+
if (!widgets || !widgets.length) {
9+
console.log("No widgets found.");
10+
return;
11+
}
12+
const funnelIdsInWidgets = widgets.map(widget => widget.funnel_type[0].split('***')[1]);
13+
const existingFunnels = await countlyDb.collection('funnels').find({ _id: { $in: funnelIdsInWidgets } }, { _id: 1, app_id: 1 }).toArray();
14+
if (!existingFunnels || !existingFunnels.length) {
15+
console.log("No funnels found.");
16+
return;
17+
}
18+
const formattedExistingFunnels = existingFunnels.map(funnel => funnel.app_id + "***" + funnel._id.toString());
19+
const missingFunnelIds = widgets.filter(function(result) {
20+
return !formattedExistingFunnels.includes(result.funnel_type[0]);
21+
}).map(function(result) {
22+
return result.funnel_type[0];
23+
});
24+
25+
if (missingFunnelIds.length) {
26+
const matchOperator = {
27+
widget_type: "funnels",
28+
"funnel_type": {
29+
$in: missingFunnelIds
30+
}
31+
};
32+
33+
try {
34+
return await dashboard.removeDeletedRecordsFromWidgets({member: {username: 'unknown'}}, matchOperator, countlyDb);
35+
}
36+
catch (error) {
37+
console.log('Error while sending a request: ', error);
38+
}
39+
}
40+
else {
41+
console.log("No deleted funnels found in widgets.");
42+
}
43+
}
44+
45+
async function recheckFormulasWidgets(countlyDb) {
46+
console.log("Detecting deleted data for formulas...");
47+
48+
const widgets = await countlyDb.collection('widgets').find({ widget_type: 'formulas', cmetric_refs: { $exists: true, $ne: [] } }, { "cmetric_refs._id": 1 }).toArray();
49+
if (!widgets || !widgets.length) {
50+
console.log("No widgets found.");
51+
return;
52+
}
53+
const ids = widgets.map(item => countlyDb.ObjectID(item.cmetric_refs[0]._id));
54+
const existingFormulas = await countlyDb.collection('calculated_metrics').find({ _id: { $in: ids } }, { _id: 1 }).toArray();
55+
if (!existingFormulas || !existingFormulas.length) {
56+
console.log("No formulas found.");
57+
return;
58+
}
59+
const missingFormulasIds = widgets.filter(widget => {
60+
return !existingFormulas.some(formula => String(formula._id) === widget.cmetric_refs[0]._id);
61+
}).map(function(result) {
62+
return result.cmetric_refs[0]._id;
63+
});
64+
65+
if (missingFormulasIds.length) {
66+
const matchOperator = {
67+
widget_type: "formulas",
68+
"cmetric_refs": {
69+
$elemMatch: {
70+
_id: {
71+
$in: missingFormulasIds
72+
}
73+
}
74+
}
75+
};
76+
77+
try {
78+
return await dashboard.removeDeletedRecordsFromWidgets({member: {username: 'unknown'}}, matchOperator, countlyDb);
79+
}
80+
catch (error) {
81+
console.log('Error while sending a request: ', error);
82+
}
83+
}
84+
else {
85+
console.log("No deleted formulas found in widgets.");
86+
}
87+
}
88+
89+
async function recheckDrillWidgets(countlyDb) {
90+
console.log("Detecting deleted data for drill...");
91+
const matchOperator = {
92+
"widget_type": "drill",
93+
"drill_query": { $size: 0 }
94+
};
95+
96+
try {
97+
return await dashboard.removeDeletedRecordsFromWidgets({member: {username: 'unknown'}}, matchOperator, countlyDb);
98+
}
99+
catch (error) {
100+
console.log('Error while sending a request: ', error);
101+
}
102+
}
103+
104+
plugins.dbConnection().then(async(countlyDb) => {
105+
try {
106+
await recheckFunnelWidgets(countlyDb);
107+
}
108+
catch (error) {
109+
console.log('Error in recheckFunnelWidgets:', error);
110+
}
111+
112+
try {
113+
await recheckFormulasWidgets(countlyDb);
114+
}
115+
catch (error) {
116+
console.log('Error in recheckFormulasWidgets:', error);
117+
}
118+
119+
try {
120+
await recheckDrillWidgets(countlyDb);
121+
}
122+
catch (error) {
123+
console.log('Error in recheckDrillWidgets:', error);
124+
}
125+
finally {
126+
countlyDb.close();
127+
}
128+
});

plugins/dashboards/api/parts/dashboards.js

+105
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,111 @@ dashboard.fetchNoteData = async function(params, apps, widget) {
562562
return widget;
563563
};
564564

565+
/**
566+
* Remove deleted records from widgets
567+
* @param {object} params params object
568+
* @param {object} matchOperator match operator for aggregation
569+
* @param {object} db database object - if coming from script
570+
* @returns {boolean} true if success
571+
*/
572+
dashboard.removeDeletedRecordsFromWidgets = async function(params, matchOperator, db) {
573+
try {
574+
if (!params || !matchOperator) {
575+
log.e('missing parameters for removeDeletedRecordsFromWidgets', params, matchOperator);
576+
return false;
577+
}
578+
579+
if (typeof matchOperator === 'string') {
580+
matchOperator = JSON.parse(matchOperator);
581+
}
582+
583+
if (typeof db !== 'undefined') {
584+
common.db = db;
585+
}
586+
587+
var pipeline = [
588+
{
589+
$match: matchOperator
590+
},
591+
{
592+
$lookup: {
593+
from: "dashboards",
594+
localField: "_id",
595+
foreignField: "widgets",
596+
as: "dashboard"
597+
}
598+
},
599+
{
600+
$unwind: "$dashboard"
601+
},
602+
{
603+
$project: {
604+
_id: 0,
605+
widget_id: "$_id",
606+
dashboard_id: "$dashboard._id",
607+
}
608+
}
609+
];
610+
var widgets = await common.db.collection('widgets').aggregate(pipeline, {allowDiskUse: true}).toArray();
611+
612+
for (const widget of widgets) {
613+
var dashboardId = widget.dashboard_id;
614+
var widgetId = widget.widget_id;
615+
if (!dashboardId || !widgetId) {
616+
log.e('dashbordId or widgetId could not found in remove widget');
617+
continue;
618+
}
619+
620+
await new Promise((resolve, reject) => {
621+
dashboard.deleteWidget(params, dashboardId, widgetId, function(success) {
622+
if (success) {
623+
resolve();
624+
}
625+
else {
626+
reject();
627+
}
628+
});
629+
});
630+
}
631+
return true;
632+
}
633+
catch (error) {
634+
log.e('Invalid request for remove widget', error);
635+
return false;
636+
}
637+
};
638+
639+
dashboard.deleteWidget = function(params, dashboardId, widgetId, callback) {
640+
common.db.collection("dashboards").update({_id: common.db.ObjectID(dashboardId)}, { $pull: {widgets: common.db.ObjectID(widgetId)}}, function(dashboardErr) {
641+
if (!dashboardErr) {
642+
common.db.collection("widgets").findAndModify({_id: common.db.ObjectID(widgetId)}, {}, {}, {remove: true}, function(widgetErr, widgetResult) {
643+
if (widgetErr || !widgetResult || !widgetResult.value) {
644+
common.returnMessage(params, 500, "Failed to remove widget");
645+
callback(false);
646+
}
647+
else {
648+
var logData = widgetResult.value;
649+
logData.dashboard = dashboard.name;
650+
651+
plugins.dispatch("/systemlogs", {params: params, action: "widget_deleted", data: logData});
652+
plugins.dispatch("/dashboard/widget/deleted", {params: params, widget: widgetResult.value});
653+
common.returnMessage(params, 200, 'Success');
654+
callback(true);
655+
}
656+
});
657+
}
658+
else {
659+
common.returnMessage(params, 500, "Failed to remove widget");
660+
callback(false);
661+
}
662+
});
663+
};
664+
665+
plugins.register("/dashboard/clean-deleted-widgets", async function(ob) {
666+
var response = await dashboard.removeDeletedRecordsFromWidgets(ob.params, ob.match);
667+
return response;
668+
}, true);
669+
565670

566671
/**
567672
* Function to fetch technology analytics data for app

0 commit comments

Comments
 (0)