diff --git a/internal/cli/admin_command.go b/internal/cli/admin_command.go index f42bec08d1..2d763d52f8 100644 --- a/internal/cli/admin_command.go +++ b/internal/cli/admin_command.go @@ -2042,7 +2042,7 @@ func (c *CLI) AdminShowUserStorageCommand(cmd *Command) (ResponseIf, error) { } encodedUserName := common.EncodeEmail(email) - apiURL := fmt.Sprintf("/admin/users/%s/admin", encodedUserName) + apiURL := fmt.Sprintf("/admin/users/%s/storage", encodedUserName) resp, err := c.AdminServerClient.Request("GET", apiURL, "admin", nil, nil) if err != nil { @@ -2053,7 +2053,7 @@ func (c *CLI) AdminShowUserStorageCommand(cmd *Command) (ResponseIf, error) { return nil, fmt.Errorf("failed to get user storage: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) } - var result CommonDataResponse + var result UserStorageResponse if err = json.Unmarshal(resp.Body, &result); err != nil { return nil, fmt.Errorf("get user storage failed: invalid JSON (%w)", err) } @@ -2089,9 +2089,9 @@ func (c *CLI) AdminShowUserQuotaCommand(cmd *Command) (ResponseIf, error) { return nil, fmt.Errorf("failed to get user storage: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) } - var result CommonDataResponse + var result UserQuotaResponse if err = json.Unmarshal(resp.Body, &result); err != nil { - return nil, fmt.Errorf("get user storage failed: invalid JSON (%w)", err) + return nil, fmt.Errorf("get user quota failed: invalid JSON (%w)", err) } if result.Code != 0 { @@ -2125,9 +2125,9 @@ func (c *CLI) AdminShowUserIndexCommand(cmd *Command) (ResponseIf, error) { return nil, fmt.Errorf("failed to get user storage: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) } - var result CommonDataResponse + var result UserIndexResponse if err = json.Unmarshal(resp.Body, &result); err != nil { - return nil, fmt.Errorf("get user storage failed: invalid JSON (%w)", err) + return nil, fmt.Errorf("get user index failed: invalid JSON (%w)", err) } if result.Code != 0 { diff --git a/internal/cli/response.go b/internal/cli/response.go index 5fd19a21d9..076a010555 100644 --- a/internal/cli/response.go +++ b/internal/cli/response.go @@ -88,14 +88,6 @@ func (r *ModelsResponse) PrintOut() { } } -type UserIndexResponse struct { - InternalData CommonDataResponse -} - -func (r *UserIndexResponse) TimeCost() float64 { - return r.InternalData.Duration -} - type CommonDataResponse struct { Code int `json:"code"` Data map[string]interface{} `json:"data"` @@ -116,6 +108,24 @@ func (r *CommonDataResponse) SetOutputFormat(format OutputFormat) { r.OutputFormat = format } +func (r *CommonDataResponse) orderedMetricTable() []map[string]interface{} { + table := make([]map[string]interface{}, 0) + if orderRaw, ok := r.Data["_order"]; ok { + if orderSlice, ok := orderRaw.([]interface{}); ok { + for _, keyRaw := range orderSlice { + key := fmt.Sprintf("%v", keyRaw) + if value, exists := r.Data[key]; exists { + table = append(table, map[string]interface{}{ + "Metric": key, + "Value": value, + }) + } + } + } + } + return table +} + func (r *CommonDataResponse) PrintOut() { if r.Code == 0 { table := make([]map[string]interface{}, 0) @@ -789,6 +799,142 @@ func chunkDocName(c map[string]interface{}) string { return "" } +type UserIndexResponse struct { + CommonDataResponse +} + +func (r *UserIndexResponse) Type() string { + return "user_index" +} + +func (r *UserIndexResponse) PrintOut() { + if r.Code != 0 { + fmt.Println("ERROR") + fmt.Printf("%d, %s\n", r.Code, r.Message) + return + } + + summaryTable := r.orderedMetricTable() + indexColumns := []string{"index", "health", "status", "docs.count", "dataset.size", "store.size"} + indexTable := make([]map[string]interface{}, 0) + indicesRaw, hasIndices := r.Data["indices"] + if hasIndices { + if indices, ok := indicesRaw.([]interface{}); ok { + for _, idx := range indices { + if m, ok := idx.(map[string]interface{}); ok { + orderedRow := make(map[string]interface{}) + for _, col := range indexColumns { + if v, exists := m[col]; exists { + orderedRow[col] = v + } else { + orderedRow[col] = "" + } + } + indexTable = append(indexTable, orderedRow) + } + } + } + } + + if r.OutputFormat == OutputFormatJSON { + payload := make(map[string]interface{}) + if len(summaryTable) > 0 { + payload["summary"] = summaryTable + } + if len(indexTable) > 0 { + payload["indices"] = indexTable + } + jsonData, err := json.MarshalIndent(payload, "", " ") + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + return + } + fmt.Println(string(jsonData)) + return + } + + if len(summaryTable) > 0 { + PrintTableSimpleByFormat(summaryTable, r.OutputFormat) + } + + if len(indexTable) > 0 { + fmt.Println() + fmt.Println("Index Details:") + PrintTableSimpleByFormatWithOrder(indexTable, indexColumns, r.OutputFormat) + } else if hasIndices { + fmt.Println() + fmt.Println("No indices found for this user.") + } +} + +type UserStorageResponse struct { + CommonDataResponse +} + +func (r *UserStorageResponse) Type() string { + return "user_storage" +} + +func (r *UserStorageResponse) PrintOut() { + if r.Code != 0 { + fmt.Println("ERROR") + fmt.Printf("%d, %s\n", r.Code, r.Message) + return + } + + summaryTable := r.orderedMetricTable() + fileColumns := []string{"name", "size"} + fileTable := make([]map[string]interface{}, 0) + filesRaw, hasFiles := r.Data["files"] + if hasFiles { + if files, ok := filesRaw.([]interface{}); ok { + for _, f := range files { + if m, ok := f.(map[string]interface{}); ok { + orderedRow := make(map[string]interface{}) + for _, col := range fileColumns { + if v, exists := m[col]; exists { + orderedRow[col] = v + } else { + orderedRow[col] = "" + } + } + fileTable = append(fileTable, orderedRow) + } + } + } + } + + if r.OutputFormat == OutputFormatJSON { + payload := make(map[string]interface{}) + if len(summaryTable) > 0 { + payload["summary"] = summaryTable + } + if len(fileTable) > 0 { + payload["files"] = fileTable + } + jsonData, err := json.MarshalIndent(payload, "", " ") + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + return + } + fmt.Println(string(jsonData)) + return + } + + if len(summaryTable) > 0 { + PrintTableSimpleByFormat(summaryTable, r.OutputFormat) + } + + if len(fileTable) > 0 { + fmt.Println() + fmt.Println("Files(Top 10):") + PrintTableSimpleByFormatWithOrder(fileTable, fileColumns, r.OutputFormat) + } else if hasFiles { + fmt.Println() + fmt.Println("No files found for this user.") + } +} + func truncateStr(s string, maxLen int) string { s = strings.TrimSpace(s) runes := []rune(s) @@ -797,3 +943,33 @@ func truncateStr(s string, maxLen int) string { } return string(runes[:maxLen]) + "..." } + +type UserQuotaResponse struct { + CommonDataResponse +} + +func (r *UserQuotaResponse) Type() string { + return "user_quota" +} + +func (r *UserQuotaResponse) PrintOut() { + if r.Code != 0 { + fmt.Println("ERROR") + fmt.Printf("%d, %s\n", r.Code, r.Message) + return + } + + summaryTable := make([]map[string]interface{}, 0) + if rowsRaw, ok := r.Data["rows"]; ok { + if rows, ok := rowsRaw.([]interface{}); ok { + for _, row := range rows { + if m, ok := row.(map[string]interface{}); ok { + summaryTable = append(summaryTable, m) + } + } + } + } + if len(summaryTable) > 0 { + PrintTableSimpleByFormatWithOrder(summaryTable, []string{"Metric", "Used", "Limit"}, r.OutputFormat) + } +} diff --git a/internal/cli/table.go b/internal/cli/table.go index 18fad5fa1a..494c2bbe18 100644 --- a/internal/cli/table.go +++ b/internal/cli/table.go @@ -188,8 +188,119 @@ func PrintTableSimpleByFormat(data []map[string]interface{}, format OutputFormat valueWidth = getStringWidth(value) } // Pad to column width - padding := colWidths[col] - valueWidth + len(value) - rowParts = append(rowParts, fmt.Sprintf(" %-*s ", padding, value)) + rowParts = append(rowParts, fmt.Sprintf(" %s ", padCell(value, colWidths[col], false))) + } + fmt.Println("|" + strings.Join(rowParts, "|") + "|") + } + + fmt.Println(separator) + } +} + +// PrintTableSimpleByFormatWithOrder prints data with columns in the specified order +func PrintTableSimpleByFormatWithOrder(data []map[string]interface{}, columns []string, format OutputFormat) { + if len(data) == 0 { + if format == OutputFormatJSON { + fmt.Println("[]") + } else if format == OutputFormatPlain { + fmt.Println("(empty)") + } else { + fmt.Println("No data to print") + } + return + } + + if format == OutputFormatJSON { + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + return + } + fmt.Println(string(jsonData)) + return + } + + colIsNumeric := make(map[string]bool) + for _, col := range columns { + isNumeric := true + for _, item := range data { + if val, ok := item[col]; ok { + if !isNumericValue(val) { + isNumeric = false + break + } + } + } + colIsNumeric[col] = isNumeric + } + + colWidths := make(map[string]int) + for _, col := range columns { + maxWidth := getStringWidth(strings.ToLower(col)) + for _, item := range data { + value := formatValue(item[col]) + valueWidth := getStringWidth(value) + if valueWidth > maxWidth { + maxWidth = valueWidth + } + } + if maxWidth > maxColWidth { + maxWidth = maxColWidth + } + if maxWidth < 2 { + maxWidth = 2 + } + colWidths[col] = maxWidth + } + + if format == OutputFormatPlain { + headerParts := make([]string, 0, len(columns)) + for _, col := range columns { + headerParts = append(headerParts, padCell(strings.ToLower(col), colWidths[col], colIsNumeric[col])) + } + fmt.Println(strings.Join(headerParts, " ")) + + for _, item := range data { + rowParts := make([]string, 0, len(columns)) + for _, col := range columns { + value := formatValue(item[col]) + valueWidth := getStringWidth(value) + if valueWidth > colWidths[col] { + runes := []rune(value) + value = truncateStringByWidth(runes, colWidths[col]) + valueWidth = getStringWidth(value) + } + rowParts = append(rowParts, padCell(value, colWidths[col], colIsNumeric[col])) + } + fmt.Println(strings.Join(rowParts, " ")) + } + } else { + separatorParts := make([]string, 0, len(columns)) + for _, col := range columns { + separatorParts = append(separatorParts, strings.Repeat("-", colWidths[col]+2)) + } + separator := "+" + strings.Join(separatorParts, "+") + "+" + + fmt.Println(separator) + headerParts := make([]string, 0, len(columns)) + for _, col := range columns { + headerParts = append(headerParts, fmt.Sprintf(" %-*s ", colWidths[col], col)) + } + fmt.Println("|" + strings.Join(headerParts, "|") + "|") + fmt.Println(separator) + + for _, item := range data { + rowParts := make([]string, 0, len(columns)) + for _, col := range columns { + value := formatValue(item[col]) + valueWidth := getStringWidth(value) + if valueWidth > colWidths[col] { + runes := []rune(value) + value = truncateStringByWidth(runes, colWidths[col]) + valueWidth = getStringWidth(value) + } + rowParts = append(rowParts, fmt.Sprintf(" %s ", padCell(value, colWidths[col], false))) + } fmt.Println("|" + strings.Join(rowParts, "|") + "|") } @@ -293,5 +404,3 @@ func isHalfWidth(r rune) bool { } return false } - - diff --git a/internal/engine/elasticsearch/client.go b/internal/engine/elasticsearch/client.go index cee7dff18c..0a2bf8c46a 100644 --- a/internal/engine/elasticsearch/client.go +++ b/internal/engine/elasticsearch/client.go @@ -159,7 +159,7 @@ func (e *elasticsearchEngine) CreateIndexTemplate(ctx context.Context, templateN // Build template body with proper structure templateBody := map[string]interface{}{ "index_patterns": []string{indexPattern}, - "priority": p, // Configurable priority to override existing templates + "priority": p, // Configurable priority to override existing templates "template": map[string]interface{}{ "settings": templateSettings, "mappings": templateMappings, @@ -376,3 +376,38 @@ func extractErrorReason(bodyBytes []byte) string { return "" } + +// GetIndexStats gets statistics for specified indices using the _cat/indices API +// Returns index, health, status, docs.count, store.size, dataset.size for each index +func (e *elasticsearchEngine) GetIndexStats(indices []string) ([]map[string]interface{}, error) { + if len(indices) == 0 { + return []map[string]interface{}{}, nil + } + + req := esapi.CatIndicesRequest{ + Index: indices, + Format: "json", + H: []string{"index", "health", "status", "docs.count", "store.size", "dataset.size"}, + } + + res, err := req.Do(context.Background(), e.client) + if err != nil { + return nil, fmt.Errorf("failed to get index stats: %w", err) + } + defer res.Body.Close() + + if res.IsError() { + if res.StatusCode == 404 { + return []map[string]interface{}{}, nil + } + bodyBytes, _ := io.ReadAll(res.Body) + return nil, fmt.Errorf("elasticsearch cat indices error: %s, body: %s", res.Status(), string(bodyBytes)) + } + + var results []map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&results); err != nil { + return nil, fmt.Errorf("failed to decode index stats: %w", err) + } + + return results, nil +}