Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ If you want to test it locally, see [Docker](#docker).
| `ui.dark-mode` | Whether to enable dark mode by default. Note that this is superseded by the user's operating system theme preferences. | `true` |
| `ui.default-sort-by` | Default sorting option for endpoints in the dashboard. Can be `name`, `group`, or `health`. Note that user preferences override this. | `name` |
| `ui.default-filter-by` | Default filter option for endpoints in the dashboard. Can be `none`, `failing`, or `unstable`. Note that user preferences override this. | `none` |
| `ui.default-group-collapse` | Default collapse option for statuses in the dashboard. Can be either `true` or `false`. Note that user preferences override this. | `false` |
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |

If you want more verbose logging, you may set the `GATUS_LOG_LEVEL` environment variable to `DEBUG`.
Expand Down
9 changes: 8 additions & 1 deletion config/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ const (
)

var (
defaultDarkMode = true
defaultDarkMode = true
defaultGroupCollapse = true

ErrButtonValidationFailed = errors.New("invalid button configuration: missing required name or link")
ErrInvalidDefaultSortBy = errors.New("invalid default-sort-by value: must be 'name', 'group', or 'health'")
ErrInvalidDefaultFilterBy = errors.New("invalid default-filter-by value: must be 'none', 'failing', or 'unstable'")
ErrInvalidDefaultGroupCollapse = errors.New("invalid default-group-collapse value: must be a boolean")
)

// Config is the configuration for the UI of Gatus
Expand All @@ -40,6 +42,7 @@ type Config struct {
DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default
DefaultSortBy string `yaml:"default-sort-by,omitempty"` // DefaultSortBy is the default sort option ('name', 'group', 'health')
DefaultFilterBy string `yaml:"default-filter-by,omitempty"` // DefaultFilterBy is the default filter option ('none', 'failing', 'unstable')
DefaultGroupCollapse *bool `yaml:"default-group-collapse,omitempty"` // DefaultGroupCollapse is a flag to enable/disable collapsing of groups by default

//////////////////////////////////////////////
// Non-configurable - used for UI rendering //
Expand Down Expand Up @@ -80,6 +83,7 @@ func GetDefaultConfig() *Config {
DarkMode: &defaultDarkMode,
DefaultSortBy: defaultSortBy,
DefaultFilterBy: defaultFilterBy,
DefaultGroupCollapse: &defaultGroupCollapse,
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
}
}
Expand Down Expand Up @@ -117,6 +121,9 @@ func (cfg *Config) ValidateAndSetDefaults() error {
} else if cfg.DefaultFilterBy != "none" && cfg.DefaultFilterBy != "failing" && cfg.DefaultFilterBy != "unstable" {
return ErrInvalidDefaultFilterBy
}
if cfg.DefaultGroupCollapse == nil {
cfg.DefaultGroupCollapse = &defaultGroupCollapse
}
for _, btn := range cfg.Buttons {
if err := btn.Validate(); err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion web/app/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<script type="text/javascript">
window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}", defaultGroupCollapse: "{{ .UI.DefaultGroupCollapse }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
// Initialize theme immediately to prevent flash
(function() {
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
Expand Down
41 changes: 36 additions & 5 deletions web/app/src/views/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
<Activity v-if="showAverageResponseTime" class="h-5 w-5" />
<Timer v-else class="h-5 w-5" />
</Button>

<Button variant="ghost" size="icon" @click="refreshData" title="Refresh data">
<RefreshCw class="h-5 w-5" />
</Button>
</div>
</div>

<!-- Announcement Banner -->
<AnnouncementBanner :announcements="props.announcements" />

<!-- Search bar -->
<SearchBar
@search="handleSearch"
Expand Down Expand Up @@ -178,7 +181,7 @@

<script setup>
/* eslint-disable no-undef */
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { Activity, Timer, RefreshCw, AlertCircle, ChevronLeft, ChevronRight, ChevronDown, ChevronUp, CheckCircle } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import EndpointCard from '@/components/EndpointCard.vue'
Expand Down Expand Up @@ -211,6 +214,23 @@ const groupByGroup = ref(false)
const sortBy = ref(localStorage.getItem('gatus:sort-by') || 'name')
const uncollapsedGroups = ref(new Set())

function readBooleanFromLocalStorage(key, fallback) {
const v = localStorage.getItem(key)
if (v === null) return fallback
if (v === 'true' || v === '1') return true
if (v === 'false' || v === '0') return false
return fallback
}

const collapseByDefault = ref(
readBooleanFromLocalStorage(
'gatus:collapse',
(typeof window !== 'undefined' && typeof window.config?.defaultGroupCollapse !== 'undefined')
? !!window.config.defaultGroupCollapse
: true
)
)

const filteredEndpoints = computed(() => {
let filtered = [...endpointStatuses.value]

Expand Down Expand Up @@ -503,21 +523,32 @@ const toggleGroupCollapse = (groupName) => {
}

const initializeCollapsedGroups = () => {
// Get saved uncollapsed groups from localStorage
try {
const saved = localStorage.getItem('gatus:uncollapsed-groups')
if (saved) {
uncollapsedGroups.value = new Set(JSON.parse(saved))
return
}
// 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')
// On error, uncollapsedGroups stays empty (all collapsed by default)
}

if (!collapseByDefault.value) {
const groups = Object.keys(combinedGroups.value || {})
uncollapsedGroups.value = new Set(groups) // expanded by default
} else {
uncollapsedGroups.value = new Set() // collapsed by default
}
Comment on lines -506 to 542
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some comments here? I see you removed all comments, it's a little hard to follow what the code is doing.

}

watch(() => combinedGroups.value, () => {
if (!localStorage.getItem('gatus:uncollapsed-groups')) {
initializeCollapsedGroups()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this (initializeCollapsedGroups) not called before? Or am I missing something?
Looks like initializeCollapsedGroups was previously dead code

}
})

onMounted(() => {
fetchData()
})
</script>
</script>
2 changes: 1 addition & 1 deletion web/static/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}", defaultGroupCollapse: "{{ .UI.DefaultGroupCollapse }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
// Initialize theme immediately to prevent flash
(function() {
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
Expand Down
2 changes: 1 addition & 1 deletion web/static/js/app.js

Large diffs are not rendered by default.