• StatusPage.vue
  • <template>
        <div v-if="loadedTheme" class="container mt-3">
            <!-- Sidebar for edit mode -->
            <div v-if="enableEditMode" class="sidebar" data-testid="edit-sidebar">
                <div class="sidebar-body">
                    <div class="my-3">
                        <label for="slug" class="form-label">{{ $t("Slug") }}</label>
                        <div class="input-group">
                            <span id="basic-addon3" class="input-group-text">/status/</span>
                            <input id="slug" v-model="config.slug" type="text" class="form-control">
                        </div>
                    </div>
    
                    <div class="my-3">
                        <label for="title" class="form-label">{{ $t("Title") }}</label>
                        <input id="title" v-model="config.title" type="text" class="form-control">
                    </div>
    
                    <!-- 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>
                        <div class="form-text">
                            {{ $t("markdownSupported") }}
                        </div>
                    </div>
    
                    <!-- 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>
                        <div class="form-text">
                            {{ $t("markdownSupported") }}
                        </div>
                    </div>
    
                    <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">
                        <div class="form-text">
                            {{ $t("Refresh Interval Description", [config.autoRefreshInterval]) }}
                        </div>
                    </div>
    
                    <div class="my-3">
                        <label for="switch-theme" class="form-label">{{ $t("Theme") }}</label>
                        <select id="switch-theme" v-model="config.theme" class="form-select" data-testid="theme-select">
                            <option value="auto">{{ $t("Auto") }}</option>
                            <option value="light">{{ $t("Light") }}</option>
                            <option value="dark">{{ $t("Dark") }}</option>
                        </select>
                    </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">
                        <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">
                        <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>
                    </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">
                    </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">
                                <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)">
                                    <font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
                                </button>
                            </li>
                        </ul>
                    </div>
    
                    <!-- 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">
                    </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>
                    </div>
    
                    <div class="danger-zone">
                        <button class="btn btn-danger me-2" @click="deleteDialog">
                            <font-awesome-icon icon="trash" />
                            {{ $t("Delete") }}
                        </button>
                    </div>
                </div>
    
                <!-- Sidebar Footer -->
                <div class="sidebar-footer">
                    <button class="btn btn-success me-2" :disabled="loading" data-testid="save-button" @click="save">
                        <font-awesome-icon icon="save" />
                        {{ $t("Save") }}
                    </button>
    
                    <button class="btn btn-danger me-2" @click="discard">
                        <font-awesome-icon icon="undo" />
                        {{ $t("Discard") }}
                    </button>
                </div>
            </div>
    
            <!-- Main Status Page -->
            <div :class="{ edit: enableEditMode}" class="main">
                <!-- Logo & Title -->
                <h1 class="mb-4 title-flex">
                    <!-- Logo -->
                    <span class="logo-wrapper" @click="showImageCropUploadMethod">
                        <img :src="logoURL" alt class="logo me-2" :class="logoClass" />
                        <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
                    </span>
    
                    <!-- 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"
                    />
    
                    <!-- Title -->
                    <Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
                </h1>
    
                <!-- Admin functions -->
                <div v-if="hasToken" class="mb-4">
                    <div v-if="!enableEditMode">
                        <button class="btn btn-primary me-2" data-testid="edit-button" @click="edit">
                            <font-awesome-icon icon="edit" />
                            {{ $t("Edit Status Page") }}
                        </button>
    
                        <a href="/manage-status-page" class="btn btn-primary">
                            <font-awesome-icon icon="tachometer-alt" />
                            {{ $t("Go to Dashboard") }}
                        </a>
                    </div>
    
                    <div v-else>
                        <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>
                    </div>
                </div>
    
                <!-- 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" />
    
                    <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" />
                    <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>
    
                    <!-- Incident Date -->
                    <div class="date mt-3">
                        {{ $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) }})
                        </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">
                            <font-awesome-icon icon="bullhorn" />
                            {{ $t("Post") }}
                        </button>
    
                        <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
                            <font-awesome-icon icon="edit" />
                            {{ $t("Edit") }}
                        </button>
    
                        <button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
                            <font-awesome-icon icon="times" />
                            {{ $t("Cancel") }}
                        </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">
                                {{ $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>
                            </ul>
                        </div>
    
                        <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
                            <font-awesome-icon icon="unlink" />
                            {{ $t("Delete") }}
                        </button>
                    </div>
                </div>
    
                <!-- Overall Status -->
                <div class="shadow-box list  p-4 overall-status mb-4">
                    <div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
                        <font-awesome-icon icon="question-circle" class="ok" />
                        {{ $t("No Services") }}
                    </div>
    
                    <template v-else>
                        <div v-if="allUp">
                            <font-awesome-icon icon="check-circle" class="ok" />
                            {{ $t("All Systems Operational") }}
                        </div>
    
                        <div v-else-if="partialDown">
                            <font-awesome-icon icon="exclamation-circle" class="warning" />
                            {{ $t("Partially Degraded Service") }}
                        </div>
    
                        <div v-else-if="allDown">
                            <font-awesome-icon icon="times-circle" class="danger" />
                            {{ $t("Degraded Service") }}
                        </div>
    
                        <div v-else-if="isMaintenance">
                            <font-awesome-icon icon="wrench" class="status-maintenance" />
                            {{ $t("maintenanceStatus-under-maintenance") }}
                        </div>
    
                        <div v-else>
                            <font-awesome-icon icon="question-circle" style="color: #efefef;" />
                        </div>
                    </template>
                </div>
    
                <!-- 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"
                    >
                        <h4 class="alert-heading">{{ maintenance.title }}</h4>
                        <!-- eslint-disable-next-line vue/no-v-html-->
                        <div class="content" v-html="maintenanceHTML(maintenance.description)"></div>
                        <MaintenanceTime :maintenance="maintenance" />
                    </div>
                </template>
    
                <!-- 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" />
                <!-- 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="editMode" class="mb-4">
                    <div>
                        <button class="btn btn-primary btn-add-group me-2" data-testid="add-group-button" @click="addGroup">
                            <font-awesome-icon icon="plus" />
                            {{ $t("Add Group") }}
                        </button>
                    </div>
    
                    <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"
                            >
                                <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>
                                    </div>
                                </template>
                            </VueMultiselect>
                        </div>
                        <div v-else class="text-center">
                            {{ $t("No monitors available.") }}  <router-link to="/add">{{ $t("Add one") }}</router-link>
                        </div>
                    </div>
                </div>
    
                <div class="mb-4">
                    <div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
                        <!-- 👀 Nothing here, please add a group or a monitor. -->
                        👀 {{ $t("statusPageNothing") }}
                    </div>
    
                    <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" />
                    <!-- 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>
                    </p>
    
                    <div class="refresh-info mb-2">
                        <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">
                {{ $t("deleteStatusPageMsg") }}
            </Confirm>
    
            <component is="style" v-if="config.customCSS" type="text/css">
                {{ config.customCSS }}
            </component>
        </div>
    </template>
    
    <script>
    import axios from "axios";
    import dayjs from "dayjs";
    import duration from "dayjs/plugin/duration";
    import Favico from "favico.js";
    // import highlighting library (you can use any library you want just return html string)
    import { highlight, languages } from "prismjs/components/prism-core";
    import "prismjs/components/prism-css";
    import "prismjs/themes/prism-tomorrow.css"; // import syntax highlighting styles
    import ImageCropUpload from "vue-image-crop-upload";
    // import Prism Editor
    import { PrismEditor } from "vue-prism-editor";
    import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhere
    import { useToast } from "vue-toastification";
    import { marked } from "marked";
    import DOMPurify from "dompurify";
    import Confirm from "../components/Confirm.vue";
    import PublicGroupList from "../components/PublicGroupList.vue";
    import MaintenanceTime from "../components/MaintenanceTime.vue";
    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";
    
    const toast = useToast();
    dayjs.extend(duration);
    
    const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
    
    // eslint-disable-next-line no-unused-vars
    let feedInterval;
    
    const favicon = new Favico({
        animation: "none"
    });
    
    export default {
    
        components: {
            PublicGroupList,
            ImageCropUpload,
            Confirm,
            PrismEditor,
            MaintenanceTime,
            Tag,
            VueMultiselect
        },
    
        // Leave Page for vue route change
        beforeRouteLeave(to, from, next) {
            if (this.editMode) {
                const answer = window.confirm(leavePageMsg);
                if (answer) {
                    next();
                } else {
                    next(false);
                }
            }
            next();
        },
    
        props: {
            /** Override for the status page slug */
            overrideSlug: {
                type: String,
                required: false,
                default: null,
            },
        },
    
        data() {
            return {
                slug: null,
                enableEditMode: false,
                enableEditIncidentMode: false,
                hasToken: false,
                config: {},
                selectedMonitor: null,
                incident: null,
                previousIncident: null,
                showImageCropUpload: false,
                imgDataUrl: "/icon.svg",
                loadedTheme: false,
                loadedData: false,
                baseURL: "",
                clickedEditButton: false,
                maintenanceList: [],
                lastUpdateTime: dayjs(),
                updateCountdown: null,
                updateCountdownText: null,
                loading: true,
            };
        },
        computed: {
    
            logoURL() {
                if (this.imgDataUrl.startsWith("data:")) {
                    return this.imgDataUrl;
                } else {
                    return this.baseURL + this.imgDataUrl;
                }
            },
    
            /**
             * If the monitor is added to public list, which will not be in this list.
             * @returns {object[]} List of monitors
             */
            sortedMonitorList() {
                let result = [];
    
                for (let id in this.$root.monitorList) {
                    if (this.$root.monitorList[id] && ! (id in this.$root.publicMonitorList)) {
                        let monitor = this.$root.monitorList[id];
                        result.push(monitor);
                    }
                }
    
                result.sort((m1, m2) => {
    
                    if (m1.active !== m2.active) {
                        if (m1.active === 0) {
                            return 1;
                        }
    
                        if (m2.active === 0) {
                            return -1;
                        }
                    }
    
                    if (m1.weight !== m2.weight) {
                        if (m1.weight > m2.weight) {
                            return -1;
                        }
    
                        if (m1.weight < m2.weight) {
                            return 1;
                        }
                    }
    
                    return m1.pathName.localeCompare(m2.pathName);
                });
    
                return result;
            },
    
            editMode() {
                return this.enableEditMode && this.$root.socket.connected;
            },
    
            editIncidentMode() {
                return this.enableEditIncidentMode;
            },
    
            isPublished() {
                return this.config.published;
            },
    
            logoClass() {
                if (this.editMode) {
                    return {
                        "edit-mode": true,
                    };
                }
                return {};
            },
    
            incidentClass() {
                return "bg-" + this.incident.style;
            },
    
            maintenanceClass() {
                return "bg-maintenance";
            },
    
            overallStatus() {
    
                if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
                    return -1;
                }
    
                let status = STATUS_PAGE_ALL_UP;
                let hasUp = false;
    
                for (let id in this.$root.publicLastHeartbeatList) {
                    let beat = this.$root.publicLastHeartbeatList[id];
    
                    if (beat.status === MAINTENANCE) {
                        return STATUS_PAGE_MAINTENANCE;
                    } else if (beat.status === UP) {
                        hasUp = true;
                    } else {
                        status = STATUS_PAGE_PARTIAL_DOWN;
                    }
                }
    
                if (! hasUp) {
                    status = STATUS_PAGE_ALL_DOWN;
                }
    
                return status;
            },
    
            allUp() {
                return this.overallStatus === STATUS_PAGE_ALL_UP;
            },
    
            partialDown() {
                return this.overallStatus === STATUS_PAGE_PARTIAL_DOWN;
            },
    
            allDown() {
                return this.overallStatus === STATUS_PAGE_ALL_DOWN;
            },
    
            isMaintenance() {
                return this.overallStatus === STATUS_PAGE_MAINTENANCE;
            },
    
            incidentHTML() {
                if (this.incident.content != null) {
                    return DOMPurify.sanitize(marked(this.incident.content));
                } else {
                    return "";
                }
            },
    
            descriptionHTML() {
                if (this.config.description != null) {
                    return DOMPurify.sanitize(marked(this.config.description));
                } else {
                    return "";
                }
            },
    
            footerHTML() {
                if (this.config.footerText != null) {
                    return DOMPurify.sanitize(marked(this.config.footerText));
                } else {
                    return "";
                }
            },
    
            lastUpdateTimeDisplay() {
                return this.$root.datetime(this.lastUpdateTime);
            }
        },
        watch: {
    
            /**
             * If connected to the socket and logged in, request private data of this statusPage
             * @param {boolean} loggedIn Is the client logged in?
             * @returns {void}
             */
            "$root.loggedIn"(loggedIn) {
                if (loggedIn) {
                    this.$root.getSocket().emit("getStatusPage", this.slug, (res) => {
                        if (res.ok) {
                            this.config = res.config;
    
                            if (!this.config.customCSS) {
                                this.config.customCSS = "body {\n" +
                                    "  \n" +
                                    "}\n";
                            }
    
                        } else {
                            this.$root.toastError(res.msg);
                        }
                    });
                }
            },
    
            /**
             * Selected a monitor and add to the list.
             * @param {object} monitor Monitor to add
             * @returns {void}
             */
            selectedMonitor(monitor) {
                if (monitor) {
                    if (this.$root.publicGroupList.length === 0) {
                        this.addGroup();
                    }
    
                    const firstGroup = this.$root.publicGroupList[0];
    
                    firstGroup.monitorList.push(monitor);
                    this.selectedMonitor = null;
                }
            },
    
            // Set Theme
            "config.theme"() {
                this.$root.statusPageTheme = this.config.theme;
                this.loadedTheme = true;
            },
    
            "config.title"(title) {
                document.title = title;
            },
    
            "$root.monitorList"() {
                let count = Object.keys(this.$root.monitorList).length;
    
                // Since publicGroupList is getting from public rest api, monitors' tags may not present if showTags = false
                if (count > 0) {
                    for (let group of this.$root.publicGroupList) {
                        for (let monitor of group.monitorList) {
                            if (monitor.tags === undefined && this.$root.monitorList[monitor.id]) {
                                monitor.tags = this.$root.monitorList[monitor.id].tags;
                            }
                        }
                    }
                }
            }
    
        },
        async created() {
            this.hasToken = ("token" in this.$root.storage());
    
            // Browser change page
            // https://stackoverflow.com/questions/7317273/warn-user-before-leaving-web-page-with-unsaved-changes
            window.addEventListener("beforeunload", (e) => {
                if (this.editMode) {
                    (e || window.event).returnValue = leavePageMsg;
                    return leavePageMsg;
                } else {
                    return null;
                }
            });
    
            // Special handle for dev
            this.baseURL = getResBaseURL();
        },
        async mounted() {
            this.slug = this.overrideSlug || this.$route.params.slug;
    
            if (!this.slug) {
                this.slug = "default";
            }
    
            this.getData().then((res) => {
                this.config = res.data.config;
    
                if (!this.config.domainNameList) {
                    this.config.domainNameList = [];
                }
    
                if (this.config.icon) {
                    this.imgDataUrl = this.config.icon;
                }
    
                this.incident = res.data.incident;
                this.maintenanceList = res.data.maintenanceList;
                this.$root.publicGroupList = res.data.publicGroupList;
    
                this.loading = false;
    
                // Configure auto-refresh loop
                feedInterval = setInterval(() => {
                    this.updateHeartbeatList();
                }, (this.config.autoRefreshInterval + 10) * 1000);
    
                this.updateUpdateTimer();
            }).catch( function (error) {
                if (error.response.status === 404) {
                    location.href = "/page-not-found";
                }
                console.log(error);
            });
    
            this.updateHeartbeatList();
    
            // Go to edit page if ?edit present
            // null means ?edit present, but no value
            if (this.$route.query.edit || this.$route.query.edit === null) {
                this.edit();
            }
        },
        methods: {
    
            /**
             * Get status page data
             * It should be preloaded in window.preloadData
             * @returns {Promise<any>} Status page data
             */
            getData: function () {
                if (window.preloadData) {
                    return new Promise(resolve => resolve({
                        data: window.preloadData
                    }));
                } else {
                    return axios.get("/api/status-page/" + this.slug);
                }
            },
    
            /**
             * Provide syntax highlighting for CSS
             * @param {string} code Text to highlight
             * @returns {string} Highlighted CSS
             */
            highlighter(code) {
                return highlight(code, languages.css);
            },
    
            /**
             * Update the heartbeat list and update favicon if necessary
             * @returns {void}
             */
            updateHeartbeatList() {
                // If editMode, it will use the data from websocket.
                if (! this.editMode) {
                    axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
                        const { heartbeatList, uptimeList } = res.data;
    
                        this.$root.heartbeatList = heartbeatList;
                        this.$root.uptimeList = uptimeList;
    
                        const heartbeatIds = Object.keys(heartbeatList);
                        const downMonitors = heartbeatIds.reduce((downMonitorsAmount, currentId) => {
                            const monitorHeartbeats = heartbeatList[currentId];
                            const lastHeartbeat = monitorHeartbeats.at(-1);
    
                            if (lastHeartbeat) {
                                return lastHeartbeat.status === 0 ? downMonitorsAmount + 1 : downMonitorsAmount;
                            } else {
                                return downMonitorsAmount;
                            }
                        }, 0);
    
                        favicon.badge(downMonitors);
    
                        this.loadedData = true;
                        this.lastUpdateTime = dayjs();
                        this.updateUpdateTimer();
                    });
                }
            },
    
            /**
             * Setup timer to display countdown to refresh
             * @returns {void}
             */
            updateUpdateTimer() {
                clearInterval(this.updateCountdown);
    
                this.updateCountdown = setInterval(() => {
                    const countdown = dayjs.duration(this.lastUpdateTime.add(this.config.autoRefreshInterval, "seconds").add(10, "seconds").diff(dayjs()));
                    if (countdown.as("seconds") < 0) {
                        clearInterval(this.updateCountdown);
                    } else {
                        this.updateCountdownText = countdown.format("mm:ss");
                    }
                }, 1000);
            },
    
            /**
             * Enable editing mode
             * @returns {void}
             */
            edit() {
                if (this.hasToken) {
                    this.$root.initSocketIO(true);
                    this.enableEditMode = true;
                    this.clickedEditButton = true;
    
                    // Try to fix #1658
                    this.loadedData = true;
                }
            },
    
            /**
             * Save the status page
             * @returns {void}
             */
            save() {
                this.loading = true;
                let startTime = new Date();
                this.config.slug = this.config.slug.trim().toLowerCase();
    
                this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
                    if (res.ok) {
                        this.enableEditMode = false;
                        this.$root.publicGroupList = res.publicGroupList;
    
                        // Add some delay, so that the side menu animation would be better
                        let endTime = new Date();
                        let time = 100 - (endTime - startTime) / 1000;
    
                        if (time < 0) {
                            time = 0;
                        }
    
                        setTimeout(() => {
                            this.loading = false;
                            location.href = "/status/" + this.config.slug;
                        }, time);
    
                    } else {
                        this.loading = false;
                        toast.error(res.msg);
                    }
                });
            },
    
            /**
             * Show dialog confirming deletion
             * @returns {void}
             */
            deleteDialog() {
                this.$refs.confirmDelete.show();
            },
    
            /**
             * Request deletion of this status page
             * @returns {void}
             */
            deleteStatusPage() {
                this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
                    if (res.ok) {
                        this.enableEditMode = false;
                        location.href = "/manage-status-page";
                    } else {
                        this.$root.toastError(res.msg);
                    }
                });
            },
    
            /**
             * Returns label for a specified monitor
             * @param {object} monitor Object representing monitor
             * @returns {string} Monitor label
             */
            monitorSelectorLabel(monitor) {
                return `${monitor.name}`;
            },
    
            /**
             * Add a group to the status page
             * @returns {void}
             */
            addGroup() {
                let groupName = this.$t("Untitled Group");
    
                if (this.$root.publicGroupList.length === 0) {
                    groupName = this.$t("Services");
                }
    
                this.$root.publicGroupList.unshift({
                    name: groupName,
                    monitorList: [],
                });
            },
    
            /**
             * Add a domain to the status page
             * @returns {void}
             */
            addDomainField() {
                this.config.domainNameList.push("");
            },
    
            /**
             * Discard changes to status page
             * @returns {void}
             */
            discard() {
                location.href = "/status/" + this.slug;
            },
    
            /**
             * Set URL of new image after successful crop operation
             * @param {string} imgDataUrl URL of image in data:// format
             * @returns {void}
             */
            cropSuccess(imgDataUrl) {
                this.imgDataUrl = imgDataUrl;
            },
    
            /**
             * Show image crop dialog if in edit mode
             * @returns {void}
             */
            showImageCropUploadMethod() {
                if (this.editMode) {
                    this.showImageCropUpload = true;
                }
            },
    
            /**
             * Create an incident for this status page
             * @returns {void}
             */
            createIncident() {
                this.enableEditIncidentMode = true;
    
                if (this.incident) {
                    this.previousIncident = this.incident;
                }
    
                this.incident = {
                    title: "",
                    content: "",
                    style: "primary",
                };
            },
    
            /**
             * Post the incident to the status page
             * @returns {void}
             */
            postIncident() {
                if (this.incident.title === "" || this.incident.content === "") {
                    this.$root.toastError("Please input title and content");
                    return;
                }
    
                this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
    
                    if (res.ok) {
                        this.enableEditIncidentMode = false;
                        this.incident = res.incident;
                    } else {
                        this.$root.toastError(res.msg);
                    }
    
                });
    
            },
    
            /**
             * Click Edit Button
             * @returns {void}
             */
            editIncident() {
                this.enableEditIncidentMode = true;
                this.previousIncident = Object.assign({}, this.incident);
            },
    
            /**
             * Cancel creation or editing of incident
             * @returns {void}
             */
            cancelIncident() {
                this.enableEditIncidentMode = false;
    
                if (this.previousIncident) {
                    this.incident = this.previousIncident;
                    this.previousIncident = null;
                }
            },
    
            /**
             * Unpin the incident
             * @returns {void}
             */
            unpinIncident() {
                this.$root.getSocket().emit("unpinIncident", this.slug, () => {
                    this.incident = null;
                });
            },
    
            /**
             * Get the relative time difference of a date from now
             * @param {any} date Date to get time difference
             * @returns {string} Time difference
             */
            dateFromNow(date) {
                return dayjs.utc(date).fromNow();
            },
    
            /**
             * Remove a domain from the status page
             * @param {number} index Index of domain to remove
             * @returns {void}
             */
            removeDomain(index) {
                this.config.domainNameList.splice(index, 1);
            },
    
            /**
             * Generate sanitized HTML from maintenance description
             * @param {string} description Text to sanitize
             * @returns {string} Sanitized HTML
             */
            maintenanceHTML(description) {
                if (description) {
                    return DOMPurify.sanitize(marked(description));
                } else {
                    return "";
                }
            },
    
        }
    };
    </script>
    
    <style lang="scss" scoped>
    @import "../assets/vars.scss";
    
    .overall-status {
        font-weight: bold;
        font-size: 25px;
    
        .ok {
            color: $primary;
        }
    
        .warning {
            color: $warning;
        }
    
        .danger {
            color: $danger;
        }
    }
    
    h1 {
        font-size: 30px;
    
        img {
            vertical-align: middle;
            height: 60px;
            width: 60px;
        }
    }
    
    .main {
        transition: all ease-in-out 0.1s;
    
        &.edit {
            margin-left: 300px;
        }
    }
    
    .sidebar {
        position: fixed;
        left: 0;
        top: 0;
        width: 300px;
        height: 100vh;
    
        border-right: 1px solid #ededed;
    
        .danger-zone {
            border-top: 1px solid #ededed;
            padding-top: 15px;
        }
    
        .sidebar-body {
            padding: 0 10px 10px 10px;
            overflow-x: hidden;
            overflow-y: auto;
            height: calc(100% - 70px);
        }
    
        .sidebar-footer {
            border-top: 1px solid #ededed;
            border-right: 1px solid #ededed;
            padding: 10px;
            width: 300px;
            height: 70px;
            position: fixed;
            left: 0;
            bottom: 0;
            background-color: white;
            display: flex;
            align-items: center;
        }
    }
    
    footer {
        text-align: center;
        font-size: 14px;
    }
    
    .description span {
        min-width: 50px;
    }
    
    .title-flex {
        display: flex;
        align-items: center;
        gap: 10px;
    }
    
    .logo-wrapper {
        display: inline-block;
        position: relative;
    
        &:hover {
            .icon-upload {
                transform: scale(1.2);
            }
        }
    
        .icon-upload {
            transition: all $easing-in 0.2s;
            position: absolute;
            bottom: 6px;
            font-size: 20px;
            left: -14px;
            background-color: white;
            padding: 5px;
            border-radius: 10px;
            cursor: pointer;
            box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9);
        }
    }
    
    .logo {
        transition: all $easing-in 0.2s;
    
        &.edit-mode {
            cursor: pointer;
    
            &:hover {
                transform: scale(1.2);
            }
        }
    }
    
    .incident {
        .content {
            &[contenteditable="true"] {
                min-height: 60px;
            }
        }
    
        .date {
            font-size: 12px;
        }
    }
    
    .maintenance-bg-info {
        color: $maintenance;
    }
    
    .maintenance-icon {
        font-size: 35px;
        vertical-align: middle;
    }
    
    .dark .shadow-box {
        background-color: #0d1117;
    }
    
    .status-maintenance {
        color: $maintenance;
        margin-right: 5px;
    }
    
    .mobile {
        h1 {
            font-size: 22px;
        }
    
        .overall-status {
            font-size: 20px;
        }
    }
    
    .dark {
        .sidebar {
            background-color: $dark-header-bg;
            border-right-color: $dark-border-color;
    
            .danger-zone {
                border-top-color: $dark-border-color;
            }
    
            .sidebar-footer {
                border-right-color: $dark-border-color;
                border-top-color: $dark-border-color;
                background-color: $dark-header-bg;
            }
        }
    }
    
    .domain-name-list {
        li {
            display: flex;
            align-items: center;
            padding: 10px 0 10px 10px;
    
            .domain-input {
                flex-grow: 1;
                background-color: transparent;
                border: none;
                color: $dark-font-color;
                outline: none;
    
                &::placeholder {
                    color: #1d2634;
                }
            }
        }
    }
    
    .bg-maintenance {
        .alert-heading {
            font-weight: bold;
        }
    }
    
    .refresh-info {
        opacity: 0.7;
    }
    
    </style>