Skip to content

Commit 8445928

Browse files
committed
fix: Delete widgets when a dashboard is removed
1 parent 760ed5c commit 8445928

File tree

3 files changed

+143
-13
lines changed

3 files changed

+143
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Fixes:
33
- [core] Fix user analytics widget chart
44
- [core] Fix mongo connection url parsing
5+
- [dashboards] Delete associated widgets and reports when a dashboard is removed
56

67
## Version 25.03.10
78
Enterprise Fixes:
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Description : Remove orphan widget and long_tasks records left behind after a dashboard is deleted
3+
* Server : countly
4+
* Path : $(countly dir)/bin/scripts/fix-data
5+
* Command : node delete_widgets_of_deleted_dashboards.js [--dry-run]
6+
* Usage :
7+
* # Preview only
8+
* node delete_widgets_of_deleted_dashboards.js --dry-run
9+
* # Actual deletion
10+
* node delete_widgets_of_deleted_dashboards.js
11+
*/
12+
const DRY_RUN = process.argv.includes('--dry-run');
13+
14+
const BATCH_SIZE = 1000;
15+
const pluginManager = require('../../../plugins/pluginManager.js');
16+
17+
/**
18+
* Deletes documents in batches to avoid oversized commands
19+
* @param {Object} db - MongoDB connection
20+
* @param {String} collection - Collection name
21+
* @param {Array} ids - List of document ids to delete
22+
*/
23+
async function deleteByChunks(db, collection, ids) {
24+
let bucket = [];
25+
26+
for (const id of ids) {
27+
bucket.push(id);
28+
29+
if (bucket.length === BATCH_SIZE) {
30+
await runDelete(bucket);
31+
bucket = [];
32+
}
33+
}
34+
35+
if (bucket.length) {
36+
await runDelete(bucket);
37+
}
38+
39+
/**
40+
* Executes the delete operation for a batch of ids
41+
* @param {Array} batch - Array of document ids to delete
42+
* @returns {Promise<void>}
43+
* */
44+
async function runDelete(batch) {
45+
if (DRY_RUN) {
46+
console.log(`[dry-run] ${collection}: would delete ${batch.length}`);
47+
}
48+
else {
49+
const res = await db.collection(collection).deleteMany({ _id: { $in: batch } });
50+
console.log(`[deleted] ${collection}: ${res.deletedCount}`);
51+
}
52+
}
53+
}
54+
55+
(async() => {
56+
const db = await pluginManager.dbConnection('countly');
57+
58+
try {
59+
const dashboardWidgets = [];
60+
61+
const dashCursor = db.collection('dashboards').find({widgets: {$exists: true, $not: {$size: 0}}}, {projection: {widgets: 1}});
62+
63+
while (await dashCursor.hasNext()) {
64+
const dash = await dashCursor.next();
65+
for (const w of dash.widgets) {
66+
const idStr = (w && w.$oid) ? w.$oid : (w + '');
67+
if (idStr && !dashboardWidgets.includes(idStr)) {
68+
dashboardWidgets.push(idStr);
69+
}
70+
}
71+
}
72+
73+
await dashCursor.close();
74+
75+
const orphanWidgetIds = [];
76+
const orphanLongTaskIds = [];
77+
78+
const widgetCursor = db.collection('widgets').find({}, {projection: {_id: 1, drill_report: 1}});
79+
80+
while (await widgetCursor.hasNext()) {
81+
const w = await widgetCursor.next();
82+
if (!dashboardWidgets.includes(String(w._id))) {
83+
orphanWidgetIds.push(w._id);
84+
if (Array.isArray(w.drill_report)) {
85+
orphanLongTaskIds.push(...w.drill_report);
86+
}
87+
}
88+
}
89+
await widgetCursor.close();
90+
91+
console.log(`Orphan widgets found: ${orphanWidgetIds.length}`);
92+
console.log(`Linked long_tasks to drop: ${orphanLongTaskIds.length}`);
93+
94+
await deleteByChunks(db, 'widgets', orphanWidgetIds);
95+
await deleteByChunks(db, 'long_tasks', orphanLongTaskIds);
96+
97+
98+
console.log(DRY_RUN ? 'Dry-run finished' : 'Cleanup completed.');
99+
}
100+
catch (err) {
101+
console.error(err);
102+
}
103+
finally {
104+
db.close();
105+
}
106+
})();

plugins/dashboards/api/api.js

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,30 +1093,53 @@ plugins.setConfigs("dashboards", {
10931093
filterCond.owner_id = memberId;
10941094
}
10951095

1096-
common.db.collection("dashboards").findOne({_id: common.db.ObjectID(dashboardId)}, function(err, dashboard) {
1096+
common.db.collection("dashboards").findOne({_id: common.db.ObjectID(dashboardId)}, async function(err, dashboard) {
10971097
if (err || !dashboard) {
10981098
common.returnMessage(params, 400, "Dashboard with the given id doesn't exist");
10991099
}
11001100
else {
1101-
hasViewAccessToDashboard(params.member, dashboard, function(er, status) {
1101+
hasViewAccessToDashboard(params.member, dashboard, async function(er, status) {
11021102
if (er || !status) {
11031103
return common.returnOutput(params, {error: true, dashboard_access_denied: true});
11041104
}
11051105

1106-
common.db.collection("dashboards").remove(
1107-
filterCond,
1108-
function(error, result) {
1109-
if (!error && result) {
1110-
if (result && result.result && result.result.n === 1) {
1111-
plugins.dispatch("/systemlogs", {params: params, action: "dashboard_deleted", data: dashboard});
1106+
try {
1107+
// Remove the dashboard
1108+
const result = await common.db.collection("dashboards").deleteOne(filterCond);
1109+
1110+
if (result && result.deletedCount > 0) {
1111+
// Collect widget IDs from the dashboard
1112+
const widgetIds = (dashboard.widgets || []).map(w => common.db.ObjectID(w.$oid || w));
1113+
1114+
// Delete widgets from the widgets collection
1115+
if (widgetIds.length) {
1116+
const widgets = await common.db.collection("widgets").find({_id: {$in: widgetIds}}).toArray();
1117+
const drillReportIds = widgets.reduce((acc, widget) => {
1118+
if (Array.isArray(widget.drill_report)) {
1119+
acc.push(...widget.drill_report);
1120+
}
1121+
return acc;
1122+
}, []);
1123+
1124+
await common.db.collection("widgets").deleteMany({_id: {$in: widgetIds}});
1125+
1126+
// Delete drill_reports from the long_tasks collection
1127+
if (drillReportIds.length) {
1128+
await common.db.collection("long_tasks").deleteMany({_id: {$in: drillReportIds}});
11121129
}
1113-
common.returnOutput(params, result);
1114-
}
1115-
else {
1116-
common.returnMessage(params, 500, "Failed to delete dashboard");
11171130
}
1131+
1132+
plugins.dispatch("/systemlogs", {params: params, action: "dashboard_deleted", data: dashboard});
1133+
common.returnOutput(params, result);
11181134
}
1119-
);
1135+
else {
1136+
common.returnMessage(params, 500, "Failed to delete dashboard");
1137+
}
1138+
}
1139+
catch (error) {
1140+
console.error("Error during dashboard deletion:", error);
1141+
common.returnMessage(params, 500, "An error occurred while deleting the dashboard");
1142+
}
11201143
});
11211144
}
11221145
});

0 commit comments

Comments
 (0)