2024-10-17 17:23:05 +03:00

1768 lines
84 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Edgegap.Editor.Api;
using Edgegap.Editor.Api.Models;
using Edgegap.Editor.Api.Models.Requests;
using Edgegap.Editor.Api.Models.Results;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using Application = UnityEngine.Application;
namespace Edgegap.Editor
{
/// <summary>
/// Editor logic event handler for "UI Builder" EdgegapWindow.uxml, superceding` EdgegapWindow.cs`.
/// </summary>
public class EdgegapWindowV2 : EditorWindow
{
#region Vars
public static bool IsLogLevelDebug =>
EdgegapWindowMetadata.LOG_LEVEL == EdgegapWindowMetadata.LogLevel.Debug;
private bool IsInitd;
private VisualTreeAsset _visualTree;
private bool _isApiTokenVerified; // Toggles the rest of the UI
private bool _isContainerRegistryReady;
private Sprite _appIconSpriteObj;
private string _appIconBase64Str;
#pragma warning disable CS0414 // MIRROR CHANGE: hide unused warning
private ApiEnvironment _apiEnvironment; // TODO: Swap out hard-coding with UI element?
#pragma warning restore CS0414 // END MIRROR CHANGE
private GetRegistryCredentialsResult _credentials;
private static readonly Regex _appNameAllowedCharsRegex = new Regex(@"^[a-zA-Z0-9_\-+\.]*$"); // MIRROR CHANGE: 'new()' not supported in Unity 2020
private GetCreateAppResult _loadedApp;
/// <summary>TODO: Make this a list</summary>
private GetDeploymentStatusResult _lastKnownDeployment;
private string _deploymentRequestId;
private string _userExternalIp;
private bool _isAwaitingDeploymentReadyStatus;
#endregion // Vars
#region Vars -> Interactable Elements
private Button _debugBtn;
/// <summary>(!) This is saved manually to EditorPrefs via Base64 instead of via UiBuilder</summary>
private TextField _apiTokenInput;
private Button _apiTokenVerifyBtn;
private Button _apiTokenGetBtn;
private VisualElement _postAuthContainer;
private Foldout _appInfoFoldout;
private Button _appLoadExistingBtn;
private TextField _appNameInput;
/// <summary>`Sprite` type</summary>
private ObjectField _appIconSpriteObjInput;
private Button _appCreateBtn;
private Label _appCreateResultLabel;
private Foldout _containerRegistryFoldout;
private TextField _containerNewTagVersionInput;
private TextField _containerPortNumInput;
// MIRROR CHANGE: EnumField Port type fails to resolve unless in Assembly-CSharp-Editor.dll. replace with regular Dropdown instead.
/// <summary>`ProtocolType` type</summary>
// private EnumField _containerTransportTypeEnumInput;
private PopupField<string> _containerTransportTypeEnumInput;
// END MIRROR CHANGE
private Toggle _containerUseCustomRegistryToggle;
private VisualElement _containerCustomRegistryWrapper;
private TextField _containerRegistryUrlInput;
private TextField _containerImageRepositoryInput;
private TextField _containerUsernameInput;
private TextField _containerTokenInput;
private Button _containerBuildAndPushServerBtn;
private Label _containerBuildAndPushResultLabel;
private Foldout _deploymentsFoldout;
private Button _deploymentsRefreshBtn;
private Button _deploymentsCreateBtn;
/// <summary>display:none (since it's on its own line), rather than !visible.</summary>
private Label _deploymentsStatusLabel;
private VisualElement _deploymentsServerDataContainer;
private Button _deploymentConnectionCopyUrlBtn;
private TextField _deploymentsConnectionUrlReadonlyInput;
private Label _deploymentsConnectionStatusLabel;
private Button _deploymentsConnectionStopBtn;
private Button _footerDocumentationBtn;
private Button _footerNeedMoreGameServersBtn;
#endregion // Vars
// MIRROR CHANGE
// get the path of this .cs file so we don't need to hardcode paths to
// the .uxml and .uss files:
// https://forum.unity.com/threads/too-many-hard-coded-paths-in-the-templates-and-documentation.728138/
// this way users can move this folder without breaking UIToolkit paths.
internal string StylesheetPath =>
Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this)));
// END MIRROR CHANGE
// MIRROR CHANGE: images are dragged into the script in inspector and assigned to the UI at runtime. this way we don't need to hardcode it.
public Texture2D LogoImage;
public Texture2D ClipboardImage;
// END MIRROR CHANGE
[MenuItem("Edgegap/Edgegap Hosting")] // MIRROR CHANGE: more obvious title
public static void ShowEdgegapToolWindow()
{
EdgegapWindowV2 window = GetWindow<EdgegapWindowV2>();
window.titleContent = new GUIContent("Edgegap Hosting"); // MIRROR CHANGE: 'Edgegap Server Management' is too long for the tab space
window.maxSize = new Vector2(635, 900);
window.minSize = window.maxSize;
}
#region Unity Funcs
protected void OnEnable()
{
#if UNITY_2021_3_OR_NEWER // MIRROR CHANGE: only load stylesheet in supported Unity versions, otherwise it shows errors in U2020
// Set root VisualElement and style: V2 still uses EdgegapWindow.[uxml|uss]
// BEGIN MIRROR CHANGE
_visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>($"{StylesheetPath}/EdgegapWindow.uxml");
StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"{StylesheetPath}/EdgegapWindow.uss");
// END MIRROR CHANGE
rootVisualElement.styleSheets.Add(styleSheet);
#endif
}
#pragma warning disable CS1998 // MIRROR CHANGE: disable async warning in U2020
public async void CreateGUI()
#pragma warning restore CS1998 // END MIRROR CHANGE
{
// MIRROR CHANGE: the UI requires 'GroupBox', which is not available in Unity 2019/2020.
// showing it will break all of Unity's Editor UIs, not just this one.
// instead, show a warning that the Edgegap plugin only works on Unity 2021+
#if !UNITY_2021_3_OR_NEWER
Debug.LogWarning("The Edgegap Hosting plugin requires UIToolkit in Unity 2021.3 or newer. Please upgrade your Unity version to use this.");
#else
// Get UI elements from UI Builder
rootVisualElement.Clear();
_visualTree.CloneTree(rootVisualElement);
// Register callbacks and sync UI builder elements to fields here
InitUIElements();
syncFormWithObjectStatic();
await syncFormWithObjectDynamicAsync(); // API calls
IsInitd = true;
#endif
}
/// <summary>The user closed the window. Save the data.</summary>
protected void OnDisable()
{
#if UNITY_2021_3_OR_NEWER // MIRROR CHANGE: only load stylesheet in supported Unity versions, otherwise it shows errors in U2020
// MIRROR CHANGE: sometimes this is called without having been registered, throwing NRE
if (_debugBtn == null) return;
// END MIRROR CHANGE
unregisterClickEvents();
unregisterFieldCallbacks();
SyncObjectWithForm();
#endif
}
#endregion // Unity Funcs
#region Init
/// <summary>
/// Binds the form inputs to the associated variables and initializes the inputs as required.
/// Requires the VisualElements to be loaded before this call. Otherwise, the elements cannot be found.
/// </summary>
private void InitUIElements()
{
setVisualElementsToFields();
assertVisualElementKeys();
closeDisableGroups();
registerClickCallbacks();
registerFieldCallbacks();
initToggleDynamicUi();
AssignImages(); // MIRROR CHANGE
}
private void closeDisableGroups()
{
_appInfoFoldout.value = false;
_containerRegistryFoldout.value = false;
_deploymentsFoldout.value = false;
_appInfoFoldout.SetEnabled(false);
_containerRegistryFoldout.SetEnabled(false);
_deploymentsFoldout.SetEnabled(false);
}
// MIRROR CHANGE: assign images to the UI at runtime instead of hardcoding it
void AssignImages()
{
// header logo
VisualElement logoElement = rootVisualElement.Q<VisualElement>("header-logo-img");
logoElement.style.backgroundImage = LogoImage;
// clipboard button
VisualElement copyElement = rootVisualElement.Q<VisualElement>("DeploymentConnectionCopyUrlBtn");
copyElement.style.backgroundImage = ClipboardImage;
}
// END MIRROR CHANGE
/// <summary>Set fields referencing UI Builder's fields. In order of appearance from top-to-bottom.</summary>
private void setVisualElementsToFields()
{
_debugBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEBUG_BTN_ID);
_apiTokenInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.API_TOKEN_TXT_ID);
_apiTokenVerifyBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID);
_apiTokenGetBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID);
_postAuthContainer = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID);
_appInfoFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID);
_appNameInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.APP_NAME_TXT_ID);
_appLoadExistingBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID);
_appIconSpriteObjInput = rootVisualElement.Q<ObjectField>(EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID);
_appCreateBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.APP_CREATE_BTN_ID);
_appCreateResultLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID);
_containerRegistryFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID);
_containerNewTagVersionInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_NEW_TAG_VERSION_TXT_ID);
_containerPortNumInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID);
// MIRROR CHANGE: dynamically resolving PortType fails if not in Assembly-CSharp-Editor.dll. Hardcode UDP/TCP/WS instead.
// this finds the placeholder and dynamically replaces it with a popup field
VisualElement dropdownPlaceholder = rootVisualElement.Q<VisualElement>("MIRROR_CHANGE_PORT_HARDCODED");
List<string> options = Enum.GetNames(typeof(ProtocolType)).Cast<string>().ToList();
_containerTransportTypeEnumInput = new PopupField<string>("Protocol Type", options, 0);
dropdownPlaceholder.Add(_containerTransportTypeEnumInput);
// END MIRROR CHANGE
_containerUseCustomRegistryToggle = rootVisualElement.Q<Toggle>(EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID);
_containerCustomRegistryWrapper = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID);
_containerRegistryUrlInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID);
_containerImageRepositoryInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID);
_containerUsernameInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID);
_containerTokenInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID);
_containerBuildAndPushServerBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_BTN_ID);
_containerBuildAndPushResultLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID);
_deploymentsFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID);
_deploymentsRefreshBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID);
_deploymentsCreateBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID);
_deploymentsStatusLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID);
_deploymentsServerDataContainer = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID); // Dynamic
_deploymentConnectionCopyUrlBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID);
_deploymentsConnectionUrlReadonlyInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID);
_deploymentsConnectionStatusLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID);
_deploymentsConnectionStopBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID);
_footerDocumentationBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID);
_footerNeedMoreGameServersBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID);
_apiEnvironment = EdgegapWindowMetadata.API_ENVIRONMENT; // (!) TODO: Hard-coded while unused in UI
}
/// <summary>
/// Sanity check: If we implicitly changed an #Id, we need to know early so we can update the const.
/// In order of appearance seen in setVisualElementsToFields().
/// </summary>
private void assertVisualElementKeys()
{
// MIRROR CHANGE: this doesn't compile in Unity 2019
/*
try
{
Assert.IsTrue(_apiTokenInput is { name: EdgegapWindowMetadata.API_TOKEN_TXT_ID },
$"Expected {nameof(_apiTokenInput)} via #{EdgegapWindowMetadata.API_TOKEN_TXT_ID}");
Assert.IsTrue(_apiTokenVerifyBtn is { name: EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID },
$"Expected {nameof(_apiTokenVerifyBtn)} via #{EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID}");
Assert.IsTrue(_apiTokenGetBtn is { name: EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID },
$"Expected {nameof(_apiTokenGetBtn)} via #{EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID}");
Assert.IsTrue(_postAuthContainer is { name: EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID },
$"Expected {nameof(_postAuthContainer)} via #{EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID}");
Assert.IsTrue(_appInfoFoldout is { name: EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID },
$"Expected {nameof(_appInfoFoldout)} via #{EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID}");
Assert.IsTrue(_appNameInput is { name: EdgegapWindowMetadata.APP_NAME_TXT_ID },
$"Expected {nameof(_appNameInput)} via #{EdgegapWindowMetadata.APP_NAME_TXT_ID}");
Assert.IsTrue(_appLoadExistingBtn is { name: EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID },
$"Expected {nameof(_appLoadExistingBtn)} via #{EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID}");
Assert.IsTrue(_appIconSpriteObjInput is { name: EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID },
$"Expected {nameof(_appIconSpriteObjInput)} via #{EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID}");
Assert.IsTrue(_appCreateBtn is { name: EdgegapWindowMetadata.APP_CREATE_BTN_ID },
$"Expected {nameof(_appCreateBtn)} via #{EdgegapWindowMetadata.APP_CREATE_BTN_ID}");
Assert.IsTrue(_appCreateResultLabel is { name: EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID },
$"Expected {nameof(_appCreateResultLabel)} via #{EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID}");
Assert.IsTrue(_containerRegistryFoldout is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID },
$"Expected {nameof(_containerRegistryFoldout)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID}");
Assert.IsTrue(_containerPortNumInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID },
$"Expected {nameof(_containerPortNumInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID}");
// MIRROR CHANGE: disable and replaced with hardcoded port type dropdown
// Assert.IsTrue(_containerTransportTypeEnumInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_TRANSPORT_TYPE_ENUM_ID },
// $"Expected {nameof(_containerTransportTypeEnumInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_TRANSPORT_TYPE_ENUM_ID}");
// END MIRROR CHANGE
Assert.IsTrue(_containerUseCustomRegistryToggle is { name: EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID },
$"Expected {nameof(_containerUseCustomRegistryToggle)} via #{EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID}");
Assert.IsTrue(_containerCustomRegistryWrapper is { name: EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID },
$"Expected {nameof(_containerCustomRegistryWrapper)} via #{EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID}");
Assert.IsTrue(_containerRegistryUrlInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID },
$"Expected {nameof(_containerRegistryUrlInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID}");
Assert.IsTrue(_containerImageRepositoryInput is { name: EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID },
$"Expected {nameof(_containerImageRepositoryInput)} via #{EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID}");
Assert.IsTrue(_containerUsernameInput is { name: EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID },
$"Expected {nameof(_containerUsernameInput)} via #{EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID}");
Assert.IsTrue(_containerTokenInput is { name: EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID },
$"Expected {nameof(_containerTokenInput)} via #{EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID}");
Assert.IsTrue(_containerTokenInput is { name: EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID },
$"Expected {nameof(_containerTokenInput)} via #{EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID}");
Assert.IsTrue(_containerBuildAndPushResultLabel is { name: EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID },
$"Expected {nameof(_containerBuildAndPushResultLabel)} via #{EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID}");
Assert.IsTrue(_deploymentsFoldout is { name: EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID },
$"Expected {nameof(_deploymentsFoldout)} via #{EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID}");
Assert.IsTrue(_deploymentsRefreshBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID },
$"Expected {nameof(_deploymentsRefreshBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID}");
Assert.IsTrue(_deploymentsCreateBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID },
$"Expected {nameof(_deploymentsCreateBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID}");
Assert.IsTrue(_deploymentsStatusLabel is { name: EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID },
$"Expected {nameof(_deploymentsStatusLabel)} via #{EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID}");
Assert.IsTrue(_deploymentsServerDataContainer is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID },
$"Expected {nameof(_deploymentsServerDataContainer)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID}");
Assert.IsTrue(_deploymentConnectionCopyUrlBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID },
$"Expected {nameof(_deploymentConnectionCopyUrlBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID}");
Assert.IsTrue(_deploymentsConnectionUrlReadonlyInput is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID },
$"Expected {nameof(_deploymentsConnectionUrlReadonlyInput)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID}");
Assert.IsTrue(_deploymentsConnectionStatusLabel is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID },
$"Expected {nameof(_deploymentsConnectionStatusLabel)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID}");
Assert.IsTrue(_deploymentsConnectionStopBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID },
$"Expected {nameof(_deploymentsConnectionStopBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID}");
Assert.IsTrue(_footerDocumentationBtn is { name: EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID },
$"Expected {nameof(_footerDocumentationBtn)} via #{EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID}");
Assert.IsTrue(_footerNeedMoreGameServersBtn is { name: EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID },
$"Expected {nameof(_footerNeedMoreGameServersBtn)} via #{EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID}");
// // TODO: Explicitly set, for now in v2 - but remember to assert later if we stop hard-coding these >>
// _apiEnvironment
// _appVersionName
}
catch (Exception e)
{
Debug.LogError(e.Message);
_postAuthContainer.SetEnabled(false);
}
*/ // END MIRROR CHANGE
}
/// <summary>
/// Register non-btn change actionss. We'll want to save for persistence, validate, etc
/// </summary>
private void registerFieldCallbacks()
{
_apiTokenInput.RegisterValueChangedCallback(onApiTokenInputChanged);
_apiTokenInput.RegisterCallback<FocusOutEvent>(onApiTokenInputFocusOut);
_appNameInput.RegisterValueChangedCallback(onAppNameInputChanged);
_containerPortNumInput.RegisterCallback<FocusOutEvent>(onContainerPortNumInputFocusOut);
_containerUseCustomRegistryToggle.RegisterValueChangedCallback(onContainerUseCustomRegistryToggle);
_containerNewTagVersionInput.RegisterValueChangedCallback(onContainerNewTagVersionInputChanged);
}
/// <summary>
/// Prevents memory leaks, mysterious errors and "ghost" values set from a previous session.
/// Should parity the opposute of registerFieldCallbacks().
/// </summary>
private void unregisterFieldCallbacks()
{
_apiTokenInput.UnregisterValueChangedCallback(onApiTokenInputChanged);
_apiTokenInput.UnregisterCallback<FocusOutEvent>(onApiTokenInputFocusOut);
_containerUseCustomRegistryToggle.UnregisterValueChangedCallback(onContainerUseCustomRegistryToggle);
_containerPortNumInput.UnregisterCallback<FocusOutEvent>(onContainerPortNumInputFocusOut);
}
/// <summary>
/// Register click actions, mostly from buttons: Need to -= unregistry them @ OnDisable()
/// </summary>
private void registerClickCallbacks()
{
_debugBtn.clickable.clicked += onDebugBtnClick;
_apiTokenVerifyBtn.clickable.clicked += onApiTokenVerifyBtnClick;
_apiTokenGetBtn.clickable.clicked += onApiTokenGetBtnClick;
_appCreateBtn.clickable.clicked += onAppCreateBtnClickAsync;
_appLoadExistingBtn.clickable.clicked += onAppLoadExistingBtnClickAsync;
_containerBuildAndPushServerBtn.clickable.clicked += onContainerBuildAndPushServerBtnClickAsync;
_deploymentConnectionCopyUrlBtn.clickable.clicked += onDeploymentConnectionCopyUrlBtnClick;
_deploymentsRefreshBtn.clickable.clicked += onDeploymentsRefreshBtnClick;
_deploymentsCreateBtn.clickable.clicked += onDeploymentCreateBtnClick;
_footerDocumentationBtn.clickable.clicked += onFooterDocumentationBtnClick;
_footerNeedMoreGameServersBtn.clickable.clicked += onFooterNeedMoreGameServersBtnClick;
}
/// <summary>
/// Prevents memory leaks, mysterious errors and "ghost" values set from a previous session.
/// Should parity the opposute of registerClickEvents().
/// </summary>
private void unregisterClickEvents()
{
_debugBtn.clickable.clicked -= onDebugBtnClick;
_apiTokenVerifyBtn.clickable.clicked -= onApiTokenVerifyBtnClick;
_apiTokenGetBtn.clickable.clicked -= onApiTokenGetBtnClick;
_appCreateBtn.clickable.clicked -= onAppCreateBtnClickAsync;
_appLoadExistingBtn.clickable.clicked -= onAppLoadExistingBtnClickAsync;
_containerBuildAndPushServerBtn.clickable.clicked -= onContainerBuildAndPushServerBtnClickAsync;
_deploymentConnectionCopyUrlBtn.clickable.clicked -= onDeploymentConnectionCopyUrlBtnClick;
_deploymentsRefreshBtn.clickable.clicked -= onDeploymentsRefreshBtnClick;
_deploymentsCreateBtn.clickable.clicked -= onDeploymentCreateBtnClick;
_footerDocumentationBtn.clickable.clicked -= onFooterDocumentationBtnClick;
_footerNeedMoreGameServersBtn.clickable.clicked -= onFooterNeedMoreGameServersBtnClick;
}
private void initToggleDynamicUi()
{
hideResultLabels();
_deploymentsRefreshBtn.SetEnabled(false);
loadPersistentDataFromEditorPrefs();
setDeploymentBtnsFromCache();
_debugBtn.visible = EdgegapWindowMetadata.SHOW_DEBUG_BTN;
}
/// <summary>
/// Based on existing _deploymentsConnection[Url|Status]Label txt
/// </summary>
private void setDeploymentBtnsFromCache()
{
bool showDeploymentConnectionStopBtn = !string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.text);
if (!showDeploymentConnectionStopBtn)
return;
// We found some leftover connection cache >>
_deploymentsConnectionStopBtn.visible = true;
_deploymentsRefreshBtn.SetEnabled(true);
// Enable stop btn?
bool isDeployed = _deploymentsConnectionStatusLabel.text.ToLowerInvariant().Contains("deployed");
_deploymentsConnectionStopBtn.SetEnabled(isDeployed);
if (isDeployed)
_deploymentsConnectionStopBtn.clickable.clickedWithEventInfo += onDynamicStopServerBtnAsync; // Unsub'd from within
}
/// <summary>For example, result labels (success/err) should be hidden on init</summary>
private void hideResultLabels()
{
_appCreateResultLabel.visible = false;
_containerBuildAndPushResultLabel.visible = false;
_deploymentsStatusLabel.style.display = DisplayStyle.None;
}
#region Init -> Button clicks
/// <summary>
/// Experiment here! You may want to log what you're doing
/// in case you inadvertently leave it on.
/// </summary>
private void onDebugBtnClick() => debugEnableAllGroups();
private void debugEnableAllGroups()
{
Debug.Log("debugEnableAllGroups");
_appInfoFoldout.SetEnabled(true);
_appInfoFoldout.SetEnabled(true);
_containerRegistryFoldout.SetEnabled(true);
_deploymentsFoldout.SetEnabled(true);
if (_containerUseCustomRegistryToggle.value)
_containerCustomRegistryWrapper.SetEnabled(true);
}
private void onApiTokenVerifyBtnClick() => _ = verifyApiTokenGetRegistryCredsAsync();
private void onApiTokenGetBtnClick() => openGetApiTokenWebsite();
/// <summary>Process UI + validation before/after API logic</summary>
private async void onAppCreateBtnClickAsync()
{
// Assert data locally before calling API
assertAppNameExists();
_appCreateResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor("Creating...",
EdgegapWindowMetadata.StatusColors.Processing);
try { await createAppAsync(); }
finally
{
_appCreateBtn.SetEnabled(checkHasAppName());
_appCreateResultLabel.visible = _appCreateResultLabel.text != EdgegapWindowMetadata.LOADING_RICH_STR;
}
}
/// <summary>Process UI + validation before/after API logic</summary>
private async void onAppLoadExistingBtnClickAsync()
{
// Assert UI data locally before calling API
assertAppNameExists();
try { await GetAppAsync(); }
finally
{
_appLoadExistingBtn.SetEnabled(checkHasAppName());
_appCreateResultLabel.visible = _appCreateResultLabel.text != EdgegapWindowMetadata.LOADING_RICH_STR;
}
}
/// <summary>Copy url to clipboard</summary>
private void onDeploymentConnectionCopyUrlBtnClick()
{
if (string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.value))
return; // Nothing to copy
EditorGUIUtility.systemCopyBuffer = _deploymentsConnectionUrlReadonlyInput.value;
_deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor("Copied URL!",
EdgegapWindowMetadata.StatusColors.Success);
_deploymentsStatusLabel.style.display = DisplayStyle.Flex;
_ = clearDeploymentStatusAfterDelay(seconds: 1);
}
private async Task clearDeploymentStatusAfterDelay(int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
_deploymentsStatusLabel.style.display = DisplayStyle.None;
}
/// <summary>Process UI + validation before/after API logic</summary>
private async void onContainerBuildAndPushServerBtnClickAsync()
{
// Assert data locally before calling API
// Validate custom container registry, app name
try
{
assertAppNameExists();
Assert.IsTrue(
!_containerImageRepositoryInput.value.EndsWith("/"),
$"Expected {nameof(_containerImageRepositoryInput)} to !contain " +
"trailing slash (should end with /appName)");
}
catch (Exception e)
{
Debug.LogError($"onContainerBuildAndPushServerBtnClickAsync Error: {e}");
throw;
}
// Hide previous result labels, disable btns (to reenable when done)
hideResultLabels();
_containerBuildAndPushServerBtn.SetEnabled(false);
// Show new loading status
_containerBuildAndPushResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
EdgegapWindowMetadata.PROCESSING_RICH_STR,
EdgegapWindowMetadata.StatusColors.Processing);
try { await buildAndPushServerAsync(); }
finally
{
_containerBuildAndPushServerBtn.SetEnabled(checkHasAppName());
_containerBuildAndPushResultLabel.visible = _containerBuildAndPushResultLabel.text != EdgegapWindowMetadata.PROCESSING_RICH_STR;
}
}
private bool checkHasAppName() => _appNameInput.value.Length > 0;
private void onDeploymentsRefreshBtnClick() => _ = refreshDeploymentsAsync();
private void onFooterDocumentationBtnClick() => openDocumentationWebsite();
private void onFooterNeedMoreGameServersBtnClick() => openNeedMoreGameServersWebsite();
/// <summary>AKA "Create New Deployment" Btn</summary>
private void onDeploymentCreateBtnClick() => _ = createDeploymentStartServerAsync();
#endregion // Init -> /Button Clicks
#endregion // Init
/// <summary>Throw if !appName val</summary>
private void assertAppNameExists() =>
Assert.IsTrue(!string.IsNullOrEmpty(_appNameInput.value),
$"Expected {nameof(_appNameInput)} val");
/// <summary>
/// Save persistent read-only data: If the human didn't type it, it won't save automatically.
/// </summary>
private void SyncObjectWithForm()
{
_appIconSpriteObj = _appIconSpriteObjInput.value as Sprite;
}
/// <summary>TODO: Load persistent data?</summary>
private void syncFormWithObjectStatic()
{
// Only show the rest of the form if apiToken is verified
_postAuthContainer.SetEnabled(_isApiTokenVerified);
_appIconSpriteObjInput.value = _appIconSpriteObj;
_containerCustomRegistryWrapper.SetEnabled(_containerUseCustomRegistryToggle.value);
_containerUseCustomRegistryToggle.value = _containerUseCustomRegistryToggle.value;
// Only enable certain elements if appName exists
bool hasAppName = checkHasAppName();
_appCreateBtn.SetEnabled(hasAppName);
_appLoadExistingBtn.SetEnabled(hasAppName);
}
/// <summary>
/// Dynamically set form based on API call results.
/// => If APIToken is cached via EditorPrefs, verify => gets registry creds.
/// => If appName is cached via ViewDataKey, loads the app.
/// </summary>
private async Task syncFormWithObjectDynamicAsync()
{
if (string.IsNullOrEmpty(_apiTokenInput.value))
return;
// We found a cached api token: Verify =>
if (IsLogLevelDebug) Debug.Log("syncFormWithObjectDynamicAsync: Found apiToken; " +
"calling verifyApiTokenGetRegistryCredsAsync =>");
await verifyApiTokenGetRegistryCredsAsync();
// Was the API token verified + we found a cached app name? Load the app =>
// But ignore errs, since we're just *assuming* the app exists since the appName was filled
if (_isApiTokenVerified && checkHasAppName())
{
if (IsLogLevelDebug) Debug.Log("syncFormWithObjectDynamicAsync: Found apiToken && appName; " +
"calling GetAppAsync =>");
try { await GetAppAsync(); }
finally { _appLoadExistingBtn.SetEnabled(checkHasAppName()); }
}
}
#region Immediate non-button changes
/// <summary>
/// On change, validate -> update custom container registry suffix.
/// Toggle create app btn if 1+ char
/// </summary>
/// <param name="evt"></param>
private void onAppNameInputChanged(ChangeEvent<string> evt)
{
// Validate: Only allow alphanumeric, underscore, dash, plus, period
if (!_appNameAllowedCharsRegex.IsMatch(evt.newValue))
_appNameInput.value = evt.previousValue; // Revert to the previous value
else
setContainerImageRepositoryVal(); // Valid -> Update the custom container registry suffix
// Toggle btns on 1+ char entered
bool hasAppName = checkHasAppName();
_appCreateBtn.SetEnabled(hasAppName);
_appLoadExistingBtn.SetEnabled(hasAppName);
}
/// <summary>On focus out, clamp port between 1024~49151</summary>
/// <param name="evt"></param>
private void onContainerPortNumInputFocusOut(FocusOutEvent evt)
{
// Use TryParse to avoid exceptions
if (int.TryParse(_containerPortNumInput.value, out int port))
{
// Clamp the port to the range and set the value back to the TextField
_containerPortNumInput.value = Mathf.Clamp(
port,
EdgegapWindowMetadata.PORT_MIN,
EdgegapWindowMetadata.PORT_MAX)
.ToString();
}
else
{
// If input is !valid, set to default
_containerPortNumInput.value = EdgegapWindowMetadata.PORT_DEFAULT.ToString();
}
}
/// <summary>
/// While changing the token, we temporarily unmask. On change, set state to !verified.
/// </summary>
/// <param name="evt"></param>
private void onApiTokenInputChanged(ChangeEvent<string> evt)
{
// Unmask while changing
TextField apiTokenTxt = evt.target as TextField;
apiTokenTxt.isPasswordField = false;
// Token changed? Reset form to !verified state and fold all groups
_isApiTokenVerified = false;
_postAuthContainer.SetEnabled(false);
closeDisableGroups();
// Toggle "Verify" btn on 1+ char entered
_apiTokenVerifyBtn.SetEnabled(evt.newValue.Length > 0);
}
/// <summary>Unmask while typing</summary>
/// <param name="evt"></param>
private void onApiTokenInputFocusOut(FocusOutEvent evt)
{
TextField apiTokenTxt = evt.target as TextField;
apiTokenTxt.isPasswordField = true;
}
/// <summary>On toggle, enable || disable the custom registry inputs (below the Toggle).</summary>
private void onContainerUseCustomRegistryToggle(ChangeEvent<bool> evt) =>
_containerCustomRegistryWrapper.SetEnabled(evt.newValue);
/// <summary>On empty, we fallback to "latest", a fallback val from EdgegapWindowMetadata.cs</summary>
/// <param name="evt"></param>
private void onContainerNewTagVersionInputChanged(ChangeEvent<string> evt)
{
if (!string.IsNullOrEmpty(evt.newValue))
return;
// Set fallback value -> select all for UX, since the user may not expect this
_containerNewTagVersionInput.value = EdgegapWindowMetadata.DEFAULT_VERSION_TAG;
_containerNewTagVersionInput.SelectAll();
}
#endregion // Immediate non-button changes
/// <summary>
/// Used for converting a Sprite to a base64 string: By default, textures are !readable,
/// and we don't want to have to instruct users how to make it readable for UX.
/// Instead, we'll make a copy of that texture -> make it readable.
/// </summary>
/// <param name="original"></param>
/// <returns></returns>
private Texture2D makeTextureReadable(Texture2D original)
{
RenderTexture rt = RenderTexture.GetTemporary(
original.width,
original.height
);
Graphics.Blit(original, rt);
Texture2D readableTexture = new Texture2D(original.width, original.height);
Rect rect = new Rect(
0,
0,
rt.width,
rt.height);
readableTexture.ReadPixels(rect, destX: 0, destY: 0);
readableTexture.Apply();
RenderTexture.ReleaseTemporary(rt);
return readableTexture;
}
/// <summary>From Base64 string -> to Sprite</summary>
/// <param name="imgBase64Str">Edgegap build app requires a max size of 200</param>
/// <returns>Sprite</returns>
private Sprite getSpriteFromBase64Str(string imgBase64Str)
{
if (string.IsNullOrEmpty(imgBase64Str))
return null;
try
{
byte[] imageBytes = Convert.FromBase64String(imgBase64Str);
Texture2D texture = new Texture2D(2, 2);
texture.LoadImage(imageBytes);
Rect rect = new Rect( // MIRROR CHANGE: 'new()' not supported in Unity 2020
x: 0.0f,
y: 0.0f,
texture.width,
texture.height);
return Sprite.Create(
texture,
rect,
pivot: new Vector2(0.5f, 0.5f),
pixelsPerUnit: 100.0f);
}
catch (Exception e)
{
Debug.Log($"Warning: getSpriteFromBase64Str failed (returning null) - {e}");
return null;
}
}
/// <summary>From Sprite -> to Base64 string</summary>
/// <param name="sprite"></param>
/// <param name="maxKbSize">Edgegap build app requires a max size of 200</param>
/// <returns>imageBase64Str</returns>
private string getBase64StrFromSprite(Sprite sprite, int maxKbSize = 200)
{
if (sprite == null)
return null;
try
{
Texture2D texture = makeTextureReadable(sprite.texture);
// Crop the texture to the sprite's rectangle (instead of the entire texture)
Texture2D croppedTexture = new Texture2D(
(int)sprite.rect.width,
(int)sprite.rect.height);
Color[] pixels = texture.GetPixels(
(int)sprite.rect.x,
(int)sprite.rect.y,
(int)sprite.rect.width,
(int)sprite.rect.height
);
croppedTexture.SetPixels(pixels);
croppedTexture.Apply();
// Encode to PNG ->
byte[] textureBytes = croppedTexture.EncodeToPNG();
// Validate size
const int oneKb = 1024;
int pngTextureSizeKb = textureBytes.Length / oneKb;
bool isPngLessThanMaxSize = pngTextureSizeKb < maxKbSize;
if (!isPngLessThanMaxSize)
{
textureBytes = croppedTexture.EncodeToJPG();
int jpgTextureSizeKb = textureBytes.Length / oneKb;
bool isJpgLessThanMaxSize = pngTextureSizeKb < maxKbSize;
Assert.IsTrue(isJpgLessThanMaxSize, $"Expected texture PNG to be < {maxKbSize}kb " +
$"in size (but found {jpgTextureSizeKb}kb); then tried JPG, but is still {jpgTextureSizeKb}kb in size");
Debug.LogWarning($"App icon PNG was too large (max {maxKbSize}), so we converted to JPG");
}
string base64ImageString = Convert.ToBase64String(textureBytes); // eg: "Aaabbcc=="
return base64ImageString;
}
catch (Exception e)
{
Debug.LogError($"Error: {e.Message}");
return null;
}
}
/// <summary>
/// Verifies token => apps/container groups -> gets registry creds (if any).
/// TODO: UX - Show loading spinner.
/// </summary>
private async Task verifyApiTokenGetRegistryCredsAsync()
{
if (IsLogLevelDebug) Debug.Log("verifyApiTokenGetRegistryCredsAsync");
// Disable most ui while we verify
_isApiTokenVerified = false;
_apiTokenVerifyBtn.SetEnabled(false);
SyncContainerEnablesToState();
hideResultLabels();
EdgegapWizardApi wizardApi = getWizardApi();
EdgegapHttpResult initQuickStartResultCode = await wizardApi.InitQuickStart();
_apiTokenVerifyBtn.SetEnabled(true);
_isApiTokenVerified = initQuickStartResultCode.IsResultCode204;
if (!_isApiTokenVerified)
{
SyncContainerEnablesToState();
return;
}
// Verified: Let's see if we have active registry credentials // TODO: This will later be a result model
EdgegapHttpResult<GetRegistryCredentialsResult> getRegistryCredentialsResult = await wizardApi.GetRegistryCredentials();
if (getRegistryCredentialsResult.IsResultCode200)
{
// Success
_credentials = getRegistryCredentialsResult.Data;
persistUnmaskedApiToken(_apiTokenInput.value);
prefillContainerRegistryForm(_credentials);
}
else
{
// Fail
}
// Unlock the rest of the form, whether we prefill the container registry or not
SyncContainerEnablesToState();
}
/// <summary>
/// We have container registry params; we'll prefill registry container fields.
/// </summary>
/// <param name="credentials">GetRegistryCredentialsResult</param>
private void prefillContainerRegistryForm(GetRegistryCredentialsResult credentials)
{
if (IsLogLevelDebug) Debug.Log("prefillContainerRegistryForm");
if (credentials == null)
throw new Exception($"!{nameof(credentials)}");
_containerRegistryUrlInput.value = credentials.RegistryUrl;
setContainerImageRepositoryVal();
_containerUsernameInput.value = credentials.Username;
_containerTokenInput.value = credentials.Token;
}
/// <summary>
/// Sets to "{credentials.Project}/{appName}" from cached credentials, forcing lowercased appName.
/// </summary>
private void setContainerImageRepositoryVal()
{
// ex: "xblade1-9sa8dfh9sda8hf/mygame1"
string project = _credentials?.Project ?? "";
string appName = _appNameInput?.value.ToLowerInvariant() ?? "";
_containerImageRepositoryInput.value = $"{project}/{appName}";
}
public static string Base64Encode(string plainText)
{
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainBytes);
}
public static string Base64Decode(string base64EncodedText)
{
byte[] base64Bytes = Convert.FromBase64String(base64EncodedText);
return Encoding.UTF8.GetString(base64Bytes);
}
/// <summary>
/// Toggle container groups and foldouts on/off based on:
/// - _isApiTokenVerified
/// </summary>
private void SyncContainerEnablesToState()
{
// Requires _isApiTokenVerified
_postAuthContainer.SetEnabled(_isApiTokenVerified); // Entire body container
_appInfoFoldout.SetEnabled(_isApiTokenVerified);
_appInfoFoldout.value = _isApiTokenVerified;
// + Requires _isContainerRegistryReady
bool isApiTokenVerifiedAndContainerReady = _isApiTokenVerified && _isContainerRegistryReady;
_containerRegistryFoldout.SetEnabled(isApiTokenVerifiedAndContainerReady);
_containerRegistryFoldout.value = isApiTokenVerifiedAndContainerReady;
_deploymentsFoldout.SetEnabled(isApiTokenVerifiedAndContainerReady);
_deploymentsFoldout.value = isApiTokenVerifiedAndContainerReady;
// + Requires _containerUseCustomRegistryToggleBool
_containerCustomRegistryWrapper.SetEnabled(isApiTokenVerifiedAndContainerReady &&
_containerUseCustomRegistryToggle.value);
}
private void openGetApiTokenWebsite()
{
if (IsLogLevelDebug) Debug.Log("openGetApiTokenWebsite");
Application.OpenURL(EdgegapWindowMetadata.EDGEGAP_GET_A_TOKEN_URL);
}
/// <returns>isSuccess; sets _isContainerRegistryReady + _loadedApp</returns>
private async Task<bool> GetAppAsync()
{
if (IsLogLevelDebug) Debug.Log("GetAppAsync");
// Hide previous result labels, disable btns (to reenable when done)
hideResultLabels();
_appCreateBtn.SetEnabled(false);
_apiTokenVerifyBtn.SetEnabled(false);
// Show new loading status
_appCreateResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
EdgegapWindowMetadata.LOADING_RICH_STR,
EdgegapWindowMetadata.StatusColors.Processing);
_appCreateResultLabel.visible = true;
EdgegapAppApi appApi = getAppApi();
EdgegapHttpResult<GetCreateAppResult> getAppResult = await appApi.GetApp(_appNameInput.value);
onGetCreateApplicationResult(getAppResult);
return _isContainerRegistryReady;
}
/// <summary>
/// TODO: Add err handling for reaching app limit (max 2 for free tier).
/// </summary>
private async Task createAppAsync()
{
if (IsLogLevelDebug) Debug.Log("createAppAsync");
// Hide previous result labels, disable btns (to reenable when done)
hideResultLabels();
_appCreateBtn.SetEnabled(false);
_apiTokenVerifyBtn.SetEnabled(false);
EdgegapAppApi appApi = getAppApi();
CreateAppRequest createAppRequest = new CreateAppRequest( // MIRROR CHANGE: 'new()' not supported in Unity 2020
_appNameInput.value,
isActive: true,
getBase64StrFromSprite(_appIconSpriteObj) ?? "");
EdgegapHttpResult<GetCreateAppResult> createAppResult = await appApi.CreateApp(createAppRequest);
onGetCreateApplicationResult(createAppResult);
}
/// <summary>Get || Create results both handled here. On success, sets _isContainerRegistryReady + _loadedApp data</summary>
/// <param name="result"></param>
private void onGetCreateApplicationResult(EdgegapHttpResult<GetCreateAppResult> result)
{
// Assert the result itself || result's create time exists
bool isSuccess = result.IsResultCode200 || result.IsResultCode409; // 409 == app already exists
_isContainerRegistryReady = isSuccess;
_loadedApp = result.Data;
_appCreateResultLabel.text = getFriendlyCreateAppResultStr(result);
_containerRegistryFoldout.value = _isContainerRegistryReady;
_appCreateBtn.SetEnabled(true);
_apiTokenVerifyBtn.SetEnabled(true);
SyncContainerEnablesToState();
// Only show status label if we're init'd; otherwise, we auto-tried to get the existing app that
// we knew had a chance of not being there
_appCreateResultLabel.visible = IsInitd;
// App base64 img? Parse to sprite, overwrite app image UI/cache
if (!string.IsNullOrEmpty(_loadedApp.Image))
{
_appIconSpriteObj = getSpriteFromBase64Str(_loadedApp.Image);
_appIconSpriteObjInput.value = _appIconSpriteObj;
}
// On fail, shake the "Add more game servers" btn // 400 == # of apps limit reached
bool isCreate = result.HttpMethod == HttpMethod.Post;
bool isCreateFailAppNumCapMaxed = isCreate && !_isContainerRegistryReady && result.IsResultCode400;
if (isCreateFailAppNumCapMaxed)
shakeNeedMoreGameServersBtn();
}
/// <summary>Slight animation shake</summary>
private void shakeNeedMoreGameServersBtn()
{
ButtonShaker shaker = new ButtonShaker(_footerNeedMoreGameServersBtn);
_ = shaker.ApplyShakeAsync();
}
/// <returns>Generally "Success" || "Error: {error}" || "Warning: {error}"</returns>
private string getFriendlyCreateAppResultStr(EdgegapHttpResult<GetCreateAppResult> createAppResult)
{
string coloredResultStr = null;
if (!_isContainerRegistryReady)
{
// Error
string resultStr = $"<b>Error:</b> {createAppResult?.Error?.ErrorMessage}";
coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor(
resultStr, EdgegapWindowMetadata.StatusColors.Error);
}
else if (createAppResult.IsResultCode409)
{
// Warn: App already exists - Still success, but just a warn
string resultStr = $"<b>Warning:</b> {createAppResult.Error.ErrorMessage}";
coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor(
resultStr, EdgegapWindowMetadata.StatusColors.Warn);
}
else
{
// Success
coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor(
"Success", EdgegapWindowMetadata.StatusColors.Success);
}
return coloredResultStr;
}
/// <summary>Open contact form in desired locale</summary>
private void openNeedMoreGameServersWebsite() =>
Application.OpenURL(EdgegapWindowMetadata.EDGEGAP_ADD_MORE_GAME_SERVERS_URL);
private void openDocumentationWebsite()
{
// MIRROR CHANGE
/*
string documentationUrl = _apiEnvironment.GetDocumentationUrl();
if (!string.IsNullOrEmpty(documentationUrl))
Application.OpenURL(documentationUrl);
else
{
string apiEnvName = Enum.GetName(typeof(ApiEnvironment), _apiEnvironment);
Debug.LogWarning($"Could not open documentation for api environment " +
$"{apiEnvName}: No documentation URL.");
}
*/
// link to our step by step guide
Application.OpenURL("https://mirror-networking.gitbook.io/docs/hosting/edgegap-hosting-plugin-guide");
// END MIRROR CHANGE
}
/// <summary>
/// Currently only refreshes an existing deployment. AKA "OnRefresh".
/// TODO: Consider dynamically adding the entire list via GET all deployments.
/// </summary>
private async Task refreshDeploymentsAsync()
{
if (IsLogLevelDebug) Debug.Log("refreshDeploymentsAsync");
// Sanity check requestId - if refreshBtn is enabled, we *should* have it
if (string.IsNullOrEmpty(_deploymentRequestId))
{
// We must have stale data - reset
clearDeploymentConnections();
return;
}
hideResultLabels();
// clearDeploymentConnections(); // We want to leave the old URL while we only have one
_deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
"<i>Refreshing...</i>", EdgegapWindowMetadata.StatusColors.Processing);
EdgegapDeploymentsApi deployApi = getDeployApi();
EdgegapHttpResult<GetDeploymentStatusResult> getDeploymentStatusResponse =
await deployApi.GetDeploymentStatusAsync(_deploymentRequestId);
bool isActiveStatus = getDeploymentStatusResponse?.StatusCode != null &&
getDeploymentStatusResponse.Data.CurrentStatus == EdgegapWindowMetadata.READY_STATUS;
if (isActiveStatus)
onCreateDeploymentOrRefreshSuccess(getDeploymentStatusResponse.Data);
else
{
onCreateDeploymentStartServerFail();
if (!getDeploymentStatusResponse.HasErr)
onRefreshDeploymentStoppedStatus(); // Only a "soft" fail - not a true err
}
}
/// <summary>The deployment simply stopped (a "soft" fail - not an actual err)</summary>
private void onRefreshDeploymentStoppedStatus()
{
// Override to Create fail -- instead, we've simplfy stopped (a "soft" fail)
_deploymentsConnectionStatusLabel.text = getConnectionStoppedRichStr();
_deploymentsStatusLabel.style.display = DisplayStyle.None;
_deploymentsConnectionStopBtn.SetEnabled(false);
_deploymentsConnectionStopBtn.visible = true;
}
/// <summary>Don't use this if you want to keep the last-known connection info.</summary>
private void clearDeploymentConnections()
{
_deploymentsConnectionUrlReadonlyInput.value = "";
_deploymentsConnectionStatusLabel.text = "";
_deploymentsConnectionStopBtn.visible = false;
_deploymentsRefreshBtn.SetEnabled(false);
}
/// <summary>
/// V2 Successor to legacy startServerCallbackAsync() from "Create New Deployment" Btn.
/// </summary>
private async Task createDeploymentStartServerAsync()
{
// Hide previous result labels, disable btns (to reenable when done)
if (IsLogLevelDebug) Debug.Log("createDeploymentStartServerAsync");
hideResultLabels();
_deploymentsCreateBtn.SetEnabled(false);
_deploymentsRefreshBtn.SetEnabled(false);
// _deploymentsConnectionUrlReadonlyInput.value = ""; // We currently want to keep the last known connection, even on err
_deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
EdgegapWindowMetadata.DEPLOY_REQUEST_RICH_STR,
EdgegapWindowMetadata.StatusColors.Processing);
try
{
EdgegapDeploymentsApi deployApi = getDeployApi();
// Get (+cache) external IP async, required to create a deployment. Prioritize cache.
_userExternalIp = await getExternalIpAddress();
CreateDeploymentRequest createDeploymentReq = new CreateDeploymentRequest( // MIRROR CHANGE: 'new()' not supported in Unity 2020
_appNameInput.value,
_containerNewTagVersionInput.value,
_userExternalIp);
// Request to deploy (it won't be active, yet) =>
EdgegapHttpResult<CreateDeploymentResult> createDeploymentResponse =
await deployApi.CreateDeploymentAsync(createDeploymentReq);
if (!createDeploymentResponse.IsResultCode200)
{
onCreateDeploymentStartServerFail(createDeploymentResponse);
return;
}
else
{
// Update status
_deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
"<i>Deploying...</i>", EdgegapWindowMetadata.StatusColors.Processing);
}
// Check the status of the deployment for READY every 2s =>
const int pollIntervalSecs = EdgegapWindowMetadata.DEPLOYMENT_READY_STATUS_POLL_SECONDS;
EdgegapHttpResult<GetDeploymentStatusResult> getDeploymentStatusResponse = await deployApi.AwaitReadyStatusAsync(
createDeploymentResponse.Data.RequestId,
TimeSpan.FromSeconds(pollIntervalSecs));
// Process create deployment response
bool isSuccess = createDeploymentResponse.IsResultCode200;
if (isSuccess)
onCreateDeploymentOrRefreshSuccess(getDeploymentStatusResponse.Data);
else
onCreateDeploymentStartServerFail(createDeploymentResponse);
_deploymentsStatusLabel.style.display = DisplayStyle.Flex;
}
finally
{
_deploymentsCreateBtn.SetEnabled(true);
}
}
/// <summary>
/// CreateDeployment || RefreshDeployment success handler.
/// </summary>
/// <param name="getDeploymentStatusResult">Only pass from CreateDeployment</param>
private void onCreateDeploymentOrRefreshSuccess(GetDeploymentStatusResult getDeploymentStatusResult)
{
// Success
hideResultLabels();
_deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
"Success", EdgegapWindowMetadata.StatusColors.Success);
_deploymentsStatusLabel.style.display = DisplayStyle.Flex;
// Cache the deployment result -> persist the requestId
_lastKnownDeployment = getDeploymentStatusResult;
_deploymentRequestId = getDeploymentStatusResult.RequestId;
EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR, _deploymentRequestId);
// ------------
// Set the static connection row label data >>
// TODO: This will be dynamically inserted via MVC-style template when we support multiple deployments >>
// Get external port
// BUG(WORKAROUND): Expected `ports` to be List<AppPortsData>, but received Dictionary<string, AppPortsData>
KeyValuePair<string, DeploymentPortsData> portsDataKvp = getDeploymentStatusResult.PortsDict.FirstOrDefault();
Assert.IsNotNull(portsDataKvp.Value, $"Expected ({nameof(portsDataKvp)} from `getDeploymentStatusResult.PortsDict`)");
DeploymentPortsData deploymentPortData = portsDataKvp.Value;
string externalPortStr = deploymentPortData.External.ToString();
string domainWithExternalPort = $"{getDeploymentStatusResult.Fqdn}:{externalPortStr}";
_deploymentsConnectionUrlReadonlyInput.value = domainWithExternalPort;
string newConnectionStatus = EdgegapWindowMetadata.WrapRichTextInColor(
"Deployed", EdgegapWindowMetadata.StatusColors.Success);
// Change + Persist read-only fields (ViewDataKeys only save automatically from human input)
setPersistDeploymentsConnectionUrlLabelTxt(domainWithExternalPort);
setPersistDeploymentsConnectionStatusLabelTxt(newConnectionStatus);
// ------------
// Configure + show stop button
_deploymentsConnectionStopBtn.clickable.clickedWithEventInfo += onDynamicStopServerBtnAsync; // Unsubscribes on click
_deploymentsConnectionStopBtn.visible = true;
_deploymentsConnectionStopBtn.SetEnabled(true);
// Show refresh btn (currently targeting only this one)
_deploymentsRefreshBtn.SetEnabled(true);
}
private void onCreateDeploymentStartServerFail(EdgegapHttpResult<CreateDeploymentResult> result = null)
{
_deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
"Failed to Start", EdgegapWindowMetadata.StatusColors.Error);
_deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
result?.Error.ErrorMessage ?? "Unknown Error",
EdgegapWindowMetadata.StatusColors.Error);
_deploymentsStatusLabel.style.display = DisplayStyle.Flex;
_deploymentsRefreshBtn.SetEnabled(true);
Debug.Log("(!) Check your deployments here: https://app.edgegap.com/deployment-management/deployments/list");
// Shake "need more servers" btn on 403
// MIRROR CHANGE: use old C# syntax that is supported in Unity 2019
// bool reachedNumDeploymentsHardcap = result is { IsResultCode403: true };
bool reachedNumDeploymentsHardcap = result != null && result.IsResultCode403;
// END MIRROR CHANGE
if (reachedNumDeploymentsHardcap)
shakeNeedMoreGameServersBtn();
}
/// <summary>
/// This is triggered from a dynamic button, so we need to pass in the event info (TODO: Use evt info later).
/// </summary>
/// <param name="evt"></param>
private void onDynamicStopServerBtnAsync(EventBase evt) =>
_ = onDynamicStopServerAsync();
/// <summary>
/// Stops the deployment, updating the UI accordingly.
/// TODO: Cache a list of deployments and/or store a hidden field for requestId.
/// </summary>
private async Task onDynamicStopServerAsync()
{
// Prepare to stop (UI, status flags, callback unsubs)
if (IsLogLevelDebug) Debug.Log("onDynamicStopServerAsync");
hideResultLabels();
_deploymentsConnectionStopBtn.SetEnabled(false);
_deploymentsRefreshBtn.SetEnabled(false);
_deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
"<i>Requesting Stop...</i>", EdgegapWindowMetadata.StatusColors.Processing);
EdgegapDeploymentsApi deployApi = getDeployApi();
EdgegapHttpResult<StopActiveDeploymentResult> stopResponse = null;
try
{
stopResponse = await deployApi.StopActiveDeploymentAsync(_deploymentRequestId);
if (!stopResponse.IsResultCode200)
{
onDynamicStopServerAsyncFail(stopResponse.Error.ErrorMessage);
return;
}
// ---------
// 200, but only PENDING deleted (if we create a new one before it's deleted,
// user may get hit with max # of deployments reached err)
_deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
"<i>Stopping...</i>", EdgegapWindowMetadata.StatusColors.Warn);
TimeSpan pollIntervalSecs = TimeSpan.FromSeconds(EdgegapWindowMetadata.DEPLOYMENT_STOP_STATUS_POLL_SECONDS);
stopResponse = await deployApi.AwaitTerminatedDeleteStatusAsync(_deploymentRequestId, pollIntervalSecs);
}
finally
{
_deploymentsConnectionStopBtn.clickable.clickedWithEventInfo -= onDynamicStopServerBtnAsync;
}
bool isStopSuccess = stopResponse.IsResultCode410;
if (!isStopSuccess)
{
onDynamicStopServerAsyncFail(stopResponse.Error.ErrorMessage);
return;
}
// Success: Hide the static row // TODO: Delete the template row, when dynamic
// clearDeploymentConnections(); // Use this if you don't want to show the last connection info
string stoppedStr = getConnectionStoppedRichStr();
_deploymentsStatusLabel.text = ""; // Overrides any previous errs, in case we attempted to created a new deployment while deleting
setPersistDeploymentsConnectionStatusLabelTxt(stoppedStr);
}
private string getConnectionStoppedRichStr() =>
EdgegapWindowMetadata.WrapRichTextInColor(
"Stopped", EdgegapWindowMetadata.StatusColors.Error);
private void onDynamicStopServerAsyncFail(string friendlyErrMsg)
{
_deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
friendlyErrMsg, EdgegapWindowMetadata.StatusColors.Error);
}
/// <summary>Sets and returns `_userExternalIp`, prioritizing local cache</summary>
private async Task<string> getExternalIpAddress()
{
if (!string.IsNullOrEmpty(_userExternalIp))
return _userExternalIp;
EdgegapIpApi ipApi = getIpApi();
EdgegapHttpResult<GetYourPublicIpResult> getYourPublicIpResponseTask = await ipApi.GetYourPublicIp();
_userExternalIp = getYourPublicIpResponseTask?.Data?.PublicIp;
Assert.IsTrue(!string.IsNullOrEmpty(_userExternalIp),
$"Expected getYourPublicIpResponseTask.Data.PublicIp");
return _userExternalIp;
}
#region Api Builders
private EdgegapDeploymentsApi getDeployApi() => new EdgegapDeploymentsApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020
EdgegapWindowMetadata.API_ENVIRONMENT,
_apiTokenInput.value.Trim(),
EdgegapWindowMetadata.LOG_LEVEL);
private EdgegapIpApi getIpApi() => new EdgegapIpApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020
EdgegapWindowMetadata.API_ENVIRONMENT,
_apiTokenInput.value.Trim(),
EdgegapWindowMetadata.LOG_LEVEL);
private EdgegapWizardApi getWizardApi() => new EdgegapWizardApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020
EdgegapWindowMetadata.API_ENVIRONMENT,
_apiTokenInput.value.Trim(),
EdgegapWindowMetadata.LOG_LEVEL);
private EdgegapAppApi getAppApi() => new EdgegapAppApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020
EdgegapWindowMetadata.API_ENVIRONMENT,
_apiTokenInput.value.Trim(),
EdgegapWindowMetadata.LOG_LEVEL);
#endregion // Api Builders
private float ProgressCounter = 0;
// MIRROR CHANGE: added title parameter for more detailed progress while waiting
void ShowBuildWorkInProgress(string title, string status)
{
EditorUtility.DisplayProgressBar(title, status, ProgressCounter++ / 50);
}
// END MIRROR CHANGE
/// <summary>Build & Push - Legacy from v1, modified for v2</summary>
private async Task buildAndPushServerAsync()
{
if (IsLogLevelDebug) Debug.Log("buildAndPushServerAsync");
// Legacy Code Start >>
// SetToolUIState(ToolState.Building);
SyncObjectWithForm();
ProgressCounter = 0;
try
{
// check for installation and setup docker file
if (!await EdgegapBuildUtils.DockerSetupAndInstallationCheck())
{
onBuildPushError("Docker installation not found. " +
"Docker can be downloaded from:\n\nhttps://www.docker.com/");
return;
}
// MIRROR CHANGE
// make sure Linux build target is installed before attemping to build.
// if it's not installed, tell the user about it.
if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.Standalone, BuildTarget.StandaloneLinux64))
{
onBuildPushError($"Linux Build Support is missing.\n\nPlease open Unity Hub -> Installs -> Unity {Application.unityVersion} -> Add Modules -> Linux Build Support (IL2CPP & Mono & Dedicated Server) -> Install\n\nAfterwards restart Unity!");
return;
}
// END MIRROR CHANGE
if (!EdgegapWindowMetadata.SKIP_SERVER_BUILD_WHEN_PUSHING)
{
// create server build
BuildReport buildResult = EdgegapBuildUtils.BuildServer();
if (buildResult.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
{
onBuildPushError("Edgegap build failed");
return;
}
}
else
Debug.LogWarning(nameof(EdgegapWindowMetadata.SKIP_SERVER_BUILD_WHEN_PUSHING));
string registry = _containerRegistryUrlInput.value;
string imageName = _containerImageRepositoryInput.value;
string tag = _containerNewTagVersionInput.value;
// MIRROR CHANGE ///////////////////////////////////////////////
// registry, repository and tag can not contain whitespaces.
// otherwise the docker command will throw an error:
// "ERROR: "docker buildx build" requires exactly 1 argument."
// catch this early and notify the user immediately.
if (registry.Contains(" "))
{
onBuildPushError($"Container Registry is not allowed to contain whitespace: '{registry}'");
return;
}
if (imageName.Contains(" "))
{
onBuildPushError($"Image Repository is not allowed to contain whitespace: '{imageName}'");
return;
}
if (tag.Contains(" "))
{
onBuildPushError($"Tag is not allowed to contain whitespace: '{tag}'");
return;
}
// END MIRROR CHANGE ///////////////////////////////////////////
// // increment tag for quicker iteration // TODO? `_autoIncrementTag` !exists in V2.
// if (_autoIncrementTag)
// {
// tag = EdgegapBuildUtils.IncrementTag(tag);
// }
// create docker image
if (!EdgegapWindowMetadata.SKIP_DOCKER_IMAGE_BUILD_WHEN_PUSHING)
{
// MIRROR CHANGE: CROSS PLATFORM BUILD SUPPORT
// await EdgegapBuildUtils.DockerBuild(
// registry,
// imageName,
// tag,
// ShowBuildWorkInProgress);
await EdgegapBuildUtils.RunCommand_DockerBuild(registry, imageName, tag, status => ShowBuildWorkInProgress("Building Docker Image", status));
}
else
Debug.LogWarning(nameof(EdgegapWindowMetadata.SKIP_DOCKER_IMAGE_BUILD_WHEN_PUSHING));
// (v2) Login to registry
bool isContainerLoginSuccess = await EdgegapBuildUtils.LoginContainerRegistry(
_containerRegistryUrlInput.value,
_containerUsernameInput.value,
_containerTokenInput.value,
status => ShowBuildWorkInProgress("Logging into container registry.", status)); // MIRROR CHANGE: DETAILED LOGGING
if (!isContainerLoginSuccess)
{
onBuildPushError("Unable to login to docker registry. " +
"Make sure your registry url + username are correct. " +
$"See doc:\n\n{EdgegapWindowMetadata.EDGEGAP_DOC_BTN_HOW_TO_LOGIN_VIA_CLI_URL}");
return;
}
// MIRROR CHANGE: DETAILED DOCKER PUSH ERROR HANDLING
// push docker image
(bool isPushSuccess, string error) = await EdgegapBuildUtils.RunCommand_DockerPush(registry, imageName, tag, status => ShowBuildWorkInProgress("Uploading Docker Image (this may take a while)", status));
if (!isPushSuccess)
{
// catch common issues with detailed solutions
if (error.Contains("Cannot connect to the Docker daemon"))
{
onBuildPushError($"{error}\nTo solve this, you can install and run Docker Desktop from:\n\nhttps://www.docker.com/products/docker-desktop");
return;
}
if (error.Contains("unauthorized to access repository"))
{
onBuildPushError($"Docker authorization failed:\n\n{error}\nTo solve this, you can open a terminal and enter 'docker login {registry}', then enter your credentials.");
return;
}
// project not found?
if (Regex.IsMatch(error, @".*project .* not found.*", RegexOptions.IgnoreCase))
{
onBuildPushError($"{error}\nTo solve this, make sure that Image Repository is 'project/game' where 'project' is from the Container Registry page on the Edgegap website.");
return;
}
// otherwise show generic error message
onBuildPushError("Unable to push docker image to registry. " +
$"Make sure your {registry} registry url + username are correct. " +
$"See doc:\n\n{EdgegapWindowMetadata.EDGEGAP_DOC_BTN_HOW_TO_LOGIN_VIA_CLI_URL}");
return;
}
// END MIRROR CHANGE
// update edgegap server settings for new tag
ShowBuildWorkInProgress("Build and Push", "Updating server info on Edgegap");
EdgegapAppApi appApi = getAppApi();
AppPortsData[] ports =
{
new AppPortsData() // MIRROR CHANGE: 'new()' not supported in Unity 2020
{
Port = int.Parse(_containerPortNumInput.value), // OnInputChange clamps + validates,
ProtocolStr = _containerTransportTypeEnumInput.value.ToString(),
TlsUpgrade = _containerTransportTypeEnumInput.value.ToString() == ProtocolType.WS.ToString() // If the protocol is WebSocket, we seemlessly add tls_upgrade. If we want to add it to other protocols, we need to change this.
},
};
UpdateAppVersionRequest updateAppVerReq = new UpdateAppVersionRequest(_appNameInput.value) // MIRROR CHANGE: 'new()' not supported in Unity 2020
{
VersionName = _containerNewTagVersionInput.value,
DockerImage = imageName,
DockerRepository = registry,
DockerTag = tag,
PrivateUsername = _containerUsernameInput.value,
PrivateToken = _containerTokenInput.value,
Ports = ports,
};
EdgegapHttpResult<UpsertAppVersionResult> updateAppVersionResult = await appApi.UpsertAppVersion(updateAppVerReq);
if (updateAppVersionResult.HasErr)
{
onBuildPushError($"Unable to update docker tag/version:\n{updateAppVersionResult.Error.ErrorMessage}");
return;
}
// cleanup
onBuildAndPushSuccess(tag);
}
catch (Exception ex)
{
EditorUtility.ClearProgressBar();
Debug.LogError(ex);
string errMsg = "Edgegapbuild and push failed";
if (ex.Message.Contains("docker daemon is not running"))
{
errMsg += ":\nDocker is installed, but the daemon/app (such as `Docker Desktop`) is not running. " +
"Please start Docker Desktop and try again.";
}
else
errMsg += $":\n{ex.Message}";
onBuildPushError(errMsg);
}
// MIRROR CHANGE: always clear otherwise it gets stuck there forever!
finally
{
EditorUtility.ClearProgressBar();
}
// END MIRROR CHANGE
}
private void onBuildAndPushSuccess(string tag)
{
// _containerImageTag = tag; // TODO?
syncFormWithObjectStatic();
EditorUtility.ClearProgressBar();
_containerBuildAndPushResultLabel.text = $"Success ({tag})";
_containerBuildAndPushResultLabel.visible = true;
Debug.Log("Server built and pushed successfully");
}
/// <summary>(v2) Docker cmd error, detected by "ERROR" in log stream.</summary>
private void onBuildPushError(string msg)
{
EditorUtility.ClearProgressBar();
_containerBuildAndPushResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
"Error", EdgegapWindowMetadata.StatusColors.Error);
EditorUtility.DisplayDialog("Error", msg, "Ok"); // Show this last! It's blocking!
}
#region Persistence Helpers
/// <summary>
/// Load from EditorPrefs, persisting from a previous session, if the field is empty
/// - ApiToken; !persisted via ViewDataKey so we don't save plaintext
/// - DeploymentRequestId
/// - DeploymentConnectionUrl
/// - DeploymentConnectionStatus
/// </summary>
private void loadPersistentDataFromEditorPrefs()
{
// ApiToken
if (string.IsNullOrEmpty(_apiTokenInput.value))
setMaskedApiTokenFromEditorPrefs();
// DeploymentRequestId
if (string.IsNullOrEmpty(_deploymentRequestId))
_deploymentRequestId = EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR);
// DeploymentConnectionUrl
if (string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.text))
{
_deploymentsConnectionUrlReadonlyInput.value = getDeploymentsConnectionUrlLabelTxt();
bool hasVal = !string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.value);
if (hasVal && string.IsNullOrEmpty(_deploymentRequestId))
{
// Fallback -- if no requestId, we can actually get it from the url since we have this (desync)
_deploymentRequestId = _deploymentsConnectionUrlReadonlyInput.value.Split('.')[0];
EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR, _deploymentRequestId);
}
// TODO (Optional): Show a status label to remind these are cached vals; refresh for live?
}
// DeploymentConnectionStatus
if (string.IsNullOrEmpty(_deploymentsConnectionStatusLabel.text) || _deploymentsConnectionStatusLabel.text == "Unknown")
_deploymentsConnectionStatusLabel.text = getDeploymentsConnectionStatusLabelTxt();
}
/// <summary>Set Label -> Persist to EditorPrefs</summary>
/// <param name="newDomainWithPort"></param>
private void setPersistDeploymentsConnectionUrlLabelTxt(string newDomainWithPort)
{
_deploymentsConnectionUrlReadonlyInput.value = newDomainWithPort;
EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_URL_KEY_STR, newDomainWithPort);
}
/// <summary>Get persistent data fromEditorPrefs</summary>
private string getDeploymentsConnectionUrlLabelTxt() =>
EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_URL_KEY_STR);
/// <summary>Set label -> persist to EditorPrefs</summary>
/// <param name="newStatus"></param>
private void setPersistDeploymentsConnectionStatusLabelTxt(string newStatus)
{
_deploymentsConnectionStatusLabel.text = newStatus;
EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_STATUS_KEY_STR, newStatus);
}
/// <summary>Get persistent data fromEditorPrefs</summary>
private string getDeploymentsConnectionStatusLabelTxt() =>
EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_STATUS_KEY_STR);
/// <summary>Set to base64 -> Save to EditorPrefs</summary>
/// <param name="value"></param>
private void persistUnmaskedApiToken(string value)
{
EditorPrefs.SetString(
EdgegapWindowMetadata.API_TOKEN_KEY_STR,
Base64Encode(value));
}
/// <summary>
/// Get apiToken from EditorPrefs -> Base64 Decode -> Set to apiTokenInput
/// </summary>
private void setMaskedApiTokenFromEditorPrefs()
{
string apiTokenBase64Str = EditorPrefs.GetString(
EdgegapWindowMetadata.API_TOKEN_KEY_STR, null);
if (apiTokenBase64Str == null)
return;
string decodedApiToken = Base64Decode(apiTokenBase64Str);
_apiTokenInput.SetValueWithoutNotify(decodedApiToken);
}
#endregion // Persistence Helpers
}
}