mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
fix(ui): use Proxmox tag colours from datacenter config
Pulse was generating tag colours from a hash of the tag name instead of using the colours configured in Proxmox. Now polls /cluster/options once per PVE instance and merges the tag-style colour map into state, which the frontend uses as the first-priority colour source for tag badges. Falls back to the existing special-tag and hash-based colours when Proxmox hasn't set a custom colour for a tag.
This commit is contained in:
parent
da928cd9d3
commit
caff845c1a
9 changed files with 98 additions and 3 deletions
|
|
@ -2,6 +2,7 @@ import { Component, For, Show } from 'solid-js';
|
|||
import { getTagColorWithSpecial } from '@/utils/tagColors';
|
||||
import { useDarkMode } from '@/App';
|
||||
import { showTooltip, hideTooltip } from '@/components/shared/Tooltip';
|
||||
import { getGlobalWebSocketStore } from '@/stores/websocket-global';
|
||||
|
||||
interface TagBadgesProps {
|
||||
tags?: string[];
|
||||
|
|
@ -16,12 +17,14 @@ export const TagBadges: Component<TagBadgesProps> = (props) => {
|
|||
const maxVisible = () => props.maxVisible === 0 ? Infinity : (props.maxVisible ?? 3);
|
||||
const darkModeSignal = useDarkMode();
|
||||
const isDark = () => props.isDarkMode ?? darkModeSignal();
|
||||
const ws = getGlobalWebSocketStore();
|
||||
const pveTagColors = () => ws.state.pveTagColors;
|
||||
|
||||
const visibleTags = () => props.tags?.slice(0, maxVisible()) || [];
|
||||
const hiddenTags = () => props.tags?.slice(maxVisible()) || [];
|
||||
|
||||
const TagDot: Component<{ tag: string }> = (dotProps) => {
|
||||
const colors = () => getTagColorWithSpecial(dotProps.tag, isDark());
|
||||
const colors = () => getTagColorWithSpecial(dotProps.tag, isDark(), pveTagColors());
|
||||
const isActive = () => props.activeSearch?.includes(`tags:${dotProps.tag}`) || false;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export function createWebSocketStore(url: string) {
|
|||
activeAlerts: [],
|
||||
recentlyResolved: [],
|
||||
lastUpdate: '',
|
||||
pveTagColors: {},
|
||||
// Unified resources for cross-platform monitoring
|
||||
resources: [],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export interface State {
|
|||
recentlyResolved: ResolvedAlert[];
|
||||
lastUpdate: string;
|
||||
temperatureMonitoringEnabled?: boolean;
|
||||
pveTagColors?: Record<string, string>;
|
||||
// Unified resources (new data model - eventually replaces legacy arrays above)
|
||||
resources?: Resource[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,14 +93,23 @@ const specialTagColors: Record<string, TagColorTheme> = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Get color for a tag, checking special colors first
|
||||
* Get color for a tag.
|
||||
* Priority: Proxmox-supplied hex color → special hardcoded colors → hash-based fallback.
|
||||
* @param colorMap Optional map of lowercase tag name → "#rrggbb" from Proxmox datacenter config.
|
||||
*/
|
||||
export function getTagColorWithSpecial(
|
||||
tag: string,
|
||||
isDarkMode: boolean,
|
||||
colorMap?: Record<string, string>,
|
||||
): { bg: string; text: string; border: string } {
|
||||
const lowerTag = tag.toLowerCase();
|
||||
|
||||
// Use Proxmox-supplied colour when available
|
||||
if (colorMap?.[lowerTag]) {
|
||||
const hex = colorMap[lowerTag];
|
||||
return { bg: hex, text: '#ffffff', border: hex };
|
||||
}
|
||||
|
||||
// Check if it's a special tag
|
||||
if (specialTagColors[lowerTag]) {
|
||||
return isDarkMode ? specialTagColors[lowerTag].dark : specialTagColors[lowerTag].light;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type State struct {
|
|||
RecentlyResolved []ResolvedAlert `json:"recentlyResolved"`
|
||||
LastUpdate time.Time `json:"lastUpdate"`
|
||||
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
|
||||
PVETagColors map[string]string `json:"pveTagColors,omitempty"`
|
||||
}
|
||||
|
||||
// Alert represents an active alert (simplified for State)
|
||||
|
|
@ -2968,6 +2969,22 @@ func (s *State) SetConnectionHealth(instanceID string, healthy bool) {
|
|||
s.ConnectionHealth[instanceID] = healthy
|
||||
}
|
||||
|
||||
// MergeTagColors merges tag→colour entries into the shared PVETagColors map.
|
||||
// Entries from the latest poll overwrite previous ones for the same tag name.
|
||||
func (s *State) MergeTagColors(colors map[string]string) {
|
||||
if len(colors) == 0 {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.PVETagColors == nil {
|
||||
s.PVETagColors = make(map[string]string, len(colors))
|
||||
}
|
||||
for k, v := range colors {
|
||||
s.PVETagColors[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveConnectionHealth removes a connection health entry if it exists.
|
||||
func (s *State) RemoveConnectionHealth(instanceID string) {
|
||||
s.mu.Lock()
|
||||
|
|
|
|||
|
|
@ -586,6 +586,7 @@ type StateFrontend struct {
|
|||
Stats map[string]any `json:"stats"` // Empty object for now
|
||||
LastUpdate int64 `json:"lastUpdate"` // Unix timestamp
|
||||
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"` // Global temperature monitoring setting
|
||||
PVETagColors map[string]string `json:"pveTagColors,omitempty"` // Tag name → "#rrggbb" from Proxmox datacenter config
|
||||
// Unified resources - the new way to access all monitored entities
|
||||
Resources []ResourceFrontend `json:"resources,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ type StateSnapshot struct {
|
|||
RecentlyResolved []ResolvedAlert `json:"recentlyResolved"`
|
||||
LastUpdate time.Time `json:"lastUpdate"`
|
||||
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
|
||||
PVETagColors map[string]string `json:"pveTagColors,omitempty"`
|
||||
}
|
||||
|
||||
// GetSnapshot returns a snapshot of the current state without mutex
|
||||
|
|
@ -81,10 +82,16 @@ func (s *State) GetSnapshot() StateSnapshot {
|
|||
TemperatureMonitoringEnabled: s.TemperatureMonitoringEnabled,
|
||||
}
|
||||
|
||||
// Copy map
|
||||
// Copy maps
|
||||
for k, v := range s.ConnectionHealth {
|
||||
snapshot.ConnectionHealth[k] = v
|
||||
}
|
||||
if len(s.PVETagColors) > 0 {
|
||||
snapshot.PVETagColors = make(map[string]string, len(s.PVETagColors))
|
||||
for k, v := range s.PVETagColors {
|
||||
snapshot.PVETagColors[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
|
@ -418,5 +425,6 @@ func (s StateSnapshot) ToFrontend() StateFrontend {
|
|||
Stats: make(map[string]any),
|
||||
LastUpdate: s.LastUpdate.Unix() * 1000, // JavaScript timestamp
|
||||
TemperatureMonitoringEnabled: s.TemperatureMonitoringEnabled,
|
||||
PVETagColors: s.PVETagColors,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6380,6 +6380,19 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
|||
// Reset auth failures on successful connection
|
||||
m.resetAuthFailures(instanceName, "pve")
|
||||
|
||||
// Best-effort: fetch tag colour map from Proxmox datacenter config.
|
||||
// Not all Proxmox versions expose this; errors are silently ignored.
|
||||
type clusterOptionsGetter interface {
|
||||
GetClusterOptions(ctx context.Context) (*proxmox.ClusterOptions, error)
|
||||
}
|
||||
if og, ok := client.(clusterOptionsGetter); ok {
|
||||
if opts, err := og.GetClusterOptions(ctx); err == nil && opts != nil {
|
||||
if colors := proxmox.ParseTagColorMap(opts.TagStyle); len(colors) > 0 {
|
||||
m.state.MergeTagColors(colors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if client is a ClusterClient to determine health status
|
||||
connectionHealthStr := "healthy"
|
||||
if clusterClient, ok := client.(*proxmox.ClusterClient); ok {
|
||||
|
|
|
|||
|
|
@ -1788,6 +1788,48 @@ func (c *Client) GetClusterResources(ctx context.Context, resourceType string) (
|
|||
return result.Data, nil
|
||||
}
|
||||
|
||||
// ClusterOptions holds selected Proxmox datacenter configuration options.
|
||||
type ClusterOptions struct {
|
||||
TagStyle string `json:"tag-style,omitempty"`
|
||||
}
|
||||
|
||||
// GetClusterOptions fetches datacenter-level options (e.g. tag colour map).
|
||||
func (c *Client) GetClusterOptions(ctx context.Context) (*ClusterOptions, error) {
|
||||
resp, err := c.get(ctx, "/cluster/options")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data ClusterOptions `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result.Data, nil
|
||||
}
|
||||
|
||||
// ParseTagColorMap parses a Proxmox tag-style string and returns a map of
|
||||
// lowercase tag name → "#rrggbb" hex colour string.
|
||||
// Example input: "color-map=production:ff0000;staging:ffaa00,ordering=config"
|
||||
func ParseTagColorMap(tagStyle string) map[string]string {
|
||||
colors := make(map[string]string)
|
||||
for _, part := range strings.Split(tagStyle, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if !strings.HasPrefix(part, "color-map=") {
|
||||
continue
|
||||
}
|
||||
for _, pair := range strings.Split(strings.TrimPrefix(part, "color-map="), ";") {
|
||||
kv := strings.SplitN(pair, ":", 2)
|
||||
if len(kv) == 2 && len(kv[1]) == 6 {
|
||||
colors[strings.ToLower(strings.TrimSpace(kv[0]))] = "#" + kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return colors
|
||||
}
|
||||
|
||||
// ZFSPoolStatus represents the status of a ZFS pool (list endpoint)
|
||||
type ZFSPoolStatus struct {
|
||||
Name string `json:"name"`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue