From 7cc97834369627c4889021d174915ca1d8365082 Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakflying@hotmail.com>
Date: Mon, 26 Jun 2023 13:21:51 +0800
Subject: [PATCH 1/5] Fix: Active needs to return bool instead of 0

---
 server/model/monitor.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/model/monitor.js b/server/model/monitor.js
index 2dfe2e65..017e6813 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -155,7 +155,7 @@ class Monitor extends BeanModel {
     async isActive() {
         const parentActive = await Monitor.isParentActive(this.id);
 
-        return this.active && parentActive;
+        return (this.active === 1) && parentActive;
     }
 
     /**

From 79b38e0e7bf7247da499e7f1266d3e674bfafd55 Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakflying@hotmail.com>
Date: Mon, 26 Jun 2023 13:23:06 +0800
Subject: [PATCH 2/5] Feat: Improve monitorList filtering

---
 src/components/MonitorList.vue               |  90 ++++--
 src/components/MonitorListFilter.vue         | 283 +++++++++++++++++++
 src/components/MonitorListFilterDropdown.vue | 131 +++++++++
 src/mixins/socket.js                         |   5 +-
 4 files changed, 490 insertions(+), 19 deletions(-)
 create mode 100644 src/components/MonitorListFilter.vue
 create mode 100644 src/components/MonitorListFilterDropdown.vue

diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index c69169cc..9ee46e24 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -1,17 +1,25 @@
 <template>
     <div class="shadow-box mb-3" :style="boxStyle">
         <div class="list-header">
-            <div class="placeholder"></div>
-            <div class="search-wrapper">
-                <a v-if="searchText == ''" class="search-icon">
-                    <font-awesome-icon icon="search" />
-                </a>
-                <a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
-                    <font-awesome-icon icon="times" />
-                </a>
-                <form>
-                    <input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
-                </form>
+            <div class="header-top">
+                <div class="placeholder"></div>
+                <div class="search-wrapper">
+                    <a v-if="searchText == ''" class="search-icon">
+                        <font-awesome-icon icon="search" />
+                    </a>
+                    <a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
+                        <font-awesome-icon icon="times" />
+                    </a>
+                    <form>
+                        <input
+                            v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
+                            autocomplete="off"
+                        />
+                    </form>
+                </div>
+            </div>
+            <div class="header-filter">
+                <MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
             </div>
         </div>
         <div class="monitor-list" :class="{ scrollbar: scrollbar }">
@@ -19,18 +27,23 @@
                 {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
             </div>
 
-            <MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" />
+            <MonitorListItem
+                v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item"
+                :isSearch="searchText !== ''"
+            />
         </div>
     </div>
 </template>
 
 <script>
 import MonitorListItem from "../components/MonitorListItem.vue";
+import MonitorListFilter from "./MonitorListFilter.vue";
 import { getMonitorRelativeURL } from "../util.ts";
 
 export default {
     components: {
         MonitorListItem,
+        MonitorListFilter,
     },
     props: {
         /** Should the scrollbar be shown */
@@ -42,6 +55,11 @@ export default {
         return {
             searchText: "",
             windowTop: 0,
+            filterState: {
+                status: null,
+                active: null,
+                tags: null,
+            }
         };
     },
     computed: {
@@ -72,8 +90,8 @@ export default {
                 const loweredSearchText = this.searchText.toLowerCase();
                 result = result.filter(monitor => {
                     return monitor.name.toLowerCase().includes(loweredSearchText)
-                    || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
-                    || tag.value?.toLowerCase().includes(loweredSearchText));
+                        || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
+                            || tag.value?.toLowerCase().includes(loweredSearchText));
                 });
             } else {
                 result = result.filter(monitor => monitor.parent === null);
@@ -105,6 +123,27 @@ export default {
                 return m1.name.localeCompare(m2.name);
             });
 
+            if (this.filterState.status != null && this.filterState.status.length > 0) {
+                result.map(monitor => {
+                    if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
+                        monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
+                    }
+                });
+                result = result.filter(monitor => this.filterState.status.includes(monitor.status));
+            }
+
+            if (this.filterState.active != null && this.filterState.active.length > 0) {
+                result = result.filter(monitor => this.filterState.active.includes(monitor.active));
+            }
+
+            if (this.filterState.tags != null && this.filterState.tags.length > 0) {
+                result = result.filter(monitor => {
+                    return monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
+                        .filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
+                        .length > 0;
+                });
+            }
+
             return result;
         },
     },
