Skip to content
Open
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
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
35 changes: 21 additions & 14 deletions config/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,28 @@ 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'")
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
type Config struct {
Title string `yaml:"title,omitempty"` // Title of the page
Description string `yaml:"description,omitempty"` // Meta description of the page
Header string `yaml:"header,omitempty"` // Header is the text at the top of the page
Logo string `yaml:"logo,omitempty"` // Logo to display on the page
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
CustomCSS string `yaml:"custom-css,omitempty"` // Custom CSS to include in the page
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')
Title string `yaml:"title,omitempty"` // Title of the page
Description string `yaml:"description,omitempty"` // Meta description of the page
Header string `yaml:"header,omitempty"` // Header is the text at the top of the page
Logo string `yaml:"logo,omitempty"` // Logo to display on the page
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
CustomCSS string `yaml:"custom-css,omitempty"` // Custom CSS to include in the page
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.