Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 80 additions & 21 deletions web/app/src/components/SearchBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
type="text"
placeholder="Search endpoints..."
class="pl-10 text-sm sm:text-base"
@input="$emit('search', searchQuery)"
@input="handleSearchChange($event.target.value, false)"
@blur="handleSearchChange($event.target.value, true)"
@keyup.enter="handleSearchChange($event.target.value, true)"
/>
</div>
</div>
Expand All @@ -25,7 +27,7 @@
@update:model-value="handleFilterChange"
/>
</div>

<div class="flex items-center gap-2 flex-1 sm:flex-initial">
<label class="text-xs sm:text-sm font-medium text-muted-foreground whitespace-nowrap">Sort by:</label>
<Select
Expand All @@ -41,14 +43,37 @@
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Search } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { Select } from '@/components/ui/select'

const searchQuery = ref('')
const filterBy = ref(localStorage.getItem('gatus:filter-by') || (typeof window !== 'undefined' && window.config?.defaultFilterBy) || 'none')
const sortBy = ref(localStorage.getItem('gatus:sort-by') || (typeof window !== 'undefined' && window.config?.defaultSortBy) || 'name')
const STORAGE_KEYS = {
FILTER_BY: 'gatus:filter-by',
SORT_BY: 'gatus:sort-by',
}

const router = useRouter()
const route = useRoute()

const [defaultFilterBy, defaultSortBy] = (() => {
let filter = 'none'
let sort = 'name'
if (typeof window !== 'undefined' && window.config) {
if (window.config.defaultFilterBy && window.config.defaultFilterBy !== '{{ .UI.DefaultFilterBy }}') {
filter = window.config.defaultFilterBy
}
if (window.config.defaultSortBy && window.config.defaultSortBy !== '{{ .UI.DefaultSortBy }}') {
sort = window.config.defaultSortBy
}
}
return [filter, sort]
})()

const searchQuery = ref(route.query.search || '')
const filterBy = ref(route.query.filter || localStorage.getItem(STORAGE_KEYS.FILTER_BY) || defaultFilterBy)
const sortBy = ref(route.query.sort || localStorage.getItem(STORAGE_KEYS.SORT_BY) || defaultSortBy)

const filterOptions = [
{ label: 'None', value: 'none' },
Expand All @@ -64,40 +89,74 @@ const sortOptions = [

const emit = defineEmits(['search', 'update:showOnlyFailing', 'update:showRecentFailures', 'update:groupByGroup', 'update:sortBy', 'initializeCollapsedGroups'])

const handleSearchChange = (value, push = true) => {
searchQuery.value = value
const query = { ...route.query }
query.search = searchQuery.value || undefined
push ? router.push({ query }) : router.replace({ query })

emit('search', searchQuery.value)
}

const handleFilterChange = (value, store = true) => {
filterBy.value = value
if (store)
localStorage.setItem('gatus:filter-by', value)

// Reset all filter states first
emit('update:showOnlyFailing', false)
emit('update:showRecentFailures', false)

// Apply the selected filter
if (value === 'failing') {
emit('update:showOnlyFailing', true)
} else if (value === 'unstable') {
emit('update:showRecentFailures', true)
if (store) {
const query = { ...route.query }
query.filter = value
router.push({ query })
localStorage.setItem(STORAGE_KEYS.FILTER_BY, value)
}

emit('update:showOnlyFailing', value === 'failing')
emit('update:showRecentFailures', value === 'unstable')
}

const handleSortChange = (value, store = true) => {
sortBy.value = value
if (store)
localStorage.setItem('gatus:sort-by', value)
if (store) {
const query = { ...route.query }
query.sort = value
router.push({ query })
localStorage.setItem(STORAGE_KEYS.SORT_BY, value)
}

emit('update:sortBy', value)
emit('update:groupByGroup', value === 'group')

// When switching to group view, initialize collapsed groups
if (value === 'group') {
emit('initializeCollapsedGroups')
}
}

onMounted(() => {
if (route.query.search)
emit('search', searchQuery.value)

// Apply saved or application wide filter/sort state on load but do not store it in localstorage
handleFilterChange(filterBy.value, false)
handleSortChange(sortBy.value, false)
})

// Ensure browser history navigation (back/forward) re-applies search, filter, and sort
watch(
() => route.query.search,
(value) => {
handleSearchChange(value || '')
}
)

watch(
() => route.query.filter,
(value) => {
handleFilterChange(value || localStorage.getItem(STORAGE_KEYS.FILTER_BY) || defaultFilterBy, false)
}
)

watch(
() => route.query.sort,
(value) => {
handleSortChange(value || localStorage.getItem(STORAGE_KEYS.SORT_BY) || defaultSortBy, false)
}
)
</script>
2 changes: 1 addition & 1 deletion web/app/src/components/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const DEFAULT_REFRESH_INTERVAL = '300'
const THEME_COOKIE_NAME = 'theme'
const THEME_COOKIE_MAX_AGE = 31536000 // 1 year
const STORAGE_KEYS = {
REFRESH_INTERVAL: 'gatus:refresh-interval'
REFRESH_INTERVAL: 'gatus:refresh-interval',
}

// Helper functions
Expand Down
15 changes: 10 additions & 5 deletions web/app/src/views/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ import Loading from '@/components/Loading.vue'
import AnnouncementBanner from '@/components/AnnouncementBanner.vue'
import PastAnnouncements from '@/components/PastAnnouncements.vue'

const STORAGE_KEYS = {
COLLAPSED_GROUPS: 'gatus:collapsed-groups',
UNCOLLAPSED_GROUPS: 'gatus:uncollapsed-groups',
}

const props = defineProps({
announcements: {
type: Array,
Expand Down Expand Up @@ -222,7 +227,7 @@ const showOnlyFailing = ref(false)
const showRecentFailures = ref(false)
const showAverageResponseTime = ref(true)
const groupByGroup = ref(false)
const sortBy = ref(localStorage.getItem('gatus:sort-by') || 'name')
const sortBy = ref(undefined)
const uncollapsedGroups = ref(new Set())
const resultPageSize = 50

Expand Down Expand Up @@ -513,21 +518,21 @@ const toggleGroupCollapse = (groupName) => {
}
// Save to localStorage
const uncollapsed = Array.from(uncollapsedGroups.value)
localStorage.setItem('gatus:uncollapsed-groups', JSON.stringify(uncollapsed))
localStorage.removeItem('gatus:collapsed-groups') // Remove old key if it exists
localStorage.setItem(STORAGE_KEYS.UNCOLLAPSED_GROUPS, JSON.stringify(uncollapsed))
localStorage.removeItem(STORAGE_KEYS.COLLAPSED_GROUPS) // Remove old key if it exists
}

const initializeCollapsedGroups = () => {
// Get saved uncollapsed groups from localStorage
try {
const saved = localStorage.getItem('gatus:uncollapsed-groups')
const saved = localStorage.getItem(STORAGE_KEYS.UNCOLLAPSED_GROUPS)
if (saved) {
uncollapsedGroups.value = new Set(JSON.parse(saved))
}
// If no saved state, uncollapsedGroups stays empty (all collapsed by default)
} catch (e) {
console.warn('Failed to parse saved uncollapsed groups:', e)
localStorage.removeItem('gatus:uncollapsed-groups')
localStorage.removeItem(STORAGE_KEYS.UNCOLLAPSED_GROUPS)
// On error, uncollapsedGroups stays empty (all collapsed by default)
}
}
Expand Down