Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat: Full server-side pagination for important events #3515

Merged
merged 8 commits into from
Sep 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8,287 changes: 4,053 additions & 4,234 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"knex": "^2.4.2",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"mitt": "~3.0.1",
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
Expand Down
76 changes: 67 additions & 9 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));

log.info("server", "Importing Node libraries");
const fs = require("fs");

Check warning on line 55 in server/server.js

View workflow job for this annotation

GitHub Actions / check-linters

'fs' is assigned a value but never used

log.info("server", "Importing 3rd-party libraries");

Expand Down Expand Up @@ -144,7 +144,7 @@
}

// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client");
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const TwoFA = require("./2fa");
Expand Down Expand Up @@ -1003,8 +1003,6 @@
});

await server.sendMonitorList(socket);
// Clear heartbeat list on client
await sendImportantHeartbeatList(socket, monitorID, true, true);

} catch (e) {
callback({
Expand Down Expand Up @@ -1174,6 +1172,72 @@
}
});

socket.on("monitorImportantHeartbeatListCount", async (monitorID, callback) => {
try {
checkLogin(socket);

let count;
if (monitorID == null) {
count = await R.count("heartbeat", "important = 1");
} else {
count = await R.count("heartbeat", "monitor_id = ? AND important = 1", [
monitorID,
]);
}

callback({
ok: true,
count: count,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});

socket.on("monitorImportantHeartbeatListPaged", async (monitorID, offset, count, callback) => {
try {
checkLogin(socket);

let list;
if (monitorID == null) {
list = await R.find("heartbeat", `
important = 1
ORDER BY time DESC
LIMIT ?
OFFSET ?
`, [
count,
offset,
]);
} else {
list = await R.find("heartbeat", `
monitor_id = ?
AND important = 1
ORDER BY time DESC
LIMIT ?
OFFSET ?
`, [
monitorID,
count,
offset,
]);
}

callback({
ok: true,
data: list,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});

socket.on("changePassword", async (password, callback) => {
try {
checkLogin(socket);
Expand Down Expand Up @@ -1573,8 +1637,6 @@
monitorID,
]);

await sendImportantHeartbeatList(socket, monitorID, true, true);

callback({
ok: true,
});
Expand Down Expand Up @@ -1755,10 +1817,6 @@
await sendHeartbeatList(socket, monitorID);
}

for (let monitorID in monitorList) {
await sendImportantHeartbeatList(socket, monitorID);
}

for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
Expand Down
19 changes: 4 additions & 15 deletions src/mixins/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useToast } from "vue-toastification";
import jwtDecode from "jwt-decode";
import Favico from "favico.js";
import dayjs from "dayjs";
import mitt from "mitt";

import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
import { getDevContainerServerHostname, isDevContainer, getToastSuccessTimeout, getToastErrorTimeout } from "../util-frontend.js";
const toast = useToast();
Expand Down Expand Up @@ -39,7 +41,6 @@ export default {
maintenanceList: {},
apiKeyList: {},
heartbeatList: { },
importantHeartbeatList: { },
avgPingList: { },
uptimeList: { },
tlsInfoList: {},
Expand All @@ -59,6 +60,7 @@ export default {
currentPassword: "",
},
faviconUpdateDebounce: null,
emitter: mitt(),
};
},

Expand Down Expand Up @@ -201,11 +203,7 @@ export default {
}
}

if (! (data.monitorID in this.importantHeartbeatList)) {
this.importantHeartbeatList[data.monitorID] = [];
}

this.importantHeartbeatList[data.monitorID].unshift(data);
this.emitter.emit("newImportantHeartbeat", data);
}
});

Expand All @@ -229,14 +227,6 @@ export default {
this.tlsInfoList[monitorID] = JSON.parse(data);
});

socket.on("importantHeartbeatList", (monitorID, data, overwrite) => {
if (! (monitorID in this.importantHeartbeatList) || overwrite) {
this.importantHeartbeatList[monitorID] = data;
} else {
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID]);
}
});