@@ -134,7 +173,14 @@ export default {
         /** Clear the search bar */
         clearSearchText() {
             this.searchText = "";
-        }
+        },
+        /**
+         * Update the MonitorList Filter
+         * @param {object} newFilter Object with new filter
+         */
+        updateFilter(newFilter) {
+            this.filterState = newFilter;
+        },
     },
 };
 </script>
@@ -159,8 +205,6 @@ export default {
     margin: -10px;
     margin-bottom: 10px;
     padding: 10px;
-    display: flex;
-    justify-content: space-between;
 
     .dark & {
         background-color: $dark-header-bg;
@@ -168,6 +212,17 @@ export default {
     }
 }
 
+.header-top {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.header-filter {
+    display: flex;
+    align-items: center;
+}
+
 @media (max-width: 770px) {
     .list-header {
         margin: -20px;
@@ -216,5 +271,4 @@ export default {
     padding-left: 67px;
     margin-top: 5px;
 }
-
 </style>
diff --git a/src/components/MonitorListFilter.vue b/src/components/MonitorListFilter.vue
new file mode 100644
index 00000000..022f7d91
--- /dev/null
+++ b/src/components/MonitorListFilter.vue
@@ -0,0 +1,283 @@
+<template>
+    <div class="px-2 pt-2 d-flex">
+        <button
+            type="button"
+            :title="$t('Clear current filters')"
+            class="clear-filters-btn btn"
+            :class="{ 'active': numFiltersActive > 0}"
+            tabindex="0"
+            @click="clearFilters"
+        >
+            <font-awesome-icon icon="stream" />
+            <span v-if="numFiltersActive > 0" class="px-1 fw-bold">{{ numFiltersActive }}</span>
+            <font-awesome-icon v-if="numFiltersActive > 0" icon="times" />
+        </button>
+        <MonitorListFilterDropdown
+            :filterActive="filterState.status?.length > 0"
+        >
+            <template #status>
+                <Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" />
+                <span v-else>
+                    {{ $t('Status') }}
+                </span>
+            </template>
+            <template #dropdown>
+                <li>
+                    <div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(1)">
+                        <div class="d-flex align-items-center justify-content-between">
+                            <Status :status="1" />
+                            <span class="ps-3">
+                                {{ $root.stats.up }}
+                                <span v-if="filterState.status?.includes(1)" class="px-1 filter-active">
+                                    <font-awesome-icon icon="check" />
+                                </span>
+                            </span>
+                        </div>
+                    </div>
+                </li>
+                <li>
+                    <div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(0)">
+                        <div class="d-flex align-items-center justify-content-between">
+                            <Status :status="0" />
+                            <span class="ps-3">
+                                {{ $root.stats.down }}
+                                <span v-if="filterState.status?.includes(0)" class="px-1 filter-active">
+                                    <font-awesome-icon icon="check" />
+                                </span>
+                            </span>
+                        </div>
+                    </div>
+                </li>
+                <li>
+                    <div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(2)">
+                        <div class="d-flex align-items-center justify-content-between">
+                            <Status :status="2" />
+                            <span class="ps-3">
+                                {{ $root.stats.pending }}
+                                <span v-if="filterState.status?.includes(2)" class="px-1 filter-active">
+                                    <font-awesome-icon icon="check" />
+                                </span>
+                            </span>
+                        </div>
+                    </div>
+                </li>
+                <li>
+                    <div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(3)">
+                        <div class="d-flex align-items-center justify-content-between">
+                            <Status :status="3" />
+                            <span class="ps-3">
+                                {{ $root.stats.maintenance }}
+                                <span v-if="filterState.status?.includes(3)" class="px-1 filter-active">
+                                    <font-awesome-icon icon="check" />
+                                </span>
+                            </span>
+                        </div>
+                    </div>
+                </li>
+            </template>
+        </MonitorListFilterDropdown>
+        <MonitorListFilterDropdown :filterActive="filterState.active?.length > 0">
+            <template #status>
+                <span v-if="filterState.active?.length === 1">
+                    <span v-if="filterState.active[0]">{{ $t("Running") }}</span>
+                    <span v-else>{{ $t("Paused") }}</span>
+                </span>
+                <span v-else>
+                    {{ $t('Active') }}
+                </span>
+            </template>
+            <template #dropdown>
+                <li>
+                    <div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(true)">
+                        <div class="d-flex align-items-center justify-content-between">
+                            <span>{{ $t("Running") }}</span>
+                            <span class="ps-3">
+                                {{ $root.stats.active }}
+                                <span v-if="filterState.active?.includes(true)" class="px-1 filter-active">
+                                    <font-awesome-icon icon="check" />
+                                </span>
+                            </span>
+                        </div>
+                    </div>
+                </li>
+                <li>
+                    <div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(false)">
+                        <div class="d-flex align-items-center justify-content-between">
+                            <span>{{ $t("Paused") }}</span>
+                            <span class="ps-3">
+                                {{ $root.stats.pause }}
+                                <span v-if="filterState.active?.includes(false)" class="px-1 filter-active">
+                                    <font-awesome-icon icon="check" />
+                                </span>
+                            </span>
+                        </div>
+                    </div>
+                </li>
+            </template>
+        </MonitorListFilterDropdown>
+        <MonitorListFilterDropdown :filterActive="filterState.tags?.length > 0">
+            <template #status>
+                <Tag
+                    v-if="filterState.tags?.length === 1"
+                    :item="tagsList.find(tag => tag.id === filterState.tags[0])"
+                    :size="'sm'"
+                />
+                <span v-else>
+                    {{ $t('Tags') }}
+                </span>
+            </template>
+            <template #dropdown>
+                <li v-for="tag in tagsList" :key="tag.id">
+                    <div class="dropdown-item" tabindex="0" @click.stop="toggleTagFilter(tag)">
+                        <div class="d-flex align-items-center justify-content-between">
+                            <span><Tag :item="tag" :size="'sm'" /></span>
+                            <span class="ps-3">
+                                {{ getTaggedMonitorCount(tag) }}
+                                <span v-if="filterState.tags?.includes(tag.id)" class="px-1 filter-active">
+                                    <font-awesome-icon icon="check" />
+                                </span>
+                            </span>
+                        </div>
+                    </div>
+                </li>
+            </template>
+        </MonitorListFilterDropdown>
+    </div>
+</template>
+
+<script>
+import MonitorListFilterDropdown from "./MonitorListFilterDropdown.vue";
+import Status from "./Status.vue";
+import Tag from "./Tag.vue";
+
+export default {
+    components: {
+        MonitorListFilterDropdown,
+        Status,
+        Tag,
+    },
+    props: {
+        filterState: {
+            type: Object,
+            required: true,
+        }
+    },
+    emits: [ "updateFilter" ],
+    data() {
+        return {
+            tagsList: [],
+        };
+    },
+    computed: {
+        numFiltersActive() {
+            let num = 0;
+
+            Object.values(this.filterState).forEach(item => {
+                if (item != null && item.length > 0) {
+                    num += 1;
+                }
+            });
+
+            return num;
+        }
+    },
+    mounted() {
+        this.getExistingTags();
+    },
+    methods: {
+        toggleStatusFilter(status) {
+            let newFilter = {
+                ...this.filterState
+            };
+
+            if (newFilter.status == null) {
+                newFilter.status = [ status ];
+            } else {
+                if (newFilter.status.includes(status)) {
+                    newFilter.status = newFilter.status.filter(item => item !== status);
+                } else {
+                    newFilter.status.push(status);
+                }
+            }
+            this.$emit("updateFilter", newFilter);
+        },
+        toggleActiveFilter(active) {
+            let newFilter = {
+                ...this.filterState
+            };
+
+            if (newFilter.active == null) {
+                newFilter.active = [ active ];
+            } else {
+                if (newFilter.active.includes(active)) {
+                    newFilter.active = newFilter.active.filter(item => item !== active);
+                } else {
+                    newFilter.active.push(active);
+                }
+            }
+            this.$emit("updateFilter", newFilter);
+        },
+        toggleTagFilter(tag) {
+            let newFilter = {
+                ...this.filterState
+            };
+
+            if (newFilter.tags == null) {
+                newFilter.tags = [ tag.id ];
+            } else {
+                if (newFilter.tags.includes(tag.id)) {
+                    newFilter.tags = newFilter.tags.filter(item => item !== tag.id);
+                } else {
+                    newFilter.tags.push(tag.id);
+                }
+            }
+            this.$emit("updateFilter", newFilter);
+        },
+        clearFilters() {
+            this.$emit("updateFilter", {
+                status: null,
+            });
+        },
+        getExistingTags() {
+            this.$root.getSocket().emit("getTags", (res) => {
+                if (res.ok) {
+                    this.tagsList = res.tags;
+                }
+            });
+        },
+        getTaggedMonitorCount(tag) {
+            return Object.values(this.$root.monitorList).filter(monitor => {
+                return monitor.tags.find(monitorTag => monitorTag.tag_id === tag.id);
+            }).length;
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../assets/vars.scss";
+
+.clear-filters-btn {
+    font-size: 0.8em;
+    margin-right: 5px;
+    display: flex;
+    align-items: center;
+    padding: 2px 10px;
+    border-radius: 16px;
+    background-color: transparent;
+
+    .dark & {
+        color: $dark-font-color;
+        border: 1px solid $dark-font-color2;
+    }
+
+    &.active {
+        border: 1px solid $highlight;
+        background-color: $highlight-white;
+
+        .dark & {
+            background-color: $dark-font-color2;
+        }
+    }
+}
+</style>
diff --git a/src/components/MonitorListFilterDropdown.vue b/src/components/MonitorListFilterDropdown.vue
new file mode 100644
index 00000000..adf3d6b3
--- /dev/null
+++ b/src/components/MonitorListFilterDropdown.vue
@@ -0,0 +1,131 @@
+<template>
+    <div class="dropdown" @focusin="open = true" @focusout="handleFocusOut">
+        <button type="button" class="filter-dropdown-status" :class="{ 'active': filterActive }" tabindex="0">
+            <div class="px-1 d-flex align-items-center">
+                <slot name="status"></slot>
+            </div>
+            <span class="px-1">
+                <font-awesome-icon icon="angle-down" />
+            </span>
+        </button>
+        <ul class="filter-dropdown-menu" :class="{ 'open': open }">
+            <slot name="dropdown"></slot>
+        </ul>
+    </div>
+</template>
+
+<script>
+
+export default {
+    components: {
+
+    },
+    props: {
+        filterActive: {
+            type: Boolean,
+            required: true,
+        }
+    },
+    data() {
+        return {
+            open: false
+        };
+    },
+    methods: {
+        handleFocusOut(e) {
+            if (e.relatedTarget != null && this.$el.contains(e.relatedTarget)) {
+                return;
+            }
+            this.open = false;
+        }
+    }
+};
+</script>
+
+<style lang="scss">
+@import "../assets/vars.scss";
+
+.filter-dropdown-menu {
+    z-index: 100;
+    transition: all 0.2s;
+    padding: 5px 0px !important;
+    border-radius: 16px;
+    overflow: hidden;
+
+    position: absolute;
+    inset: 0px auto auto 0px;
+    margin: 0px;
+    transform: translate(0px, 36px);
+    box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
+    visibility: hidden;
+    list-style: none;
+    height: 0;
+    opacity: 0;
+    background: white;
+
+    &.open {
+        height: unset;
+        visibility: inherit;
+        opacity: 1;
+    }
+
+    .dropdown-item {
+        padding: 5px 15px;
+    }
+
+    .dropdown-item:focus {
+        background: $highlight-white;
+
+        .dark & {
+            background: $dark-bg2;
+        }
+    }
+
+    .dark & {
+        background-color: $dark-bg;
+        color: $dark-font-color;
+        border-color: $dark-border-color;
+
+        .dropdown-item {
+            color: $dark-font-color;
+
+            &.active {
+                color: $dark-font-color2;
+                background-color: $highlight !important;
+            }
+
+            &:hover {
+                background-color: $dark-bg2;
+            }
+        }
+    }
+}
+
+.filter-dropdown-status {
+    display: flex;
+    align-items: center;
+    padding: 4px 10px;
+    margin-left: 5px;
+    border: 1px solid #ced4da;
+    border-radius: 25px;
+    background-color: transparent;
+
+    .dark & {
+        color: $dark-font-color;
+        border: 1px solid $dark-font-color2;
+    }
+
+    &.active {
+        border: 1px solid $highlight;
+        background-color: $highlight-white;
+
+        .dark & {
+            background-color: $dark-font-color2;
+        }
+    }
+}
+
+.filter-active {
+    color: $highlight;
+}
+</style>
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index e2834251..8e078b10 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -693,9 +693,11 @@ export default {
 
         stats() {
             let result = {
+                active: 0,
                 up: 0,
                 down: 0,
                 maintenance: 0,
+                pending: 0,
                 unknown: 0,
                 pause: 0,
             };
@@ -707,12 +709,13 @@ export default {
                 if (monitor && ! monitor.active) {
                     result.pause++;
                 } else if (beat) {
+                    result.active++;
                     if (beat.status === UP) {
                         result.up++;
                     } else if (beat.status === DOWN) {
                         result.down++;
                     } else if (beat.status === PENDING) {
-                        result.up++;
+                        result.pending++;
                     } else if (beat.status === MAINTENANCE) {
                         result.maintenance++;
                     } else {

From cea894cc6d7dcba0ab599f0b85bf5dccb85d899c Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakflying@hotmail.com>
Date: Mon, 26 Jun 2023 13:39:19 +0800
Subject: [PATCH 3/5] Chore: Fix lint

---
 src/components/MonitorListFilterDropdown.vue | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/components/MonitorListFilterDropdown.vue b/src/components/MonitorListFilterDropdown.vue
index adf3d6b3..01b9678f 100644
--- a/src/components/MonitorListFilterDropdown.vue
+++ b/src/components/MonitorListFilterDropdown.vue
@@ -48,14 +48,14 @@ export default {
 .filter-dropdown-menu {
     z-index: 100;
     transition: all 0.2s;
-    padding: 5px 0px !important;
+    padding: 5px 0 !important;
     border-radius: 16px;
     overflow: hidden;
 
     position: absolute;
-    inset: 0px auto auto 0px;
-    margin: 0px;
-    transform: translate(0px, 36px);
+    inset: 0 auto auto 0;
+    margin: 0;
+    transform: translate(0, 36px);
     box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
     visibility: hidden;
     list-style: none;

From f8c9a20afdaae76b3d2307c296f5631beddde27b Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakflying@hotmail.com>
Date: Mon, 26 Jun 2023 13:42:46 +0800
Subject: [PATCH 4/5] Chore: Disable clear filters button

---
 src/components/MonitorListFilter.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/MonitorListFilter.vue b/src/components/MonitorListFilter.vue
index 022f7d91..2dcd062f 100644
--- a/src/components/MonitorListFilter.vue
+++ b/src/components/MonitorListFilter.vue
@@ -6,6 +6,7 @@
             class="clear-filters-btn btn"
             :class="{ 'active': numFiltersActive > 0}"
             tabindex="0"
+            :disabled="numFiltersActive === 0"
             @click="clearFilters"
         >
             <font-awesome-icon icon="stream" />

From e2a87eb4300d67c6911775ca24962475fbe1580a Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 16 Jul 2023 21:15:25 +0800
Subject: [PATCH 5/5] Improve the filter translate keys

---
 src/components/MonitorListFilter.vue | 6 +++---
 src/lang/en.json                     | 2 ++
 src/lang/zh-HK.json                  | 2 ++
 3 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/components/MonitorListFilter.vue b/src/components/MonitorListFilter.vue
index 2dcd062f..dbb1eb94 100644
--- a/src/components/MonitorListFilter.vue
+++ b/src/components/MonitorListFilter.vue
@@ -81,10 +81,10 @@
             <template #status>
                 <span v-if="filterState.active?.length === 1">
                     <span v-if="filterState.active[0]">{{ $t("Running") }}</span>
-                    <span v-else>{{ $t("Paused") }}</span>
+                    <span v-else>{{ $t("filterActivePaused") }}</span>
                 </span>
                 <span v-else>
-                    {{ $t('Active') }}
+                    {{ $t("filterActive") }}
                 </span>
             </template>
             <template #dropdown>
@@ -104,7 +104,7 @@
                 <li>
                     <div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(false)">
                         <div class="d-flex align-items-center justify-content-between">
-                            <span>{{ $t("Paused") }}</span>
+                            <span>{{ $t("filterActivePaused") }}</span>
                             <span class="ps-3">
                                 {{ $root.stats.pause }}
                                 <span v-if="filterState.active?.includes(false)" class="px-1 filter-active">
diff --git a/src/lang/en.json b/src/lang/en.json
index bc4f9ce3..ceafe06c 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -155,6 +155,8 @@
     "Disable 2FA": "Disable 2FA",
     "2FA Settings": "2FA Settings",
     "Two Factor Authentication": "Two Factor Authentication",
+    "filterActive": "Active",
+    "filterActivePaused": "Paused",
     "Active": "Active",
     "Inactive": "Inactive",
     "Token": "Token",
diff --git a/src/lang/zh-HK.json b/src/lang/zh-HK.json
index fd5d35e3..aa43caa5 100644
--- a/src/lang/zh-HK.json
+++ b/src/lang/zh-HK.json
@@ -139,6 +139,8 @@
     "Disable 2FA": "關閉 2FA",
     "2FA Settings": "2FA 設定",
     "Two Factor Authentication": "雙重認證",
+    "filterActive": "執行狀態",
+    "filterActivePaused": "已暫停",
     "Active": "生效",
     "Inactive": "未生效",
     "Token": "Token",