mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-02-11 21:41:46 +01:00
@@ -6,9 +6,7 @@
|
|||||||
package metricstore
|
package metricstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
||||||
@@ -16,25 +14,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// HealthCheckResponse represents the result of a health check operation.
|
// HealthCheckResponse represents the result of a health check operation.
|
||||||
//
|
|
||||||
// Status indicates the monitoring state (Full, Partial, Failed).
|
|
||||||
// Error contains any error encountered during the health check.
|
|
||||||
type HealthCheckResponse struct {
|
type HealthCheckResponse struct {
|
||||||
Status schema.MonitoringState
|
Status schema.MonitoringState
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaxMissingDataPoints is a threshold that allows a node to be healthy with certain number of data points missing.
|
// MaxMissingDataPoints is the threshold for stale data detection.
|
||||||
// Suppose a node does not receive last 5 data points, then healthCheck endpoint will still say a
|
// A buffer is considered healthy if the gap between its last data point
|
||||||
// node is healthy. Anything more than 5 missing points in metrics of the node will deem the node unhealthy.
|
// and the current time is within MaxMissingDataPoints * frequency.
|
||||||
const MaxMissingDataPoints int64 = 5
|
const MaxMissingDataPoints int64 = 5
|
||||||
|
|
||||||
// isBufferHealthy checks if a buffer has received data for the last MaxMissingDataPoints.
|
// bufferExists returns true if the buffer is non-nil and contains data.
|
||||||
//
|
|
||||||
// Returns true if the buffer is healthy (recent data within threshold), false otherwise.
|
|
||||||
// A nil buffer or empty buffer is considered unhealthy.
|
|
||||||
func (b *buffer) bufferExists() bool {
|
func (b *buffer) bufferExists() bool {
|
||||||
// Check if the buffer is empty
|
|
||||||
if b == nil || b.data == nil || len(b.data) == 0 {
|
if b == nil || b.data == nil || len(b.data) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -42,233 +33,139 @@ func (b *buffer) bufferExists() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// isBufferHealthy checks if a buffer has received data for the last MaxMissingDataPoints.
|
// isBufferHealthy returns true if the buffer has recent data within
|
||||||
//
|
// MaxMissingDataPoints * frequency of the current time.
|
||||||
// Returns true if the buffer is healthy (recent data within threshold), false otherwise.
|
|
||||||
// A nil buffer or empty buffer is considered unhealthy.
|
|
||||||
func (b *buffer) isBufferHealthy() bool {
|
func (b *buffer) isBufferHealthy() bool {
|
||||||
// Get the last endtime of the buffer
|
|
||||||
bufferEnd := b.start + b.frequency*int64(len(b.data))
|
bufferEnd := b.start + b.frequency*int64(len(b.data))
|
||||||
t := time.Now().Unix()
|
t := time.Now().Unix()
|
||||||
|
|
||||||
// Check if the buffer has recent data (within MaxMissingDataPoints threshold)
|
return t-bufferEnd <= MaxMissingDataPoints*b.frequency
|
||||||
if t-bufferEnd > MaxMissingDataPoints*b.frequency {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeUniqueSorted merges two lists, sorts them, and removes duplicates.
|
// collectMetricStatus walks the subtree rooted at l and classifies each
|
||||||
// Requires 'cmp.Ordered' because we need to sort the data.
|
// expected metric into the healthy or degraded map.
|
||||||
func mergeList[string cmp.Ordered](list1, list2 []string) []string {
|
|
||||||
// 1. Combine both lists
|
|
||||||
result := append(list1, list2...)
|
|
||||||
|
|
||||||
// 2. Sort the combined list
|
|
||||||
slices.Sort(result)
|
|
||||||
|
|
||||||
// 3. Compact removes consecutive duplicates (standard in Go 1.21+)
|
|
||||||
// e.g. [1, 1, 2, 3, 3] -> [1, 2, 3]
|
|
||||||
result = slices.Compact(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// getHealthyMetrics recursively collects healthy and degraded metrics at this level and below.
|
|
||||||
//
|
//
|
||||||
// A metric is considered:
|
// Classification rules (evaluated per buffer, pessimistic):
|
||||||
// - Healthy: buffer has recent data within MaxMissingDataPoints threshold AND has few/no NaN values
|
// - A single stale buffer marks the metric as degraded permanently.
|
||||||
// - Degraded: buffer exists and has recent data, but contains more than MaxMissingDataPoints NaN values
|
// - A healthy buffer only counts if no stale buffer has been seen.
|
||||||
//
|
// - Metrics absent from the global config or without any buffer remain
|
||||||
// This routine walks the entire subtree starting from the current level.
|
// in neither map and are later reported as missing.
|
||||||
//
|
func (l *Level) collectMetricStatus(m *MemoryStore, expectedMetrics []string, healthy, degraded map[string]bool) {
|
||||||
// Parameters:
|
|
||||||
// - m: MemoryStore containing the global metric configuration
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// - []string: Flat list of healthy metric names from this level and all children
|
|
||||||
// - []string: Flat list of degraded metric names (exist but have too many missing values)
|
|
||||||
// - error: Non-nil only for internal errors during recursion
|
|
||||||
//
|
|
||||||
// The routine mirrors healthCheck() but provides more granular classification:
|
|
||||||
// - healthCheck() finds problems (stale/missing)
|
|
||||||
// - getHealthyMetrics() separates healthy from degraded metrics
|
|
||||||
func (l *Level) getHealthyMetrics(m *MemoryStore, expectedMetrics []string) ([]string, []string, error) {
|
|
||||||
l.lock.RLock()
|
l.lock.RLock()
|
||||||
defer l.lock.RUnlock()
|
defer l.lock.RUnlock()
|
||||||
|
|
||||||
globalMetrics := m.Metrics
|
for _, metricName := range expectedMetrics {
|
||||||
|
if degraded[metricName] {
|
||||||
|
continue // already degraded, cannot improve
|
||||||
|
}
|
||||||
|
mc := m.Metrics[metricName]
|
||||||
|
b := l.metrics[mc.offset]
|
||||||
|
if b.bufferExists() {
|
||||||
|
if !b.isBufferHealthy() {
|
||||||
|
degraded[metricName] = true
|
||||||
|
delete(healthy, metricName)
|
||||||
|
} else if !degraded[metricName] {
|
||||||
|
healthy[metricName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, lvl := range l.children {
|
||||||
|
lvl.collectMetricStatus(m, expectedMetrics, healthy, degraded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHealthyMetrics walks the complete subtree rooted at l and classifies
|
||||||
|
// each expected metric by comparing the collected status against the
|
||||||
|
// expected list.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - missingList: metrics not found in global config or without any buffer
|
||||||
|
// - degradedList: metrics with at least one stale buffer in the subtree
|
||||||
|
func (l *Level) getHealthyMetrics(m *MemoryStore, expectedMetrics []string) ([]string, []string) {
|
||||||
|
healthy := make(map[string]bool, len(expectedMetrics))
|
||||||
|
degraded := make(map[string]bool)
|
||||||
|
|
||||||
|
l.collectMetricStatus(m, expectedMetrics, healthy, degraded)
|
||||||
|
|
||||||
missingList := make([]string, 0)
|
missingList := make([]string, 0)
|
||||||
degradedList := make([]string, 0)
|
degradedList := make([]string, 0)
|
||||||
|
|
||||||
// Phase 1: Check metrics at this level
|
|
||||||
for _, metricName := range expectedMetrics {
|
for _, metricName := range expectedMetrics {
|
||||||
offset := globalMetrics[metricName].offset
|
if healthy[metricName] {
|
||||||
b := l.metrics[offset]
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if !b.bufferExists() {
|
if degraded[metricName] {
|
||||||
missingList = append(missingList, metricName)
|
|
||||||
} else if !b.isBufferHealthy() {
|
|
||||||
degradedList = append(degradedList, metricName)
|
degradedList = append(degradedList, metricName)
|
||||||
|
} else {
|
||||||
|
missingList = append(missingList, metricName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Recursively check child levels
|
return degradedList, missingList
|
||||||
for _, lvl := range l.children {
|
|
||||||
childMissing, childDegraded, err := lvl.getHealthyMetrics(m, expectedMetrics)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
missingList = mergeList(missingList, childMissing)
|
|
||||||
degradedList = mergeList(degradedList, childDegraded)
|
|
||||||
}
|
|
||||||
|
|
||||||
return missingList, degradedList, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHealthyMetrics returns healthy and degraded metrics for a specific node as flat lists.
|
// GetHealthyMetrics returns missing and degraded metric lists for a node.
|
||||||
//
|
//
|
||||||
// This routine walks the metric tree starting from the specified node selector
|
// It walks the metric tree starting from the node identified by selector
|
||||||
// and collects all metrics that have received data within the last MaxMissingDataPoints
|
// and classifies each expected metric:
|
||||||
// (default: 5 data points). Metrics are classified into two categories:
|
// - Missing: no buffer anywhere in the subtree, or metric not in global config
|
||||||
|
// - Degraded: at least one stale buffer exists in the subtree
|
||||||
//
|
//
|
||||||
// - Healthy: Buffer has recent data AND contains few/no NaN (missing) values
|
// Metrics present in expectedMetrics but absent from both returned lists
|
||||||
// - Degraded: Buffer has recent data BUT contains more than MaxMissingDataPoints NaN values
|
// are considered fully healthy.
|
||||||
//
|
|
||||||
// The returned lists include both node-level metrics (e.g., "load", "mem_used") and
|
|
||||||
// hardware-level metrics (e.g., "cpu_user", "gpu_temp") in flat slices.
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - selector: Hierarchical path to the target node, typically []string{cluster, hostname}.
|
|
||||||
// Example: []string{"emmy", "node001"} navigates to the "node001" host in the "emmy" cluster.
|
|
||||||
// The selector must match the hierarchy used during metric ingestion.
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// - []string: Flat list of healthy metric names (recent data, few missing values)
|
|
||||||
// - []string: Flat list of degraded metric names (recent data, many missing values)
|
|
||||||
// - error: Non-nil if the node is not found or internal errors occur
|
|
||||||
//
|
|
||||||
// Example usage:
|
|
||||||
//
|
|
||||||
// selector := []string{"emmy", "node001"}
|
|
||||||
// healthyMetrics, degradedMetrics, err := ms.GetHealthyMetrics(selector)
|
|
||||||
// if err != nil {
|
|
||||||
// // Node not found or internal error
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// fmt.Printf("Healthy metrics: %v\n", healthyMetrics)
|
|
||||||
// // Output: ["load", "mem_used", "cpu_user", ...]
|
|
||||||
// fmt.Printf("Degraded metrics: %v\n", degradedMetrics)
|
|
||||||
// // Output: ["gpu_temp", "network_rx", ...] (metrics with many NaN values)
|
|
||||||
//
|
|
||||||
// Note: This routine provides more granular classification than HealthCheck:
|
|
||||||
// - HealthCheck reports stale/missing metrics (problems)
|
|
||||||
// - GetHealthyMetrics separates fully healthy from degraded metrics (quality levels)
|
|
||||||
func (m *MemoryStore) GetHealthyMetrics(selector []string, expectedMetrics []string) ([]string, []string, error) {
|
func (m *MemoryStore) GetHealthyMetrics(selector []string, expectedMetrics []string) ([]string, []string, error) {
|
||||||
lvl := m.root.findLevel(selector)
|
lvl := m.root.findLevel(selector)
|
||||||
if lvl == nil {
|
if lvl == nil {
|
||||||
return nil, nil, fmt.Errorf("[METRICSTORE]> error while GetHealthyMetrics, host not found: %#v", selector)
|
return nil, nil, fmt.Errorf("[METRICSTORE]> GetHealthyMetrics: host not found: %#v", selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
missingList, degradedList, err := lvl.getHealthyMetrics(m, expectedMetrics)
|
degradedList, missingList := lvl.getHealthyMetrics(m, expectedMetrics)
|
||||||
if err != nil {
|
return degradedList, missingList, nil
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return missingList, degradedList, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthCheck performs health checks on multiple nodes and returns their monitoring states.
|
// HealthCheck evaluates multiple nodes against a set of expected metrics
|
||||||
|
// and returns a monitoring state per node.
|
||||||
//
|
//
|
||||||
// This routine provides a batch health check interface that evaluates multiple nodes
|
// States:
|
||||||
// against a specific set of expected metrics. For each node, it determines the overall
|
// - MonitoringStateFull: all expected metrics are healthy
|
||||||
// monitoring state based on which metrics are healthy, degraded, or missing.
|
// - MonitoringStatePartial: some metrics are missing or degraded
|
||||||
//
|
// - MonitoringStateFailed: node not found, or no healthy metrics at all
|
||||||
// Health Status Classification:
|
|
||||||
// - MonitoringStateFull: All expected metrics are healthy (recent data, few missing values)
|
|
||||||
// - MonitoringStatePartial: Some metrics are degraded (many missing values) or missing
|
|
||||||
// - MonitoringStateFailed: Node not found or all expected metrics are missing/stale
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - cluster: Cluster name (first element of selector path)
|
|
||||||
// - nodes: List of node hostnames to check
|
|
||||||
// - expectedMetrics: List of metric names that should be present on each node
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// - map[string]schema.MonitoringState: Map keyed by hostname containing monitoring state for each node
|
|
||||||
// - error: Non-nil only for internal errors (individual node failures are captured as MonitoringStateFailed)
|
|
||||||
//
|
|
||||||
// Example usage:
|
|
||||||
//
|
|
||||||
// cluster := "emmy"
|
|
||||||
// nodes := []string{"node001", "node002", "node003"}
|
|
||||||
// expectedMetrics := []string{"load", "mem_used", "cpu_user", "cpu_system"}
|
|
||||||
// healthStates, err := ms.HealthCheck(cluster, nodes, expectedMetrics)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// for hostname, state := range healthStates {
|
|
||||||
// fmt.Printf("Node %s: %s\n", hostname, state)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Note: This routine is optimized for batch operations where you need to check
|
|
||||||
// the same set of metrics across multiple nodes.
|
|
||||||
func (m *MemoryStore) HealthCheck(cluster string,
|
func (m *MemoryStore) HealthCheck(cluster string,
|
||||||
nodes []string, expectedMetrics []string,
|
nodes []string, expectedMetrics []string,
|
||||||
) (map[string]schema.MonitoringState, error) {
|
) (map[string]schema.MonitoringState, error) {
|
||||||
results := make(map[string]schema.MonitoringState, len(nodes))
|
results := make(map[string]schema.MonitoringState, len(nodes))
|
||||||
|
|
||||||
// Create a set of expected metrics for fast lookup
|
|
||||||
expectedSet := make(map[string]bool, len(expectedMetrics))
|
|
||||||
for _, metric := range expectedMetrics {
|
|
||||||
expectedSet[metric] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each node
|
|
||||||
for _, hostname := range nodes {
|
for _, hostname := range nodes {
|
||||||
selector := []string{cluster, hostname}
|
selector := []string{cluster, hostname}
|
||||||
status := schema.MonitoringStateFull
|
|
||||||
healthyCount := 0
|
|
||||||
degradedCount := 0
|
|
||||||
missingCount := 0
|
|
||||||
|
|
||||||
// Get healthy and degraded metrics for this node
|
degradedList, missingList, err := m.GetHealthyMetrics(selector, expectedMetrics)
|
||||||
missingList, degradedList, err := m.GetHealthyMetrics(selector, expectedMetrics)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Node not found or internal error
|
|
||||||
results[hostname] = schema.MonitoringStateFailed
|
results[hostname] = schema.MonitoringStateFailed
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
missingCount = len(missingList)
|
degradedCount := len(degradedList)
|
||||||
degradedCount = len(degradedList)
|
missingCount := len(missingList)
|
||||||
uniqueList := mergeList(missingList, degradedList)
|
|
||||||
healthyCount = len(expectedMetrics) - len(uniqueList)
|
healthyCount := len(expectedMetrics) - degradedCount - missingCount
|
||||||
|
|
||||||
// Debug log missing and degraded metrics
|
|
||||||
if missingCount > 0 {
|
|
||||||
cclog.ComponentDebug("metricstore", "HealthCheck: node", hostname, "missing metrics:", missingList)
|
|
||||||
}
|
|
||||||
if degradedCount > 0 {
|
if degradedCount > 0 {
|
||||||
cclog.ComponentDebug("metricstore", "HealthCheck: node", hostname, "degraded metrics:", degradedList)
|
cclog.ComponentInfo("metricstore", "HealthCheck: node ", hostname, "degraded metrics:", degradedList)
|
||||||
|
}
|
||||||
|
if missingCount > 0 {
|
||||||
|
cclog.ComponentInfo("metricstore", "HealthCheck: node ", hostname, "missing metrics:", missingList)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine overall health status
|
switch {
|
||||||
if missingCount > 0 || degradedCount > 0 {
|
case degradedCount == 0 && missingCount == 0:
|
||||||
if healthyCount == 0 {
|
results[hostname] = schema.MonitoringStateFull
|
||||||
// No healthy metrics at all
|
case healthyCount == 0:
|
||||||
status = schema.MonitoringStateFailed
|
results[hostname] = schema.MonitoringStateFailed
|
||||||
} else {
|
default:
|
||||||
// Some healthy, some degraded/missing
|
results[hostname] = schema.MonitoringStatePartial
|
||||||
status = schema.MonitoringStatePartial
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// else: all metrics healthy, status remains MonitoringStateFull
|
|
||||||
|
|
||||||
results[hostname] = status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
|
|||||||
@@ -303,39 +303,39 @@ func TestGetHealthyMetrics(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
selector []string
|
selector []string
|
||||||
expectedMetrics []string
|
expectedMetrics []string
|
||||||
wantMissing []string
|
|
||||||
wantDegraded []string
|
wantDegraded []string
|
||||||
|
wantMissing []string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "mixed health states",
|
name: "mixed health states",
|
||||||
selector: []string{"testcluster", "testnode"},
|
selector: []string{"testcluster", "testnode"},
|
||||||
expectedMetrics: []string{"load", "mem_used", "cpu_user"},
|
expectedMetrics: []string{"load", "mem_used", "cpu_user"},
|
||||||
wantMissing: []string{"cpu_user"},
|
|
||||||
wantDegraded: []string{"mem_used"},
|
wantDegraded: []string{"mem_used"},
|
||||||
|
wantMissing: []string{"cpu_user"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "node not found",
|
name: "node not found",
|
||||||
selector: []string{"testcluster", "nonexistent"},
|
selector: []string{"testcluster", "nonexistent"},
|
||||||
expectedMetrics: []string{"load"},
|
expectedMetrics: []string{"load"},
|
||||||
wantMissing: nil,
|
|
||||||
wantDegraded: nil,
|
wantDegraded: nil,
|
||||||
|
wantMissing: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "check only healthy metric",
|
name: "check only healthy metric",
|
||||||
selector: []string{"testcluster", "testnode"},
|
selector: []string{"testcluster", "testnode"},
|
||||||
expectedMetrics: []string{"load"},
|
expectedMetrics: []string{"load"},
|
||||||
wantMissing: []string{},
|
|
||||||
wantDegraded: []string{},
|
wantDegraded: []string{},
|
||||||
|
wantMissing: []string{},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
missing, degraded, err := ms.GetHealthyMetrics(tt.selector, tt.expectedMetrics)
|
degraded, missing, err := ms.GetHealthyMetrics(tt.selector, tt.expectedMetrics)
|
||||||
|
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("GetHealthyMetrics() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("GetHealthyMetrics() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
@@ -346,17 +346,6 @@ func TestGetHealthyMetrics(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check missing list
|
|
||||||
if len(missing) != len(tt.wantMissing) {
|
|
||||||
t.Errorf("GetHealthyMetrics() missing = %v, want %v", missing, tt.wantMissing)
|
|
||||||
} else {
|
|
||||||
for i, m := range tt.wantMissing {
|
|
||||||
if missing[i] != m {
|
|
||||||
t.Errorf("GetHealthyMetrics() missing[%d] = %v, want %v", i, missing[i], m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check degraded list
|
// Check degraded list
|
||||||
if len(degraded) != len(tt.wantDegraded) {
|
if len(degraded) != len(tt.wantDegraded) {
|
||||||
t.Errorf("GetHealthyMetrics() degraded = %v, want %v", degraded, tt.wantDegraded)
|
t.Errorf("GetHealthyMetrics() degraded = %v, want %v", degraded, tt.wantDegraded)
|
||||||
@@ -367,6 +356,17 @@ func TestGetHealthyMetrics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check missing list
|
||||||
|
if len(missing) != len(tt.wantMissing) {
|
||||||
|
t.Errorf("GetHealthyMetrics() missing = %v, want %v", missing, tt.wantMissing)
|
||||||
|
} else {
|
||||||
|
for i, m := range tt.wantMissing {
|
||||||
|
if missing[i] != m {
|
||||||
|
t.Errorf("GetHealthyMetrics() missing[%d] = %v, want %v", i, missing[i], m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ API_USER="demo" # User for JWT generation
|
|||||||
# BASE NETWORK CONFIG
|
# BASE NETWORK CONFIG
|
||||||
SERVICE_ADDRESS="http://localhost:8080"
|
SERVICE_ADDRESS="http://localhost:8080"
|
||||||
NATS_SERVER="nats://0.0.0.0:4222"
|
NATS_SERVER="nats://0.0.0.0:4222"
|
||||||
|
REST_URL="${SERVICE_ADDRESS}/api/write"
|
||||||
|
|
||||||
# NATS CREDENTIALS
|
# NATS CREDENTIALS
|
||||||
NATS_USER="root"
|
NATS_USER="root"
|
||||||
@@ -27,18 +28,22 @@ JWT_STATIC="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzU3Nzg4NDQsImlhdCI
|
|||||||
ALEX_HOSTS="a0603 a0903 a0832 a0329 a0702 a0122 a1624 a0731 a0224 a0704 a0631 a0225 a0222 a0427 a0603 a0429 a0833 a0705 a0901 a0601 a0227 a0804 a0322 a0226 a0126 a0129 a0605 a0801 a0934 a1622 a0902 a0428 a0537 a1623 a1722 a0228 a0701 a0326 a0327 a0123 a0321 a1621 a0323 a0124 a0534 a0931 a0324 a0933 a0424 a0905 a0128 a0532 a0805 a0521 a0535 a0932 a0127 a0325 a0633 a0831 a0803 a0426 a0425 a0229 a1721 a0602 a0632 a0223 a0422 a0423 a0536 a0328 a0703 anvme7 a0125 a0221 a0604 a0802 a0522 a0531 a0533 a0904"
|
ALEX_HOSTS="a0603 a0903 a0832 a0329 a0702 a0122 a1624 a0731 a0224 a0704 a0631 a0225 a0222 a0427 a0603 a0429 a0833 a0705 a0901 a0601 a0227 a0804 a0322 a0226 a0126 a0129 a0605 a0801 a0934 a1622 a0902 a0428 a0537 a1623 a1722 a0228 a0701 a0326 a0327 a0123 a0321 a1621 a0323 a0124 a0534 a0931 a0324 a0933 a0424 a0905 a0128 a0532 a0805 a0521 a0535 a0932 a0127 a0325 a0633 a0831 a0803 a0426 a0425 a0229 a1721 a0602 a0632 a0223 a0422 a0423 a0536 a0328 a0703 anvme7 a0125 a0221 a0604 a0802 a0522 a0531 a0533 a0904"
|
||||||
FRITZ_HOSTS="f0201 f0202 f0203 f0204 f0205 f0206 f0207 f0208 f0209 f0210 f0211 f0212 f0213 f0214 f0215 f0217 f0218 f0219 f0220 f0221 f0222 f0223 f0224 f0225 f0226 f0227 f0228 f0229 f0230 f0231 f0232 f0233 f0234 f0235 f0236 f0237 f0238 f0239 f0240 f0241 f0242 f0243 f0244 f0245 f0246 f0247 f0248 f0249 f0250 f0251 f0252 f0253 f0254 f0255 f0256 f0257 f0258 f0259 f0260 f0261 f0262 f0263 f0264 f0378"
|
FRITZ_HOSTS="f0201 f0202 f0203 f0204 f0205 f0206 f0207 f0208 f0209 f0210 f0211 f0212 f0213 f0214 f0215 f0217 f0218 f0219 f0220 f0221 f0222 f0223 f0224 f0225 f0226 f0227 f0228 f0229 f0230 f0231 f0232 f0233 f0234 f0235 f0236 f0237 f0238 f0239 f0240 f0241 f0242 f0243 f0244 f0245 f0246 f0247 f0248 f0249 f0250 f0251 f0252 f0253 f0254 f0255 f0256 f0257 f0258 f0259 f0260 f0261 f0262 f0263 f0264 f0378"
|
||||||
|
|
||||||
METRICS_STD="cpu_load cpu_user flops_any cpu_irq cpu_system ipc cpu_idle cpu_iowait core_power clock"
|
ALEX_METRICS_HWTHREAD="cpu_user flops_any clock core_power ipc"
|
||||||
METRICS_NODE="cpu_irq cpu_load mem_cached net_bytes_in cpu_user cpu_idle nfs4_read mem_used nfs4_write nfs4_total ib_xmit ib_xmit_pkts net_bytes_out cpu_iowait ib_recv cpu_system ib_recv_pkts"
|
ALEX_METRICS_SOCKET="mem_bw cpu_power"
|
||||||
|
ALEX_METRICS_ACC="acc_utilization acc_mem_used acc_power nv_mem_util nv_temp nv_sm_clock"
|
||||||
|
ALEX_METRICS_NODE="cpu_load mem_used net_bytes_in net_bytes_out"
|
||||||
|
|
||||||
|
FRITZ_METRICS_HWTHREAD="cpu_user flops_any flops_sp flops_dp clock ipc vectorization_ratio"
|
||||||
|
FRITZ_METRICS_SOCKET="mem_bw cpu_power mem_power"
|
||||||
|
FRITZ_METRICS_NODE="cpu_load mem_used ib_recv ib_xmit ib_recv_pkts ib_xmit_pkts nfs4_read nfs4_total"
|
||||||
|
|
||||||
ACCEL_IDS="00000000:49:00.0 00000000:0E:00.0 00000000:D1:00.0 00000000:90:00.0 00000000:13:00.0 00000000:96:00.0 00000000:CC:00.0 00000000:4F:00.0"
|
ACCEL_IDS="00000000:49:00.0 00000000:0E:00.0 00000000:D1:00.0 00000000:90:00.0 00000000:13:00.0 00000000:96:00.0 00000000:CC:00.0 00000000:4F:00.0"
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# SETUP ENV (URL & TOKEN)
|
# SETUP ENV (URL & TOKEN)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
if [ "$CONNECTION_SCOPE" == "INTERNAL" ]; then
|
if [ "$CONNECTION_SCOPE" == "INTERNAL" ]; then
|
||||||
# 1. Set URL for Internal Mode
|
|
||||||
REST_URL="${SERVICE_ADDRESS}/metricstore/api/write"
|
|
||||||
|
|
||||||
# 2. Generate JWT dynamically
|
# 2. Generate JWT dynamically
|
||||||
echo "Setup: INTERNAL mode selected."
|
echo "Setup: INTERNAL mode selected."
|
||||||
echo "Generating JWT for user: $API_USER"
|
echo "Generating JWT for user: $API_USER"
|
||||||
@@ -48,10 +53,7 @@ if [ "$CONNECTION_SCOPE" == "INTERNAL" ]; then
|
|||||||
echo "Error: Failed to generate JWT from cc-backend."
|
echo "Error: Failed to generate JWT from cc-backend."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# 1. Set URL for External Mode
|
|
||||||
REST_URL="${SERVICE_ADDRESS}/api/write"
|
|
||||||
|
|
||||||
# 2. Use Static JWT
|
# 2. Use Static JWT
|
||||||
echo "Setup: EXTERNAL mode selected."
|
echo "Setup: EXTERNAL mode selected."
|
||||||
echo "Using static JWT."
|
echo "Using static JWT."
|
||||||
@@ -96,7 +98,7 @@ while [ true ]; do
|
|||||||
# 1. ALEX: HWTHREAD
|
# 1. ALEX: HWTHREAD
|
||||||
echo "Generating Alex: hwthread"
|
echo "Generating Alex: hwthread"
|
||||||
{
|
{
|
||||||
for metric in $METRICS_STD; do
|
for metric in $ALEX_METRICS_HWTHREAD; do
|
||||||
for hostname in $ALEX_HOSTS; do
|
for hostname in $ALEX_HOSTS; do
|
||||||
for id in {0..127}; do
|
for id in {0..127}; do
|
||||||
echo "$metric,cluster=alex,hostname=$hostname,type=hwthread,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
echo "$metric,cluster=alex,hostname=$hostname,type=hwthread,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
||||||
@@ -109,7 +111,7 @@ while [ true ]; do
|
|||||||
# 2. FRITZ: HWTHREAD
|
# 2. FRITZ: HWTHREAD
|
||||||
echo "Generating Fritz: hwthread"
|
echo "Generating Fritz: hwthread"
|
||||||
{
|
{
|
||||||
for metric in $METRICS_STD; do
|
for metric in $FRITZ_METRICS_HWTHREAD; do
|
||||||
for hostname in $FRITZ_HOSTS; do
|
for hostname in $FRITZ_HOSTS; do
|
||||||
for id in {0..71}; do
|
for id in {0..71}; do
|
||||||
echo "$metric,cluster=fritz,hostname=$hostname,type=hwthread,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
echo "$metric,cluster=fritz,hostname=$hostname,type=hwthread,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
||||||
@@ -122,7 +124,7 @@ while [ true ]; do
|
|||||||
# 3. ALEX: ACCELERATOR
|
# 3. ALEX: ACCELERATOR
|
||||||
echo "Generating Alex: accelerator"
|
echo "Generating Alex: accelerator"
|
||||||
{
|
{
|
||||||
for metric in $METRICS_STD; do
|
for metric in $ALEX_METRICS_ACC; do
|
||||||
for hostname in $ALEX_HOSTS; do
|
for hostname in $ALEX_HOSTS; do
|
||||||
for id in $ACCEL_IDS; do
|
for id in $ACCEL_IDS; do
|
||||||
echo "$metric,cluster=alex,hostname=$hostname,type=accelerator,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
echo "$metric,cluster=alex,hostname=$hostname,type=accelerator,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
||||||
@@ -132,23 +134,10 @@ while [ true ]; do
|
|||||||
} > sample_alex.txt
|
} > sample_alex.txt
|
||||||
send_payload "sample_alex.txt" "alex"
|
send_payload "sample_alex.txt" "alex"
|
||||||
|
|
||||||
# 4. ALEX: MEMORY DOMAIN
|
|
||||||
echo "Generating Alex: memoryDomain"
|
|
||||||
{
|
|
||||||
for metric in $METRICS_STD; do
|
|
||||||
for hostname in $ALEX_HOSTS; do
|
|
||||||
for id in {0..7}; do
|
|
||||||
echo "$metric,cluster=alex,hostname=$hostname,type=memoryDomain,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
done
|
|
||||||
} > sample_alex.txt
|
|
||||||
send_payload "sample_alex.txt" "alex"
|
|
||||||
|
|
||||||
# 5. ALEX: SOCKET
|
# 5. ALEX: SOCKET
|
||||||
echo "Generating Alex: socket"
|
echo "Generating Alex: socket"
|
||||||
{
|
{
|
||||||
for metric in $METRICS_STD; do
|
for metric in $ALEX_METRICS_SOCKET; do
|
||||||
for hostname in $ALEX_HOSTS; do
|
for hostname in $ALEX_HOSTS; do
|
||||||
for id in {0..1}; do
|
for id in {0..1}; do
|
||||||
echo "$metric,cluster=alex,hostname=$hostname,type=socket,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
echo "$metric,cluster=alex,hostname=$hostname,type=socket,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
||||||
@@ -161,7 +150,7 @@ while [ true ]; do
|
|||||||
# 6. FRITZ: SOCKET
|
# 6. FRITZ: SOCKET
|
||||||
echo "Generating Fritz: socket"
|
echo "Generating Fritz: socket"
|
||||||
{
|
{
|
||||||
for metric in $METRICS_STD; do
|
for metric in $FRITZ_METRICS_SOCKET; do
|
||||||
for hostname in $FRITZ_HOSTS; do
|
for hostname in $FRITZ_HOSTS; do
|
||||||
for id in {0..1}; do
|
for id in {0..1}; do
|
||||||
echo "$metric,cluster=fritz,hostname=$hostname,type=socket,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
echo "$metric,cluster=fritz,hostname=$hostname,type=socket,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
|
||||||
@@ -174,7 +163,7 @@ while [ true ]; do
|
|||||||
# 7. ALEX: NODE
|
# 7. ALEX: NODE
|
||||||
echo "Generating Alex: node"
|
echo "Generating Alex: node"
|
||||||
{
|
{
|
||||||
for metric in $METRICS_NODE; do
|
for metric in $ALEX_METRICS_NODE; do
|
||||||
for hostname in $ALEX_HOSTS; do
|
for hostname in $ALEX_HOSTS; do
|
||||||
echo "$metric,cluster=alex,hostname=$hostname,type=node value=$((1 + RANDOM % 100)).0 $timestamp"
|
echo "$metric,cluster=alex,hostname=$hostname,type=node value=$((1 + RANDOM % 100)).0 $timestamp"
|
||||||
done
|
done
|
||||||
@@ -185,7 +174,7 @@ while [ true ]; do
|
|||||||
# 8. FRITZ: NODE
|
# 8. FRITZ: NODE
|
||||||
echo "Generating Fritz: node"
|
echo "Generating Fritz: node"
|
||||||
{
|
{
|
||||||
for metric in $METRICS_NODE; do
|
for metric in $FRITZ_METRICS_NODE; do
|
||||||
for hostname in $FRITZ_HOSTS; do
|
for hostname in $FRITZ_HOSTS; do
|
||||||
echo "$metric,cluster=fritz,hostname=$hostname,type=node value=$((1 + RANDOM % 100)).0 $timestamp"
|
echo "$metric,cluster=fritz,hostname=$hostname,type=node value=$((1 + RANDOM % 100)).0 $timestamp"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -68,12 +68,8 @@
|
|||||||
energyFootprint { hardware, metric, value }
|
energyFootprint { hardware, metric, value }
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
const client = getContextClient();
|
|
||||||
const ccconfig = getContext("cc-config");
|
|
||||||
const showRoofline = !!ccconfig[`jobView_showRoofline`];
|
|
||||||
const showStatsTable = !!ccconfig[`jobView_showStatTable`];
|
|
||||||
|
|
||||||
/* Note: Actual metric data queried in <Metric> Component, only require base infos here -> reduce backend load by requesting just stats */
|
/* Note: Actual metric data queried in <Metric> Component, only require base infos here -> reduce backend load by requesting just stats */
|
||||||
|
const client = getContextClient();
|
||||||
const query = gql`
|
const query = gql`
|
||||||
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
|
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
|
||||||
scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
|
scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
|
||||||
@@ -89,25 +85,68 @@
|
|||||||
/* State Init */
|
/* State Init */
|
||||||
let plots = $state({});
|
let plots = $state({});
|
||||||
let isMetricsSelectionOpen = $state(false);
|
let isMetricsSelectionOpen = $state(false);
|
||||||
let selectedMetrics = $state([]);
|
|
||||||
let selectedScopes = $state([]);
|
|
||||||
let totalMetrics = $state(0);
|
let totalMetrics = $state(0);
|
||||||
|
|
||||||
/* Derived */
|
/* Derived Init Return */
|
||||||
const showSummary = $derived((!!ccconfig[`jobView_showFootprint`] || !!ccconfig[`jobView_showPolarPlot`]))
|
const thisJob = $derived($initq?.data ? $initq.data.job : null);
|
||||||
|
|
||||||
|
/* Derived Settings */
|
||||||
|
const globalMetrics = $derived(thisJob ? getContext("globalMetrics") : null);
|
||||||
|
const clusterInfo = $derived(thisJob ? getContext("clusters") : null);
|
||||||
|
const ccconfig = $derived(thisJob ? getContext("cc-config") : null);
|
||||||
|
const showRoofline = $derived(ccconfig ? !!ccconfig[`jobView_showRoofline`] : false);
|
||||||
|
const showStatsTable = $derived(ccconfig ? !!ccconfig[`jobView_showStatTable`] : false);
|
||||||
|
const showSummary = $derived(ccconfig ? (!!ccconfig[`jobView_showFootprint`] || !!ccconfig[`jobView_showPolarPlot`]) : false)
|
||||||
|
|
||||||
|
/* Derived Var Preprocessing*/
|
||||||
|
let selectedMetrics = $derived.by(() => {
|
||||||
|
if(thisJob && ccconfig) {
|
||||||
|
if (thisJob.cluster) {
|
||||||
|
if (thisJob.subCluster) {
|
||||||
|
return ccconfig[`metricConfig_jobViewPlotMetrics:${thisJob.cluster}:${thisJob.subCluster}`] ||
|
||||||
|
ccconfig[`metricConfig_jobViewPlotMetrics:${thisJob.cluster}`] ||
|
||||||
|
ccconfig.metricConfig_jobViewPlotMetrics
|
||||||
|
}
|
||||||
|
return ccconfig[`metricConfig_jobViewPlotMetrics:${thisJob.cluster}`] ||
|
||||||
|
ccconfig.metricConfig_jobViewPlotMetrics
|
||||||
|
}
|
||||||
|
return ccconfig.metricConfig_jobViewPlotMetrics
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedScopes = $derived.by(() => {
|
||||||
|
const pendingScopes = ["node"]
|
||||||
|
if (thisJob) {
|
||||||
|
const accScopeDefault = [...selectedMetrics].some(function (m) {
|
||||||
|
const thisCluster = clusterInfo.find((c) => c.name == thisJob.cluster);
|
||||||
|
const subCluster = thisCluster.subClusters.find((sc) => sc.name == thisJob.subCluster);
|
||||||
|
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (accScopeDefault) pendingScopes.push("accelerator")
|
||||||
|
if (thisJob.numNodes === 1) {
|
||||||
|
pendingScopes.push("socket")
|
||||||
|
pendingScopes.push("core")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return[...new Set(pendingScopes)];
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Derived Query and Postprocessing*/
|
||||||
const jobMetrics = $derived(queryStore({
|
const jobMetrics = $derived(queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: query,
|
query: query,
|
||||||
variables: { dbid, selectedMetrics, selectedScopes },
|
variables: { dbid, selectedMetrics, selectedScopes },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const missingMetrics = $derived.by(() => {
|
const missingMetrics = $derived.by(() => {
|
||||||
if ($initq?.data && $jobMetrics?.data) {
|
if (thisJob && $jobMetrics?.data) {
|
||||||
let job = $initq.data.job;
|
|
||||||
let metrics = $jobMetrics.data.scopedJobStats;
|
let metrics = $jobMetrics.data.scopedJobStats;
|
||||||
let metricNames = $initq.data.globalMetrics.reduce((names, gm) => {
|
let metricNames = globalMetrics.reduce((names, gm) => {
|
||||||
if (gm.availability.find((av) => av.cluster === job.cluster)) {
|
if (gm.availability.find((av) => av.cluster === thisJob.cluster)) {
|
||||||
names.push(gm.name);
|
names.push(gm.name);
|
||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
@@ -118,9 +157,10 @@
|
|||||||
!metrics.some((jm) => jm.name == metric) &&
|
!metrics.some((jm) => jm.name == metric) &&
|
||||||
selectedMetrics.includes(metric) &&
|
selectedMetrics.includes(metric) &&
|
||||||
!checkMetricDisabled(
|
!checkMetricDisabled(
|
||||||
|
globalMetrics,
|
||||||
metric,
|
metric,
|
||||||
$initq.data.job.cluster,
|
thisJob.cluster,
|
||||||
$initq.data.job.subCluster,
|
thisJob.subCluster,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -129,17 +169,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const missingHosts = $derived.by(() => {
|
const missingHosts = $derived.by(() => {
|
||||||
if ($initq?.data && $jobMetrics?.data) {
|
if (thisJob && $jobMetrics?.data) {
|
||||||
let job = $initq.data.job;
|
|
||||||
let metrics = $jobMetrics.data.scopedJobStats;
|
let metrics = $jobMetrics.data.scopedJobStats;
|
||||||
let metricNames = $initq.data.globalMetrics.reduce((names, gm) => {
|
let metricNames = globalMetrics.reduce((names, gm) => {
|
||||||
if (gm.availability.find((av) => av.cluster === job.cluster)) {
|
if (gm.availability.find((av) => av.cluster === thisJob.cluster)) {
|
||||||
names.push(gm.name);
|
names.push(gm.name);
|
||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return job.resources
|
return thisJob.resources
|
||||||
.map(({ hostname }) => ({
|
.map(({ hostname }) => ({
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
metrics: metricNames.filter(
|
metrics: metricNames.filter(
|
||||||
@@ -165,51 +204,19 @@
|
|||||||
? "Loading..."
|
? "Loading..."
|
||||||
: $initq?.error
|
: $initq?.error
|
||||||
? "Error"
|
? "Error"
|
||||||
: `Job ${$initq.data.job.jobId} - ClusterCockpit`;
|
: `Job ${thisJob.jobId} - ClusterCockpit`;
|
||||||
});
|
|
||||||
|
|
||||||
/* On Init */
|
|
||||||
getContext("on-init")(() => {
|
|
||||||
let job = $initq.data.job;
|
|
||||||
if (!job) return;
|
|
||||||
const pendingMetrics = (
|
|
||||||
ccconfig[`metricConfig_jobViewPlotMetrics:${job.cluster}:${job.subCluster}`] ||
|
|
||||||
ccconfig[`metricConfig_jobViewPlotMetrics:${job.cluster}`]
|
|
||||||
) ||
|
|
||||||
$initq.data.globalMetrics.reduce((names, gm) => {
|
|
||||||
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
|
|
||||||
names.push(gm.name);
|
|
||||||
}
|
|
||||||
return names;
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Select default Scopes to load: Check before if any metric has accelerator scope by default
|
|
||||||
const accScopeDefault = [...pendingMetrics].some(function (m) {
|
|
||||||
const cluster = $initq.data.clusters.find((c) => c.name == job.cluster);
|
|
||||||
const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster);
|
|
||||||
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
|
|
||||||
});
|
|
||||||
|
|
||||||
const pendingScopes = ["node"]
|
|
||||||
if (accScopeDefault) pendingScopes.push("accelerator")
|
|
||||||
if (job.numNodes === 1) {
|
|
||||||
pendingScopes.push("socket")
|
|
||||||
pendingScopes.push("core")
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedMetrics = [...new Set(pendingMetrics)];
|
|
||||||
selectedScopes = [...new Set(pendingScopes)];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Functions */
|
/* Functions */
|
||||||
const orderAndMap = (grouped, selectedMetrics) =>
|
const orderAndMap = (grouped, inputMetrics) =>
|
||||||
selectedMetrics.map((metric) => ({
|
inputMetrics.map((metric) => ({
|
||||||
metric: metric,
|
metric: metric,
|
||||||
data: grouped.find((group) => group[0].name == metric),
|
data: grouped.find((group) => group[0].name == metric),
|
||||||
disabled: checkMetricDisabled(
|
disabled: checkMetricDisabled(
|
||||||
|
globalMetrics,
|
||||||
metric,
|
metric,
|
||||||
$initq.data.job.cluster,
|
thisJob.cluster,
|
||||||
$initq.data.job.subCluster,
|
thisJob.subCluster,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
@@ -219,34 +226,34 @@
|
|||||||
<Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0">
|
<Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0">
|
||||||
{#if $initq.error}
|
{#if $initq.error}
|
||||||
<Card body color="danger">{$initq.error.message}</Card>
|
<Card body color="danger">{$initq.error.message}</Card>
|
||||||
{:else if $initq?.data}
|
{:else if thisJob}
|
||||||
<Card class="overflow-auto" style="height: auto;">
|
<Card class="overflow-auto" style="height: auto;">
|
||||||
<TabContent> <!-- on:tab={(e) => (status = e.detail)} -->
|
<TabContent> <!-- on:tab={(e) => (status = e.detail)} -->
|
||||||
{#if $initq.data?.job?.metaData?.message}
|
{#if thisJob?.metaData?.message}
|
||||||
<TabPane tabId="admin-msg" tab="Admin Note" active>
|
<TabPane tabId="admin-msg" tab="Admin Note" active>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Card body class="mb-2" color="warning">
|
<Card body class="mb-2" color="warning">
|
||||||
<h5>Job {$initq.data?.job?.jobId} ({$initq.data?.job?.cluster})</h5>
|
<h5>Job {thisJob?.jobId} ({thisJob?.cluster})</h5>
|
||||||
The following note was added by administrators:
|
The following note was added by administrators:
|
||||||
</Card>
|
</Card>
|
||||||
<Card body>
|
<Card body>
|
||||||
{@html $initq.data.job.metaData.message}
|
{@html thisJob.metaData.message}
|
||||||
</Card>
|
</Card>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
{/if}
|
{/if}
|
||||||
<TabPane tabId="meta-info" tab="Job Info" active={$initq.data?.job?.metaData?.message?false:true}>
|
<TabPane tabId="meta-info" tab="Job Info" active={thisJob?.metaData?.message?false:true}>
|
||||||
<CardBody class="pb-2">
|
<CardBody class="pb-2">
|
||||||
<JobInfo job={$initq.data.job} {username} {authlevel} {roles} showTagEdit/>
|
<JobInfo job={thisJob} {username} {authlevel} {roles} showTagEdit/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
|
{#if thisJob.concurrentJobs != null && thisJob.concurrentJobs.items.length != 0}
|
||||||
<TabPane tabId="shared-jobs">
|
<TabPane tabId="shared-jobs">
|
||||||
<span slot="tab">
|
<span slot="tab">
|
||||||
{$initq.data.job.concurrentJobs.items.length} Concurrent Jobs
|
{thisJob.concurrentJobs.items.length} Concurrent Jobs
|
||||||
</span>
|
</span>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<ConcurrentJobs cJobs={$initq.data.job.concurrentJobs} showLinks={(authlevel > roles.manager)}/>
|
<ConcurrentJobs cJobs={thisJob.concurrentJobs} showLinks={(authlevel > roles.manager)}/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -261,9 +268,9 @@
|
|||||||
<Col xs={12} md={6} xl={4} xxl={3} class="mb-3 mb-xxl-0">
|
<Col xs={12} md={6} xl={4} xxl={3} class="mb-3 mb-xxl-0">
|
||||||
{#if $initq.error}
|
{#if $initq.error}
|
||||||
<Card body color="danger">{$initq.error.message}</Card>
|
<Card body color="danger">{$initq.error.message}</Card>
|
||||||
{:else if $initq?.data}
|
{:else if thisJob}
|
||||||
{#if showSummary}
|
{#if showSummary}
|
||||||
<JobSummary job={$initq.data.job}/>
|
<JobSummary job={thisJob}/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<Spinner secondary />
|
<Spinner secondary />
|
||||||
@@ -274,9 +281,9 @@
|
|||||||
<Col xs={12} md={12} xl={5} xxl={6}>
|
<Col xs={12} md={12} xl={5} xxl={6}>
|
||||||
{#if $initq.error}
|
{#if $initq.error}
|
||||||
<Card body color="danger">{$initq.error.message}</Card>
|
<Card body color="danger">{$initq.error.message}</Card>
|
||||||
{:else if $initq?.data}
|
{:else if thisJob}
|
||||||
{#if showRoofline}
|
{#if showRoofline}
|
||||||
<JobRoofline job={$initq.data.job} clusters={$initq.data.clusters}/>
|
<JobRoofline job={thisJob} {clusterInfo}/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<Spinner secondary />
|
<Spinner secondary />
|
||||||
@@ -285,10 +292,10 @@
|
|||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- Row 2: Energy Information if available -->
|
<!-- Row 2: Energy Information if available -->
|
||||||
{#if $initq?.data && $initq.data.job.energyFootprint.length != 0}
|
{#if thisJob && thisJob?.energyFootprint?.length != 0}
|
||||||
<Row class="mb-3">
|
<Row class="mb-3">
|
||||||
<Col>
|
<Col>
|
||||||
<EnergySummary jobId={$initq.data.job.jobId} jobEnergy={$initq.data.job.energy} jobEnergyFootprint={$initq.data.job.energyFootprint}/>
|
<EnergySummary jobId={thisJob.jobId} jobEnergy={thisJob.energy} jobEnergyFootprint={thisJob.energyFootprint}/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -297,7 +304,7 @@
|
|||||||
<Card class="mb-3">
|
<Card class="mb-3">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Row class="mb-2">
|
<Row class="mb-2">
|
||||||
{#if $initq?.data}
|
{#if thisJob}
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Button outline onclick={() => (isMetricsSelectionOpen = true)} color="primary">
|
<Button outline onclick={() => (isMetricsSelectionOpen = true)} color="primary">
|
||||||
Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
|
Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
|
||||||
@@ -310,7 +317,7 @@
|
|||||||
{#if $jobMetrics.error}
|
{#if $jobMetrics.error}
|
||||||
<Row class="mt-2">
|
<Row class="mt-2">
|
||||||
<Col>
|
<Col>
|
||||||
{#if $initq?.data && ($initq.data.job?.monitoringStatus == 0 || $initq.data.job?.monitoringStatus == 2)}
|
{#if thisJob && (thisJob?.monitoringStatus == 0 || thisJob?.monitoringStatus == 2)}
|
||||||
<Card body color="warning">Not monitored or archiving failed</Card>
|
<Card body color="warning">Not monitored or archiving failed</Card>
|
||||||
<br />
|
<br />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -323,18 +330,18 @@
|
|||||||
<Spinner secondary />
|
<Spinner secondary />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{:else if $initq?.data && $jobMetrics?.data?.scopedJobStats}
|
{:else if thisJob && $jobMetrics?.data?.scopedJobStats}
|
||||||
<!-- Note: Ignore '#snippet' Error in IDE -->
|
<!-- Note: Ignore '#snippet' Error in IDE -->
|
||||||
{#snippet gridContent(item)}
|
{#snippet gridContent(item)}
|
||||||
{#if item.data}
|
{#if item.data}
|
||||||
<Metric
|
<Metric
|
||||||
bind:this={plots[item.metric]}
|
bind:this={plots[item.metric]}
|
||||||
job={$initq.data.job}
|
job={thisJob}
|
||||||
metricName={item.metric}
|
metricName={item.metric}
|
||||||
metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit}
|
metricUnit={globalMetrics.find((gm) => gm.name == item.metric)?.unit}
|
||||||
nativeScope={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.scope}
|
nativeScope={globalMetrics.find((gm) => gm.name == item.metric)?.scope}
|
||||||
presetScopes={item.data.map((x) => x.scope)}
|
presetScopes={item.data.map((x) => x.scope)}
|
||||||
isShared={$initq.data.job.shared != "none"}
|
isShared={thisJob.shared != "none"}
|
||||||
/>
|
/>
|
||||||
{:else if item.disabled == true}
|
{:else if item.disabled == true}
|
||||||
<Card color="info">
|
<Card color="info">
|
||||||
@@ -342,7 +349,7 @@
|
|||||||
<b>Disabled Metric</b>
|
<b>Disabled Metric</b>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<p>Metric <b>{item.metric}</b> is disabled for cluster <b>{$initq.data.job.cluster}:{$initq.data.job.subCluster}</b>.</p>
|
<p>Metric <b>{item.metric}</b> is disabled for cluster <b>{thisJob.cluster}:{thisJob.subCluster}</b>.</p>
|
||||||
<p class="mb-1">To remove this card, open metric selection and press "Close and Apply".</p>
|
<p class="mb-1">To remove this card, open metric selection and press "Close and Apply".</p>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -353,7 +360,7 @@
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<p>No dataset(s) returned for <b>{item.metric}</b>.</p>
|
<p>No dataset(s) returned for <b>{item.metric}</b>.</p>
|
||||||
<p class="mb-1">Metric was not found in metric store for cluster <b>{$initq.data.job.cluster}</b>.</p>
|
<p class="mb-1">Metric was not found in metric store for cluster <b>{thisJob.cluster}</b>.</p>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -374,7 +381,7 @@
|
|||||||
<!-- Metadata && Statistcics Table -->
|
<!-- Metadata && Statistcics Table -->
|
||||||
<Row class="mb-3">
|
<Row class="mb-3">
|
||||||
<Col>
|
<Col>
|
||||||
{#if $initq?.data}
|
{#if thisJob}
|
||||||
<Card>
|
<Card>
|
||||||
<TabContent>
|
<TabContent>
|
||||||
{#if somethingMissing}
|
{#if somethingMissing}
|
||||||
@@ -409,12 +416,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if showStatsTable}
|
{#if showStatsTable}
|
||||||
<!-- Includes <TabPane> Statistics Table with Independent GQL Query -->
|
<!-- Includes <TabPane> Statistics Table with Independent GQL Query -->
|
||||||
<StatsTab job={$initq.data.job} clusters={$initq.data.clusters} tabActive={!somethingMissing}/>
|
<StatsTab job={thisJob} {clusterInfo} {globalMetrics} {ccconfig} tabActive={!somethingMissing}/>
|
||||||
{/if}
|
{/if}
|
||||||
<TabPane tabId="job-script" tab="Job Script">
|
<TabPane tabId="job-script" tab="Job Script">
|
||||||
<div class="pre-wrapper">
|
<div class="pre-wrapper">
|
||||||
{#if $initq.data.job.metaData?.jobScript}
|
{#if thisJob.metaData?.jobScript}
|
||||||
<pre><code>{$initq.data.job.metaData?.jobScript}</code></pre>
|
<pre><code>{thisJob.metaData?.jobScript}</code></pre>
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning">No job script available</Card>
|
<Card body color="warning">No job script available</Card>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -422,8 +429,8 @@
|
|||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane tabId="slurm-info" tab="Slurm Info">
|
<TabPane tabId="slurm-info" tab="Slurm Info">
|
||||||
<div class="pre-wrapper">
|
<div class="pre-wrapper">
|
||||||
{#if $initq.data.job.metaData?.slurmInfo}
|
{#if thisJob.metaData?.slurmInfo}
|
||||||
<pre><code>{$initq.data.job.metaData?.slurmInfo}</code></pre>
|
<pre><code>{thisJob.metaData?.slurmInfo}</code></pre>
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning"
|
<Card body color="warning"
|
||||||
>No additional slurm information available</Card
|
>No additional slurm information available</Card
|
||||||
@@ -437,15 +444,15 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{#if $initq?.data}
|
{#if thisJob}
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
bind:isOpen={isMetricsSelectionOpen}
|
bind:isOpen={isMetricsSelectionOpen}
|
||||||
bind:totalMetrics
|
bind:totalMetrics
|
||||||
presetMetrics={selectedMetrics}
|
presetMetrics={selectedMetrics}
|
||||||
cluster={$initq.data.job.cluster}
|
cluster={thisJob.cluster}
|
||||||
subCluster={$initq.data.job.subCluster}
|
subCluster={thisJob.subCluster}
|
||||||
configName="metricConfig_jobViewPlotMetrics"
|
configName="metricConfig_jobViewPlotMetrics"
|
||||||
preInitialized
|
{globalMetrics}
|
||||||
applyMetrics={(newMetrics) =>
|
applyMetrics={(newMetrics) =>
|
||||||
selectedMetrics = [...newMetrics]
|
selectedMetrics = [...newMetrics]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
const ccconfig = getContext("cc-config");
|
|
||||||
const matchedJobCompareLimit = 500;
|
const matchedJobCompareLimit = 500;
|
||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
@@ -52,26 +51,36 @@
|
|||||||
let isMetricsSelectionOpen = $state(false);
|
let isMetricsSelectionOpen = $state(false);
|
||||||
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
||||||
|
|
||||||
|
/* Derived Init Return */
|
||||||
|
const thisInit = $derived($initq?.data ? true : false);
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
|
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
|
||||||
|
const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
|
||||||
let presetProject = $derived(filterPresets?.project ? filterPresets.project : "");
|
let presetProject = $derived(filterPresets?.project ? filterPresets.project : "");
|
||||||
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
|
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
|
||||||
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
|
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
|
||||||
let metrics = $derived.by(() => {
|
let metrics = $derived.by(() => {
|
||||||
if (selectedCluster) {
|
if (thisInit && ccconfig) {
|
||||||
if (selectedSubCluster) {
|
if (selectedCluster) {
|
||||||
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
|
if (selectedSubCluster) {
|
||||||
ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
|
||||||
|
ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
||||||
|
ccconfig.metricConfig_jobListMetrics
|
||||||
|
}
|
||||||
|
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
||||||
ccconfig.metricConfig_jobListMetrics
|
ccconfig.metricConfig_jobListMetrics
|
||||||
}
|
}
|
||||||
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
return ccconfig.metricConfig_jobListMetrics
|
||||||
ccconfig.metricConfig_jobListMetrics
|
|
||||||
}
|
}
|
||||||
return ccconfig.metricConfig_jobListMetrics
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
let showFootprint = $derived(selectedCluster
|
let showFootprint = $derived((thisInit && ccconfig)
|
||||||
? !!ccconfig[`jobList_showFootprint:${selectedCluster}`]
|
? selectedCluster
|
||||||
: !!ccconfig.jobList_showFootprint
|
? ccconfig[`jobList_showFootprint:${selectedCluster}`]
|
||||||
|
: ccconfig.jobList_showFootprint
|
||||||
|
: {}
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Functions */
|
/* Functions */
|
||||||
@@ -219,6 +228,7 @@
|
|||||||
<Sorting
|
<Sorting
|
||||||
bind:isOpen={isSortingOpen}
|
bind:isOpen={isSortingOpen}
|
||||||
presetSorting={sorting}
|
presetSorting={sorting}
|
||||||
|
{globalMetrics}
|
||||||
applySorting={(newSort) =>
|
applySorting={(newSort) =>
|
||||||
sorting = {...newSort}
|
sorting = {...newSort}
|
||||||
}
|
}
|
||||||
@@ -232,6 +242,7 @@
|
|||||||
subCluster={selectedSubCluster}
|
subCluster={selectedSubCluster}
|
||||||
configName="metricConfig_jobListMetrics"
|
configName="metricConfig_jobListMetrics"
|
||||||
footprintSelect
|
footprintSelect
|
||||||
|
{globalMetrics}
|
||||||
applyMetrics={(newMetrics) =>
|
applyMetrics={(newMetrics) =>
|
||||||
metrics = [...newMetrics]
|
metrics = [...newMetrics]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,14 +49,10 @@
|
|||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
const initialized = getContext("initialized")
|
const client = getContextClient();
|
||||||
const globalMetrics = getContext("globalMetrics")
|
|
||||||
const ccconfig = getContext("cc-config");
|
|
||||||
const clusters = getContext("clusters");
|
|
||||||
const nowEpoch = Date.now();
|
const nowEpoch = Date.now();
|
||||||
const paging = { itemsPerPage: 50, page: 1 };
|
const paging = { itemsPerPage: 50, page: 1 };
|
||||||
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||||
const client = getContextClient();
|
|
||||||
const nodeMetricsQuery = gql`
|
const nodeMetricsQuery = gql`
|
||||||
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
|
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
|
||||||
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
|
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
|
||||||
@@ -112,14 +108,32 @@
|
|||||||
let from = $state(presetFrom ? presetFrom : new Date(nowEpoch - (4 * 3600 * 1000)));
|
let from = $state(presetFrom ? presetFrom : new Date(nowEpoch - (4 * 3600 * 1000)));
|
||||||
// svelte-ignore state_referenced_locally
|
// svelte-ignore state_referenced_locally
|
||||||
let to = $state(presetTo ? presetTo : new Date(nowEpoch));
|
let to = $state(presetTo ? presetTo : new Date(nowEpoch));
|
||||||
let systemUnits = $state({});
|
|
||||||
|
/* Derived Init Return */
|
||||||
|
const thisInit = $derived($initq?.data ? true : false);
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
|
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
|
||||||
|
const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
|
||||||
|
const clusterInfos = $derived(thisInit ? getContext("clusters") : null);
|
||||||
|
|
||||||
const filter = $derived([
|
const filter = $derived([
|
||||||
{ cluster: { eq: cluster } },
|
{ cluster: { eq: cluster } },
|
||||||
{ node: { contains: hostname } },
|
{ node: { contains: hostname } },
|
||||||
{ state: ["running"] },
|
{ state: ["running"] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const systemUnits = $derived.by(() => {
|
||||||
|
const pendingUnits = {};
|
||||||
|
if (thisInit) {
|
||||||
|
const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
|
||||||
|
for (let sm of systemMetrics) {
|
||||||
|
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {...pendingUnits};
|
||||||
|
});
|
||||||
|
|
||||||
const nodeMetricsData = $derived(queryStore({
|
const nodeMetricsData = $derived(queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: nodeMetricsQuery,
|
query: nodeMetricsQuery,
|
||||||
@@ -140,20 +154,6 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const thisNodeState = $derived($nodeMetricsData?.data?.nodeMetrics[0]?.state ? $nodeMetricsData.data.nodeMetrics[0].state : 'notindb');
|
const thisNodeState = $derived($nodeMetricsData?.data?.nodeMetrics[0]?.state ? $nodeMetricsData.data.nodeMetrics[0].state : 'notindb');
|
||||||
|
|
||||||
/* Effect */
|
|
||||||
$effect(() => {
|
|
||||||
loadUnits($initialized);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Functions */
|
|
||||||
function loadUnits(isInitialized) {
|
|
||||||
if (!isInitialized) return
|
|
||||||
const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
|
|
||||||
for (let sm of systemMetrics) {
|
|
||||||
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={{ xs: 2, lg: 5 }}>
|
<Row cols={{ xs: 2, lg: 5 }}>
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
<MetricPlot
|
<MetricPlot
|
||||||
metric={item.name}
|
metric={item.name}
|
||||||
timestep={item.metric.timestep}
|
timestep={item.metric.timestep}
|
||||||
cluster={clusters.find((c) => c.name == cluster)}
|
cluster={clusterInfos.find((c) => c.name == cluster)}
|
||||||
subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster}
|
subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster}
|
||||||
series={item.metric.series}
|
series={item.metric.series}
|
||||||
enableFlip
|
enableFlip
|
||||||
@@ -277,6 +277,7 @@
|
|||||||
.map((m) => ({
|
.map((m) => ({
|
||||||
...m,
|
...m,
|
||||||
disabled: checkMetricDisabled(
|
disabled: checkMetricDisabled(
|
||||||
|
globalMetrics,
|
||||||
m.name,
|
m.name,
|
||||||
cluster,
|
cluster,
|
||||||
$nodeMetricsData.data.nodeMetrics[0].subCluster,
|
$nodeMetricsData.data.nodeMetrics[0].subCluster,
|
||||||
|
|||||||
@@ -51,13 +51,6 @@
|
|||||||
/* Const Init */
|
/* Const Init */
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const ccconfig = getContext("cc-config");
|
|
||||||
const initialized = getContext("initialized");
|
|
||||||
const globalMetrics = getContext("globalMetrics");
|
|
||||||
const resampleConfig = getContext("resampling") || null;
|
|
||||||
|
|
||||||
const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
|
|
||||||
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
|
||||||
const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb'];
|
const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb'];
|
||||||
const nowDate = new Date(Date.now());
|
const nowDate = new Date(Date.now());
|
||||||
|
|
||||||
@@ -65,35 +58,55 @@
|
|||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
let selectedResolution = $state(resampleConfig ? resampleDefault : 0);
|
|
||||||
let hostnameFilter = $state("");
|
let hostnameFilter = $state("");
|
||||||
let hoststateFilter = $state("all");
|
let hoststateFilter = $state("all");
|
||||||
let pendingHostnameFilter = $state("");
|
let pendingHostnameFilter = $state("");
|
||||||
let isMetricsSelectionOpen = $state(false);
|
let isMetricsSelectionOpen = $state(false);
|
||||||
|
|
||||||
|
/* Derived Init Return */
|
||||||
|
const thisInit = $derived($initq?.data ? true : false);
|
||||||
|
|
||||||
/* Derived States */
|
/* Derived States */
|
||||||
|
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
|
||||||
|
const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
|
||||||
|
const resampleConfig = $derived(thisInit ? getContext("resampling") : null);
|
||||||
|
const resampleResolutions = $derived(resampleConfig ? [...resampleConfig.resolutions] : []);
|
||||||
|
const resampleDefault = $derived(resampleConfig ? Math.max(...resampleConfig.resolutions) : 0);
|
||||||
|
const displayNodeOverview = $derived((displayType === 'OVERVIEW'));
|
||||||
|
|
||||||
|
const systemMetrics = $derived(globalMetrics ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
|
||||||
|
const systemUnits = $derived.by(() => {
|
||||||
|
const pendingUnits = {};
|
||||||
|
if (thisInit && systemMetrics.length > 0) {
|
||||||
|
for (let sm of systemMetrics) {
|
||||||
|
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {...pendingUnits};
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedResolution = $derived(resampleDefault);
|
||||||
let to = $derived(presetTo ? presetTo : new Date(Date.now()));
|
let to = $derived(presetTo ? presetTo : new Date(Date.now()));
|
||||||
let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4)));
|
let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4)));
|
||||||
const displayNodeOverview = $derived((displayType === 'OVERVIEW'));
|
|
||||||
const systemMetrics = $derived($initialized ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
|
|
||||||
const presetSystemUnits = $derived(loadUnits(systemMetrics));
|
|
||||||
let selectedMetric = $derived.by(() => {
|
let selectedMetric = $derived.by(() => {
|
||||||
let configKey = `nodeOverview_selectedMetric`;
|
let configKey = `nodeOverview_selectedMetric`;
|
||||||
if (cluster) configKey += `:${cluster}`;
|
if (cluster) configKey += `:${cluster}`;
|
||||||
if (subCluster) configKey += `:${subCluster}`;
|
if (subCluster) configKey += `:${subCluster}`;
|
||||||
|
|
||||||
if ($initialized) {
|
if (thisInit) {
|
||||||
if (ccconfig[configKey]) return ccconfig[configKey]
|
if (ccconfig[configKey]) return ccconfig[configKey]
|
||||||
else if (systemMetrics.length !== 0) return systemMetrics[0].name
|
else if (systemMetrics.length !== 0) return systemMetrics[0].name
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
});
|
});
|
||||||
|
|
||||||
let selectedMetrics = $derived.by(() => {
|
let selectedMetrics = $derived.by(() => {
|
||||||
let configKey = `nodeList_selectedMetrics`;
|
let configKey = `nodeList_selectedMetrics`;
|
||||||
if (cluster) configKey += `:${cluster}`;
|
if (cluster) configKey += `:${cluster}`;
|
||||||
if (subCluster) configKey += `:${subCluster}`;
|
if (subCluster) configKey += `:${subCluster}`;
|
||||||
|
|
||||||
if ($initialized) {
|
if (thisInit) {
|
||||||
if (ccconfig[configKey]) return ccconfig[configKey]
|
if (ccconfig[configKey]) return ccconfig[configKey]
|
||||||
else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
|
else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
|
||||||
}
|
}
|
||||||
@@ -108,16 +121,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* Functions */
|
/* Functions */
|
||||||
function loadUnits(systemMetrics) {
|
|
||||||
let pendingUnits = {};
|
|
||||||
if (systemMetrics.length > 0) {
|
|
||||||
for (let sm of systemMetrics) {
|
|
||||||
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return {...pendingUnits};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wait after input for some time to prevent too many requests
|
// Wait after input for some time to prevent too many requests
|
||||||
function updateHostnameFilter() {
|
function updateHostnameFilter() {
|
||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
@@ -157,7 +160,7 @@
|
|||||||
|
|
||||||
<!-- ROW1: Tools-->
|
<!-- ROW1: Tools-->
|
||||||
<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">
|
<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">
|
||||||
{#if $initq?.data}
|
{#if thisInit}
|
||||||
<!-- List Metric Select Col-->
|
<!-- List Metric Select Col-->
|
||||||
{#if !displayNodeOverview}
|
{#if !displayNodeOverview}
|
||||||
<Col>
|
<Col>
|
||||||
@@ -234,7 +237,7 @@
|
|||||||
<Input type="select" bind:value={selectedMetric}>
|
<Input type="select" bind:value={selectedMetric}>
|
||||||
{#each systemMetrics as metric (metric.name)}
|
{#each systemMetrics as metric (metric.name)}
|
||||||
<option value={metric.name}
|
<option value={metric.name}
|
||||||
>{metric.name} {presetSystemUnits[metric.name] ? "("+presetSystemUnits[metric.name]+")" : ""}</option
|
>{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<option disabled>No available options</option>
|
<option disabled>No available options</option>
|
||||||
@@ -266,10 +269,11 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#if displayNodeOverview}
|
{#if displayNodeOverview}
|
||||||
<!-- ROW2-1: Node Overview (Grid Included)-->
|
<!-- ROW2-1: Node Overview (Grid Included)-->
|
||||||
<NodeOverview {cluster} {ccconfig} {selectedMetric} {from} {to} {hostnameFilter} {hoststateFilter}/>
|
<NodeOverview {cluster} {ccconfig} {selectedMetric} {globalMetrics} {from} {to} {hostnameFilter} {hoststateFilter}/>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- ROW2-2: Node List (Grid Included)-->
|
<!-- ROW2-2: Node List (Grid Included)-->
|
||||||
<NodeList {cluster} {subCluster} {ccconfig} pendingSelectedMetrics={selectedMetrics} {selectedResolution} {hostnameFilter} {hoststateFilter} {from} {to} {presetSystemUnits}/>
|
<NodeList {cluster} {subCluster} {ccconfig} {globalMetrics}
|
||||||
|
pendingSelectedMetrics={selectedMetrics} {selectedResolution} {hostnameFilter} {hoststateFilter} {from} {to} {systemUnits}/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -279,6 +283,7 @@
|
|||||||
presetMetrics={selectedMetrics}
|
presetMetrics={selectedMetrics}
|
||||||
{cluster}
|
{cluster}
|
||||||
{subCluster}
|
{subCluster}
|
||||||
|
{globalMetrics}
|
||||||
configName="nodeList_selectedMetrics"
|
configName="nodeList_selectedMetrics"
|
||||||
applyMetrics={(newMetrics) =>
|
applyMetrics={(newMetrics) =>
|
||||||
selectedMetrics = [...newMetrics]
|
selectedMetrics = [...newMetrics]
|
||||||
|
|||||||
@@ -56,12 +56,10 @@
|
|||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
const ccconfig = getContext("cc-config");
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const durationBinOptions = ["1m","10m","1h","6h","12h"];
|
const durationBinOptions = ["1m","10m","1h","6h","12h"];
|
||||||
const metricBinOptions = [10, 20, 50, 100];
|
const metricBinOptions = [10, 20, 50, 100];
|
||||||
const matchedJobCompareLimit = 500;
|
const matchedJobCompareLimit = 500;
|
||||||
const shortDuration = ccconfig.jobList_hideShortRunningJobs; // Always configured
|
|
||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
// List & Control Vars
|
// List & Control Vars
|
||||||
@@ -73,7 +71,6 @@
|
|||||||
let isSortingOpen = $state(false);
|
let isSortingOpen = $state(false);
|
||||||
let isMetricsSelectionOpen = $state(false);
|
let isMetricsSelectionOpen = $state(false);
|
||||||
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
||||||
let selectedHistogramsBuffer = $state({ all: (ccconfig['userView_histogramMetrics'] || []) })
|
|
||||||
let jobCompare = $state(null);
|
let jobCompare = $state(null);
|
||||||
let matchedCompareJobs = $state(0);
|
let matchedCompareJobs = $state(0);
|
||||||
let showCompare = $state(false);
|
let showCompare = $state(false);
|
||||||
@@ -84,26 +81,48 @@
|
|||||||
let numDurationBins = $state("1h");
|
let numDurationBins = $state("1h");
|
||||||
let numMetricBins = $state(10);
|
let numMetricBins = $state(10);
|
||||||
|
|
||||||
|
/* Derived Init Return */
|
||||||
|
const thisInit = $derived($initq?.data ? true : false);
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
|
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
|
||||||
|
const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
|
||||||
|
const shortDuration = $derived(ccconfig?.jobList_hideShortRunningJobs);
|
||||||
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
|
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
|
||||||
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
|
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
|
||||||
let metrics = $derived.by(() => {
|
let metrics = $derived.by(() => {
|
||||||
if (selectedCluster) {
|
if (thisInit && ccconfig) {
|
||||||
if (selectedSubCluster) {
|
if (selectedCluster) {
|
||||||
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
|
if (selectedSubCluster) {
|
||||||
ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
|
||||||
|
ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
||||||
|
ccconfig.metricConfig_jobListMetrics
|
||||||
|
}
|
||||||
|
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
||||||
ccconfig.metricConfig_jobListMetrics
|
ccconfig.metricConfig_jobListMetrics
|
||||||
}
|
}
|
||||||
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
return ccconfig.metricConfig_jobListMetrics
|
||||||
ccconfig.metricConfig_jobListMetrics
|
|
||||||
}
|
}
|
||||||
return ccconfig.metricConfig_jobListMetrics
|
return [];
|
||||||
});
|
});
|
||||||
let showFootprint = $derived(filterPresets.cluster
|
|
||||||
? !!ccconfig[`jobList_showFootprint:${filterPresets.cluster}`]
|
let showFootprint = $derived((thisInit && ccconfig)
|
||||||
: !!ccconfig.jobList_showFootprint
|
? filterPresets?.cluster
|
||||||
|
? ccconfig[`jobList_showFootprint:${filterPresets.cluster}`]
|
||||||
|
: ccconfig.jobList_showFootprint
|
||||||
|
: {}
|
||||||
);
|
);
|
||||||
let selectedHistograms = $derived(selectedCluster ? selectedHistogramsBuffer[selectedCluster] : selectedHistogramsBuffer['all']);
|
|
||||||
|
let selectedHistograms = $derived.by(() => {
|
||||||
|
if (thisInit && ccconfig) {
|
||||||
|
if (selectedCluster) {
|
||||||
|
return ccconfig[`userView_histogramMetrics:${selectedCluster}`] // No Fallback; Unspecific lists an include unavailable metrics
|
||||||
|
}
|
||||||
|
return ccconfig.userView_histogramMetrics
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
});
|
||||||
|
|
||||||
let stats = $derived(
|
let stats = $derived(
|
||||||
queryStore({
|
queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
@@ -159,19 +178,9 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!selectedHistogramsBuffer[selectedCluster]) {
|
|
||||||
selectedHistogramsBuffer[selectedCluster] = ccconfig[`userView_histogramMetrics:${selectedCluster}`];
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/* On Mount */
|
/* On Mount */
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
filterComponent.updateFilters();
|
filterComponent.updateFilters();
|
||||||
// Why? -> `$derived(ccconfig[$cluster])` only loads array from last Backend-Query if $cluster changed reactively (without reload)
|
|
||||||
if (filterPresets?.cluster) {
|
|
||||||
selectedHistogramsBuffer[filterPresets.cluster] = ccconfig[`userView_histogramMetrics:${filterPresets.cluster}`];
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -508,6 +517,7 @@
|
|||||||
<Sorting
|
<Sorting
|
||||||
bind:isOpen={isSortingOpen}
|
bind:isOpen={isSortingOpen}
|
||||||
presetSorting={sorting}
|
presetSorting={sorting}
|
||||||
|
{globalMetrics}
|
||||||
applySorting={(newSort) =>
|
applySorting={(newSort) =>
|
||||||
sorting = {...newSort}
|
sorting = {...newSort}
|
||||||
}
|
}
|
||||||
@@ -521,6 +531,7 @@
|
|||||||
subCluster={selectedSubCluster}
|
subCluster={selectedSubCluster}
|
||||||
configName="metricConfig_jobListMetrics"
|
configName="metricConfig_jobListMetrics"
|
||||||
footprintSelect
|
footprintSelect
|
||||||
|
{globalMetrics}
|
||||||
applyMetrics={(newMetrics) =>
|
applyMetrics={(newMetrics) =>
|
||||||
metrics = [...newMetrics]
|
metrics = [...newMetrics]
|
||||||
}
|
}
|
||||||
@@ -531,7 +542,8 @@
|
|||||||
bind:isOpen={isHistogramSelectionOpen}
|
bind:isOpen={isHistogramSelectionOpen}
|
||||||
presetSelectedHistograms={selectedHistograms}
|
presetSelectedHistograms={selectedHistograms}
|
||||||
configName="userView_histogramMetrics"
|
configName="userView_histogramMetrics"
|
||||||
|
{globalMetrics}
|
||||||
applyChange={(newSelection) => {
|
applyChange={(newSelection) => {
|
||||||
selectedHistogramsBuffer[selectedCluster || 'all'] = [...newSelection];
|
selectedHistograms = [...newSelection];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,10 +39,6 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const ccconfig = getContext("cc-config");
|
|
||||||
const initialized = getContext("initialized");
|
|
||||||
const globalMetrics = getContext("globalMetrics");
|
|
||||||
const usePaging = ccconfig?.jobList_usePaging || false;
|
|
||||||
const jobInfoColumnWidth = 250;
|
const jobInfoColumnWidth = 250;
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const query = gql`
|
const query = gql`
|
||||||
@@ -100,11 +96,18 @@
|
|||||||
let headerPaddingTop = $state(0);
|
let headerPaddingTop = $state(0);
|
||||||
let jobs = $state([]);
|
let jobs = $state([]);
|
||||||
let page = $state(1);
|
let page = $state(1);
|
||||||
let itemsPerPage = $state(usePaging ? (ccconfig?.jobList_jobsPerPage || 10) : 10);
|
|
||||||
let triggerMetricRefresh = $state(false);
|
let triggerMetricRefresh = $state(false);
|
||||||
let tableWidth = $state(0);
|
let tableWidth = $state(0);
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
|
const initialized = $derived(getContext("initialized") || false);
|
||||||
|
const ccconfig = $derived(initialized ? getContext("cc-config") : null);
|
||||||
|
const globalMetrics = $derived(initialized ? getContext("globalMetrics") : null);
|
||||||
|
const clusterInfos = $derived(initialized ? getContext("clusters"): null);
|
||||||
|
const resampleConfig = $derived(initialized ? getContext("resampling") : null);
|
||||||
|
const usePaging = $derived(ccconfig?.jobList_usePaging || false);
|
||||||
|
|
||||||
|
let itemsPerPage = $derived(usePaging ? (ccconfig?.jobList_jobsPerPage || 10) : 10);
|
||||||
let filter = $derived([...filterBuffer]);
|
let filter = $derived([...filterBuffer]);
|
||||||
let paging = $derived({ itemsPerPage, page });
|
let paging = $derived({ itemsPerPage, page });
|
||||||
const plotWidth = $derived.by(() => {
|
const plotWidth = $derived.by(() => {
|
||||||
@@ -274,7 +277,7 @@
|
|||||||
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
|
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
|
||||||
>
|
>
|
||||||
{metric}
|
{metric}
|
||||||
{#if $initialized}
|
{#if initialized}
|
||||||
({getUnit(metric)})
|
({getUnit(metric)})
|
||||||
{/if}
|
{/if}
|
||||||
</th>
|
</th>
|
||||||
@@ -292,7 +295,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else}
|
||||||
{#each jobs as job (job.id)}
|
{#each jobs as job (job.id)}
|
||||||
<JobListRow {triggerMetricRefresh} {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
|
<JobListRow {triggerMetricRefresh} {job} {metrics} {plotWidth} {showFootprint} {globalMetrics} {clusterInfos} {resampleConfig}
|
||||||
|
previousSelect={selectedJobs.includes(job.id)}
|
||||||
selectJob={(detail) => selectedJobs = [...selectedJobs, detail]}
|
selectJob={(detail) => selectedJobs = [...selectedJobs, detail]}
|
||||||
unselectJob={(detail) => selectedJobs = selectedJobs.filter(item => item !== detail)}
|
unselectJob={(detail) => selectedJobs = selectedJobs.filter(item => item !== detail)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -30,11 +30,10 @@
|
|||||||
setFilter
|
setFilter
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
|
||||||
const clusters = getContext("clusters");
|
|
||||||
const initialized = getContext("initialized");
|
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
|
const initialized = $derived(getContext("initialized") || false);
|
||||||
|
const clusterInfos = $derived($initialized ? getContext("clusters") : null);
|
||||||
let pendingCluster = $derived(presetCluster);
|
let pendingCluster = $derived(presetCluster);
|
||||||
let pendingPartition = $derived(presetPartition);
|
let pendingPartition = $derived(presetPartition);
|
||||||
</script>
|
</script>
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
>
|
>
|
||||||
Any Cluster
|
Any Cluster
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{#each clusters as cluster}
|
{#each clusterInfos as cluster}
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
disabled={disableClusterSelection}
|
disabled={disableClusterSelection}
|
||||||
active={pendingCluster == cluster.name}
|
active={pendingCluster == cluster.name}
|
||||||
@@ -80,7 +79,7 @@
|
|||||||
>
|
>
|
||||||
Any Partition
|
Any Partition
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{#each clusters?.find((c) => c.name == pendingCluster)?.partitions as partition}
|
{#each clusterInfos?.find((c) => c.name == pendingCluster)?.partitions as partition}
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
active={pendingPartition == partition}
|
active={pendingPartition == partition}
|
||||||
onclick={() => (pendingPartition = partition)}
|
onclick={() => (pendingPartition = partition)}
|
||||||
|
|||||||
@@ -42,8 +42,8 @@
|
|||||||
contains: "Contains",
|
contains: "Contains",
|
||||||
}
|
}
|
||||||
|
|
||||||
const findMaxNumAccels = (clusters) =>
|
const findMaxNumAccels = (infos) =>
|
||||||
clusters.reduce(
|
infos.reduce(
|
||||||
(max, cluster) =>
|
(max, cluster) =>
|
||||||
Math.max(
|
Math.max(
|
||||||
max,
|
max,
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Limited to Single-Node Thread Count
|
// Limited to Single-Node Thread Count
|
||||||
const findMaxNumHWThreadsPerNode = (clusters) =>
|
const findMaxNumHWThreadsPerNode = (infos) =>
|
||||||
clusters.reduce(
|
infos.reduce(
|
||||||
(max, cluster) =>
|
(max, cluster) =>
|
||||||
Math.max(
|
Math.max(
|
||||||
max,
|
max,
|
||||||
@@ -92,8 +92,8 @@
|
|||||||
let threadState = $derived(presetNumHWThreads);
|
let threadState = $derived(presetNumHWThreads);
|
||||||
let accState = $derived(presetNumAccelerators);
|
let accState = $derived(presetNumAccelerators);
|
||||||
|
|
||||||
const clusters = $derived(getContext("clusters"));
|
const initialized = $derived(getContext("initialized") || false);
|
||||||
const initialized = $derived(getContext("initialized"));
|
const clusterInfos = $derived($initialized ? getContext("clusters") : null);
|
||||||
// Is Selection Active
|
// Is Selection Active
|
||||||
const nodesActive = $derived(!(JSON.stringify(nodesState) === JSON.stringify({ from: 1, to: maxNumNodes })));
|
const nodesActive = $derived(!(JSON.stringify(nodesState) === JSON.stringify({ from: 1, to: maxNumNodes })));
|
||||||
const threadActive = $derived(!(JSON.stringify(threadState) === JSON.stringify({ from: 1, to: maxNumHWThreads })));
|
const threadActive = $derived(!(JSON.stringify(threadState) === JSON.stringify({ from: 1, to: maxNumHWThreads })));
|
||||||
@@ -109,12 +109,12 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($initialized) {
|
if ($initialized) {
|
||||||
if (activeCluster != null) {
|
if (activeCluster != null) {
|
||||||
const { subClusters } = clusters.find((c) => c.name == activeCluster);
|
const { subClusters } = clusterInfos.find((c) => c.name == activeCluster);
|
||||||
maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
|
maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
|
||||||
maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]);
|
maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]);
|
||||||
} else if (clusters.length > 0) {
|
} else if (clusterInfos.length > 0) {
|
||||||
maxNumAccelerators = findMaxNumAccels(clusters);
|
maxNumAccelerators = findMaxNumAccels(clusterInfos);
|
||||||
maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters);
|
maxNumHWThreads = findMaxNumHWThreadsPerNode(clusterInfos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,8 +31,8 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
const allTags = $derived(getContext("tags"))
|
const initialized = $derived(getContext("initialized") || false)
|
||||||
const initialized = $derived(getContext("initialized"))
|
const allTags = $derived($initialized ? getContext("tags") : [])
|
||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
let searchTerm = $state("");
|
let searchTerm = $state("");
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
const allTags = $derived(getContext('tags'));
|
const initialized = $derived(getContext('initialized') || false);
|
||||||
const initialized = $derived(getContext('initialized'));
|
const allTags = $derived($initialized ? getContext('tags') : []);
|
||||||
|
|
||||||
/* Effects */
|
/* Effects */
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -48,8 +48,6 @@
|
|||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
let initialized = getContext("initialized")
|
|
||||||
let allTags = getContext("tags")
|
|
||||||
let newTagType = $state("");
|
let newTagType = $state("");
|
||||||
let newTagName = $state("");
|
let newTagName = $state("");
|
||||||
let filterTerm = $state("");
|
let filterTerm = $state("");
|
||||||
@@ -57,10 +55,13 @@
|
|||||||
let isOpen = $state(false);
|
let isOpen = $state(false);
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
|
const initialized = $derived(getContext("initialized") || false );
|
||||||
|
let allTags = $derived(initialized ? getContext("tags") : [])
|
||||||
let newTagScope = $derived(username);
|
let newTagScope = $derived(username);
|
||||||
|
|
||||||
const isAdmin = $derived((roles && authlevel == roles.admin));
|
const isAdmin = $derived((roles && authlevel == roles.admin));
|
||||||
const isSupport = $derived((roles && authlevel == roles.support));
|
const isSupport = $derived((roles && authlevel == roles.support));
|
||||||
const allTagsFiltered = $derived(($initialized, jobTags, fuzzySearchTags(filterTerm, allTags))); // $init und JobTags only for triggering react
|
const allTagsFiltered = $derived((initialized, jobTags, fuzzySearchTags(filterTerm, allTags))); // $init und JobTags only for triggering react
|
||||||
const usedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'used', isAdmin, isSupport));
|
const usedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'used', isAdmin, isSupport));
|
||||||
const unusedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'unused', isAdmin, isSupport));
|
const unusedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'unused', isAdmin, isSupport));
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,13 @@
|
|||||||
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query [Default: false]
|
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query [Default: false]
|
||||||
- `selectJob Func`: The callback function to select a job for comparison
|
- `selectJob Func`: The callback function to select a job for comparison
|
||||||
- `unselectJob Func`: The callback function to unselect a job from comparison
|
- `unselectJob Func`: The callback function to unselect a job from comparison
|
||||||
|
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
|
||||||
|
- `clusterInfos [Obj]`: Includes the backend supplied cluster topology
|
||||||
|
- `resampleConfig [Obj]`: Includes the backend supplied resampling info
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import { getContext } from "svelte";
|
|
||||||
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||||
import { maxScope, checkMetricDisabled } from "../utils.js";
|
import { maxScope, checkMetricDisabled } from "../utils.js";
|
||||||
import JobInfo from "./JobInfo.svelte";
|
import JobInfo from "./JobInfo.svelte";
|
||||||
@@ -33,13 +35,13 @@
|
|||||||
triggerMetricRefresh = false,
|
triggerMetricRefresh = false,
|
||||||
selectJob,
|
selectJob,
|
||||||
unselectJob,
|
unselectJob,
|
||||||
|
globalMetrics,
|
||||||
|
clusterInfos,
|
||||||
|
resampleConfig
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const cluster = getContext("clusters");
|
|
||||||
const resampleConfig = getContext("resampling") || null;
|
|
||||||
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
|
||||||
const query = gql`
|
const query = gql`
|
||||||
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) {
|
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) {
|
||||||
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) {
|
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) {
|
||||||
@@ -73,11 +75,11 @@
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
let selectedResolution = $state(resampleDefault);
|
|
||||||
let zoomStates = $state({});
|
let zoomStates = $state({});
|
||||||
let thresholdStates = $state({});
|
let thresholdStates = $state({});
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
|
const resampleDefault = $derived(resampleConfig ? Math.max(...resampleConfig.resolutions) : 0);
|
||||||
const jobId = $derived(job?.id);
|
const jobId = $derived(job?.id);
|
||||||
const scopes = $derived.by(() => {
|
const scopes = $derived.by(() => {
|
||||||
if (job.numNodes == 1) {
|
if (job.numNodes == 1) {
|
||||||
@@ -87,6 +89,8 @@
|
|||||||
return ["node"];
|
return ["node"];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let selectedResolution = $derived(resampleDefault);
|
||||||
let isSelected = $derived(previousSelect);
|
let isSelected = $derived(previousSelect);
|
||||||
let metricsQuery = $derived(queryStore({
|
let metricsQuery = $derived(queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
@@ -94,6 +98,7 @@
|
|||||||
variables: { id: jobId, metrics, scopes, selectedResolution },
|
variables: { id: jobId, metrics, scopes, selectedResolution },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const refinedData = $derived($metricsQuery?.data?.jobMetrics ? sortAndSelectScope($metricsQuery.data.jobMetrics) : []);
|
const refinedData = $derived($metricsQuery?.data?.jobMetrics ? sortAndSelectScope($metricsQuery.data.jobMetrics) : []);
|
||||||
|
|
||||||
/* Effects */
|
/* Effects */
|
||||||
@@ -160,6 +165,7 @@
|
|||||||
return {
|
return {
|
||||||
name: jobMetric.data.name,
|
name: jobMetric.data.name,
|
||||||
disabled: checkMetricDisabled(
|
disabled: checkMetricDisabled(
|
||||||
|
globalMetrics,
|
||||||
jobMetric.data.name,
|
jobMetric.data.name,
|
||||||
job.cluster,
|
job.cluster,
|
||||||
job.subCluster,
|
job.subCluster,
|
||||||
@@ -220,7 +226,7 @@
|
|||||||
series={metric.data.metric.series}
|
series={metric.data.metric.series}
|
||||||
statisticsSeries={metric.data.metric.statisticsSeries}
|
statisticsSeries={metric.data.metric.statisticsSeries}
|
||||||
metric={metric.data.name}
|
metric={metric.data.name}
|
||||||
cluster={cluster.find((c) => c.name == job.cluster)}
|
cluster={clusterInfos.find((c) => c.name == job.cluster)}
|
||||||
subCluster={job.subCluster}
|
subCluster={job.subCluster}
|
||||||
isShared={job.shared != "none"}
|
isShared={job.shared != "none"}
|
||||||
numhwthreads={job.numHWThreads}
|
numhwthreads={job.numHWThreads}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import { formatNumber, formatDurationTime } from "../units.js";
|
import { formatNumber, formatDurationTime } from "../units.js";
|
||||||
import { getContext, onDestroy } from "svelte";
|
import { getContext, onDestroy } from "svelte";
|
||||||
import { Card } from "@sveltestrap/sveltestrap";
|
import { Card, CardHeader, CardBody } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- `ìsOpen Bool`: Is selection opened [Bindable]
|
- `ìsOpen Bool`: Is selection opened [Bindable]
|
||||||
- `configName String`: The config id string to be updated in database on selection change
|
- `configName String`: The config id string to be updated in database on selection change
|
||||||
- `presetSelectedHistograms [String]`: The currently selected metrics to display as histogram
|
- `presetSelectedHistograms [String]`: The currently selected metrics to display as histogram
|
||||||
|
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
|
||||||
- `applyChange Func`: The callback function to apply current selection
|
- `applyChange Func`: The callback function to apply current selection
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@@ -24,10 +25,11 @@
|
|||||||
|
|
||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
cluster,
|
cluster = "",
|
||||||
isOpen = $bindable(),
|
isOpen = $bindable(),
|
||||||
configName,
|
configName,
|
||||||
presetSelectedHistograms,
|
presetSelectedHistograms,
|
||||||
|
globalMetrics,
|
||||||
applyChange
|
applyChange
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -42,11 +44,11 @@
|
|||||||
function loadHistoMetrics(thisCluster) {
|
function loadHistoMetrics(thisCluster) {
|
||||||
// isInit Check Removed: Parent Component has finished Init-Query: Globalmetrics available here.
|
// isInit Check Removed: Parent Component has finished Init-Query: Globalmetrics available here.
|
||||||
if (!thisCluster) {
|
if (!thisCluster) {
|
||||||
return getContext("globalMetrics")
|
return globalMetrics
|
||||||
.filter((gm) => gm?.footprint)
|
.filter((gm) => gm?.footprint)
|
||||||
.map((fgm) => { return fgm.name })
|
.map((fgm) => { return fgm.name })
|
||||||
} else {
|
} else {
|
||||||
return getContext("globalMetrics")
|
return globalMetrics
|
||||||
.filter((gm) => gm?.availability.find((av) => av.cluster == thisCluster))
|
.filter((gm) => gm?.availability.find((av) => av.cluster == thisCluster))
|
||||||
.filter((agm) => agm?.footprint)
|
.filter((agm) => agm?.footprint)
|
||||||
.map((afgm) => { return afgm.name })
|
.map((afgm) => { return afgm.name })
|
||||||
|
|||||||
@@ -9,13 +9,12 @@
|
|||||||
- `cluster String?`: The currently selected cluster [Default: null]
|
- `cluster String?`: The currently selected cluster [Default: null]
|
||||||
- `subCluster String?`: The currently selected subCluster [Default: null]
|
- `subCluster String?`: The currently selected subCluster [Default: null]
|
||||||
- `footprintSelect Bool?`: Render checkbox for footprint display in upstream component [Default: false]
|
- `footprintSelect Bool?`: Render checkbox for footprint display in upstream component [Default: false]
|
||||||
- `preInitialized Bool?`: If the parent component has a dedicated call to init() [Default: false]
|
|
||||||
- `configName String`: The config key for the last saved selection (constant)
|
- `configName String`: The config key for the last saved selection (constant)
|
||||||
|
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
|
||||||
- `applyMetrics Func`: The callback function to apply current selection
|
- `applyMetrics Func`: The callback function to apply current selection
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
@@ -35,14 +34,12 @@
|
|||||||
cluster = null,
|
cluster = null,
|
||||||
subCluster = null,
|
subCluster = null,
|
||||||
footprintSelect = false,
|
footprintSelect = false,
|
||||||
preInitialized = false, // Job View is Pre-Init'd: $initialized "alone" store returns false
|
|
||||||
configName,
|
configName,
|
||||||
|
globalMetrics,
|
||||||
applyMetrics
|
applyMetrics
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const globalMetrics = getContext("globalMetrics");
|
|
||||||
const initialized = getContext("initialized");
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
@@ -58,27 +55,23 @@
|
|||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
let pendingShowFootprint = $state(!!showFootprint);
|
let pendingShowFootprint = $state(!!showFootprint);
|
||||||
let listedMetrics = $state([]);
|
|
||||||
let columnHovering = $state(null);
|
let columnHovering = $state(null);
|
||||||
|
|
||||||
/* Derives States */
|
/* Derives States */
|
||||||
let pendingMetrics = $derived(presetMetrics);
|
const allMetrics = $derived(loadAvailable(globalMetrics));
|
||||||
const allMetrics = $derived(loadAvailable(preInitialized || $initialized));
|
let pendingMetrics = $derived(presetMetrics || []);
|
||||||
|
let listedMetrics = $derived([...presetMetrics, ...allMetrics.difference(new Set(presetMetrics))]); // List (preset) active metrics first, then list inactives
|
||||||
|
|
||||||
/* Reactive Effects */
|
/* Reactive Effects */
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
totalMetrics = allMetrics?.size || 0;
|
totalMetrics = allMetrics?.size || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
listedMetrics = [...presetMetrics, ...allMetrics.difference(new Set(presetMetrics))]; // List (preset) active metrics first, then list inactives
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Functions */
|
/* Functions */
|
||||||
function loadAvailable(init) {
|
function loadAvailable(gms) {
|
||||||
const availableMetrics = new Set();
|
const availableMetrics = new Set();
|
||||||
if (init) {
|
if (gms) {
|
||||||
for (let gm of globalMetrics) {
|
for (let gm of gms) {
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
availableMetrics.add(gm.name)
|
availableMetrics.add(gm.name)
|
||||||
} else {
|
} else {
|
||||||
@@ -90,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return availableMetrics
|
return availableMetrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
function printAvailability(metric, cluster) {
|
function printAvailability(metric, cluster) {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
- `presetSorting Object?`: The latest sort selection state
|
- `presetSorting Object?`: The latest sort selection state
|
||||||
- Default { field: "startTime", type: "col", order: "DESC" }
|
- Default { field: "startTime", type: "col", order: "DESC" }
|
||||||
- `isOpen Bool?`: Is modal opened [Bindable, Default: false]
|
- `isOpen Bool?`: Is modal opened [Bindable, Default: false]
|
||||||
|
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
|
||||||
- `applySorting Func`: The callback function to apply current selection
|
- `applySorting Func`: The callback function to apply current selection
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte";
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Button,
|
Button,
|
||||||
@@ -25,12 +25,11 @@
|
|||||||
let {
|
let {
|
||||||
isOpen = $bindable(false),
|
isOpen = $bindable(false),
|
||||||
presetSorting = { field: "startTime", type: "col", order: "DESC" },
|
presetSorting = { field: "startTime", type: "col", order: "DESC" },
|
||||||
|
globalMetrics,
|
||||||
applySorting
|
applySorting
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const initialized = getContext("initialized");
|
|
||||||
const globalMetrics = getContext("globalMetrics");
|
|
||||||
const fixedSortables = $state([
|
const fixedSortables = $state([
|
||||||
{ field: "startTime", type: "col", text: "Start Time (Default)", order: "DESC" },
|
{ field: "startTime", type: "col", text: "Start Time (Default)", order: "DESC" },
|
||||||
{ field: "duration", type: "col", text: "Duration", order: "DESC" },
|
{ field: "duration", type: "col", text: "Duration", order: "DESC" },
|
||||||
@@ -42,22 +41,11 @@
|
|||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
let activeColumnIdx = $state(0);
|
let activeColumnIdx = $state(0);
|
||||||
let metricSortables = $state([]);
|
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
let sorting = $derived({...presetSorting})
|
let sorting = $derived({...presetSorting})
|
||||||
let sortableColumns = $derived([...fixedSortables, ...metricSortables]);
|
let metricSortables = $derived.by(() => {
|
||||||
|
return globalMetrics.map((gm) => {
|
||||||
/* Effect */
|
|
||||||
$effect(() => {
|
|
||||||
if ($initialized) {
|
|
||||||
loadMetricSortables();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Functions */
|
|
||||||
function loadMetricSortables() {
|
|
||||||
metricSortables = globalMetrics.map((gm) => {
|
|
||||||
if (gm?.footprint) {
|
if (gm?.footprint) {
|
||||||
return {
|
return {
|
||||||
field: gm.name + '_' + gm.footprint,
|
field: gm.name + '_' + gm.footprint,
|
||||||
@@ -68,8 +56,10 @@
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}).filter((r) => r != null)
|
}).filter((r) => r != null)
|
||||||
};
|
});
|
||||||
|
let sortableColumns = $derived([...fixedSortables, ...metricSortables]);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function loadActiveIndex() {
|
function loadActiveIndex() {
|
||||||
activeColumnIdx = sortableColumns.findIndex(
|
activeColumnIdx = sortableColumns.findIndex(
|
||||||
(col) => col.field == sorting.field,
|
(col) => col.field == sorting.field,
|
||||||
|
|||||||
@@ -302,19 +302,17 @@ export function stickyHeader(datatableHeaderSelector, updatePading) {
|
|||||||
onDestroy(() => document.removeEventListener("scroll", onscroll));
|
onDestroy(() => document.removeEventListener("scroll", onscroll));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkMetricDisabled(m, c, s) { // [m]etric, [c]luster, [s]ubcluster
|
export function checkMetricDisabled(gm, m, c, s) { // [g]lobal[m]etrics, [m]etric, [c]luster, [s]ubcluster
|
||||||
const metrics = getContext("globalMetrics");
|
const available = gm?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s)
|
||||||
const available = metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s)
|
|
||||||
// Return inverse logic
|
// Return inverse logic
|
||||||
return !available
|
return !available
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkMetricsDisabled(ma, c, s) { // [m]etric[a]rray, [c]luster, [s]ubcluster
|
export function checkMetricsDisabled(gm, ma, c, s) { // [g]lobal[m]etrics, [m]etric[a]rray, [c]luster, [s]ubcluster
|
||||||
let result = {};
|
let result = {};
|
||||||
const metrics = getContext("globalMetrics");
|
|
||||||
ma.forEach((m) => {
|
ma.forEach((m) => {
|
||||||
// Return named inverse logic: !available
|
// Return named inverse logic: !available
|
||||||
result[m] = !(metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s))
|
result[m] = !(gm?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s))
|
||||||
});
|
});
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `job Object`: The GQL job object
|
- `job Object`: The GQL job object
|
||||||
- `clusters Array`: The GQL clusters array
|
- `clusterInfo Array`: The GQL clusters array
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
job,
|
job,
|
||||||
clusters,
|
clusterInfo,
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<div bind:clientWidth={roofWidth}>
|
<div bind:clientWidth={roofWidth}>
|
||||||
<Roofline
|
<Roofline
|
||||||
width={roofWidth}
|
width={roofWidth}
|
||||||
subCluster={clusters
|
subCluster={clusterInfo
|
||||||
.find((c) => c.name == job.cluster)
|
.find((c) => c.name == job.cluster)
|
||||||
.subClusters.find((sc) => sc.name == job.subCluster)}
|
.subClusters.find((sc) => sc.name == job.subCluster)}
|
||||||
data={transformDataForRoofline(
|
data={transformDataForRoofline(
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `job Object`: The job object
|
- `job Object`: The job object
|
||||||
- `clusters Object`: The clusters object
|
- `clusterInfo Object`: The clusters object
|
||||||
- `tabActive bool`: Boolean if StatsTabe Tab is Active on Creation
|
- `tabActive bool`: Boolean if StatsTabe Tab is Active on Creation
|
||||||
|
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
|
||||||
|
- `ccconfig Object?`: The ClusterCockpit Config Context
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -13,7 +15,6 @@
|
|||||||
gql,
|
gql,
|
||||||
getContextClient
|
getContextClient
|
||||||
} from "@urql/svelte";
|
} from "@urql/svelte";
|
||||||
import { getContext } from "svelte";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Button,
|
Button,
|
||||||
@@ -29,8 +30,10 @@
|
|||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
job,
|
job,
|
||||||
clusters,
|
clusterInfo,
|
||||||
tabActive,
|
tabActive,
|
||||||
|
globalMetrics,
|
||||||
|
ccconfig
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
@@ -55,65 +58,73 @@
|
|||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
let moreScopes = $state(false);
|
let moreScopes = $state(false);
|
||||||
let selectedScopes = $state([]);
|
|
||||||
let selectedMetrics = $state([]);
|
|
||||||
let totalMetrics = $state(0); // For Info Only, filled by MetricSelection Component
|
let totalMetrics = $state(0); // For Info Only, filled by MetricSelection Component
|
||||||
let isMetricSelectionOpen = $state(false);
|
let isMetricSelectionOpen = $state(false);
|
||||||
|
|
||||||
/* Derived */
|
/* Derived Var Preprocessing*/
|
||||||
|
let selectedTableMetrics = $derived.by(() => {
|
||||||
|
if(job && ccconfig) {
|
||||||
|
if (job.cluster) {
|
||||||
|
if (job.subCluster) {
|
||||||
|
return ccconfig[`metricConfig_jobViewTableMetrics:${job.cluster}:${job.subCluster}`] ||
|
||||||
|
ccconfig[`metricConfig_jobViewTableMetrics:${job.cluster}`] ||
|
||||||
|
ccconfig.metricConfig_jobViewTableMetrics
|
||||||
|
}
|
||||||
|
return ccconfig[`metricConfig_jobViewTableMetrics:${job.cluster}`] ||
|
||||||
|
ccconfig.metricConfig_jobViewTableMetrics
|
||||||
|
}
|
||||||
|
return ccconfig.metricConfig_jobViewTableMetrics
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedTableScopes = $derived.by(() => {
|
||||||
|
if (job) {
|
||||||
|
if (!moreScopes) {
|
||||||
|
// Select default Scopes to load: Check before if any metric has accelerator scope by default
|
||||||
|
const pendingScopes = ["node"]
|
||||||
|
const accScopeDefault = [...selectedTableMetrics].some(function (m) {
|
||||||
|
const cluster = clusterInfo.find((c) => c.name == job.cluster);
|
||||||
|
const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster);
|
||||||
|
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (job.numNodes === 1) {
|
||||||
|
pendingScopes.push("socket")
|
||||||
|
pendingScopes.push("core")
|
||||||
|
pendingScopes.push("hwthread")
|
||||||
|
if (accScopeDefault) { pendingScopes.push("accelerator") }
|
||||||
|
}
|
||||||
|
return[...new Set(pendingScopes)];
|
||||||
|
} else {
|
||||||
|
// If flag set: Always load all scopes
|
||||||
|
return ["node", "socket", "core", "hwthread", "accelerator"];
|
||||||
|
}
|
||||||
|
} // Fallback
|
||||||
|
return ["node"]
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Derived Query */
|
||||||
const scopedStats = $derived(queryStore({
|
const scopedStats = $derived(queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: query,
|
query: query,
|
||||||
variables: { dbid: job.id, selectedMetrics, selectedScopes },
|
variables: {
|
||||||
|
dbid: job.id,
|
||||||
|
selectedMetrics: selectedTableMetrics,
|
||||||
|
selectedScopes: selectedTableScopes
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Functions */
|
|
||||||
function loadScopes() {
|
|
||||||
// Archived Jobs Load All Scopes By Default (See Backend)
|
|
||||||
moreScopes = true;
|
|
||||||
selectedScopes = ["node", "socket", "core", "hwthread", "accelerator"];
|
|
||||||
};
|
|
||||||
|
|
||||||
/* On Init */
|
|
||||||
// Handle Job Query on Init -> is not executed anymore
|
|
||||||
getContext("on-init")(() => {
|
|
||||||
if (!job) return;
|
|
||||||
|
|
||||||
const pendingMetrics = (
|
|
||||||
getContext("cc-config")[`metricConfig_jobViewTableMetrics:${job.cluster}:${job.subCluster}`] ||
|
|
||||||
getContext("cc-config")[`metricConfig_jobViewTableMetrics:${job.cluster}`]
|
|
||||||
) || getContext("cc-config")["metricConfig_jobViewTableMetrics"];
|
|
||||||
|
|
||||||
// Select default Scopes to load: Check before if any metric has accelerator scope by default
|
|
||||||
const accScopeDefault = [...pendingMetrics].some(function (m) {
|
|
||||||
const cluster = clusters.find((c) => c.name == job.cluster);
|
|
||||||
const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster);
|
|
||||||
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
|
|
||||||
});
|
|
||||||
|
|
||||||
const pendingScopes = ["node"]
|
|
||||||
if (job.numNodes === 1) {
|
|
||||||
pendingScopes.push("socket")
|
|
||||||
pendingScopes.push("core")
|
|
||||||
pendingScopes.push("hwthread")
|
|
||||||
if (accScopeDefault) { pendingScopes.push("accelerator") }
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedMetrics = [...pendingMetrics];
|
|
||||||
selectedScopes = [...pendingScopes];
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}>
|
<TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}>
|
||||||
<Row>
|
<Row>
|
||||||
<Col class="m-2">
|
<Col class="m-2">
|
||||||
<Button outline onclick={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem">
|
<Button outline onclick={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem">
|
||||||
Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
|
Select Metrics (Selected {selectedTableMetrics.length} of {totalMetrics} available)
|
||||||
</Button>
|
</Button>
|
||||||
{#if job.numNodes > 1 && job.state === "running"}
|
{#if job.numNodes > 1 && job.state === "running"}
|
||||||
<Button class="px-2 ml-auto" color="success" outline onclick={loadScopes} disabled={moreScopes}>
|
<Button class="px-2 ml-auto" color="success" outline onclick={() => (moreScopes = !moreScopes)} disabled={moreScopes}>
|
||||||
{#if !moreScopes}
|
{#if !moreScopes}
|
||||||
<Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes
|
<Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes
|
||||||
{:else}
|
{:else}
|
||||||
@@ -141,7 +152,7 @@
|
|||||||
<StatsTable
|
<StatsTable
|
||||||
hosts={job.resources.map((r) => r.hostname).sort()}
|
hosts={job.resources.map((r) => r.hostname).sort()}
|
||||||
jobStats={$scopedStats?.data?.scopedJobStats}
|
jobStats={$scopedStats?.data?.scopedJobStats}
|
||||||
{selectedMetrics}
|
selectedMetrics={selectedTableMetrics}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
@@ -149,12 +160,12 @@
|
|||||||
<MetricSelection
|
<MetricSelection
|
||||||
bind:isOpen={isMetricSelectionOpen}
|
bind:isOpen={isMetricSelectionOpen}
|
||||||
bind:totalMetrics
|
bind:totalMetrics
|
||||||
presetMetrics={selectedMetrics}
|
presetMetrics={selectedTableMetrics}
|
||||||
cluster={job.cluster}
|
cluster={job.cluster}
|
||||||
subCluster={job.subCluster}
|
subCluster={job.subCluster}
|
||||||
configName="metricConfig_jobViewTableMetrics"
|
configName="metricConfig_jobViewTableMetrics"
|
||||||
preInitialized
|
{globalMetrics}
|
||||||
applyMetrics={(newMetrics) =>
|
applyMetrics={(newMetrics) =>
|
||||||
selectedMetrics = [...newMetrics]
|
selectedTableMetrics = [...newMetrics]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
- `cluster String`: The nodes' cluster
|
- `cluster String`: The nodes' cluster
|
||||||
- `subCluster String`: The nodes' subCluster [Default: ""]
|
- `subCluster String`: The nodes' subCluster [Default: ""]
|
||||||
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
|
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
|
||||||
|
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
|
||||||
- `pendingSelectedMetrics [String]`: The array of selected metrics [Default []]
|
- `pendingSelectedMetrics [String]`: The array of selected metrics [Default []]
|
||||||
- `selectedResolution Number?`: The selected data resolution [Default: 0]
|
- `selectedResolution Number?`: The selected data resolution [Default: 0]
|
||||||
- `hostnameFilter String?`: The active hostnamefilter [Default: ""]
|
- `hostnameFilter String?`: The active hostnamefilter [Default: ""]
|
||||||
- `hoststateFilter String?`: The active hoststatefilter [Default: ""]
|
- `hoststateFilter String?`: The active hoststatefilter [Default: ""]
|
||||||
- `presetSystemUnits Object`: The object of metric units [Default: null]
|
- `systemUnits Object`: The object of metric units [Default: null]
|
||||||
- `from Date?`: The selected "from" date [Default: null]
|
- `from Date?`: The selected "from" date [Default: null]
|
||||||
- `to Date?`: The selected "to" date [Default: null]
|
- `to Date?`: The selected "to" date [Default: null]
|
||||||
-->
|
-->
|
||||||
@@ -27,11 +28,12 @@
|
|||||||
cluster,
|
cluster,
|
||||||
subCluster = "",
|
subCluster = "",
|
||||||
ccconfig = null,
|
ccconfig = null,
|
||||||
|
globalMetrics = null,
|
||||||
pendingSelectedMetrics = [],
|
pendingSelectedMetrics = [],
|
||||||
selectedResolution = 0,
|
selectedResolution = 0,
|
||||||
hostnameFilter = "",
|
hostnameFilter = "",
|
||||||
hoststateFilter = "",
|
hoststateFilter = "",
|
||||||
presetSystemUnits = null,
|
systemUnits = null,
|
||||||
from = null,
|
from = null,
|
||||||
to = null
|
to = null
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -236,7 +238,7 @@
|
|||||||
scope="col"
|
scope="col"
|
||||||
style="padding-top: {headerPaddingTop}px"
|
style="padding-top: {headerPaddingTop}px"
|
||||||
>
|
>
|
||||||
{metric} ({presetSystemUnits[metric]})
|
{metric} ({systemUnits[metric]})
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -250,7 +252,7 @@
|
|||||||
</Row>
|
</Row>
|
||||||
{:else}
|
{:else}
|
||||||
{#each nodes as nodeData (nodeData.host)}
|
{#each nodes as nodeData (nodeData.host)}
|
||||||
<NodeListRow {nodeData} {cluster} {selectedMetrics}/>
|
<NodeListRow {nodeData} {cluster} {selectedMetrics} {globalMetrics}/>
|
||||||
{:else}
|
{:else}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan={selectedMetrics.length + 1}> No nodes found </td>
|
<td colspan={selectedMetrics.length + 1}> No nodes found </td>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
- `hostnameFilter String?`: The active hoststatefilter [Default: ""]
|
- `hostnameFilter String?`: The active hoststatefilter [Default: ""]
|
||||||
- `from Date?`: The selected "from" date [Default: null]
|
- `from Date?`: The selected "from" date [Default: null]
|
||||||
- `to Date?`: The selected "to" date [Default: null]
|
- `to Date?`: The selected "to" date [Default: null]
|
||||||
|
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
|
||||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import { Row, Col, Card, CardHeader, CardBody, Spinner, Badge } from "@sveltestrap/sveltestrap";
|
import { Row, Col, Card, CardHeader, CardBody, Spinner, Badge } from "@sveltestrap/sveltestrap";
|
||||||
import { checkMetricDisabled } from "../generic/utils.js";
|
import { checkMetricDisabled } from "../generic/utils.js";
|
||||||
@@ -26,11 +26,11 @@
|
|||||||
hostnameFilter = "",
|
hostnameFilter = "",
|
||||||
hoststateFilter = "",
|
hoststateFilter = "",
|
||||||
from = null,
|
from = null,
|
||||||
to = null
|
to = null,
|
||||||
|
globalMetrics
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const initialized = getContext("initialized");
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
// Node State Colors
|
// Node State Colors
|
||||||
const stateColors = {
|
const stateColors = {
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mappedData = $derived(handleQueryData($initialized, $nodesQuery?.data));
|
const mappedData = $derived(handleQueryData($nodesQuery?.data));
|
||||||
const filteredData = $derived(mappedData.filter((h) => {
|
const filteredData = $derived(mappedData.filter((h) => {
|
||||||
if (hostnameFilter) {
|
if (hostnameFilter) {
|
||||||
if (hoststateFilter == 'all') return h.host.includes(hostnameFilter)
|
if (hoststateFilter == 'all') return h.host.includes(hostnameFilter)
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
/* Functions */
|
/* Functions */
|
||||||
function handleQueryData(isInitialized, queryData) {
|
function handleQueryData(queryData) {
|
||||||
let rawData = []
|
let rawData = []
|
||||||
if (queryData) {
|
if (queryData) {
|
||||||
rawData = queryData.nodeMetrics.filter((h) => {
|
rawData = queryData.nodeMetrics.filter((h) => {
|
||||||
@@ -120,7 +120,8 @@
|
|||||||
data: h.metrics.filter(
|
data: h.metrics.filter(
|
||||||
(m) => m?.name == selectedMetric && m.scope == "node",
|
(m) => m?.name == selectedMetric && m.scope == "node",
|
||||||
),
|
),
|
||||||
disabled: isInitialized ? checkMetricDisabled(selectedMetric, cluster, h.subCluster) : null,
|
// TODO: Move To New Func Variant With Disabled Check on WHole Cluster Level: This never Triggers!
|
||||||
|
disabled: checkMetricDisabled(globalMetrics, selectedMetric, cluster, h.subCluster),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.host.localeCompare(b.host))
|
.sort((a, b) => a.host.localeCompare(b.host))
|
||||||
}
|
}
|
||||||
@@ -163,6 +164,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if item?.data}
|
{#if item?.data}
|
||||||
{#if item.disabled === true}
|
{#if item.disabled === true}
|
||||||
|
<!-- TODO: Will never be Shown: Overview Single Metric Return Will be Null, see Else Case-->
|
||||||
<Card body class="mx-3" color="info"
|
<Card body class="mx-3" color="info"
|
||||||
>Metric disabled for subcluster <code
|
>Metric disabled for subcluster <code
|
||||||
>{selectedMetric}:{item.subCluster}</code
|
>{selectedMetric}:{item.subCluster}</code
|
||||||
@@ -182,7 +184,7 @@
|
|||||||
enableFlip
|
enableFlip
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{:else if item.disabled === null}
|
{:else}
|
||||||
<Card body class="mx-3" color="info">
|
<Card body class="mx-3" color="info">
|
||||||
Global Metric List Not Initialized
|
Global Metric List Not Initialized
|
||||||
Can not determine {selectedMetric} availability: Please Reload Page
|
Can not determine {selectedMetric} availability: Please Reload Page
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
- `cluster String`: The nodes' cluster
|
- `cluster String`: The nodes' cluster
|
||||||
- `nodeData Object`: The node data object including metric data
|
- `nodeData Object`: The node data object including metric data
|
||||||
- `selectedMetrics [String]`: The array of selected metrics
|
- `selectedMetrics [String]`: The array of selected metrics
|
||||||
|
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
cluster,
|
cluster,
|
||||||
nodeData,
|
nodeData,
|
||||||
selectedMetrics,
|
selectedMetrics,
|
||||||
|
globalMetrics
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Var Init*/
|
/* Var Init*/
|
||||||
@@ -92,6 +94,7 @@
|
|||||||
if (scopedNodeMetric?.data) {
|
if (scopedNodeMetric?.data) {
|
||||||
return {
|
return {
|
||||||
disabled: checkMetricDisabled(
|
disabled: checkMetricDisabled(
|
||||||
|
globalMetrics,
|
||||||
scopedNodeMetric.data.name,
|
scopedNodeMetric.data.name,
|
||||||
cluster,
|
cluster,
|
||||||
nodeData.subCluster,
|
nodeData.subCluster,
|
||||||
|
|||||||
Reference in New Issue
Block a user