From 5bd98353aff8ba7100c36f5df0bc63f0ea4257aa Mon Sep 17 00:00:00 2001 From: Hiago Silva Souza <hiasilva@gmail.com> Date: Sat, 25 Jan 2025 00:24:20 -0300 Subject: [PATCH 1/5] feat: generate badge from a status page --- package-lock.json | 4 +- server/model/status_page.js | 85 ++++++++++++- server/routers/status-page-router.js | 8 +- src/components/BadgeBuilderDialog.vue | 153 ++++++++++++++++++++++ src/icon.js | 2 + src/lang/en.json | 19 ++- src/lang/pt-BR.json | 19 ++- src/pages/StatusPage.vue | 177 +++++++++++++++----------- 8 files changed, 382 insertions(+), 85 deletions(-) create mode 100644 src/components/BadgeBuilderDialog.vue diff --git a/package-lock.json b/package-lock.json index 8d3f58b4d..ccf1a628c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uptime-kuma", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.8.22", diff --git a/server/model/status_page.js b/server/model/status_page.js index 38f548ebb..08a69154c 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -9,6 +9,7 @@ const { Feed } = require("feed"); const config = require("../config"); const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util"); +const { makeBadge } = require("badge-maker"); class StatusPage extends BeanModel { @@ -16,7 +17,7 @@ class StatusPage extends BeanModel { * Like this: { "test-uptime.kuma.pet": "default" } * @type {{}} */ - static domainMappingList = { }; + static domainMappingList = {}; /** * Handle responses to RSS pages @@ -61,6 +62,26 @@ class StatusPage extends BeanModel { } } + /** + * Handle responses to status page + * @param {Response} response Response object + * @param {string} slug Status page slug + * @param {object} config Config for badge + * @returns {Promise<void>} + */ + static async handleStatusPageSVGResponse(response, slug, config) { + slug = slug.replace('.svg', '') + let statusPage = await R.findOne("status_page", " slug = ? ", [ + slug.replace('.svg', '') + ]); + if (statusPage) { + response.set('Content-Type', 'image/svg+xml') + response.send(await StatusPage.renderSVG(slug, config, statusPage)); + } else { + response.status(404).send(await StatusPage.renderSVG(slug, config, null)); + } + } + /** * SSR for RSS feed * @param {statusPage} statusPage object @@ -152,6 +173,62 @@ class StatusPage extends BeanModel { return $.root().html(); } + static async renderSVG(slug, userConfig = {}, statusPage) { + let allowedBadgeStyles = ['flat', 'flat-square', 'for-the-badge', 'plastic', 'social']; + if (!allowedBadgeStyles.includes(userConfig.badgeStyle)) { + userConfig.badgeStyle = 'flat' + } + + // returns unknown svg + if (statusPage === null) { + return makeBadge({ message: userConfig.notFoundText || 'not found', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }) + } else { + + // get all heartbeats that correspond to this statusPage + const config = await statusPage.toPublicJSON(); + + // Public Group List + const showTags = !!statusPage.show_tags; + + const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [ + statusPage.id + ]); + + let heartbeats = []; + + for (let groupBean of list) { + let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry); + for (const monitor of monitorGroup.monitorList) { + const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]); + if (heartbeat) { + heartbeats.push({ + ...monitor, + status: heartbeat.status, + time: heartbeat.time + }); + } + } + } + + // calculate RSS feed description + let status = StatusPage.overallStatus(heartbeats); + + switch (status) { + case STATUS_PAGE_ALL_DOWN: + return makeBadge({ message: userConfig.allDownText ?? 'all down', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + case STATUS_PAGE_ALL_UP: + return makeBadge({ message: userConfig.allOperationalText ?? 'all operational', color: 'green', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + case STATUS_PAGE_PARTIAL_DOWN: + return makeBadge({ message: userConfig.partialDownText ?? 'partial down', color: 'orange', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + case STATUS_PAGE_MAINTENANCE: + return makeBadge({ message: userConfig.maintenanceText || 'maintenance', color: 'yellow', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + default: + return makeBadge({ message: userConfig.unknownText || 'unknown', color: 'lightgray', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + } + } + + } + /** * @param {heartbeats} heartbeats from getRSSPageData * @returns {number} status_page constant from util.ts @@ -174,7 +251,7 @@ class StatusPage extends BeanModel { } } - if (! hasUp) { + if (!hasUp) { status = STATUS_PAGE_ALL_DOWN; } @@ -231,7 +308,7 @@ class StatusPage extends BeanModel { for (let groupBean of list) { let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry); for (const monitor of monitorGroup.monitorList) { - const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]); + const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]); if (heartbeat) { heartbeats.push({ ...monitor, @@ -471,7 +548,7 @@ class StatusPage extends BeanModel { SELECT DISTINCT maintenance_id FROM maintenance_status_page WHERE status_page_id = ? - `, [ statusPageId ]); + `, [statusPageId]); for (const maintenanceID of maintenanceIDList) { let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID); diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 893f57564..9c5b88101 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -16,7 +16,13 @@ const server = UptimeKumaServer.getInstance(); router.get("/status/:slug", cache("5 minutes"), async (request, response) => { let slug = request.params.slug; slug = slug.toLowerCase(); - await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); + + if (slug.endsWith('.svg')) { + await StatusPage.handleStatusPageSVGResponse(response, slug, JSON.parse(request.query.config || '{}')); + } else { + await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); + } + }); router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => { diff --git a/src/components/BadgeBuilderDialog.vue b/src/components/BadgeBuilderDialog.vue new file mode 100644 index 000000000..8b4f9cc79 --- /dev/null +++ b/src/components/BadgeBuilderDialog.vue @@ -0,0 +1,153 @@ +<template> + <form @submit.prevent="submit"> + <div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 id="exampleModalLabel" class="modal-title"> + {{ $t("Generate Badge") }} + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" /> + </div> + <div class="modal-body"> + <div class="mb-3"> + <label for="badge-builder-badge-style" class="form-label">{{ $t("Badge Style") }}</label> + <select id="badge-builder-badge-style" v-model="config.badgeStyle" class="form-select"> + <option value="flat">Flat</option> + <option value="flat-square">Flat Square</option> + <option value="for-the-badge">For The Badge</option> + <option value="plastic">Plastic</option> + <option value="social">Social</option> + </select> + </div> + + <div class="mb-3"> + <label for="badge-not-found-text" class="form-label">{{ $t("Return to not found") }}</label> + <input id="badge-not-found-text" v-model="config.notFoundText" type="text" + class="form-control"> + </div> + + <div class="mb-3"> + <label for="badge-operational-text" class="form-label">{{ $t("Return to operational") + }}</label> + <input id="badge-operational-text" v-model="config.allOperationalText" type="text" + class="form-control"> + </div> + + + <div class="mb-3"> + <label for="badge-partial-down-text" class="form-label">{{ $t("Return to partial down") + }}</label> + <input id="badge-partial-down-text" v-model="config.partialDownText" type="text" + class="form-control"> + </div> + + <div class="mb-3"> + <label for="badge-maintenance-text" class="form-label">{{ $t("Return to maintenance") + }}</label> + <input id="badge-maintenance-text" v-model="config.maintenanceText" type="text" + class="form-control"> + </div> + + <div class="mb-3"> + <label for="badge-unknown-text" class="form-label">{{ $t("Return to unknown") + }}</label> + <input id="badge-unknown-text" v-model="config.unknownText" type="text" + class="form-control"> + </div> + + <div class="Preview" v-if="slug"> + <div>{{ $t("Preview") }}</div> + <img :src="getSvgURL(true)" class="mt-2" /> + </div> + + </div> + + + + <div class="modal-footer"> + <button type="button" @click="copyImageURL()" class="btn btn-primary"> + <!-- <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div> --> + {{ $t("Copy Image URL") }} + </button> + </div> + </div> + </div> + </div> + </form> + +</template> + +<script> +import { Modal } from "bootstrap"; +import { useToast } from "vue-toastification"; +import axios from 'axios'; +const toast = useToast(); + +export default { + components: {}, + props: {}, + emits: [], + data() { + return { + model: null, + slug: null, + config: { + badgeStyle: 'flat', + notFoundText: this.$t('not found'), + allDownText: this.$t('all down'), + allOperationalText: this.$t('operational'), + partialDownText: this.$t('partial down'), + maintenanceText: this.$t('maintenance'), + unknownText: this.$t('unknown') + } + }; + }, + + computed: { + }, + + watch: { + }, + + mounted() { + this.modal = new Modal(this.$refs.modal); + }, + methods: { + /** + * Show settings for specified notification + * @param {number} notificationID ID of notification to show + * @returns {void} + */ + show(slug) { + this.slug = slug; + this.modal.show(); + }, + + getSvgURL(disableCache = false) { + return axios.defaults.baseURL + '/status/' + this.slug + '.svg?config=' + encodeURIComponent(JSON.stringify(this.config)).replace(/%5B/g, '[').replace(/%5D/g, ']') + (disableCache ? '&noCache=' + Date.now() : ''); + }, + + copyImageURL() { + const text = this.getSvgURL(); + navigator.clipboard.writeText(text).then(() => { + toast.success(this.$t("Image url copied to clipboard.")); + }).catch(err => { + toast.error(this.$t("Unable to copy image URL.")); + }); + } + }, +}; +</script> + +<style lang="scss" scoped> +@import "../assets/vars.scss"; + +.dark { + + .modal-dialog .form-text, + .modal-dialog p { + color: $dark-font-color; + } +} +</style> diff --git a/src/icon.js b/src/icon.js index 7bdfe1ca0..4bfb279ea 100644 --- a/src/icon.js +++ b/src/icon.js @@ -18,6 +18,7 @@ import { faPlus, faSearch, faTachometerAlt, + faTag, faTimes, faTimesCircle, faTrash, @@ -64,6 +65,7 @@ library.add( faPlus, faSearch, faTachometerAlt, + faTag, faTimes, faTimesCircle, faTrash, diff --git a/src/lang/en.json b/src/lang/en.json index e215f1031..711d8b1eb 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1051,5 +1051,20 @@ "RabbitMQ Password": "RabbitMQ Password", "rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.", "SendGrid API Key": "SendGrid API Key", - "Separate multiple email addresses with commas": "Separate multiple email addresses with commas" -} + "Separate multiple email addresses with commas": "Separate multiple email addresses with commas", + "Generate Badge": "Generate Badge", + "Return to not found": "Return to not found", + "Return to operational": "Return to operational", + "Return to partial down": "Return to partial down", + "Return to maintenance": "Return to maintenance", + "Return to unknown": "Return to unknown", + "not found": "not found", + "operational": "operational", + "partial down": "partial down", + "maintenance": "maintenance", + "unknown": "unknown", + "Preview": "Preview", + "Copy Image URL": "Copy Image URL", + "Image url copied to clipboard.": "Image url copied to clipboard.", + "Unable to copy image URL.": "Unable to copy image URL." +} \ No newline at end of file diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index 8319c656d..8d8682502 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -994,5 +994,20 @@ "aboutSlackUsername": "Altera o nome de exibição do remetente da mensagem. Se quiser mencionar alguém, inclua a menção no nome amigável.", "Send rich messages": "Enviar mensagens ricas", "Host Onesender": "Servidor Onesender", - "Token Onesender": "Chave Onesender" -} + "Token Onesender": "Chave Onesender", + "Generate Badge": "Gerar Distintivo", + "Return to not found": "Retorno para não encontrado", + "Return to operational": "Retorno para operacional", + "Return to partial down": "Retorno para parcialmente degradado", + "Return to maintenance": "Retorno para em manutenção", + "Return to unknown": "Retorno para desconhecido", + "not found": "não encontrado", + "operational": "operacional", + "partial down": "parcialmente fora", + "maintenance": "manutenção", + "unknown": "desconhecido", + "Preview": "Pré-visualizar", + "Copy Image URL": "Copiar URL da imagem", + "Image url copied to clipboard.": "URL da imagem copiada para área de transferência.", + "Unable to copy image URL.": "Não é possível copiar o URL da imagem para área de transferência." +} \ No newline at end of file diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 116968282..b8d585f39 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -19,7 +19,8 @@ <!-- Description --> <div class="my-3"> <label for="description" class="form-label">{{ $t("Description") }}</label> - <textarea id="description" v-model="config.description" class="form-control" data-testid="description-input"></textarea> + <textarea id="description" v-model="config.description" class="form-control" + data-testid="description-input"></textarea> <div class="form-text"> {{ $t("markdownSupported") }} </div> @@ -28,7 +29,8 @@ <!-- Footer Text --> <div class="my-3"> <label for="footer-text" class="form-label">{{ $t("Footer Text") }}</label> - <textarea id="footer-text" v-model="config.footerText" class="form-control" data-testid="footer-text-input"></textarea> + <textarea id="footer-text" v-model="config.footerText" class="form-control" + data-testid="footer-text-input"></textarea> <div class="form-text"> {{ $t("markdownSupported") }} </div> @@ -36,7 +38,8 @@ <div class="my-3"> <label for="auto-refresh-interval" class="form-label">{{ $t("Refresh Interval") }}</label> - <input id="auto-refresh-interval" v-model="config.autoRefreshInterval" type="number" class="form-control" :min="5" data-testid="refresh-interval-input"> + <input id="auto-refresh-interval" v-model="config.autoRefreshInterval" type="number" + class="form-control" :min="5" data-testid="refresh-interval-input"> <div class="form-text"> {{ $t("Refresh Interval Description", [config.autoRefreshInterval]) }} </div> @@ -52,40 +55,49 @@ </div> <div class="my-3 form-check form-switch"> - <input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox" data-testid="show-tags-checkbox"> + <input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox" + data-testid="show-tags-checkbox"> <label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label> </div> <!-- Show Powered By --> <div class="my-3 form-check form-switch"> - <input id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox" data-testid="show-powered-by-checkbox"> + <input id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox" + data-testid="show-powered-by-checkbox"> <label class="form-check-label" for="show-powered-by">{{ $t("Show Powered By") }}</label> </div> <!-- Show certificate expiry --> <div class="my-3 form-check form-switch"> - <input id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input" type="checkbox" data-testid="show-certificate-expiry-checkbox"> - <label class="form-check-label" for="show-certificate-expiry">{{ $t("showCertificateExpiry") }}</label> + <input id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input" + type="checkbox" data-testid="show-certificate-expiry-checkbox"> + <label class="form-check-label" for="show-certificate-expiry">{{ $t("showCertificateExpiry") + }}</label> </div> <div v-if="false" class="my-3"> - <label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon") }}</sup></label> - <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control"> + <label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon") + }}</sup></label> + <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" + class="form-control"> </div> <!-- Domain Name List --> <div class="my-3"> <label class="form-label"> {{ $t("Domain Names") }} - <button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" @click="addDomainField"> + <button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" + @click="addDomainField"> <font-awesome-icon icon="plus-circle" class="action text-primary" /> </button> </label> <ul class="list-group domain-name-list"> <li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item"> - <input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" /> - <button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [ domain ])" @click="removeDomain(index)"> + <input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" + placeholder="example.com" /> + <button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [domain])" + @click="removeDomain(index)"> <font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" /> </button> </li> @@ -95,13 +107,15 @@ <!-- Google Analytics --> <div class="my-3"> <label for="googleAnalyticsTag" class="form-label">{{ $t("Google Analytics ID") }}</label> - <input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control" data-testid="google-analytics-input"> + <input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control" + data-testid="google-analytics-input"> </div> <!-- Custom CSS --> <div class="my-3"> <div class="mb-1">{{ $t("Custom CSS") }}</div> - <prism-editor v-model="config.customCSS" class="css-editor" data-testid="custom-css-input" :highlight="highlighter" line-numbers></prism-editor> + <prism-editor v-model="config.customCSS" class="css-editor" data-testid="custom-css-input" + :highlight="highlighter" line-numbers></prism-editor> </div> <div class="danger-zone"> @@ -127,7 +141,7 @@ </div> <!-- Main Status Page --> - <div :class="{ edit: enableEditMode}" class="main"> + <div :class="{ edit: enableEditMode }" class="main"> <!-- Logo & Title --> <h1 class="mb-4 title-flex"> <!-- Logo --> @@ -138,17 +152,9 @@ <!-- Uploader --> <!-- url="/api/status-page/upload-logo" --> - <ImageCropUpload - v-model="showImageCropUpload" - field="img" - :width="128" - :height="128" - :langType="$i18n.locale" - img-format="png" - :noCircle="true" - :noSquare="false" - @crop-success="cropSuccess" - /> + <ImageCropUpload v-model="showImageCropUpload" field="img" :width="128" :height="128" + :langType="$i18n.locale" img-format="png" :noCircle="true" :noSquare="false" + @crop-success="cropSuccess" /> <!-- Title --> <Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" /> @@ -162,14 +168,20 @@ {{ $t("Edit Status Page") }} </button> - <a href="/manage-status-page" class="btn btn-info"> - <font-awesome-icon icon="tachometer-alt" /> + <a href="/manage-status-page" class="btn btn-info me-2"> + <font-awesome-icon icon="tag" /> {{ $t("Go to Dashboard") }} </a> + + <a href='#' @click="$refs.badgeBuilderDialog.show(slug)" class="btn btn-info"> + <font-awesome-icon icon="tag" /> + {{ $t("Generate Badge") }} + </a> </div> <div v-else> - <button class="btn btn-primary btn-add-group me-2" data-testid="create-incident-button" @click="createIncident"> + <button class="btn btn-primary btn-add-group me-2" data-testid="create-incident-button" + @click="createIncident"> <font-awesome-icon icon="bullhorn" /> {{ $t("Create Incident") }} </button> @@ -177,28 +189,35 @@ </div> <!-- Incident --> - <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass" data-testid="incident"> + <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass" + data-testid="incident"> <strong v-if="editIncidentMode">{{ $t("Title") }}:</strong> - <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" data-testid="incident-title" /> + <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" + class="alert-heading" data-testid="incident-title" /> <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong> - <Editable v-if="editIncidentMode" v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" data-testid="incident-content-editable" /> + <Editable v-if="editIncidentMode" v-model="incident.content" tag="div" + :contenteditable="editIncidentMode" class="content" data-testid="incident-content-editable" /> <div v-if="editIncidentMode" class="form-text"> {{ $t("markdownSupported") }} </div> <!-- eslint-disable-next-line vue/no-v-html--> - <div v-if="! editIncidentMode" class="content" data-testid="incident-content" v-html="incidentHTML"></div> + <div v-if="!editIncidentMode" class="content" data-testid="incident-content" v-html="incidentHTML"> + </div> <!-- Incident Date --> <div class="date mt-3"> - {{ $t("Date Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br /> + {{ $t("Date Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ + dateFromNow(incident.createdDate) }})<br /> <span v-if="incident.lastUpdatedDate"> - {{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }}) + {{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ + dateFromNow(incident.lastUpdatedDate) }}) </span> </div> <div v-if="editMode" class="mt-3"> - <button v-if="editIncidentMode" class="btn btn-light me-2" data-testid="post-incident-button" @click="postIncident"> + <button v-if="editIncidentMode" class="btn btn-light me-2" data-testid="post-incident-button" + @click="postIncident"> <font-awesome-icon icon="bullhorn" /> {{ $t("Post") }} </button> @@ -214,16 +233,23 @@ </button> <div v-if="editIncidentMode" class="dropdown d-inline-block me-2"> - <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> + <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" + data-bs-toggle="dropdown" aria-expanded="false"> {{ $t("Style") }}: {{ $t(incident.style) }} </button> <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1"> - <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li> + <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a> + </li> + <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") + }}</a></li> + <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") + }}</a></li> + <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") + }}</a></li> + <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") + }}</a></li> + <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a> + </li> </ul> </div> @@ -270,10 +296,8 @@ <!-- Maintenance --> <template v-if="maintenanceList.length > 0"> - <div - v-for="maintenance in maintenanceList" :key="maintenance.id" - class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert" - > + <div v-for="maintenance in maintenanceList" :key="maintenance.id" + class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"> <h4 class="alert-heading">{{ maintenance.title }}</h4> <!-- eslint-disable-next-line vue/no-v-html--> <div class="content" v-html="maintenanceHTML(maintenance.description)"></div> @@ -283,9 +307,11 @@ <!-- Description --> <strong v-if="editMode">{{ $t("Description") }}:</strong> - <Editable v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" data-testid="description-editable" /> + <Editable v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div" + class="mb-4 description" data-testid="description-editable" /> <!-- eslint-disable-next-line vue/no-v-html--> - <div v-if="! enableEditMode" class="alert-heading p-2" data-testid="description" v-html="descriptionHTML"></div> + <div v-if="!enableEditMode" class="alert-heading p-2" data-testid="description" v-html="descriptionHTML"> + </div> <div v-if="editMode" class="mb-4"> <div> @@ -298,26 +324,20 @@ <div class="mt-3"> <div v-if="sortedMonitorList.length > 0 && loadedData"> <label>{{ $t("Add a monitor") }}:</label> - <VueMultiselect - v-model="selectedMonitor" - :options="sortedMonitorList" - :multiple="false" - :searchable="true" - :placeholder="$t('Add a monitor')" - label="name" - trackBy="name" - class="mt-3" - data-testid="monitor-select" - > + <VueMultiselect v-model="selectedMonitor" :options="sortedMonitorList" :multiple="false" + :searchable="true" :placeholder="$t('Add a monitor')" label="name" trackBy="name" + class="mt-3" data-testid="monitor-select"> <template #option="{ option }"> <div class="d-inline-flex"> - <span>{{ option.pathName }} <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" /></span> + <span>{{ option.pathName }} + <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" /> + </span> </div> </template> </VueMultiselect> </div> <div v-else class="text-center"> - {{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link> + {{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link> </div> </div> </div> @@ -328,29 +348,35 @@ 👀 {{ $t("statusPageNothing") }} </div> - <PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" :show-certificate-expiry="config.showCertificateExpiry" /> + <PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" + :show-certificate-expiry="config.showCertificateExpiry" /> </div> <footer class="mt-5 mb-4"> <div class="custom-footer-text text-start"> <strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong> </div> - <Editable v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" data-testid="custom-footer-editable" /> + <Editable v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode" + :noNL="false" class="alert-heading p-2" data-testid="custom-footer-editable" /> <!-- eslint-disable-next-line vue/no-v-html--> - <div v-if="! enableEditMode" class="alert-heading p-2" data-testid="footer-text" v-html="footerHTML"></div> + <div v-if="!enableEditMode" class="alert-heading p-2" data-testid="footer-text" v-html="footerHTML"> + </div> <p v-if="config.showPoweredBy" data-testid="powered-by"> - {{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a> + {{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer" + href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma") }}</a> </p> <div class="refresh-info mb-2"> - <div>{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}</div> - <div data-testid="update-countdown-text">{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}</div> + <div>{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}</div> + <div data-testid="update-countdown-text">{{ $tc("statusPageRefreshIn", [updateCountdownText]) }} + </div> </div> </footer> </div> - <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage"> + <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" + @yes="deleteStatusPage"> {{ $t("deleteStatusPageMsg") }} </Confirm> @@ -358,6 +384,8 @@ {{ config.customCSS }} </component> </div> + + <BadgeBuilderDialog ref="badgeBuilderDialog" /> </template> <script> @@ -383,6 +411,7 @@ import { getResBaseURL } from "../util-frontend"; import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts"; import Tag from "../components/Tag.vue"; import VueMultiselect from "vue-multiselect"; +import BadgeBuilderDialog from "../components/BadgeBuilderDialog.vue"; const toast = useToast(); dayjs.extend(duration); @@ -405,6 +434,7 @@ export default { PrismEditor, MaintenanceTime, Tag, + BadgeBuilderDialog, VueMultiselect }, @@ -471,7 +501,7 @@ export default { let result = []; for (let id in this.$root.monitorList) { - if (this.$root.monitorList[id] && ! (id in this.$root.publicMonitorList)) { + if (this.$root.monitorList[id] && !(id in this.$root.publicMonitorList)) { let monitor = this.$root.monitorList[id]; result.push(monitor); } @@ -555,7 +585,7 @@ export default { } } - if (! hasUp) { + if (!hasUp) { status = STATUS_PAGE_ALL_DOWN; } @@ -723,7 +753,7 @@ export default { }, (this.config.autoRefreshInterval + 10) * 1000); this.updateUpdateTimer(); - }).catch( function (error) { + }).catch(function (error) { if (error.response.status === 404) { location.href = "/page-not-found"; } @@ -770,7 +800,7 @@ export default { */ updateHeartbeatList() { // If editMode, it will use the data from websocket. - if (! this.editMode) { + if (!this.editMode) { axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => { const { heartbeatList, uptimeList } = res.data; @@ -1267,5 +1297,4 @@ footer { .refresh-info { opacity: 0.7; } - </style> From a9e59fded62bc7b37589e3305cc18e810d0b642f Mon Sep 17 00:00:00 2001 From: Hiago Silva Souza <hiasilva@gmail.com> Date: Sat, 25 Jan 2025 00:36:59 -0300 Subject: [PATCH 2/5] fix: lint problems --- server/model/status_page.js | 81 ++++++++--- server/routers/status-page-router.js | 4 +- src/components/BadgeBuilderDialog.vue | 70 ++++----- src/pages/StatusPage.vue | 195 +++++++++++++++++--------- 4 files changed, 228 insertions(+), 122 deletions(-) diff --git a/server/model/status_page.js b/server/model/status_page.js index 08a69154c..66a226879 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -63,19 +63,19 @@ class StatusPage extends BeanModel { } /** - * Handle responses to status page - * @param {Response} response Response object - * @param {string} slug Status page slug - * @param {object} config Config for badge - * @returns {Promise<void>} + * Generate SVG Status + * @param {response} response express response + * @param {slug} slug from router + * @param {config} config config for the svg + * @returns {Promise<string>} returns a svg file */ static async handleStatusPageSVGResponse(response, slug, config) { - slug = slug.replace('.svg', '') + slug = slug.replace(".svg", ""); let statusPage = await R.findOne("status_page", " slug = ? ", [ - slug.replace('.svg', '') + slug.replace(".svg", "") ]); if (statusPage) { - response.set('Content-Type', 'image/svg+xml') + response.set("Content-Type", "image/svg+xml"); response.send(await StatusPage.renderSVG(slug, config, statusPage)); } else { response.status(404).send(await StatusPage.renderSVG(slug, config, null)); @@ -173,15 +173,28 @@ class StatusPage extends BeanModel { return $.root().html(); } + /** + * render SVG file + * @param {slug} slug from router + * @param {userConfig} userConfig generated by user + * @param {statusPage} statusPage object + * @returns {Promise<string>} a SVG string + */ static async renderSVG(slug, userConfig = {}, statusPage) { - let allowedBadgeStyles = ['flat', 'flat-square', 'for-the-badge', 'plastic', 'social']; + let allowedBadgeStyles = [ "flat", "flat-square", "for-the-badge", "plastic", "social" ]; if (!allowedBadgeStyles.includes(userConfig.badgeStyle)) { - userConfig.badgeStyle = 'flat' + userConfig.badgeStyle = "flat"; } // returns unknown svg if (statusPage === null) { - return makeBadge({ message: userConfig.notFoundText || 'not found', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }) + return makeBadge({ + message: userConfig.notFoundText || "not found", + color: "red", + style: userConfig.badgeStyle, + label: slug, + labelColor: "gray" + }); } else { // get all heartbeats that correspond to this statusPage @@ -199,7 +212,7 @@ class StatusPage extends BeanModel { for (let groupBean of list) { let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry); for (const monitor of monitorGroup.monitorList) { - const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]); + const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]); if (heartbeat) { heartbeats.push({ ...monitor, @@ -215,15 +228,45 @@ class StatusPage extends BeanModel { switch (status) { case STATUS_PAGE_ALL_DOWN: - return makeBadge({ message: userConfig.allDownText ?? 'all down', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + return makeBadge({ + message: userConfig.allDownText ?? "all down", + color: "red", + style: userConfig.badgeStyle, + label: slug, + labelColor: "gray" + }); case STATUS_PAGE_ALL_UP: - return makeBadge({ message: userConfig.allOperationalText ?? 'all operational', color: 'green', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + return makeBadge({ + message: userConfig.allOperationalText ?? "all operational", + color: "green", + style: userConfig.badgeStyle, + label: slug, + labelColor: "gray" + }); case STATUS_PAGE_PARTIAL_DOWN: - return makeBadge({ message: userConfig.partialDownText ?? 'partial down', color: 'orange', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + return makeBadge({ + message: userConfig.partialDownText ?? "partial down", + color: "orange", + style: userConfig.badgeStyle, + label: slug, + labelColor: "gray" + }); case STATUS_PAGE_MAINTENANCE: - return makeBadge({ message: userConfig.maintenanceText || 'maintenance', color: 'yellow', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + return makeBadge({ + message: userConfig.maintenanceText || "maintenance", + color: "yellow", + style: userConfig.badgeStyle, + label: slug, + labelColor: "gray" + }); default: - return makeBadge({ message: userConfig.unknownText || 'unknown', color: 'lightgray', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + return makeBadge({ + message: userConfig.unknownText || "unknown", + color: "lightgray", + style: userConfig.badgeStyle, + label: slug, + labelColor: "gray" + }); } } @@ -308,7 +351,7 @@ class StatusPage extends BeanModel { for (let groupBean of list) { let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry); for (const monitor of monitorGroup.monitorList) { - const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]); + const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]); if (heartbeat) { heartbeats.push({ ...monitor, @@ -548,7 +591,7 @@ class StatusPage extends BeanModel { SELECT DISTINCT maintenance_id FROM maintenance_status_page WHERE status_page_id = ? - `, [statusPageId]); + `, [ statusPageId ]); for (const maintenanceID of maintenanceIDList) { let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID); diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 9c5b88101..c97b12d44 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -17,8 +17,8 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => { let slug = request.params.slug; slug = slug.toLowerCase(); - if (slug.endsWith('.svg')) { - await StatusPage.handleStatusPageSVGResponse(response, slug, JSON.parse(request.query.config || '{}')); + if (slug.endsWith(".svg")) { + await StatusPage.handleStatusPageSVGResponse(response, slug, JSON.parse(request.query.config || "{}")); } else { await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); } diff --git a/src/components/BadgeBuilderDialog.vue b/src/components/BadgeBuilderDialog.vue index 8b4f9cc79..5e79a1f6f 100644 --- a/src/components/BadgeBuilderDialog.vue +++ b/src/components/BadgeBuilderDialog.vue @@ -23,50 +23,56 @@ <div class="mb-3"> <label for="badge-not-found-text" class="form-label">{{ $t("Return to not found") }}</label> - <input id="badge-not-found-text" v-model="config.notFoundText" type="text" - class="form-control"> + <input + id="badge-not-found-text" v-model="config.notFoundText" type="text" + class="form-control" + > </div> <div class="mb-3"> <label for="badge-operational-text" class="form-label">{{ $t("Return to operational") - }}</label> - <input id="badge-operational-text" v-model="config.allOperationalText" type="text" - class="form-control"> + }}</label> + <input + id="badge-operational-text" v-model="config.allOperationalText" type="text" + class="form-control" + > </div> - <div class="mb-3"> <label for="badge-partial-down-text" class="form-label">{{ $t("Return to partial down") - }}</label> - <input id="badge-partial-down-text" v-model="config.partialDownText" type="text" - class="form-control"> + }}</label> + <input + id="badge-partial-down-text" v-model="config.partialDownText" type="text" + class="form-control" + > </div> <div class="mb-3"> <label for="badge-maintenance-text" class="form-label">{{ $t("Return to maintenance") - }}</label> - <input id="badge-maintenance-text" v-model="config.maintenanceText" type="text" - class="form-control"> + }}</label> + <input + id="badge-maintenance-text" v-model="config.maintenanceText" type="text" + class="form-control" + > </div> <div class="mb-3"> <label for="badge-unknown-text" class="form-label">{{ $t("Return to unknown") - }}</label> - <input id="badge-unknown-text" v-model="config.unknownText" type="text" - class="form-control"> + }}</label> + <input + id="badge-unknown-text" v-model="config.unknownText" type="text" + class="form-control" + > </div> - <div class="Preview" v-if="slug"> + <div v-if="slug" class="Preview"> <div>{{ $t("Preview") }}</div> <img :src="getSvgURL(true)" class="mt-2" /> </div> - </div> - - <div class="modal-footer"> - <button type="button" @click="copyImageURL()" class="btn btn-primary"> + <button type="button" class="btn btn-primary" @click="copyImageURL()"> <!-- <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div> --> {{ $t("Copy Image URL") }} </button> @@ -75,13 +81,12 @@ </div> </div> </form> - </template> <script> import { Modal } from "bootstrap"; import { useToast } from "vue-toastification"; -import axios from 'axios'; +import axios from "axios"; const toast = useToast(); export default { @@ -93,13 +98,13 @@ export default { model: null, slug: null, config: { - badgeStyle: 'flat', - notFoundText: this.$t('not found'), - allDownText: this.$t('all down'), - allOperationalText: this.$t('operational'), - partialDownText: this.$t('partial down'), - maintenanceText: this.$t('maintenance'), - unknownText: this.$t('unknown') + badgeStyle: "flat", + notFoundText: this.$t("not found"), + allDownText: this.$t("all down"), + allOperationalText: this.$t("operational"), + partialDownText: this.$t("partial down"), + maintenanceText: this.$t("maintenance"), + unknownText: this.$t("unknown") } }; }, @@ -114,18 +119,13 @@ export default { this.modal = new Modal(this.$refs.modal); }, methods: { - /** - * Show settings for specified notification - * @param {number} notificationID ID of notification to show - * @returns {void} - */ show(slug) { this.slug = slug; this.modal.show(); }, getSvgURL(disableCache = false) { - return axios.defaults.baseURL + '/status/' + this.slug + '.svg?config=' + encodeURIComponent(JSON.stringify(this.config)).replace(/%5B/g, '[').replace(/%5D/g, ']') + (disableCache ? '&noCache=' + Date.now() : ''); + return axios.defaults.baseURL + "/status/" + this.slug + ".svg?config=" + encodeURIComponent(JSON.stringify(this.config)).replace(/%5B/g, "[").replace(/%5D/g, "]") + (disableCache ? "&noCache=" + Date.now() : ""); }, copyImageURL() { diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index b8d585f39..601f9e869 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -19,8 +19,10 @@ <!-- Description --> <div class="my-3"> <label for="description" class="form-label">{{ $t("Description") }}</label> - <textarea id="description" v-model="config.description" class="form-control" - data-testid="description-input"></textarea> + <textarea + id="description" v-model="config.description" class="form-control" + data-testid="description-input" + ></textarea> <div class="form-text"> {{ $t("markdownSupported") }} </div> @@ -29,8 +31,10 @@ <!-- Footer Text --> <div class="my-3"> <label for="footer-text" class="form-label">{{ $t("Footer Text") }}</label> - <textarea id="footer-text" v-model="config.footerText" class="form-control" - data-testid="footer-text-input"></textarea> + <textarea + id="footer-text" v-model="config.footerText" class="form-control" + data-testid="footer-text-input" + ></textarea> <div class="form-text"> {{ $t("markdownSupported") }} </div> @@ -38,8 +42,10 @@ <div class="my-3"> <label for="auto-refresh-interval" class="form-label">{{ $t("Refresh Interval") }}</label> - <input id="auto-refresh-interval" v-model="config.autoRefreshInterval" type="number" - class="form-control" :min="5" data-testid="refresh-interval-input"> + <input + id="auto-refresh-interval" v-model="config.autoRefreshInterval" type="number" + class="form-control" :min="5" data-testid="refresh-interval-input" + > <div class="form-text"> {{ $t("Refresh Interval Description", [config.autoRefreshInterval]) }} </div> @@ -55,49 +61,63 @@ </div> <div class="my-3 form-check form-switch"> - <input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox" - data-testid="show-tags-checkbox"> + <input + id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox" + data-testid="show-tags-checkbox" + > <label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label> </div> <!-- Show Powered By --> <div class="my-3 form-check form-switch"> - <input id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox" - data-testid="show-powered-by-checkbox"> + <input + id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox" + data-testid="show-powered-by-checkbox" + > <label class="form-check-label" for="show-powered-by">{{ $t("Show Powered By") }}</label> </div> <!-- Show certificate expiry --> <div class="my-3 form-check form-switch"> - <input id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input" - type="checkbox" data-testid="show-certificate-expiry-checkbox"> + <input + id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input" + type="checkbox" data-testid="show-certificate-expiry-checkbox" + > <label class="form-check-label" for="show-certificate-expiry">{{ $t("showCertificateExpiry") - }}</label> + }}</label> </div> <div v-if="false" class="my-3"> <label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon") - }}</sup></label> - <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" - class="form-control"> + }}</sup></label> + <input + id="password" v-model="config.password" disabled type="password" autocomplete="new-password" + class="form-control" + > </div> <!-- Domain Name List --> <div class="my-3"> <label class="form-label"> {{ $t("Domain Names") }} - <button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" - @click="addDomainField"> + <button + class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" + @click="addDomainField" + > <font-awesome-icon icon="plus-circle" class="action text-primary" /> </button> </label> <ul class="list-group domain-name-list"> <li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item"> - <input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" - placeholder="example.com" /> - <button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [domain])" - @click="removeDomain(index)"> + <input + v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" + placeholder="example.com" + /> + <button + class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [domain])" + @click="removeDomain(index)" + > <font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" /> </button> </li> @@ -107,15 +127,19 @@ <!-- Google Analytics --> <div class="my-3"> <label for="googleAnalyticsTag" class="form-label">{{ $t("Google Analytics ID") }}</label> - <input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control" - data-testid="google-analytics-input"> + <input + id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control" + data-testid="google-analytics-input" + > </div> <!-- Custom CSS --> <div class="my-3"> <div class="mb-1">{{ $t("Custom CSS") }}</div> - <prism-editor v-model="config.customCSS" class="css-editor" data-testid="custom-css-input" - :highlight="highlighter" line-numbers></prism-editor> + <prism-editor + v-model="config.customCSS" class="css-editor" data-testid="custom-css-input" + :highlight="highlighter" line-numbers + ></prism-editor> </div> <div class="danger-zone"> @@ -152,9 +176,11 @@ <!-- Uploader --> <!-- url="/api/status-page/upload-logo" --> - <ImageCropUpload v-model="showImageCropUpload" field="img" :width="128" :height="128" + <ImageCropUpload + v-model="showImageCropUpload" field="img" :width="128" :height="128" :langType="$i18n.locale" img-format="png" :noCircle="true" :noSquare="false" - @crop-success="cropSuccess" /> + @crop-success="cropSuccess" + /> <!-- Title --> <Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" /> @@ -173,15 +199,17 @@ {{ $t("Go to Dashboard") }} </a> - <a href='#' @click="$refs.badgeBuilderDialog.show(slug)" class="btn btn-info"> + <a href="#" class="btn btn-info" @click="$refs.badgeBuilderDialog.show(slug)"> <font-awesome-icon icon="tag" /> {{ $t("Generate Badge") }} </a> </div> <div v-else> - <button class="btn btn-primary btn-add-group me-2" data-testid="create-incident-button" - @click="createIncident"> + <button + class="btn btn-primary btn-add-group me-2" data-testid="create-incident-button" + @click="createIncident" + > <font-awesome-icon icon="bullhorn" /> {{ $t("Create Incident") }} </button> @@ -189,15 +217,21 @@ </div> <!-- Incident --> - <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass" - data-testid="incident"> + <div + v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass" + data-testid="incident" + > <strong v-if="editIncidentMode">{{ $t("Title") }}:</strong> - <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" - class="alert-heading" data-testid="incident-title" /> + <Editable + v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" + class="alert-heading" data-testid="incident-title" + /> <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong> - <Editable v-if="editIncidentMode" v-model="incident.content" tag="div" - :contenteditable="editIncidentMode" class="content" data-testid="incident-content-editable" /> + <Editable + v-if="editIncidentMode" v-model="incident.content" tag="div" + :contenteditable="editIncidentMode" class="content" data-testid="incident-content-editable" + /> <div v-if="editIncidentMode" class="form-text"> {{ $t("markdownSupported") }} </div> @@ -216,8 +250,10 @@ </div> <div v-if="editMode" class="mt-3"> - <button v-if="editIncidentMode" class="btn btn-light me-2" data-testid="post-incident-button" - @click="postIncident"> + <button + v-if="editIncidentMode" class="btn btn-light me-2" data-testid="post-incident-button" + @click="postIncident" + > <font-awesome-icon icon="bullhorn" /> {{ $t("Post") }} </button> @@ -233,22 +269,34 @@ </button> <div v-if="editIncidentMode" class="dropdown d-inline-block me-2"> - <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" - data-bs-toggle="dropdown" aria-expanded="false"> + <button + id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" + data-bs-toggle="dropdown" aria-expanded="false" + > {{ $t("Style") }}: {{ $t(incident.style) }} </button> <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1"> - <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a> + <li> + <a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a> </li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") - }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") - }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") - }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") - }}</a></li> - <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a> + <li> + <a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") + }}</a> + </li> + <li> + <a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") + }}</a> + </li> + <li> + <a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") + }}</a> + </li> + <li> + <a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") + }}</a> + </li> + <li> + <a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a> </li> </ul> </div> @@ -296,8 +344,10 @@ <!-- Maintenance --> <template v-if="maintenanceList.length > 0"> - <div v-for="maintenance in maintenanceList" :key="maintenance.id" - class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"> + <div + v-for="maintenance in maintenanceList" :key="maintenance.id" + class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert" + > <h4 class="alert-heading">{{ maintenance.title }}</h4> <!-- eslint-disable-next-line vue/no-v-html--> <div class="content" v-html="maintenanceHTML(maintenance.description)"></div> @@ -307,8 +357,10 @@ <!-- Description --> <strong v-if="editMode">{{ $t("Description") }}:</strong> - <Editable v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div" - class="mb-4 description" data-testid="description-editable" /> + <Editable + v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div" + class="mb-4 description" data-testid="description-editable" + /> <!-- eslint-disable-next-line vue/no-v-html--> <div v-if="!enableEditMode" class="alert-heading p-2" data-testid="description" v-html="descriptionHTML"> </div> @@ -324,9 +376,11 @@ <div class="mt-3"> <div v-if="sortedMonitorList.length > 0 && loadedData"> <label>{{ $t("Add a monitor") }}:</label> - <VueMultiselect v-model="selectedMonitor" :options="sortedMonitorList" :multiple="false" + <VueMultiselect + v-model="selectedMonitor" :options="sortedMonitorList" :multiple="false" :searchable="true" :placeholder="$t('Add a monitor')" label="name" trackBy="name" - class="mt-3" data-testid="monitor-select"> + class="mt-3" data-testid="monitor-select" + > <template #option="{ option }"> <div class="d-inline-flex"> <span>{{ option.pathName }} @@ -348,35 +402,44 @@ 👀 {{ $t("statusPageNothing") }} </div> - <PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" - :show-certificate-expiry="config.showCertificateExpiry" /> + <PublicGroupList + :edit-mode="enableEditMode" :show-tags="config.showTags" + :show-certificate-expiry="config.showCertificateExpiry" + /> </div> <footer class="mt-5 mb-4"> <div class="custom-footer-text text-start"> <strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong> </div> - <Editable v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode" - :noNL="false" class="alert-heading p-2" data-testid="custom-footer-editable" /> + <Editable + v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode" + :noNL="false" class="alert-heading p-2" data-testid="custom-footer-editable" + /> <!-- eslint-disable-next-line vue/no-v-html--> <div v-if="!enableEditMode" class="alert-heading p-2" data-testid="footer-text" v-html="footerHTML"> </div> <p v-if="config.showPoweredBy" data-testid="powered-by"> - {{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer" - href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma") }}</a> + {{ $t("Powered by") }} <a + target="_blank" rel="noopener noreferrer" + href="https://github.com/louislam/uptime-kuma" + >{{ $t("Uptime Kuma") }}</a> </p> <div class="refresh-info mb-2"> <div>{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}</div> - <div data-testid="update-countdown-text">{{ $tc("statusPageRefreshIn", [updateCountdownText]) }} + <div data-testid="update-countdown-text"> + {{ $tc("statusPageRefreshIn", [updateCountdownText]) }} </div> </div> </footer> </div> - <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" - @yes="deleteStatusPage"> + <Confirm + ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" + @yes="deleteStatusPage" + > {{ $t("deleteStatusPageMsg") }} </Confirm> From 4fb371bd262798798ae4abed05813629cf0105ae Mon Sep 17 00:00:00 2001 From: Hiago Silva Souza <hiasilva@gmail.com> Date: Sat, 25 Jan 2025 00:40:56 -0300 Subject: [PATCH 3/5] fix: lint problems --- src/components/BadgeBuilderDialog.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/BadgeBuilderDialog.vue b/src/components/BadgeBuilderDialog.vue index 5e79a1f6f..4b70ec512 100644 --- a/src/components/BadgeBuilderDialog.vue +++ b/src/components/BadgeBuilderDialog.vue @@ -144,7 +144,6 @@ export default { @import "../assets/vars.scss"; .dark { - .modal-dialog .form-text, .modal-dialog p { color: $dark-font-color; From 56ae3f086844924977582cc4c188c6f522c13f40 Mon Sep 17 00:00:00 2001 From: Hiago Silva Souza <hiasilva@gmail.com> Date: Sat, 25 Jan 2025 00:46:42 -0300 Subject: [PATCH 4/5] fix: lang files formatation --- src/lang/en.json | 2 +- src/lang/pt-BR.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/en.json b/src/lang/en.json index 711d8b1eb..eecacbf99 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1067,4 +1067,4 @@ "Copy Image URL": "Copy Image URL", "Image url copied to clipboard.": "Image url copied to clipboard.", "Unable to copy image URL.": "Unable to copy image URL." -} \ No newline at end of file +} diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index 8d8682502..ab4d705c2 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -1010,4 +1010,4 @@ "Copy Image URL": "Copiar URL da imagem", "Image url copied to clipboard.": "URL da imagem copiada para área de transferência.", "Unable to copy image URL.": "Não é possível copiar o URL da imagem para área de transferência." -} \ No newline at end of file +} From c4f4548a40a89103b70a63d573c3fc0f1dd9bed6 Mon Sep 17 00:00:00 2001 From: Hiago Silva Souza <hiasilva@gmail.com> Date: Sat, 25 Jan 2025 07:53:37 -0300 Subject: [PATCH 5/5] fix: revert file to last commit to follow guide lines --- src/lang/pt-BR.json | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index ab4d705c2..8319c656d 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -994,20 +994,5 @@ "aboutSlackUsername": "Altera o nome de exibição do remetente da mensagem. Se quiser mencionar alguém, inclua a menção no nome amigável.", "Send rich messages": "Enviar mensagens ricas", "Host Onesender": "Servidor Onesender", - "Token Onesender": "Chave Onesender", - "Generate Badge": "Gerar Distintivo", - "Return to not found": "Retorno para não encontrado", - "Return to operational": "Retorno para operacional", - "Return to partial down": "Retorno para parcialmente degradado", - "Return to maintenance": "Retorno para em manutenção", - "Return to unknown": "Retorno para desconhecido", - "not found": "não encontrado", - "operational": "operacional", - "partial down": "parcialmente fora", - "maintenance": "manutenção", - "unknown": "desconhecido", - "Preview": "Pré-visualizar", - "Copy Image URL": "Copiar URL da imagem", - "Image url copied to clipboard.": "URL da imagem copiada para área de transferência.", - "Unable to copy image URL.": "Não é possível copiar o URL da imagem para área de transferência." + "Token Onesender": "Chave Onesender" }