From 10a8ce4562e3046cecbc55dca8980adfbe6d4307 Mon Sep 17 00:00:00 2001 From: "Lennart Kats (databricks)" Date: Wed, 27 Dec 2023 13:03:08 +0100 Subject: [PATCH] Improve experience for multiple builtin templates (#1052) ## Changes This enhances the template selection experience a bit as we add more and more built-in templates (like https://github.com/databricks/cli/pull/1051 and https://github.com/databricks/cli/pull/1059): ### New experience: image ### Current experience: image --------- Co-authored-by: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> --- cmd/bundle/init.go | 48 +++++++++++++++++++++++++++++++++-------- cmd/bundle/init_test.go | 14 +++++++++--- libs/cmdio/io.go | 30 +++++++++++++++----------- 3 files changed, 68 insertions(+), 24 deletions(-) diff --git a/cmd/bundle/init.go b/cmd/bundle/init.go index 18d76db1..db8250d0 100644 --- a/cmd/bundle/init.go +++ b/cmd/bundle/init.go @@ -27,35 +27,58 @@ type nativeTemplate struct { aliases []string } +const customTemplate = "custom..." + var nativeTemplates = []nativeTemplate{ { name: "default-python", - description: "The default Python template", + description: "The default Python template for Notebooks / Delta Live Tables / Workflows", }, { name: "mlops-stacks", gitUrl: "https://github.com/databricks/mlops-stacks", - description: "The Databricks MLOps Stacks template (https://github.com/databricks/mlops-stacks)", + description: "The Databricks MLOps Stacks template (github.com/databricks/mlops-stacks)", aliases: []string{"mlops-stack"}, }, + { + name: customTemplate, + description: "Bring your own template", + }, } -func nativeTemplateDescriptions() string { +// Return template descriptions for command-line help +func nativeTemplateHelpDescriptions() string { var lines []string for _, template := range nativeTemplates { - lines = append(lines, fmt.Sprintf("- %s: %s", template.name, template.description)) + if template.name != customTemplate { + lines = append(lines, fmt.Sprintf("- %s: %s", template.name, template.description)) + } } return strings.Join(lines, "\n") } -func nativeTemplateOptions() []string { - names := make([]string, 0, len(nativeTemplates)) +// Return template options for an interactive prompt +func nativeTemplateOptions() []cmdio.Tuple { + names := make([]cmdio.Tuple, 0, len(nativeTemplates)) for _, template := range nativeTemplates { - names = append(names, template.name) + tuple := cmdio.Tuple{ + Name: template.name, + Id: template.description, + } + names = append(names, tuple) } return names } +func getNativeTemplateByDescription(description string) string { + for _, template := range nativeTemplates { + if template.description == description { + return template.name + } + } + return "" +} + func getUrlForNativeTemplate(name string) string { for _, template := range nativeTemplates { if template.name == name { @@ -99,7 +122,7 @@ TEMPLATE_PATH optionally specifies which template to use. It can be one of the f - a local file system path with a template directory - a Git repository URL, e.g. https://github.com/my/repository -See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more information on templates.`, nativeTemplateDescriptions()), +See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more information on templates.`, nativeTemplateHelpDescriptions()), } var configFile string @@ -134,10 +157,17 @@ See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more inf if !cmdio.IsPromptSupported(ctx) { return errors.New("please specify a template") } - templatePath, err = cmdio.AskSelect(ctx, "Template to use", nativeTemplateOptions()) + description, err := cmdio.SelectOrdered(ctx, nativeTemplateOptions(), "Template to use") if err != nil { return err } + templatePath = getNativeTemplateByDescription(description) + } + + if templatePath == customTemplate { + cmdio.LogString(ctx, "Please specify a path or Git repository to use a custom template.") + cmdio.LogString(ctx, "See https://docs.databricks.com/en/dev-tools/bundles/templates.html to learn more about custom templates.") + return nil } // Expand templatePath to a git URL if it's an alias for a known native template diff --git a/cmd/bundle/init_test.go b/cmd/bundle/init_test.go index db4446bb..aa899159 100644 --- a/cmd/bundle/init_test.go +++ b/cmd/bundle/init_test.go @@ -3,6 +3,7 @@ package bundle import ( "testing" + "github.com/databricks/cli/libs/cmdio" "github.com/stretchr/testify/assert" ) @@ -27,11 +28,18 @@ func TestBundleInitRepoName(t *testing.T) { } func TestNativeTemplateOptions(t *testing.T) { - assert.Equal(t, []string{"default-python", "mlops-stacks"}, nativeTemplateOptions()) + expected := []cmdio.Tuple{ + {Name: "default-python", Id: "The default Python template for Notebooks / Delta Live Tables / Workflows"}, + {Name: "mlops-stacks", Id: "The Databricks MLOps Stacks template (github.com/databricks/mlops-stacks)"}, + {Name: "custom...", Id: "Bring your own template"}, + } + assert.Equal(t, expected, nativeTemplateOptions()) } -func TestNativeTemplateDescriptions(t *testing.T) { - assert.Equal(t, "- default-python: The default Python template\n- mlops-stacks: The Databricks MLOps Stacks template (https://github.com/databricks/mlops-stacks)", nativeTemplateDescriptions()) +func TestNativeTemplateHelpDescriptions(t *testing.T) { + expected := `- default-python: The default Python template for Notebooks / Delta Live Tables / Workflows +- mlops-stacks: The Databricks MLOps Stacks template (github.com/databricks/mlops-stacks)` + assert.Equal(t, expected, nativeTemplateHelpDescriptions()) } func TestGetUrlForNativeTemplate(t *testing.T) { diff --git a/libs/cmdio/io.go b/libs/cmdio/io.go index 8b421ef5..d20991a7 100644 --- a/libs/cmdio/io.go +++ b/libs/cmdio/io.go @@ -155,19 +155,13 @@ func RenderReader(ctx context.Context, r io.Reader) error { } } -type tuple struct{ Name, Id string } +type Tuple struct{ Name, Id string } -func (c *cmdIO) Select(names map[string]string, label string) (id string, err error) { +func (c *cmdIO) Select(items []Tuple, label string) (id string, err error) { if !c.interactive { return "", fmt.Errorf("expected to have %s", label) } - var items []tuple - for k, v := range names { - items = append(items, tuple{k, v}) - } - slices.SortFunc(items, func(a, b tuple) int { - return strings.Compare(a.Name, b.Name) - }) + idx, _, err := (&promptui.Select{ Label: label, Items: items, @@ -190,13 +184,25 @@ func (c *cmdIO) Select(names map[string]string, label string) (id string, err er return } +// Show a selection prompt where the user can pick one of the name/id items. +// The items are sorted alphabetically by name. func Select[V any](ctx context.Context, names map[string]V, label string) (id string, err error) { c := fromContext(ctx) - stringNames := map[string]string{} + var items []Tuple for k, v := range names { - stringNames[k] = fmt.Sprint(v) + items = append(items, Tuple{k, fmt.Sprint(v)}) } - return c.Select(stringNames, label) + slices.SortFunc(items, func(a, b Tuple) int { + return strings.Compare(a.Name, b.Name) + }) + return c.Select(items, label) +} + +// Show a selection prompt where the user can pick one of the name/id items. +// The items appear in the order specified in the "items" argument. +func SelectOrdered(ctx context.Context, items []Tuple, label string) (id string, err error) { + c := fromContext(ctx) + return c.Select(items, label) } func (c *cmdIO) Secret(label string) (value string, err error) {