tui/specials.go (299 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tui import ( "fmt" "strings" "cloud.google.com/go/domains/apiv1beta1/domainspb" "github.com/GoogleCloudPlatform/deploystack/config" "github.com/GoogleCloudPlatform/deploystack/gcloud" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) var ( projNewSuffix = "_new_project_creator" billNewSuffix = "_new_billing_selector" ) func newProjectCreator(key string) textInput { r := newTextInput("Create New Project", "", key, "Checking if project can be created", ) r.addPostProcessor(createProject) r.addContent("Project IDs are immutable and can be set only during project ") r.addContent("creation. They must start with a lowercase letter and can have ") r.addContent("lowercase ASCII letters, digits or hyphens. ") r.addContent("Project IDs must be between 6 and 30 characters. ") r.addContent("\n\n") r.addContent(textInputDefaultStyle.Render("Please enter a new project name to create:")) return r } func newProjectSelector(key, listLabel, currentProject string, preProcessor tea.Cmd) picker { result := newPicker(listLabel, "Retrieving Projects", key, currentProject, preProcessor) create := item{"Create New Project", ""} result.list.InsertItem(0, create) result.addPostProcessor(processProjectSelection) return result } func newBillingSelector(key string, preProcessor tea.Cmd, postProccessor func(string, *Queue) tea.Cmd) picker { result := newPicker("Choose an account to use to enable billing on the new project", "Retrieving Billing Accounts", key, "", preProcessor) result.postProcessor = postProccessor return result } func newYesOrNo(q *Queue, listLabel, key string, defaultNo bool, postProcessor func(string, *Queue) tea.Cmd) picker { p := newPicker(listLabel, "", key, "", getYesOrNo(q)) p.list.SetShowStatusBar(false) p.list.SetShowFilter(false) p.list.SetShowHelp(false) p.addPostProcessor(postProcessor) p.preProcessor = getNoOrYes(q) return p } func newCustom(c config.Custom) QueueModel { r := newTextInput(c.Description, c.Default, c.Name, "validating", ) switch c.Validation { case validationPhoneNumber: r.spinnerLabel = "Validating phone number" r.addPostProcessor(validatePhoneNumber) case validationYesOrNo: r.spinnerLabel = "Validating yes or no" r.addPostProcessor(validateYesOrNo) case validationInteger: r.spinnerLabel = "Validating integer" r.addPostProcessor(validateInteger) } if c.PrependProject { r.addPostProcessor(prependProject) } return &r } func newDomain(q *Queue) { contact := gcloud.ContactData{} t := newTextInput( "Enter a domain you wish to purchase and use for this application", "", "domain", "Checking Domain Availability", ) t.postProcessor = validateDomain q.add(&t) items := []struct { Name string Description string DefaultValue string Validator func(string, *Queue) tea.Cmd }{ { Name: "domain_email", Description: "Enter an email address", DefaultValue: "person@example.com", }, { Name: "domain_phone", Description: "Enter a phone number. (Please enter with country code - +1 555 555 5555 for US for example)", DefaultValue: "+14155551234", Validator: validatePhoneNumber, }, { Name: "domain_country", Description: "Enter a country code", DefaultValue: "US", }, { Name: "domain_postalcode", Description: "Enter a postal code", DefaultValue: "94502", }, { Name: "domain_state", Description: "Enter a state or administrative area", DefaultValue: "CA", }, { Name: "domain_city", Description: "Enter a city", DefaultValue: "San Francisco", }, { Name: "domain_address", Description: "Enter an address", DefaultValue: "345 Spear Street", }, { Name: "domain_name", Description: "Enter name", DefaultValue: "Googler", }, } tmp := q.Get("contact") switch v := tmp.(type) { case gcloud.ContactData: contact = v default: contact = gcloud.ContactData{} } if contact.AllContacts.Email == "" { for _, v := range items { t := newTextInput(v.Description, v.DefaultValue, v.Name, "") q.add(&t) } } f := func(q *Queue) { domain := q.Get("domain").(string) info := q.Get("domainInfo").(*domainspb.RegisterParameters) if info.YearlyPrice != nil { msg := fmt.Sprintf( "Cost for %s will be %s. %s", domain, purchaseStyle.Render( fmt.Sprintf( "%d%s", info.YearlyPrice.Units, info.YearlyPrice.CurrencyCode, ), ), textStyle.Render("Continue?"), ) p := q.models[q.current] p.clearContent() p.addContent(msg) } } dy := newYesOrNo( q, "Buying a domain is not reversible, saying 'y' will incur a charge.", "domain_consent", false, nil, ) dy.spinnerLabel = "Attempting to register domain" dy.addPreView(f) dy.addPostProcessor(registerDomain) q.add(&dy) } func newCustomPages(q *Queue) { for _, v := range q.stack.Config.CustomSettings { temp := q.stack.GetSetting(v.Name) if len(v.Options) > 0 { items := []list.Item{} for _, opt := range v.Options { i := item{value: opt, label: opt} if strings.Contains(opt, "|") { sl := strings.Split(opt, "|") i.label = sl[1] i.value = sl[0] } items = append(items, i) } f := func(items []list.Item) tea.Cmd { return func() tea.Msg { return items } } pickerPage := newPicker(v.Description, "", v.Name, v.Default, f(items)) if v.PrependProject { pickerPage.addPostProcessor(prependProject) } q.add(&pickerPage) continue } if len(temp) < 1 { tiPage := newCustom(v) q.add(tiPage) } } } func newGCEInstance(q *Queue) { r := newPicker("Do you want to accept the default configuration? (Yes or No)", "", "gce-use-defaults", "", getYesOrNo(q)) r.omitFromSettings = true r.list.SetShowFilter(false) r.list.SetShowHelp(false) r.list.SetShowStatusBar(false) r.addPostProcessor(validateGCEDefault) r.addContent(textStyle.Bold(true).Render("Configure a Compute Engine Instance")) r.addContent("\n\n") m := `Let's walk through configuring a Compute Engine Instance (Virtual Machine). you can either accept a default configuration with settings that work for trying out most use cases, or hand configure key settings. ` r.addContent(m) q.add(&r) basename := q.stack.GetSetting("basename") name := newTextInput("Enter the name of the instance", fmt.Sprintf("%s-instance", basename), "instance-name", "", ) q.add(&name) newRegion(q) newZone(q) newMachineTypeManager(q) newDiskImageManager(q) ds := newTextInput("Enter the size of the boot disk you want in GB", "100", "instance-disksize", "", ) ds.addPostProcessor(validateInteger) q.add(&ds) dt := newPicker("Pick the type of the boot disk you want", "", "instance-disktype", gcloud.DefaultDiskType, getDiskTypes(q)) q.add(&dt) dy := newYesOrNo( q, "Do you want this to be a webserver (Expose ports 80 & 443)?", "instance-webserver", false, validateGCEConfiguration, ) q.add(&dy) } func newRegion(q *Queue) { r := newPicker("Pick a region", "Retrieving regions", "region", q.stack.Config.RegionDefault, getRegions(q)) q.add(&r) } func newZone(q *Queue) { z := newPicker("Pick a zone", "Retrieving zones", "zone", gcloud.DefaultZone, getZones(q)) q.add(&z) } func newMachineTypeManager(q *Queue) { p := newPicker("Pick a Machine Type Family", "Retrieving machine type families", "instance-machine-type-family", gcloud.DefaultMachineFamily, getMachineTypeFamilies(q)) p.addContent(textStyle.Bold(true).Render("Configure a Compute Engine Instance")) p.addContent("\n\n") p.addContent("There are a large number of machine types to choose from. For more information \n") p.addContent("please refer to the following link for more information about Machine types: \n") p.addContent(url.Render("https://cloud.google.com/compute/docs/machine-types")) q.add(&p) p2 := newPicker("Pick a Machine Type", "Retrieving machine types", "instance-machine-type", gcloud.DefaultMachineType, getMachineTypes(q)) p2.addContent(textStyle.Bold(true).Render("Configure a Compute Engine Instance")) p2.addContent("\n\n") p2.addContent("There are a large number of machine types to choose from. For more information \n") p2.addContent("please refer to the following link for more information about Machine types: \n") p2.addContent(url.Render("https://cloud.google.com/compute/docs/machine-types")) q.add(&p2) } func newDiskImageManager(q *Queue) { p := newPicker("Pick an operating system", "Retrieving operating systems", "instance-image-project", gcloud.DefaultImageProject, getDiskProjects(q)) p.addContent(textStyle.Bold(true).Render("Configure a Compute Engine Instance")) p.addContent("\n\n") p.addContent("There are a large number of machine images to choose from. For more information \n") p.addContent("please refer to the following link for more information about Machine images: \n") p.addContent(url.Render("https://cloud.google.com/compute/docs/images")) q.add(&p) p2 := newPicker("Pick a disk family", "Retrieving disk family", "instance-image-family", gcloud.DefaultImageFamily, getImageFamilies(q)) p2.addContent(textStyle.Bold(true).Render("Configure a Compute Engine Instance")) p2.addContent("\n\n") p2.addContent("There are a large number of machine images to choose from. For more information \n") p2.addContent("please refer to the following link for more information about Machine images: \n") p2.addContent(url.Render("https://cloud.google.com/compute/docs/images")) q.add(&p2) p3 := newPicker("Pick a disk image", "Retrieving disk image", "instance-image", "", getImageDisks(q)) p3.addContent(textStyle.Bold(true).Render("Configure a Compute Engine Instance")) p3.addContent("\n\n") p3.addContent("There are a large number of machine images to choose from. For more information \n") p3.addContent("please refer to the following link for more information about Machine images: \n") p3.addContent(url.Render("https://cloud.google.com/compute/docs/images")) q.add(&p3) }