From 343666965822581382f8e1b1a57a545d70a9b03f Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Wed, 27 Nov 2024 17:10:29 +0900 Subject: [PATCH] refactor: implement login options Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 124 ++++++++++++++------------ cmd/other/login.go | 172 ++++++++++++++++++++++++++++--------- 2 files changed, 204 insertions(+), 92 deletions(-) diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index 4c7043f..ac6c338 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -69,62 +69,80 @@ func FetchService(serviceName string, verb string, resourceName string, options // Get current endpoint endpoint := mainViper.GetString(fmt.Sprintf("environments.%s.endpoint", currentEnv)) - // Create a styled box for the authentication guidance - headerBox := pterm.DefaultBox.WithTitle("Authentication Guide"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan)) - - authExplain := "Please login to SpaceONE Console first.\n" + - "This requires your SpaceONE credentials.\n\n" + - "Or use an App token if you're using automation." - - headerBox.Println(authExplain) - fmt.Println() - - // Create the steps content - steps := []string{ - "1. Go to SpaceONE Console", - "2. Run 'cfctl login'", - "3. Enter your credentials when prompted", - "4. Select your workspace", - } + if strings.HasSuffix(currentEnv, "-app") { + // App environment message + headerBox := pterm.DefaultBox.WithTitle("App Guide"). + WithTitleTopCenter(). + WithRightPadding(4). + WithLeftPadding(4). + WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan)) + + appTokenExplain := "Please create a Domain Admin App in SpaceONE Console.\n" + + "This requires Domain Admin privilege.\n\n" + + "Or Please create a Workspace App in SpaceONE Console.\n" + + "This requires Workspace Owner privilege." + + pterm.Info.Printf("Using endpoint: %s\n", endpoint) + headerBox.Println(appTokenExplain) + fmt.Println() + + steps := []string{ + "1. Go to SpaceONE Console", + "2. Navigate to either 'Admin > App Page' or specific 'Workspace > App page'", + "3. Click 'Create' to create your App", + "4. Copy value of either 'client_secret' from Client ID or 'token' from Spacectl (CLI)", + } + + yamlExample := pterm.DefaultBox.WithTitle("Config Example"). + WithTitleTopCenter(). + WithRightPadding(4). + WithLeftPadding(4). + Sprint(fmt.Sprintf("environment: %s\nenvironments:\n %s:\n endpoint: %s\n proxy: true\n token: %s", + currentEnv, + currentEnv, + endpoint, + pterm.FgLightCyan.Sprint("YOUR_COPIED_TOKEN"))) + + instructionBox := pterm.DefaultBox.WithTitle("Required Steps"). + WithTitleTopCenter(). + WithRightPadding(4). + WithLeftPadding(4) + + allSteps := append(steps, + fmt.Sprintf("5. Add the token under the proxy in your config file:\n%s", yamlExample), + "6. Run 'cfctl login' again") + + instructionBox.Println(strings.Join(allSteps, "\n\n")) + } else { + // User environment message + headerBox := pterm.DefaultBox.WithTitle("Authentication Required"). + WithTitleTopCenter(). + WithRightPadding(4). + WithLeftPadding(4). + WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan)) + + authExplain := "Please login to SpaceONE Console first.\n" + + "This requires your SpaceONE credentials." + + headerBox.Println(authExplain) + fmt.Println() + + steps := []string{ + "1. Run 'cfctl login'", + "2. Enter your credentials when prompted", + "3. Select your workspace", + "4. Try your command again", + } + + instructionBox := pterm.DefaultBox.WithTitle("Required Steps"). + WithTitleTopCenter(). + WithRightPadding(4). + WithLeftPadding(4) - // Determine proxy value based on endpoint - isIdentityEndpoint := strings.Contains(strings.ToLower(endpoint), "identity") - proxyValue := "true" - if !isIdentityEndpoint { - proxyValue = "false" + instructionBox.Println(strings.Join(steps, "\n\n")) } - // Create yaml config example with highlighting - yamlExample := pterm.DefaultBox.WithTitle("Config Example"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - Sprint(fmt.Sprintf("environment: %s\nenvironments:\n %s:\n endpoint: %s\n proxy: %s\n token: %s", - currentEnv, - currentEnv, - endpoint, - proxyValue, - pterm.FgLightCyan.Sprint("YOUR_TOKEN"))) - - // Create instruction box - instructionBox := pterm.DefaultBox.WithTitle("Required Steps"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4) - - // Combine all steps - allSteps := append(steps, - fmt.Sprintf("5. Verify your config file has the token:\n%s", yamlExample), - "6. Try your command again") - - // Print all steps in the instruction box - instructionBox.Println(strings.Join(allSteps, "\n\n")) - - return nil, fmt.Errorf("authentication required") + return nil, nil } config, err := loadConfig() diff --git a/cmd/other/login.go b/cmd/other/login.go index 18ff1e8..8814847 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -13,6 +13,7 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "time" @@ -114,9 +115,9 @@ func executeAppLogin(currentEnv string, mainViper *viper.Viper) { WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan)) appTokenExplain := "Please create a Domain Admin App in SpaceONE Console.\n" + - "This requires Domain Admin privileges.\n\n" + + "This requires Domain Admin privilege.\n\n" + "Or Please create a Workspace App in SpaceONE Console.\n" + - "This requires Workspace Owner privileges." + "This requires Workspace Owner privilege." headerBox.Println(appTokenExplain) fmt.Println() @@ -1166,6 +1167,7 @@ func saveToken(newToken string) { exitWithError() } + fmt.Println() pterm.Success.Printf("Token successfully saved to %s\n", configPath) } @@ -1253,10 +1255,12 @@ func selectWorkspace(workspaces []map[string]interface{}) string { func selectScopeOrWorkspace(workspaces []map[string]interface{}) string { const pageSize = 9 currentPage := 0 + searchMode := false searchTerm := "" + selectedIndex := 0 + inputBuffer := "" filteredWorkspaces := workspaces - // Initialize keyboard if err := keyboard.Open(); err != nil { pterm.Error.Println("Failed to initialize keyboard:", err) exitWithError() @@ -1264,9 +1268,15 @@ func selectScopeOrWorkspace(workspaces []map[string]interface{}) string { defer keyboard.Close() for { + // Clear screen + fmt.Print("\033[H\033[2J") + // Apply search filter if searchTerm != "" { filteredWorkspaces = filterWorkspaces(workspaces, searchTerm) + if len(filteredWorkspaces) == 0 { + filteredWorkspaces = workspaces + } } else { filteredWorkspaces = workspaces } @@ -1280,82 +1290,166 @@ func selectScopeOrWorkspace(workspaces []map[string]interface{}) string { endIndex = totalWorkspaces } - // Clear screen - fmt.Print("\033[H\033[2J") - - // Show search term if active - if searchTerm != "" { - pterm.Info.Printf("Search term: %s\n", searchTerm) - } - pterm.Info.Printf("Available Options (Page %d of %d):\n", currentPage+1, totalPages) // Always show DOMAIN ADMIN option on first page if currentPage == 0 { - pterm.DefaultBasicText.WithStyle(pterm.NewStyle(pterm.FgLightCyan)). - Printf(" 0: DOMAIN ADMIN\n") + if selectedIndex == 0 { + fmt.Printf("→ 0: DOMAIN ADMIN\n") + } else { + fmt.Printf(" 0: DOMAIN ADMIN\n") + } } - // Display current page items + // Display workspace options with selection indicator for i := startIndex; i < endIndex; i++ { name := filteredWorkspaces[i]["name"].(string) - fmt.Printf(" %d: %s\n", i+1, name) + displayIndex := i - startIndex + 1 + if displayIndex == selectedIndex { + fmt.Printf("→ %d: %s\n", i+1, name) + } else { + fmt.Printf(" %d: %s\n", i+1, name) + } } - // Show navigation help - fmt.Print("\nNavigation: [p]revious page, [n]ext page") - if searchTerm != "" { + fmt.Print("\nNavigation: [j]down [k]up [h]prev-page [l]next-page") + if searchTerm != "" && !searchMode { fmt.Print(", [c]lear search") } - fmt.Print(", [/]search, [q]uit\n") - fmt.Print("> ") + fmt.Print(", [/]search, [q]uit") + + // Show search or input prompt at the bottom + if searchMode { + fmt.Print("\n\nSearch (ESC to cancel, Enter to confirm): ") + fmt.Print(searchTerm) + } else if !searchMode { + fmt.Print("\n\nPlease select an option or input a number: ") + if inputBuffer != "" { + fmt.Print(inputBuffer) + } + } - // Get keyboard input - char, _, err := keyboard.GetKey() + char, key, err := keyboard.GetKey() if err != nil { pterm.Error.Println("Error reading keyboard input:", err) exitWithError() } + if searchMode { + switch key { + case keyboard.KeyEsc: + searchMode = false + searchTerm = "" + case keyboard.KeyBackspace, keyboard.KeyBackspace2: + if len(searchTerm) > 0 { + searchTerm = searchTerm[:len(searchTerm)-1] + } + case keyboard.KeyEnter: + searchMode = false + default: + if char != 0 { + searchTerm += string(char) + } + } + currentPage = 0 + selectedIndex = 0 + continue + } + + switch key { + case keyboard.KeyEnter: + if inputBuffer != "" { + index, err := strconv.Atoi(inputBuffer) + if err == nil && index >= 0 { + if index == 0 { + return "0" + } + index-- // 1-based to 0-based + if index < len(filteredWorkspaces) { + return filteredWorkspaces[index]["workspace_id"].(string) + } + } + inputBuffer = "" + } else { + if selectedIndex == 0 && currentPage == 0 { + return "0" + } + adjustedIndex := startIndex + (selectedIndex - 1) + if adjustedIndex >= 0 && adjustedIndex < len(filteredWorkspaces) { + return filteredWorkspaces[adjustedIndex]["workspace_id"].(string) + } + } + case keyboard.KeyBackspace, keyboard.KeyBackspace2: + if len(inputBuffer) > 0 { + inputBuffer = inputBuffer[:len(inputBuffer)-1] + } + } + switch char { - case 'n', 'N': + case 'j': // Down + if selectedIndex < min(pageSize-1, endIndex-startIndex) { + selectedIndex++ + } else if currentPage < totalPages-1 { + currentPage++ + selectedIndex = 0 + } + inputBuffer = "" + case 'k': // Up + if selectedIndex > 0 { + selectedIndex-- + } else if currentPage > 0 { + currentPage-- + selectedIndex = pageSize - 1 + } + inputBuffer = "" + case 'l': // Next page if currentPage < totalPages-1 { currentPage++ } else { currentPage = 0 } - case 'p', 'P': + selectedIndex = 0 + inputBuffer = "" + case 'h': // Previous page if currentPage > 0 { currentPage-- } else { currentPage = totalPages - 1 } + selectedIndex = 0 + inputBuffer = "" case 'q', 'Q': + fmt.Println() pterm.Error.Println("Workspace selection cancelled.") os.Exit(1) case 'c', 'C': - searchTerm = "" - currentPage = 0 + if searchTerm != "" { + searchTerm = "" + currentPage = 0 + selectedIndex = 0 + } + inputBuffer = "" case '/': - keyboard.Close() - fmt.Print("\nEnter search term: ") - var input string - fmt.Scanln(&input) - searchTerm = input + searchMode = true + searchTerm = "" currentPage = 0 - keyboard.Open() - case '0': - return "0" - case '1', '2', '3', '4', '5', '6', '7', '8', '9': - index := int(char - '0') - adjustedIndex := startIndex + (index - 1) - if adjustedIndex >= 0 && adjustedIndex < len(filteredWorkspaces) { - return filteredWorkspaces[adjustedIndex]["workspace_id"].(string) + selectedIndex = 0 + inputBuffer = "" + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + if !searchMode { + inputBuffer += string(char) } } } } +func min(a, b int) int { + if a < b { + return a + } + return b +} + func filterWorkspaces(workspaces []map[string]interface{}, searchTerm string) []map[string]interface{} { var filtered []map[string]interface{} searchTerm = strings.ToLower(searchTerm)