socket.on("connect_error", (err) => {
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
this.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("Reconnecting...")}`;
Expand Down Expand Up @@ -630,7 +620,6 @@ export default {
clearData() {
console.log("reset heartbeat list");
this.heartbeatList = {};
this.importantHeartbeatList = {};
},

/**
Expand Down
113 changes: 65 additions & 48 deletions src/pages/DashboardHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ beat.name }}</router-link></td>
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ $root.monitorList[beat.monitorID]?.name }}</router-link></td>
<td><Status :status="beat.status" /></td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td class="border-0">{{ beat.msg }}</td>
</tr>

<tr v-if="importantHeartBeatList.length === 0">
<tr v-if="importantHeartBeatListLength === 0">
<td colspan="4">
{{ $t("No important events") }}
</td>
Expand All @@ -59,7 +59,7 @@
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:records="importantHeartBeatListLength"
:per-page="perPage"
:options="paginationConfig"
/>
Expand Down Expand Up @@ -92,72 +92,89 @@ export default {
page: 1,
perPage: 25,
initialPerPage: 25,
heartBeatList: [],
paginationConfig: {
hideCount: true,
chunksNavigation: "scroll",
},
importantHeartBeatListLength: 0,
displayedRecords: [],
};
},
computed: {

importantHeartBeatList() {
let result = [];

for (let monitorID in this.$root.importantHeartbeatList) {
let list = this.$root.importantHeartbeatList[monitorID];
result = result.concat(list);
}

for (let beat of result) {
let monitor = this.$root.monitorList[beat.monitorID];

if (monitor) {
beat.name = monitor.name;
}
}

result.sort((a, b) => {
if (a.time > b.time) {
return -1;
}

if (a.time < b.time) {
return 1;
}

return 0;
});

// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.heartBeatList = result;

return result;
},

displayedRecords() {
const startIndex = this.perPage * (this.page - 1);
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
},
watch: {
importantHeartBeatList() {
perPage() {
this.$nextTick(() => {
this.updatePerPage();
this.getImportantHeartbeatListPaged();
});
},

page() {
this.getImportantHeartbeatListPaged();
},
},

mounted() {
this.getImportantHeartbeatListLength();

this.$root.emitter.on("newImportantHeartbeat", this.onNewImportantHeartbeat);

this.initialPerPage = this.perPage;

window.addEventListener("resize", this.updatePerPage);
this.updatePerPage();
},

beforeUnmount() {
this.$root.emitter.off("newImportantHeartbeat", this.onNewImportantHeartbeat);

window.removeEventListener("resize", this.updatePerPage);
},

methods: {
/**
* Updates the displayed records when a new important heartbeat arrives.
* @param {object} heartbeat - The heartbeat object received.
chakflying marked this conversation as resolved.
Show resolved Hide resolved
* @returns {void}
*/
onNewImportantHeartbeat(heartbeat) {
if (this.page === 1) {
this.displayedRecords.unshift(heartbeat);
if (this.displayedRecords.length > this.perPage) {
this.displayedRecords.pop();
}
this.importantHeartBeatListLength += 1;
}
},

/**
* Retrieves the length of the important heartbeat list for all monitors.
* @returns {void}
*/
getImportantHeartbeatListLength() {
this.$root.getSocket().emit("monitorImportantHeartbeatListCount", null, (res) => {
if (res.ok) {
this.importantHeartBeatListLength = res.count;
this.getImportantHeartbeatListPaged();
}
});
},

/**
* Retrieves the important heartbeat list for the current page.
* @returns {void}
*/
getImportantHeartbeatListPaged() {
const offset = (this.page - 1) * this.perPage;
this.$root.getSocket().emit("monitorImportantHeartbeatListPaged", null, offset, this.perPage, (res) => {
if (res.ok) {
this.displayedRecords = res.data;
}
});
},

/**
* Updates the number of items shown per page based on the available height.
* @returns {void}
*/
updatePerPage() {
const tableContainer = this.$refs.tableContainer;
const tableContainerHeight = tableContainer.offsetHeight;
Expand Down
Loading
Loading