+
-
-
-
-
-
-
-
-
-
-
-
- {{ file }}
-
- selected
-
-
-
-
-
- No metric files found.
-
-
-
-
Raw JSON
-
- {{ rawJson }}
-
-
-
diff --git a/msc-wis2node-ui/src/components/MetricFilesGraph.vue b/msc-wis2node-ui/src/components/MetricFilesGraph.vue
new file mode 100644
index 0000000..6db0332
--- /dev/null
+++ b/msc-wis2node-ui/src/components/MetricFilesGraph.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+ Metric:
+
+ Selected dataset:
+
+
+
+
+
+
diff --git a/msc-wis2node-ui/src/components/MetricFilesRaw.vue b/msc-wis2node-ui/src/components/MetricFilesRaw.vue
new file mode 100644
index 0000000..731bfd9
--- /dev/null
+++ b/msc-wis2node-ui/src/components/MetricFilesRaw.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ file }}
+ selected
+
+
+
+
+ No metric files found.
+
+
+
+
+
+
+
diff --git a/msc-wis2node-ui/src/components/MetricFilesTable.vue b/msc-wis2node-ui/src/components/MetricFilesTable.vue
new file mode 100644
index 0000000..a5a16e5
--- /dev/null
+++ b/msc-wis2node-ui/src/components/MetricFilesTable.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+ Selected Date Range:
+
+
+
+
+
diff --git a/msc-wis2node-ui/src/router/index.js b/msc-wis2node-ui/src/router/index.js
index e1eab52..907bb45 100644
--- a/msc-wis2node-ui/src/router/index.js
+++ b/msc-wis2node-ui/src/router/index.js
@@ -1,8 +1,13 @@
import { createRouter, createWebHistory } from 'vue-router'
+import wis2NodeOverview from '@/views/wis2NodeOverview.vue'
+import wis2NodeMonitoring from '@/views/wis2NodeMonitoring.vue'
const router = createRouter({
- history: createWebHistory(import.meta.env.BASE_URL),
- routes: [],
+ history: createWebHistory(),
+ routes: [
+ { path: '/', component: wis2NodeOverview },
+ { path: '/monitoring', component: wis2NodeMonitoring },
+ ],
})
export default router
diff --git a/msc-wis2node-ui/src/stores/useDarkTheme.js b/msc-wis2node-ui/src/stores/useDarkTheme.js
new file mode 100644
index 0000000..6ff61aa
--- /dev/null
+++ b/msc-wis2node-ui/src/stores/useDarkTheme.js
@@ -0,0 +1,13 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useDarkTheme = defineStore('themes', () => {
+ const isDark = ref(true)
+ const toggleTheme = () => {
+ isDark.value = !isDark.value
+ }
+ return {
+ isDark,
+ toggleTheme,
+ }
+})
diff --git a/msc-wis2node-ui/src/stores/useDataDistributionMetrics.js b/msc-wis2node-ui/src/stores/useDataDistributionMetrics.js
index cc90e14..7b0da39 100644
--- a/msc-wis2node-ui/src/stores/useDataDistributionMetrics.js
+++ b/msc-wis2node-ui/src/stores/useDataDistributionMetrics.js
@@ -1,6 +1,7 @@
// useDataDistributionMetrics.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
+import { DateTime } from 'luxon'
const baseUrl = import.meta.env.VITE_DATA_DISTRIBUTION_METRICS_URL
@@ -14,13 +15,66 @@ export const useDataDistributionMetrics = defineStore('metrics', () => {
const loadingFile = ref(false)
const error = ref(null)
+ const errorsList = ref([]) // Errors used in the Monitoring page
+
// Getters
const hasData = computed(() => !!selectedFileData.value)
+ // Data and reference keys from metric files
+ // Used for the Metric File Data graph and table
+
+ // Json for storing all metric data in the form
+ // {date_1: {title_1: {metric data}, ..., total: {metric data}, ...} }
+ const metricDataJson = ref({})
+
+ // List of total accumulated file sizes and number of files
+ // for each dataset. Loaded on startup
+ const metricFileTableTotals = ref([])
+
+ // For checking if a dataset is found for the first
+ // time, and if the data has been loaded in yet
+ const availableDatasets = ref([])
+
+ // Totals for initial graphs for all datasets across all dates
+
+ // List recording file size totals. Each entry is a different data
+ const sizeTotals = ref([])
+ // List recording total number of files. Each entry is a different data
+ const fileTotals = ref([])
+
+ // For keeping track of selected and available dates for the metric files
+ const startAndEndDates = ref([DateTime.now(), DateTime.now()])
+ const minDate = ref(DateTime.now())
+ const maxDate = ref(DateTime.now())
+ const selectedStartDate = ref(false)
+
+ // For storing selectable options of the Metrics File Data graph
+ const dropdownDatasetOptions = ref([{ label: 'All Datasets', value: 'total' }])
+ const dataTypeOptions = ref([
+ {
+ label: 'Number of files',
+ value: 'Files',
+ },
+ {
+ label: 'Gigabytes published',
+ value: 'Size',
+ },
+ ])
+
+ // For initialializing selected options of the
+ // Metrics File Data graph on startup
+ const selectedMetric = ref('Files')
+ const selectedDataset = ref('total')
+
+ const metricFileTableData = ref([])
+ const selectedTab = ref('graph')
+ const lastLoad = ref() // Visual indicator of when metric files data was retrieved
+
// Actions
// Fetch list of JSON files by scraping the HTML index
async function fetchFileList() {
+ lastLoad.value = new Date().toLocaleString()
if (!metricsBaseUrl.value) {
error.value = 'VITE_DATA_DISTRIBUTION_METRICS_URL is not configured.'
return
@@ -51,10 +105,77 @@ export const useDataDistributionMetrics = defineStore('metrics', () => {
.map((href) => href.split('/').filter(Boolean).pop())
.filter(Boolean)
- // Optional: sort filenames (directory listing might already be sorted)
- jsonFiles.sort()
+ // Ensure files contain data. Each file represents 1 date
+ let filesToUse = []
+ for (const file of jsonFiles) {
+ const url = `${metricsBaseUrl.value}/${file}`
+ const datekey = file.slice(0, -5)
+ const res = await fetch(url)
+ const resContentLen = Number(res.headers.get('content-length'))
+
+ if (res.ok && resContentLen > 0) {
+ if (selectedStartDate.value === false) {
+ minDate.value = DateTime.fromISO(datekey)
+ selectedStartDate.value = true
+ }
+
+ // Update to keep track of the upper bound of dates
+ maxDate.value = DateTime.fromISO(datekey)
+
+ const data = await res.json()
+ metricDataJson.value[datekey] = {}
+ const keys = Object.keys(data)
+ for (let key of keys) {
+ const datasetName = data[key].title
+
+ let size = data[key].bytes.slice(0, -3)
+ let units = data[key].bytes.slice(-2)
+ let gbUsed = gigabyteCalc(size, units)
+ if (key !== 'total') {
+ metricDataJson.value[datekey][datasetName] = data[key]
+
+ if (!availableDatasets.value.includes(datasetName)) {
+ availableDatasets.value.push(datasetName)
+ dropdownDatasetOptions.value.push({
+ label: datasetName,
+ value: datasetName,
+ })
+ // We then add it to the overall dataset
+ metricFileTableTotals.value.push({
+ dataset: datasetName,
+ size: gbUsed,
+ files: data[key].files,
+ })
+ } else {
+ // Already added to metricFileTableTotals, get its index and update values accordingly
+ const index = metricFileTableTotals.value.findIndex(
+ (dataset) => dataset.dataset === datasetName,
+ )
+ metricFileTableTotals.value[index].size += gbUsed
+ metricFileTableTotals.value[index].files += data[key].files
+ }
+ } else {
+ // For total values of the date
+ metricDataJson.value[datekey]['total'] = data[key]
+ fileTotals.value.push(data[key].files)
+ sizeTotals.value.push(gbUsed)
+ }
+ }
+
+ filesToUse.push(file)
+ } else {
+ console.warn(`Metric file missing data at ${url}`)
+ }
+ }
+
+ // Sort files by data size
+ metricFileTableTotals.value.sort((a, b) => b.size - a.size)
+ metricFileTableData.value = metricFileTableTotals.value
+ filesToUse.sort()
+ files.value = filesToUse
- files.value = jsonFiles
+ // Change the default values of the dates to the max range available
+ startAndEndDates.value = [minDate.value, maxDate.value]
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
files.value = []
@@ -63,6 +184,61 @@ export const useDataDistributionMetrics = defineStore('metrics', () => {
}
}
+ // Helper function for date selection option of
+ // metric files table
+ async function obtainTableResults(dates) {
+ let results = []
+ let addedDatasets = []
+ for (let date of dates) {
+ if (Object.keys(metricDataJson.value).includes(date)) {
+ const url = `${metricsBaseUrl.value}/${date}.json`
+ const res = await fetch(url)
+ if (res.ok) {
+ const data = await res.json()
+ const keys = Object.keys(data)
+ for (let key of keys) {
+ let size = data[key].bytes.slice(0, -3)
+ let units = data[key].bytes.slice(-2)
+ let gbUsed = gigabyteCalc(size, units)
+
+ const datasetName = data[key].title
+
+ if (key !== 'total') {
+ if (!addedDatasets.includes(datasetName)) {
+ addedDatasets.push(datasetName)
+ results.push({
+ dataset: datasetName,
+ size: gbUsed,
+ files: data[key].files,
+ })
+ } else {
+ const index = results.findIndex((dataset) => dataset.dataset === datasetName)
+ results[index].size = results[index].size + gbUsed
+ results[index].files = results[index].files + data[key].files
+ }
+ }
+ }
+ }
+ }
+ }
+ // Sort results by size before returning
+ results.sort((a, b) => b.size - a.size)
+ return results
+ }
+
+ function gigabyteCalc(size, units) {
+ let gbUsed = 0
+ if (units === 'Gb') {
+ gbUsed = Number(size)
+ } else if (units === 'Mb') {
+ gbUsed = Number(size) / 1000
+ } else {
+ // kb
+ gbUsed = Number(size) / 1e6
+ }
+ return gbUsed
+ }
+
// Fetch a specific JSON file and store its content
async function fetchFile(name) {
if (!metricsBaseUrl.value) {
@@ -101,6 +277,20 @@ export const useDataDistributionMetrics = defineStore('metrics', () => {
selectedFileData.value ? JSON.stringify(selectedFileData.value, null, 2) : '',
)
+ async function refresh() {
+ selectedStartDate.value = false
+ metricDataJson.value = {}
+ availableDatasets.value = []
+ dropdownDatasetOptions.value = [{ label: 'All Datasets', value: 'total' }]
+ metricFileTableTotals.value = []
+ fileTotals.value = []
+ sizeTotals.value = []
+ await fetchFileList()
+
+ const latest = files.value[files.value.length - 1]
+ await fetchFile(latest)
+ }
+
return {
// state
metricsBaseUrl,
@@ -111,6 +301,8 @@ export const useDataDistributionMetrics = defineStore('metrics', () => {
loadingFile,
error,
+ errorsList,
+
// getters
hasData,
rawJson,
@@ -118,5 +310,37 @@ export const useDataDistributionMetrics = defineStore('metrics', () => {
// actions
fetchFileList,
fetchFile,
+ refresh,
+ obtainTableResults,
+ gigabyteCalc,
+
+ // Data and reference keys from metric files
+ // Used for the Metric File Data graph and table
+ metricDataJson,
+ metricFileTableTotals,
+ availableDatasets,
+
+ // Totals for initial graphs for all datasets across
+ // all dates
+ sizeTotals,
+ fileTotals,
+
+ // For keeping track of selected and available dates for the metric files
+ startAndEndDates,
+ minDate,
+ maxDate,
+
+ // For storing selectable options of the Metrics File Data graph
+ dropdownDatasetOptions,
+ dataTypeOptions,
+
+ // For initialializing selected options of the
+ // Metrics File Data graph on startup
+ selectedMetric,
+ selectedDataset,
+
+ metricFileTableData,
+ selectedTab,
+ lastLoad,
}
})
diff --git a/msc-wis2node-ui/src/stores/useErrorMsg.js b/msc-wis2node-ui/src/stores/useErrorMsg.js
new file mode 100644
index 0000000..359138c
--- /dev/null
+++ b/msc-wis2node-ui/src/stores/useErrorMsg.js
@@ -0,0 +1,103 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import mqtt from 'mqtt'
+
+const MQTT_BROKER = import.meta.env.VITE_BROKER_URL
+const MQTT_TOPIC_ERRORS = import.meta.env.VITE_TOPIC_ERRORS
+
+const options = {
+ username: import.meta.env.VITE_BROKER_USERNAME,
+ password: import.meta.env.VITE_BROKER_PASSWORD,
+ keepalive: 60,
+ protocolVersion: 5,
+ reconnectPeriod: 1000,
+ connectTimeout: 30 * 1000,
+}
+
+export const useErrorMsg = defineStore('errors', () => {
+ // Values associated with the n-grid-items
+ // error statistics on Overview page
+ const totalNumErrors = ref(0)
+ const totalElapsedTime = ref(0)
+
+ const receivedErrorsChartData = ref([0, 0, 0, 0, 0])
+ const numErrorsInMin = ref(0)
+
+ const errorsList = ref([]) // Errors used in the Monitoring page
+
+ // Connect and subscribe to the error notifications service
+ const monitorClient = mqtt.connect(MQTT_BROKER, options)
+ monitorClient.on('connect', function () {
+ monitorClient.subscribe(MQTT_TOPIC_ERRORS, function (err) {
+ if (!err) {
+ console.debug('Connected for monitoring!')
+ }
+ })
+ })
+ monitorClient.on('error', (err) => {
+ console.error('Connection error: ', err)
+ monitorClient.end()
+ })
+ monitorClient.on('reconnect', () => {
+ console.error('Reconnecting...')
+ })
+ monitorClient.on('message', function (topic, message) {
+ try {
+ const monitorContent = JSON.parse(message.toString())
+ const invalidStatus = ['WARNING', 'ERROR', 'CRITICAL']
+ if (
+ 'data' in monitorContent &&
+ 'severity' in monitorContent.data &&
+ invalidStatus.includes(monitorContent.data.severity) &&
+ 'content' in monitorContent.data &&
+ 'title' in monitorContent.data.content
+ ) {
+ totalNumErrors.value = totalNumErrors.value + 1
+ numErrorsInMin.value = numErrorsInMin.value + 1
+ monitorContent.timeElapsed = `Time elapsed: 0 mins`
+ errorsList.value.unshift(monitorContent)
+ }
+ } catch {
+ console.error('Unexpected format detected for received error message')
+ }
+ })
+
+ function incrementErrorMins() {
+ totalElapsedTime.value = totalElapsedTime.value + 1
+
+ const currentTime = new Date()
+ for (const error of errorsList.value) {
+ if ('timeElapsed' in error) {
+ const postedTime = new Date(error.time)
+ const timeDifference = currentTime - postedTime
+ if (timeDifference / (1000 * 60 * 60) >= 1) {
+ // At least 1 hour since error was first posted
+ const timeRoundedDownHrs = Math.floor(timeDifference / (1000 * 60 * 60))
+ error.timeElapsed = `Time elapsed: ${timeRoundedDownHrs} hrs`
+ } else {
+ const timeRoundedDownMins = Math.floor(timeDifference / (1000 * 60))
+ error.timeElapsed = `Time elapsed: ${timeRoundedDownMins} mins`
+ }
+ }
+ }
+
+ // Shift over values of Received Errors graph
+ const receivedErrorsLength = receivedErrorsChartData.value.length
+ for (let x = 0; x < receivedErrorsLength; x++) {
+ if (x + 1 === receivedErrorsChartData.value.length) {
+ receivedErrorsChartData.value[x] = numErrorsInMin.value
+ } else {
+ receivedErrorsChartData.value[x] = receivedErrorsChartData.value[x + 1]
+ }
+ }
+ numErrorsInMin.value = 0
+ }
+ setInterval(incrementErrorMins, 60000)
+
+ return {
+ totalNumErrors,
+ totalElapsedTime,
+ receivedErrorsChartData,
+ errorsList,
+ }
+})
diff --git a/msc-wis2node-ui/src/stores/useNotifMsg.js b/msc-wis2node-ui/src/stores/useNotifMsg.js
new file mode 100644
index 0000000..f21eb56
--- /dev/null
+++ b/msc-wis2node-ui/src/stores/useNotifMsg.js
@@ -0,0 +1,79 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import mqtt from 'mqtt'
+
+const MQTT_BROKER = import.meta.env.VITE_BROKER_URL
+const MQTT_TOPIC_NOTIFICATION = import.meta.env.VITE_TOPIC_NOTIFICATION
+
+const options = {
+ username: import.meta.env.VITE_BROKER_USERNAME,
+ password: import.meta.env.VITE_BROKER_PASSWORD,
+ keepalive: 60,
+ protocolVersion: 5,
+ reconnectPeriod: 1000,
+ connectTimeout: 30 * 1000,
+}
+
+export const useNotifMsg = defineStore('notifs', () => {
+ // Values associated with the n-grid-items
+ // notification statistics on Overview page
+ const totalNumMsg = ref(0)
+ const currMinNumMsg = ref(0)
+ const avgMessages = ref(0)
+ const timeUntilUpdate = ref(60)
+
+ const avgMsgChartData = ref([0, 0, 0, 0, 0, 0, 0])
+
+ // Connect and subscribe to notifications service
+ const notifClient = mqtt.connect(MQTT_BROKER, options)
+ notifClient.on('connect', function () {
+ notifClient.subscribe(MQTT_TOPIC_NOTIFICATION, function (err) {
+ if (!err) {
+ console.debug('Connected for notifications!')
+ }
+ })
+ })
+ notifClient.on('error', (err) => {
+ console.error('Connection error: ', err)
+ notifClient.end()
+ })
+ notifClient.on('reconnect', () => {
+ console.error('Reconnecting...')
+ })
+ notifClient.on('message', function () {
+ currMinNumMsg.value = currMinNumMsg.value + 1
+ totalNumMsg.value = totalNumMsg.value + 1
+ })
+
+ // Update the msg/s value every min
+ function msgPerSecCalc() {
+ avgMessages.value = Math.round(currMinNumMsg.value / 60)
+ currMinNumMsg.value = 0
+ timeUntilUpdate.value = 60
+
+ // Shifting over values of notif/s graph
+ for (let i = 0; i < avgMsgChartData.value.length; i++) {
+ if (i + 1 === avgMsgChartData.value.length) {
+ avgMsgChartData.value[i] = avgMessages.value
+ } else {
+ avgMsgChartData.value[i] = avgMsgChartData.value[i + 1]
+ }
+ }
+ }
+ setInterval(msgPerSecCalc, 60000)
+
+ function countDown() {
+ if (timeUntilUpdate.value > 0) {
+ timeUntilUpdate.value = timeUntilUpdate.value - 1
+ }
+ }
+ setInterval(countDown, 1000)
+
+ return {
+ totalNumMsg,
+ currMinNumMsg,
+ avgMessages,
+ timeUntilUpdate,
+ avgMsgChartData,
+ }
+})
diff --git a/msc-wis2node-ui/src/views/wis2NodeMonitoring.vue b/msc-wis2node-ui/src/views/wis2NodeMonitoring.vue
new file mode 100644
index 0000000..87213b5
--- /dev/null
+++ b/msc-wis2node-ui/src/views/wis2NodeMonitoring.vue
@@ -0,0 +1,118 @@
+
+
+
+ List of errors
+
+
+
+
+
+
+
+
diff --git a/msc-wis2node-ui/src/views/wis2NodeOverview.vue b/msc-wis2node-ui/src/views/wis2NodeOverview.vue
new file mode 100644
index 0000000..489ad55
--- /dev/null
+++ b/msc-wis2node-ui/src/views/wis2NodeOverview.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+ MQTT Topic
+ {{ brokerTopic }}
+ Current subscription
+
+
+
+
+
+ Total messages
+ {{ totalNumMsg }}
+ in the last {{ totalElapsedTime }} minutes
+
+
+
+
+
+
+ Messages / Second
+ {{ avgMessages }}
+ Averaged over last 60s. Update in {{ timeUntilUpdate }}s
+
+
+
+
+
+
+ Dropped / Error Messages
+ {{ totalNumErrors }}
+ in the last {{ totalElapsedTime }} minutes
+
+
+
+
+
+
+
+
+