-
Notifications
You must be signed in to change notification settings - Fork 127
Allow to export dashboards from Serverless environments #3007
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mrodm
wants to merge
15
commits into
elastic:main
Choose a base branch
from
mrodm:update_export_dashboards_apis
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
dc54003
Export dashboards using EPM API endpoints
mrodm 2fa7ec3
Just execute the new method on Serverless
mrodm 66a1770
Select package from current directory as default
mrodm 54be65a
Use same method to find dashboards in Serverless via export API
mrodm d843e20
Increase number of packages per page
mrodm 7d6211b
Remove Response field in response - not present
mrodm d7b269b
Add comment
mrodm 6542265
Check success variable in import saved objects response
mrodm a8b59fb
Apply same changes for edit command when running with Serverless
mrodm b6dccd2
Remove unused parameters and fields
mrodm a2b0823
Re-phrase new dashboard option
mrodm 53cdeb3
Re-order function
mrodm 9cb72c3
Rename struct to get response from bulk_assets
mrodm 14db2d7
Make private more structs
mrodm 243539a
Delete comment
mrodm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,9 @@ package cmd | |
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/elastic/elastic-package/internal/packages" | ||
"github.com/elastic/elastic-package/internal/tui" | ||
|
||
"github.com/spf13/cobra" | ||
|
@@ -19,10 +21,14 @@ import ( | |
"github.com/elastic/elastic-package/internal/stack" | ||
) | ||
|
||
const exportDashboardsLongDescription = `Use this command to export dashboards with referenced objects from the Kibana instance. | ||
const ( | ||
exportDashboardsLongDescription = `Use this command to export dashboards with referenced objects from the Kibana instance. | ||
|
||
Use this command to download selected dashboards and other associated saved objects from Kibana. This command adjusts the downloaded saved objects according to package naming conventions (prefixes, unique IDs) and writes them locally into folders corresponding to saved object types (dashboard, visualization, map, etc.).` | ||
|
||
newDashboardOption = "Working on a new dashboard (show all available dashboards)" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Title for the option to be selected in order to show all the dashboards from the Serverless instance. This option is shown the first one in the list.
|
||
) | ||
|
||
func exportDashboardsCmd(cmd *cobra.Command, args []string) error { | ||
cmd.Println("Export Kibana dashboards") | ||
|
||
|
@@ -67,10 +73,26 @@ func exportDashboardsCmd(cmd *cobra.Command, args []string) error { | |
fmt.Printf("Warning: %s\n", message) | ||
} | ||
|
||
// Just query for dashboards if none were provided as flags | ||
if len(dashboardIDs) == 0 { | ||
dashboardIDs, err = promptDashboardIDs(cmd.Context(), kibanaClient) | ||
packageRoot, err := packages.MustFindPackageRoot() | ||
if err != nil { | ||
return fmt.Errorf("locating package root failed: %w", err) | ||
} | ||
m, err := packages.ReadPackageManifestFromPackageRoot(packageRoot) | ||
if err != nil { | ||
return fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
return fmt.Errorf("reading package manifest failed (path: %s): %w", packageRoot, err) | ||
} | ||
options := selectDashboardOptions{ | ||
ctx: cmd.Context(), | ||
kibanaClient: kibanaClient, | ||
kibanaVersion: kibanaVersion, | ||
defaultPackage: m.Name, | ||
} | ||
|
||
dashboardIDs, err = selectDashboardIDs(options) | ||
if err != nil { | ||
return fmt.Errorf("selecting dashboard IDs failed: %w", err) | ||
} | ||
|
||
if len(dashboardIDs) == 0 { | ||
|
@@ -88,12 +110,171 @@ func exportDashboardsCmd(cmd *cobra.Command, args []string) error { | |
return nil | ||
} | ||
|
||
func promptDashboardIDs(ctx context.Context, kibanaClient *kibana.Client) ([]string, error) { | ||
type selectDashboardOptions struct { | ||
ctx context.Context | ||
kibanaClient *kibana.Client | ||
kibanaVersion kibana.VersionInfo | ||
defaultPackage string | ||
} | ||
|
||
// selectDashboardIDs prompts the user to select dashboards to export. It handles | ||
// different flows depending on whether the Kibana instance is a Serverless environment or not. | ||
// In non-Serverless environments, it prompts directly for dashboard selection. | ||
// In Serverless environments, it first prompts to select an installed package or choose | ||
// to export new dashboards, and then prompts for dashboard selection accordingly. | ||
func selectDashboardIDs(options selectDashboardOptions) ([]string, error) { | ||
if options.kibanaVersion.BuildFlavor != kibana.ServerlessFlavor { | ||
// This method uses a deprecated API to search for saved objects. | ||
// And this API is not available in Serverless environments. | ||
dashboardIDs, err := promptDashboardIDsNonServerless(options.ctx, options.kibanaClient) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
} | ||
return dashboardIDs, nil | ||
} | ||
|
||
installedPackage, err := promptPackagesInstalled(options.ctx, options.kibanaClient, options.defaultPackage) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for package selection failed: %w", err) | ||
} | ||
|
||
if installedPackage == "" { | ||
fmt.Println("No installed packages were found in Kibana.") | ||
return nil, nil | ||
} | ||
|
||
if installedPackage == newDashboardOption { | ||
dashboardIDs, err := promptDashboardIDsServerless(options.ctx, options.kibanaClient) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
} | ||
return dashboardIDs, nil | ||
} | ||
|
||
// As it can be installed just one version of a package in Elastic, we can split by '-' to get the name. | ||
// This package name will be used to get the data related to a package (kibana.GetPackage). | ||
installedPackageName, _, found := strings.Cut(installedPackage, "-") | ||
if !found { | ||
return nil, fmt.Errorf("invalid package name: %s", installedPackage) | ||
} | ||
|
||
dashboardIDs, err := promptPackageDashboardIDs(options.ctx, options.kibanaClient, installedPackageName) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for package dashboard selection failed: %w", err) | ||
} | ||
|
||
return dashboardIDs, nil | ||
} | ||
|
||
func promptPackagesInstalled(ctx context.Context, kibanaClient *kibana.Client, defaultPackageName string) (string, error) { | ||
installedPackages, err := kibanaClient.FindInstalledPackages(ctx) | ||
if err != nil { | ||
return "", fmt.Errorf("finding installed packages failed: %w", err) | ||
} | ||
|
||
// First option is always to list all available dashboards even if they are not related | ||
// to any package. This is helpful in case the user is working on a new dashboard. | ||
options := []string{newDashboardOption} | ||
|
||
options = append(options, installedPackages.Strings()...) | ||
defaultOption := "" | ||
for _, ip := range installedPackages { | ||
if ip.Name == defaultPackageName { | ||
// set default package to the one matching the package in the current directory | ||
defaultOption = ip.String() | ||
break | ||
} | ||
} | ||
|
||
packagesPrompt := tui.NewSelect("Which packages would you like to export dashboards from?", options, defaultOption) | ||
|
||
var selectedOption string | ||
err = tui.AskOne(packagesPrompt, &selectedOption, tui.Required) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return selectedOption, nil | ||
} | ||
|
||
// promptPackageDashboardIDs prompts the user to select dashboards from the given package. | ||
// It requires the package name to fetch the installed package information from Kibana. | ||
func promptPackageDashboardIDs(ctx context.Context, kibanaClient *kibana.Client, packageName string) ([]string, error) { | ||
installedPackage, err := kibanaClient.GetPackage(ctx, packageName) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get package status: %w", err) | ||
} | ||
if installedPackage.Status == "not_installed" { | ||
return nil, fmt.Errorf("package %s is not installed", packageName) | ||
} | ||
|
||
// get asset titles from IDs | ||
packageAssets := []packages.Asset{} | ||
for _, asset := range installedPackage.InstallationInfo.InstalledKibanaAssets { | ||
if asset.Type != "dashboard" { | ||
continue | ||
} | ||
|
||
packageAssets = append(packageAssets, packages.Asset{ID: asset.ID, Type: asset.Type}) | ||
} | ||
|
||
assetsResponse, err := kibanaClient.GetDataFromPackageAssetIDs(ctx, packageAssets) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get package assets: %w", err) | ||
} | ||
|
||
dashboardIDOptions := []string{} | ||
for _, asset := range assetsResponse { | ||
if asset.Type != "dashboard" { | ||
continue | ||
} | ||
dashboardIDOptions = append(dashboardIDOptions, asset.String()) | ||
} | ||
|
||
if len(dashboardIDOptions) == 0 { | ||
return nil, fmt.Errorf("no dashboards found for package %s", packageName) | ||
} | ||
|
||
dashboardsPrompt := tui.NewMultiSelect("Which dashboards would you like to export?", dashboardIDOptions, []string{}) | ||
dashboardsPrompt.SetPageSize(100) | ||
|
||
var selectedOptions []string | ||
err = tui.AskOne(dashboardsPrompt, &selectedOptions, tui.Required) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var selectedIDs []string | ||
for _, option := range selectedOptions { | ||
for _, asset := range assetsResponse { | ||
if asset.String() == option { | ||
selectedIDs = append(selectedIDs, asset.ID) | ||
} | ||
} | ||
} | ||
|
||
return selectedIDs, nil | ||
} | ||
|
||
func promptDashboardIDsServerless(ctx context.Context, kibanaClient *kibana.Client) ([]string, error) { | ||
savedDashboards, err := kibanaClient.FindServerlessDashboards(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("finding dashboards failed: %w", err) | ||
} | ||
|
||
return promptDashboardIDs(savedDashboards) | ||
} | ||
|
||
func promptDashboardIDsNonServerless(ctx context.Context, kibanaClient *kibana.Client) ([]string, error) { | ||
savedDashboards, err := kibanaClient.FindDashboards(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("finding dashboards failed: %w", err) | ||
} | ||
|
||
return promptDashboardIDs(savedDashboards) | ||
} | ||
|
||
func promptDashboardIDs(savedDashboards kibana.DashboardSavedObjects) ([]string, error) { | ||
if len(savedDashboards) == 0 { | ||
return []string{}, nil | ||
} | ||
|
@@ -102,7 +283,7 @@ func promptDashboardIDs(ctx context.Context, kibanaClient *kibana.Client) ([]str | |
dashboardsPrompt.SetPageSize(100) | ||
|
||
var selectedOptions []string | ||
err = tui.AskOne(dashboardsPrompt, &selectedOptions, tui.Required) | ||
err := tui.AskOne(dashboardsPrompt, &selectedOptions, tui.Required) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could be helpful for the user to select the package where they are located if any. But this should not raise an error if the command does not run from a package.