// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Standard Library using System; using System.Collections.Generic; using System.Linq; // Unity using UnityEditor; using UnityEngine; // GameKit using AWS.GameKit.Common.Models; using AWS.GameKit.Editor.Core; using AWS.GameKit.Editor.GUILayoutExtensions; using AWS.GameKit.Editor.Models; using AWS.GameKit.Editor.Models.FeatureSettings; using AWS.GameKit.Editor.Utils; using AWS.GameKit.Runtime.Core; using AWS.GameKit.Runtime.Models; namespace AWS.GameKit.Editor.Windows.Settings { /// /// Base class for all feature settings tabs.

/// /// This tab lets users configure the feature's settings and create/redeploy/delete the feature and it's dashboard. ///
[Serializable] public abstract class FeatureSettingsTab : IDrawable { private const string LEARN_MORE_TEXT = "Learn more about dashboards"; private const string CURRENT_VALUE = "CurrentValue"; private List _resourceList; protected bool _isFeatureBeingProcessed = false; protected bool _isFeatureMainStackBeingRedeployed = false; /// /// A list of all features settings tabs that are initialized, used to create the short versions in All Features /// public static List FeatureSettingsTabsInstances = new List(); /// /// The feature which these settings are for. /// public abstract FeatureType FeatureType { get; } /// /// This feature's specific settings, excluding common settings such as dashboards. /// protected abstract IEnumerable FeatureSpecificSettings { get; } /// /// Any secrets that a feature may need to store such as password fields and secret keys. Secrets will be stored in AWS Secrets Manager. /// protected abstract IEnumerable FeatureSecrets { get; } /// /// Whether the DrawSettings() method should be called. If false, then a message saying "No feature settings" will be displayed in the settings section. /// protected virtual bool ShouldDrawSettingsSection() => true; /// /// Whether the feature allows reload of settings. /// protected virtual bool ShouldReloadFeatureSettings() => true; /// /// All of this feature's settings, including both feature-specific settings and common settings (such as dashboards). /// private IEnumerable AllFeatureSettings => CommonSettings.Concat(FeatureSpecificSettings); // Common Feature Settings private IEnumerable CommonSettings => new List() { _isDashboardEnabled }; [SerializeField] private FeatureSettingBool _isDashboardEnabled = new FeatureSettingBool("cloudwatch_dashboard_enabled", defaultValue: true); // State [SerializeField] private Vector2 _scrollPosition; private bool _isRefreshing; // Dependencies protected ICoreWrapperProvider _coreWrapper; private GameKitManager _gameKitManager; private GameKitEditorManager _gameKitEditorManager; private FeatureResourceManager _featureResourceManager; private FeatureDeploymentOrchestrator _featureDeploymentOrchestrator; protected SerializedProperty _serializedProperty; // Links private readonly LinkWidget _learnMoreAboutDashboardsDeployedLink = new LinkWidget(L10n.Tr("Active"), L10n.Tr(LEARN_MORE_TEXT), DocumentationURLs.CLOUDWATCH_DASHBOARDS_REFERENCE); private readonly LinkWidget _learnMoreAboutDashboardsUndeployedLink = new LinkWidget(L10n.Tr("Inactive"), L10n.Tr(LEARN_MORE_TEXT), DocumentationURLs.CLOUDWATCH_DASHBOARDS_REFERENCE, spaceAfterPrependedText: 10f); private readonly LinkWidget _learnMoreAboutDashboardsNoCredentialsLink = new LinkWidget(L10n.Tr("Enter valid environment and credentials to see dashboard status."), L10n.Tr(LEARN_MORE_TEXT), DocumentationURLs.CLOUDWATCH_DASHBOARDS_REFERENCE, spaceAfterPrependedText: 10f); public virtual void Initialize(SettingsDependencyContainer dependencies, SerializedProperty serializedProperty) { // Dependencies _coreWrapper = dependencies.CoreWrapper; _gameKitManager = dependencies.GameKitManager; _gameKitEditorManager = dependencies.GameKitEditorManager; _featureResourceManager = dependencies.FeatureResourceManager; _featureDeploymentOrchestrator = dependencies.FeatureDeploymentOrchestrator; _serializedProperty = serializedProperty; foreach (SecretSetting featureSecret in FeatureSecrets) { if (_coreWrapper.GameKitAccountCheckSecretExists(featureSecret.SecretIdentifier) == GameKitErrors.GAMEKIT_SUCCESS) { featureSecret.IsStoredInCloud = true; } } LoadFeatureSettings(false); if (!FeatureSettingsTabsInstances.Contains(this)) { FeatureSettingsTabsInstances.Add(this); } } public void ReloadFeatureSettings() { if (ShouldReloadFeatureSettings()) { LoadFeatureSettings(true); } } public void OnGUI() { using (EditorGUILayout.ScrollViewScope scrollView = new EditorGUILayout.ScrollViewScope(_scrollPosition)) { _scrollPosition = scrollView.scrollPosition; EditorGUILayoutElements.Description(L10n.Tr("Configure how you want this feature to function and create or redeploy the game backend."), indentationLevel: 0); EditorGUILayoutElements.SectionDivider(); if (ShouldDrawSettingsSection()) { EditorGUILayoutElements.SectionHeader(L10n.Tr("Configure")); DrawSettings(); } else { DrawNoSettingsToDisplay(); } } GUILayout.FlexibleSpace(); DrawFooter(); } /// /// Draw the settings for this feature. /// protected abstract void DrawSettings(); /// /// Display a GUI element to let the user know there are no settings to display for the current feature. /// private void DrawNoSettingsToDisplay() { GUILayout.FlexibleSpace(); EditorGUILayoutElements.SectionHeader(L10n.Tr("No feature settings"), 0, TextAnchor.MiddleCenter); GUIStyle centeredTextStyle = new GUIStyle("label"); centeredTextStyle.alignment = TextAnchor.MiddleCenter; EditorGUILayout.LabelField(L10n.Tr("This feature has no settings to configure."), centeredTextStyle); } private void DrawFooter() { EditorGUILayoutElements.SectionDivider(); EditorGUILayoutElements.SectionHeader(L10n.Tr("Deploy")); DrawDashboardDeployment(); EditorGUILayout.Space(15f); DrawFeatureDeployment(); EditorGUILayout.Space(15f); DrawFeatureDescription(); } private void DrawDashboardDeployment() { const string CLOUD_DEPLOY_STATE = "cloudwatch_dashboard_enabled"; bool isDashboardDeployed; Dictionary settings = new Dictionary(); if (_gameKitEditorManager.CredentialsSubmitted) { settings = _coreWrapper.SettingsGetFeatureVariables(FeatureType); } if (settings.ContainsKey(CLOUD_DEPLOY_STATE)) { string dashboardDeployed; settings.TryGetValue(CLOUD_DEPLOY_STATE, out dashboardDeployed); isDashboardDeployed = Boolean.Parse(dashboardDeployed); } else { isDashboardDeployed = false; } bool isDeployButtonEnabled = _featureDeploymentOrchestrator.CanCreateFeature(FeatureType).CanExecuteAction || _featureDeploymentOrchestrator.CanRedeployFeature(FeatureType).CanExecuteAction; // Dashboard status if (_gameKitEditorManager.CredentialsSubmitted) { if (isDashboardDeployed) { EditorGUILayoutElements.CustomField(L10n.Tr("Dashboard status"), () => { if (isDeployButtonEnabled) { EditorGUILayoutElements.DeploymentStatusIcon(FeatureStatus.Deployed); GetOpenDashboardLink().OnGUI(); EditorGUILayout.Space(20f); } _learnMoreAboutDashboardsDeployedLink.OnGUI(); }, indentationLevel: 0); } else { EditorGUILayoutElements.CustomField(L10n.Tr("Dashboard status"), () => { EditorGUILayoutElements.DeploymentStatusIcon(FeatureStatus.Undeployed); _learnMoreAboutDashboardsUndeployedLink.OnGUI(); }, indentationLevel: 0); } } else { EditorGUILayoutElements.CustomField(L10n.Tr("Dashboard status"), () => { EditorGUILayoutElements.DeploymentStatusIcon(FeatureStatus.Unknown); _learnMoreAboutDashboardsNoCredentialsLink.OnGUI(); }, indentationLevel: 0); } // Dashboard action using (new EditorGUILayout.HorizontalScope()) { EditorGUILayoutElements.PrefixLabel(L10n.Tr("Dashboard action"), indentationLevel: 0); EditorGUILayoutElements.KeepPreviousPrefixLabelEnabled(); DrawDashboardDeploymentButton(isDeployButtonEnabled, isDashboardDeployed); } } private IDrawable GetOpenDashboardLink() { // This LinkWidget needs to be created during OnGUI to make sure the dashboard URL matches the current environment. return new LinkWidget(L10n.Tr("Open Dashboard"), GetDashboardUrl(), new LinkWidget.Options() { // Remove the gap between this link and the "Learn more" link that comes after. // If either of these below options were removed, then there would be a large gap before the "Learn more" link. ShouldWordWrapLinkLabel = false, Alignment = LinkWidget.Alignment.None }); } private string GetDashboardUrl() { string gameName = _featureResourceManager.GetGameName(); string environmentCode = _featureResourceManager.GetLastUsedEnvironment(); string region = _featureResourceManager.GetLastUsedRegion(); return FeatureType.GetDashboardUrl(gameName, environmentCode, region); } private void DrawDashboardDeploymentButton(bool isButtonEnabled, bool isDashboardDeployed) { string buttonText = isDashboardDeployed ? L10n.Tr("Deactivate") : L10n.Tr("Activate"); Color buttonColor = isDashboardDeployed ? SettingsGUIStyles.Buttons.GUIButtonRed.Get() : SettingsGUIStyles.Buttons.GUIButtonGreen.Get(); Action clickFunction = isDashboardDeployed ? (Action)OnClickDashboardDeactivate : (Action)OnClickDashboardActivate; if (EditorGUILayoutElements.Button(buttonText, isButtonEnabled, colorWhenEnabled: buttonColor)) { clickFunction(); } } public FeatureStatus GetFeatureStatus() { FeatureStatus deploymentStatus = _featureDeploymentOrchestrator.GetFeatureStatus(FeatureType); // This ensures that we are not displaying status of Main Stack for another feature that is redeployed in parallel if( deploymentStatus == FeatureStatus.GeneratingTemplates || deploymentStatus == FeatureStatus.UploadingDashboards || deploymentStatus == FeatureStatus.UploadingLayers || deploymentStatus == FeatureStatus.UploadingFunctions || deploymentStatus == FeatureStatus.DeployingResources) { _isFeatureMainStackBeingRedeployed = false; } // Display deployment status for main stack if((_isFeatureBeingProcessed && deploymentStatus == FeatureStatus.Undeployed) || _isFeatureMainStackBeingRedeployed) { FeatureStatus mainStackStatus = _featureDeploymentOrchestrator.GetFeatureStatus(FeatureType.Main); if(mainStackStatus != FeatureStatus.Deployed) { deploymentStatus = mainStackStatus; } } return deploymentStatus; } private string AppendBlockingFeatures(string originalMessage, CanExecuteDeploymentActionResult result) { string features = string.Join(", ", result.BlockingFeatures.Select(f => f.GetDisplayName()).ToList()); return originalMessage + features; } private string DisabledDeploymentReasons(CanExecuteDeploymentActionResult result) { if (result.CanExecuteAction) { return string.Empty; } switch (result.Reason) { case DeploymentActionBlockedReason.CredentialsInvalid: return "Must submit valid credentials before this action is enabled"; case DeploymentActionBlockedReason.DependenciesMustBeCreated: return AppendBlockingFeatures("Must deploy these features first for this action to be enabled: ", result); case DeploymentActionBlockedReason.DependenciesMustBeDeleted: return AppendBlockingFeatures("Must delete these features first for this action to be enabled: ", result); case DeploymentActionBlockedReason.DependenciesStatusIsInvalid: return AppendBlockingFeatures("A feature this action depends on has an invalid status: ", result); case DeploymentActionBlockedReason.FeatureMustBeCreated: return "This feature must be created first for this action to be enabled."; case DeploymentActionBlockedReason.FeatureMustBeDeleted: return "This feature must be deleted first for this action to be enabled."; case DeploymentActionBlockedReason.FeatureStatusIsUnknown: return "The status of this feature cannot be determined, this action will be disabled until status is retrievable."; case DeploymentActionBlockedReason.OngoingDeployments: if (!result.BlockingFeatures.Contains(this.FeatureType)) { return AppendBlockingFeatures("This action is disabled while the following features are updating: ", result); } return "This action is disabled while any update is in progress for this feature."; case DeploymentActionBlockedReason.MainStackNotReady: return "This action is disabled while the Main stack is updating and not in a ready state"; default: return "Action is disabled for an unknown reason."; } } private void DrawFeatureDeployment() { // State FeatureStatus deploymentStatus = GetFeatureStatus(); CanExecuteDeploymentActionResult canCreate = _featureDeploymentOrchestrator.CanCreateFeature(FeatureType); CanExecuteDeploymentActionResult canRedeploy = _featureDeploymentOrchestrator.CanRedeployFeature(FeatureType); CanExecuteDeploymentActionResult canDelete = _featureDeploymentOrchestrator.CanDeleteFeature(FeatureType); // Deployment status if (_gameKitEditorManager.CredentialsSubmitted) { EditorGUILayoutElements.CustomField(L10n.Tr("Deployment status"), () => { EditorGUILayoutElements.DeploymentStatusIcon(deploymentStatus); GUILayout.Label(deploymentStatus.GetDisplayName(), CommonGUIStyles.DeploymentStatusText); if(EditorGUILayoutElements.DeploymentRefreshIconButton(_isRefreshing)) { OnClickFeatureRefresh(); } GUILayout.FlexibleSpace(); }, guiStyle: CommonGUIStyles.DeploymentStatus, indentationLevel: 0); } else { EditorGUILayoutElements.CustomField(L10n.Tr("Deployment status"), () => { EditorGUILayout.LabelField(L10n.Tr("No environment selected.")); }, indentationLevel: 0); } // AWS resource actions using (new EditorGUILayout.HorizontalScope()) { EditorGUILayoutElements.PrefixLabel(L10n.Tr("AWS resource actions"), indentationLevel: 0); EditorGUILayoutElements.KeepPreviousPrefixLabelEnabled(); if (EditorGUILayoutElements.Button(L10n.Tr("Create"), isEnabled: canCreate.CanExecuteAction && _gameKitEditorManager.CredentialsSubmitted, tooltip: DisabledDeploymentReasons(canCreate), colorWhenEnabled: SettingsGUIStyles.Buttons.GUIButtonGreen.Get())) { _isFeatureBeingProcessed = true; OnClickFeatureCreate(); } if (EditorGUILayoutElements.Button(L10n.Tr("Redeploy"), isEnabled: canRedeploy.CanExecuteAction && _gameKitEditorManager.CredentialsSubmitted, tooltip: DisabledDeploymentReasons(canRedeploy))) { _isFeatureMainStackBeingRedeployed = true; OnClickFeatureRedeploy(); } if (EditorGUILayoutElements.Button(L10n.Tr("Delete"), isEnabled: canDelete.CanExecuteAction && _gameKitEditorManager.CredentialsSubmitted, tooltip: DisabledDeploymentReasons(canDelete), colorWhenEnabled: SettingsGUIStyles.Buttons.GUIButtonRed.Get())) { _isFeatureBeingProcessed = true; OnClickFeatureDelete(); } } } private void DrawFeatureDescription() { using (new EditorGUILayout.VerticalScope()) { EditorGUILayoutElements.HelpBoxWithReadMore($"{FeatureType.GetVerboseDescription()} {L10n.Tr("Uses AWS services")}: {FeatureType.GetResourcesUIString()}", FeatureType.GetDocumentationUrl()); } } #region All Features summary public void DrawFeatureSummary() { // State FeatureStatus deploymentStatus = GetFeatureStatus(); CanExecuteDeploymentActionResult canCreate = _featureDeploymentOrchestrator.CanCreateFeature(FeatureType); CanExecuteDeploymentActionResult canRedeploy = _featureDeploymentOrchestrator.CanRedeployFeature(FeatureType); CanExecuteDeploymentActionResult canDelete = _featureDeploymentOrchestrator.CanDeleteFeature(FeatureType); using (new EditorGUILayout.HorizontalScope()) { // Deployment status if (_gameKitEditorManager.CredentialsSubmitted) { EditorGUILayoutElements.CustomField(L10n.Tr(FeatureType.GetDisplayName()), () => { EditorGUILayoutElements.DeploymentStatusIcon(deploymentStatus); GUILayout.Label(deploymentStatus.GetDisplayName()); GUILayout.FlexibleSpace(); }, indentationLevel: 0); } else { EditorGUILayoutElements.CustomField(L10n.Tr(FeatureType.GetDisplayName()), () => { EditorGUILayout.LabelField(L10n.Tr("No environment selected.")); }, indentationLevel: 0); } // AWS resource actions if (EditorGUILayoutElements.SmallButton(L10n.Tr("Create"), isEnabled: canCreate.CanExecuteAction && _gameKitEditorManager.CredentialsSubmitted, colorWhenEnabled: SettingsGUIStyles.Buttons.GUIButtonGreen.Get(), tooltip: DisabledDeploymentReasons(canCreate))) { _isFeatureBeingProcessed = true; OnClickFeatureCreate(); } if (EditorGUILayoutElements.SmallButton(L10n.Tr("Redeploy"), isEnabled: canRedeploy.CanExecuteAction && _gameKitEditorManager.CredentialsSubmitted, tooltip: DisabledDeploymentReasons(canRedeploy))) { _isFeatureMainStackBeingRedeployed = true; OnClickFeatureRedeploy(); } if (EditorGUILayoutElements.SmallButton(L10n.Tr("Delete"), isEnabled: canDelete.CanExecuteAction && _gameKitEditorManager.CredentialsSubmitted, colorWhenEnabled: SettingsGUIStyles.Buttons.GUIButtonRed.Get(), tooltip: DisabledDeploymentReasons(canDelete))) { _isFeatureBeingProcessed = true; OnClickFeatureDelete(); } } } #endregion #region Click Functions private void OnClickDashboardActivate() { _isDashboardEnabled.CurrentValue = true; OnToggleDashboard(); } private void OnClickDashboardDeactivate() { _isDashboardEnabled.CurrentValue = false; OnToggleDashboard(); } private void OnToggleDashboard() { if (_featureDeploymentOrchestrator.CanCreateFeature(FeatureType).CanExecuteAction) { OnClickFeatureCreate(); return; } if (_featureDeploymentOrchestrator.CanRedeployFeature(FeatureType).CanExecuteAction) { OnClickFeatureRedeploy(); return; } } /// /// Upon deployment or redeployment will save any enabled and non empty secret values, such as secret IDs, to AWS Secrets Manager /// protected void SaveSecretSettingsToCloud() { GUI.FocusControl(null); // Used to ensure that no secret ID inputs are selected. Clearing out the value of a selected field will not refresh it in the GUI if selected. foreach (SecretSetting featureSecret in FeatureSecrets) { if (!string.IsNullOrEmpty(featureSecret.SecretValue)) { if (_coreWrapper.GameKitAccountSaveSecret(featureSecret.SecretIdentifier, featureSecret.SecretValue) == GameKitErrors.GAMEKIT_SUCCESS) { featureSecret.IsStoredInCloud = true; featureSecret.SecretValue = string.Empty; } else { featureSecret.IsStoredInCloud = false; } } } } private void OnClickFeatureCreate() { SaveFeatureSettings(); SaveSecretSettingsToCloud(); _featureDeploymentOrchestrator.CreateFeature(FeatureType, (DeploymentResponseResult response) => { if (response.ResultCode == GameKitErrors.GAMEKIT_SUCCESS) { _gameKitManager.CopyAndReloadConfigFile(_featureResourceManager.GetGameName(), _featureResourceManager.GetLastUsedEnvironment()); } else { // Any errors are already logged by CreateFeature(). } _isFeatureBeingProcessed = false; }); } private void OnClickFeatureRedeploy() { SaveFeatureSettings(); SaveSecretSettingsToCloud(); _featureDeploymentOrchestrator.RedeployFeature(FeatureType, (DeploymentResponseResult response) => { if (response.ResultCode == GameKitErrors.GAMEKIT_SUCCESS) { _gameKitManager.CopyAndReloadConfigFile(_featureResourceManager.GetGameName(), _featureResourceManager.GetLastUsedEnvironment()); } else { // Any errors are already logged by RedeployFeature(). } // Reset the value when the feature stack deployment completes in either success or failure _isFeatureMainStackBeingRedeployed = false; }); } private void OnClickFeatureRefresh() { _isRefreshing = true; _featureDeploymentOrchestrator.RefreshFeatureStatuses((result => { // This API always returns GAMEKIT_SUCCESS _isRefreshing = false; _featureResourceManager.InitializeSettings(true); })); } private void OnClickFeatureDelete() { _resourceList = new List(); _featureDeploymentOrchestrator.DescribeFeatureResources(FeatureType, (MultiResourceInfoCallbackResult result) => { for (int i = 0; i < result.LogicalResourceId.Length; i++) { _resourceList.Add(result.ResourceType[i] + " resource with id " + result.LogicalResourceId[i] + " in " + result.ResourceStatus[i] + " status.\n"); } // open UI to list the resources, confirm deletion with all the interactions there. DeleteFeatureWindow.ShowWindow(FeatureTypeConverter.GetDisplayName(FeatureType), _resourceList, ()=> { _featureDeploymentOrchestrator.DeleteFeature(FeatureType, (DeploymentResponseResult response) => { if (response.ResultCode == GameKitErrors.GAMEKIT_SUCCESS) { _isDashboardEnabled.CurrentValue = false; SaveFeatureSettings(); _gameKitManager.CopyAndReloadConfigFile(_featureResourceManager.GetGameName(), _featureResourceManager.GetLastUsedEnvironment()); } _isFeatureBeingProcessed = false; }); _isRefreshing = true; _featureDeploymentOrchestrator.RefreshFeatureStatuses((result => { // This API always returns GAMEKIT_SUCCESS _isRefreshing = false; _featureResourceManager.InitializeSettings(true); })); }); }); } #endregion /// /// Load all of this feature's settings from the saveInfo.yml file.

/// /// Set all of the feature's settings in memory to their value saved in the saveInfo.yml file (if the persisted values exist), otherwise leave them with their default values. ///
private void LoadFeatureSettings(bool resetDefaultIfNotPersisted) { if (!_gameKitEditorManager.CredentialsSubmitted) { // There's no saveInfo.yml file to load. // Use the default settings for this feature. return; } foreach (IFeatureSetting featureSetting in AllFeatureSettings) { if (_featureResourceManager.TryGetFeatureVariable(FeatureType, featureSetting.VariableName, out string persistedValue)) { featureSetting.SetCurrentValueFromString(persistedValue); } else { // Use the default settings. // The featureSetting.CurrentValue was already set to the default value during the FeatureSetting's class constructor. // Reset explicitly to default if flag is set if (resetDefaultIfNotPersisted) { featureSetting.SetCurrentValueFromString(featureSetting.DefaultValueString); } } } } /// /// Save this feature's settings to the saveInfo.yml file. /// private void SaveFeatureSettings() { _featureResourceManager.SetFeatureVariables( AllFeatureSettings.Select(featureSetting => Tuple.Create(FeatureType, featureSetting.VariableName, featureSetting.CurrentValueString)), () => { }); } protected SerializedProperty GetFeatureSettingProperty(string settingName) { SerializedProperty settingProperty = _serializedProperty.FindPropertyRelative(settingName); return settingProperty.FindPropertyRelative(CURRENT_VALUE); } } }