From ae8f86adc8a3c8d6a28ac34eca156b8606a5602b Mon Sep 17 00:00:00 2001 From: Xiang Zhang Date: Mon, 23 Sep 2024 22:29:52 +0800 Subject: [PATCH] use TextArea model for service account key (#243) --- .../ticloud_serverless_export_create.md | 2 +- .../ticloud_serverless_import_start.md | 2 +- .../cli/serverless/dataimport/start/gcs.go | 8 +- .../serverless/dataimport/start/gcs_test.go | 2 +- .../cli/serverless/dataimport/start/start.go | 4 +- internal/cli/serverless/export/create.go | 10 +- internal/cli/serverless/export/ui.go | 2 +- internal/ui/text_area_model.go | 99 +++++++++++++++++++ 8 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 internal/ui/text_area_model.go diff --git a/docs/generate_doc/ticloud_serverless_export_create.md b/docs/generate_doc/ticloud_serverless_export_create.md index ca4e092f..559117b2 100644 --- a/docs/generate_doc/ticloud_serverless_export_create.md +++ b/docs/generate_doc/ticloud_serverless_export_create.md @@ -44,7 +44,7 @@ ticloud serverless export create [flags] --filter strings Specify the exported table(s) with table filter patterns. See https://docs.pingcap.com/tidb/stable/table-filter to learn table filter. --force Create without confirmation. You need to confirm when you want to export the whole cluster in non-interactive mode. --gcs.service-account-key string The base64 encoded service account key of GCS. - --gcs.uri string The GCS URI in gcs:/// format. Required when target type is GCS. + --gcs.uri string The GCS URI in gs:/// format. Required when target type is GCS. -h, --help help for create --parquet.compression string The parquet compression algorithm. One of ["GZIP" "SNAPPY" "ZSTD" "NONE"]. (default "ZSTD") --s3.access-key-id string The access key ID of the S3. You only need to set one of the s3.role-arn and [s3.access-key-id, s3.secret-access-key]. diff --git a/docs/generate_doc/ticloud_serverless_import_start.md b/docs/generate_doc/ticloud_serverless_import_start.md index fa40ef41..8d2333f4 100644 --- a/docs/generate_doc/ticloud_serverless_import_start.md +++ b/docs/generate_doc/ticloud_serverless_import_start.md @@ -47,7 +47,7 @@ ticloud serverless import start [flags] --csv.trim-last-separator Specifies whether to treat separator as the line terminator and trim all trailing separators in the CSV file. --file-type string The import file type, one of ["CSV" "SQL" "AURORA_SNAPSHOT" "PARQUET"]. --gcs.service-account-key string The base64 encoded service account key of GCS. - --gcs.uri string The GCS URI in gcs:/// format. Required when source type is GCS. + --gcs.uri string The GCS URI in gs:/// format. Required when source type is GCS. -h, --help help for start --local.concurrency int The concurrency for uploading file. (default 5) --local.file-path string The local file path to import. diff --git a/internal/cli/serverless/dataimport/start/gcs.go b/internal/cli/serverless/dataimport/start/gcs.go index 8be8ffc6..e6562816 100644 --- a/internal/cli/serverless/dataimport/start/gcs.go +++ b/internal/cli/serverless/dataimport/start/gcs.go @@ -65,7 +65,7 @@ func (o GCSOpts) Run(cmd *cobra.Command) error { authType = authTypeModel.(ui.SelectModel).Choices[authTypeModel.(ui.SelectModel).Selected].(imp.ImportGcsAuthTypeEnum) if authType == imp.IMPORTGCSAUTHTYPEENUM_SERVICE_ACCOUNT_KEY { - inputs := []string{flag.GCSURI, flag.GCSServiceAccountKey} + inputs := []string{flag.GCSURI} textInput, err := ui.InitialInputModel(inputs, inputDescription) if err != nil { return err @@ -74,7 +74,11 @@ func (o GCSOpts) Run(cmd *cobra.Command) error { if gcsUri == "" { return errors.New("empty GCS URI") } - accountKey = textInput.Inputs[1].Value() + areaInput, err := ui.InitialTextAreaModel(inputDescription[flag.GCSServiceAccountKey]) + if err != nil { + return errors.Trace(err) + } + accountKey = areaInput.Textarea.Value() if accountKey == "" { return errors.New("empty GCS service account key") } diff --git a/internal/cli/serverless/dataimport/start/gcs_test.go b/internal/cli/serverless/dataimport/start/gcs_test.go index 8c9d40bb..d9e4da9b 100644 --- a/internal/cli/serverless/dataimport/start/gcs_test.go +++ b/internal/cli/serverless/dataimport/start/gcs_test.go @@ -76,7 +76,7 @@ func (suite *GCSImportSuite) TestGCSImportArgs() { clusterID := "12345" importID := "imp-asdasd" accountKey := "xasdas" - gcsUri := "gcs://xxx" + gcsUri := "gs://xxx" t := time.Now() fileType := imp.IMPORTFILETYPEENUM_CSV csvFormat := &imp.CSVFormat{ diff --git a/internal/cli/serverless/dataimport/start/start.go b/internal/cli/serverless/dataimport/start/start.go index 0800924c..74568fd9 100644 --- a/internal/cli/serverless/dataimport/start/start.go +++ b/internal/cli/serverless/dataimport/start/start.go @@ -55,7 +55,7 @@ var inputDescription = map[string]string{ flag.S3RoleArn: "Input your S3 role arn", flag.AzureBlobURI: "Input your Azure Blob URI in azure://.blob.core.windows.net// format", flag.AzureBlobSASToken: "Input your Azure Blob SAS token", - flag.GCSURI: "Input your GCS URI in gcs:/// format", + flag.GCSURI: "Input your GCS URI in gs:/// format", flag.GCSServiceAccountKey: "Input your base64 encoded GCS service account key", flag.CSVSeparator: "Input the CSV separator: separator of each value in CSV files, skip to use default value (,)", flag.CSVDelimiter: "Input the CSV delimiter: delimiter of string type variables in CSV files, skip to use default value (\"). If you want to set empty string, please use non-interactive mode", @@ -249,7 +249,7 @@ func StartCmd(h *internal.Helper) *cobra.Command { startCmd.MarkFlagsMutuallyExclusive(flag.S3RoleArn, flag.S3SecretAccessKey) startCmd.MarkFlagsRequiredTogether(flag.S3AccessKeyID, flag.S3SecretAccessKey) - startCmd.Flags().String(flag.GCSURI, "", "The GCS URI in gcs:/// format. Required when source type is GCS.") + startCmd.Flags().String(flag.GCSURI, "", "The GCS URI in gs:/// format. Required when source type is GCS.") startCmd.Flags().String(flag.GCSServiceAccountKey, "", "The base64 encoded service account key of GCS.") startCmd.Flags().String(flag.AzureBlobURI, "", "The Azure Blob URI in azure://.blob.core.windows.net// format.") diff --git a/internal/cli/serverless/export/create.go b/internal/cli/serverless/export/create.go index 63d4854c..e5d0b630 100644 --- a/internal/cli/serverless/export/create.go +++ b/internal/cli/serverless/export/create.go @@ -225,7 +225,7 @@ func CreateCmd(h *internal.Helper) *cobra.Command { return errors.New("empty S3 role arn") } case string(export.EXPORTGCSAUTHTYPEENUM_SERVICE_ACCOUNT_KEY): - inputs := []string{flag.GCSURI, flag.GCSServiceAccountKey} + inputs := []string{flag.GCSURI} textInput, err := ui.InitialInputModel(inputs, inputDescription) if err != nil { return err @@ -234,7 +234,11 @@ func CreateCmd(h *internal.Helper) *cobra.Command { if gcsURI == "" { return errors.New("empty GCS URI") } - gcsServiceAccountKey = textInput.Inputs[1].Value() + areaInput, err := ui.InitialTextAreaModel(inputDescription[flag.GCSServiceAccountKey]) + if err != nil { + return errors.Trace(err) + } + gcsServiceAccountKey = areaInput.Textarea.Value() if gcsServiceAccountKey == "" { return errors.New("empty GCS service account key") } @@ -655,7 +659,7 @@ func CreateCmd(h *internal.Helper) *cobra.Command { createCmd.Flags().String(flag.CSVNullValue, CSVNullValueDefaultValue, "Representation of null values in CSV files.") createCmd.Flags().Bool(flag.CSVSkipHeader, CSVSkipHeaderDefaultValue, "Export CSV files of the tables without header.") createCmd.Flags().String(flag.S3RoleArn, "", "The role arn of the S3. You only need to set one of the s3.role-arn and [s3.access-key-id, s3.secret-access-key].") - createCmd.Flags().String(flag.GCSURI, "", "The GCS URI in gcs:/// format. Required when target type is GCS.") + createCmd.Flags().String(flag.GCSURI, "", "The GCS URI in gs:/// format. Required when target type is GCS.") createCmd.Flags().String(flag.GCSServiceAccountKey, "", "The base64 encoded service account key of GCS.") createCmd.Flags().String(flag.AzureBlobURI, "", "The Azure Blob URI in azure://.blob.core.windows.net// format. Required when target type is AZURE_BLOB.") createCmd.Flags().String(flag.AzureBlobSASToken, "", "The SAS token of Azure Blob.") diff --git a/internal/cli/serverless/export/ui.go b/internal/cli/serverless/export/ui.go index 39595493..062220e6 100644 --- a/internal/cli/serverless/export/ui.go +++ b/internal/cli/serverless/export/ui.go @@ -33,7 +33,7 @@ var inputDescription = map[string]string{ flag.S3RoleArn: "Input your S3 role arn", flag.AzureBlobURI: "Input your Azure Blob URI in azure://.blob.core.windows.net// format", flag.AzureBlobSASToken: "Input your Azure Blob SAS token", - flag.GCSURI: "Input your GCS URI in gcs:/// format", + flag.GCSURI: "Input your GCS URI in gs:/// format", flag.GCSServiceAccountKey: "Input your base64 encoded GCS service account key", flag.SQL: "Input the SELECT SQL statement", flag.TableFilter: "Input the table filter patterns (comma separated). Example: database.table,database.*,`database-1`.`table-1`", diff --git a/internal/ui/text_area_model.go b/internal/ui/text_area_model.go new file mode 100644 index 00000000..20375bd3 --- /dev/null +++ b/internal/ui/text_area_model.go @@ -0,0 +1,99 @@ +// Copyright 2024 PingCAP, Inc. +// +// 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 ui + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" + "github.com/tidbcloud/tidbcloud-cli/internal/util" +) + +type TextAreaModel struct { + Textarea textarea.Model + Err error + Interrupted bool +} + +func InitialTextAreaModel(placeholder string) (TextAreaModel, error) { + ta := textarea.New() + ta.Placeholder = placeholder + ta.Focus() + ta.SetWidth(80) + ta.SetHeight(20) + ta.ShowLineNumbers = false + ta.CharLimit = 0 + + p := tea.NewProgram(TextAreaModel{Textarea: ta}) + model, err := p.Run() + finalModel := model.(TextAreaModel) + if err != nil { + return finalModel, err + } + if finalModel.Interrupted { + return finalModel, util.InterruptError + } + if finalModel.Err != nil { + return finalModel, finalModel.Err + } + return finalModel, nil +} + +func (m TextAreaModel) Init() tea.Cmd { + return textarea.Blink +} + +func (m TextAreaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEsc: + if m.Textarea.Focused() { + m.Textarea.Blur() + } + case tea.KeyCtrlS: + return m, tea.Quit + case tea.KeyCtrlC: + m.Interrupted = true + return m, tea.Quit + default: + if !m.Textarea.Focused() { + cmd = m.Textarea.Focus() + cmds = append(cmds, cmd) + } + } + + // We handle errors just like any other message + case errMsg: + m.Err = msg + return m, nil + } + + m.Textarea, cmd = m.Textarea.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} + +func (m TextAreaModel) View() string { + return fmt.Sprintf( + "%s\n\n%s\n\n", + m.Textarea.View(), + helpMessageStyle("Press Ctrl+S to save and quit"), + ) +}