From 734b02fa1d416fc68dea8e3efa431991ad943190 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Tue, 12 Feb 2013 17:27:54 +1100 Subject: [PATCH] Update: Plugin Framework Install & UI --- Disco.BI/Disco.BI.csproj | 2 + Disco.BI/Properties/AssemblyInfo.cs | 4 +- .../Package Creation/PreparationClient.zip | Bin 418429 -> 418430 bytes Disco.Client/Properties/AssemblyInfo.cs | 4 +- .../Properties/AssemblyInfo.cs | 4 +- Disco.Data/Properties/AssemblyInfo.cs | 4 +- Disco.Models/Disco.Models.csproj | 232 +- Disco.Models/Properties/AssemblyInfo.cs | 4 +- Disco.Services/Disco.Services.csproj | 19 +- Disco.Services/Plugins/InstallPluginTask.cs | 43 +- Disco.Services/Plugins/PluginAttribute.cs | 35 +- Disco.Services/Plugins/PluginManifest.cs | 36 +- Disco.Services/Plugins/Plugins.cs | 114 +- Disco.Services/Properties/AssemblyInfo.cs | 4 +- .../Properties/AssemblyInfo.cs | 4 +- .../Areas/API/Controllers/PluginController.cs | 36 +- .../Areas/Config/ConfigAreaRegistration.cs | 249 ++- .../Config/Controllers/LoggingController.cs | 96 +- .../Config/Controllers/PluginsController.cs | 23 + .../Config/Models/Plugins/InstallModel.cs | 13 + ...del.cs => PluginConfigurationViewModel.cs} | 44 +- .../Config/Views/Expressions/Editor.cshtml | 104 +- .../Views/Expressions/Editor.generated.cs | 310 ++- .../Areas/Config/Views/Plugins/Index.cshtml | 164 +- .../Config/Views/Plugins/Index.generated.cs | 413 ++-- .../Areas/Config/Views/Plugins/Install.cshtml | 144 ++ .../Config/Views/Plugins/Install.generated.cs | 443 ++++ Disco.Web/ClientSource/Style/Config.css | 82 +- Disco.Web/ClientSource/Style/Config.less | 1983 +++++++++-------- Disco.Web/ClientSource/Style/Config.min.css | 2 +- .../Style/Images/Actions/installPlugin.png | Bin 0 -> 3822 bytes .../Images/Actions/installPluginHover.png | Bin 0 -> 3936 bytes Disco.Web/Disco.Web.csproj | 14 +- Disco.Web/Properties/AssemblyInfo.cs | 4 +- Disco.Web/T4MVC.cs | 47 +- 35 files changed, 2792 insertions(+), 1888 deletions(-) create mode 100644 Disco.Web/Areas/Config/Models/Plugins/InstallModel.cs rename Disco.Web/Areas/Config/Models/Plugins/{ProviderConfigurationViewModel.cs => PluginConfigurationViewModel.cs} (96%) create mode 100644 Disco.Web/Areas/Config/Views/Plugins/Install.cshtml create mode 100644 Disco.Web/Areas/Config/Views/Plugins/Install.generated.cs create mode 100644 Disco.Web/ClientSource/Style/Images/Actions/installPlugin.png create mode 100644 Disco.Web/ClientSource/Style/Images/Actions/installPluginHover.png diff --git a/Disco.BI/Disco.BI.csproj b/Disco.BI/Disco.BI.csproj index bd1e8151..b1ab4d5e 100644 --- a/Disco.BI/Disco.BI.csproj +++ b/Disco.BI/Disco.BI.csproj @@ -149,6 +149,8 @@ + + diff --git a/Disco.BI/Properties/AssemblyInfo.cs b/Disco.BI/Properties/AssemblyInfo.cs index cb24ca64..1c519291 100644 --- a/Disco.BI/Properties/AssemblyInfo.cs +++ b/Disco.BI/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.2.0208.1156")] -[assembly: AssemblyFileVersion("1.2.0208.1156")] +[assembly: AssemblyVersion("1.2.0212.1702")] +[assembly: AssemblyFileVersion("1.2.0212.1702")] diff --git a/Disco.Client/Package Creation/PreparationClient.zip b/Disco.Client/Package Creation/PreparationClient.zip index 88a7cb7d84e6608957b6ef622f8141a4a57c73d7..d9590ec0ed0e54c808e348408c48e87029d345fd 100644 GIT binary patch delta 27446 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 o20Q^4mkwbDXaUcccVPx(0fm>`VFp_PZ - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9} - Library - Properties - Disco.Models - Disco.Models - v4.5 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9} + Library + Properties + Disco.Models + Disco.Models + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Models/Properties/AssemblyInfo.cs b/Disco.Models/Properties/AssemblyInfo.cs index aa936558..af2c3ad5 100644 --- a/Disco.Models/Properties/AssemblyInfo.cs +++ b/Disco.Models/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.2.0207.1727")] -[assembly: AssemblyFileVersion("1.2.0207.1727")] +[assembly: AssemblyVersion("1.2.0212.1702")] +[assembly: AssemblyFileVersion("1.2.0212.1702")] diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj index 4372233e..fd2f0725 100644 --- a/Disco.Services/Disco.Services.csproj +++ b/Disco.Services/Disco.Services.csproj @@ -101,23 +101,6 @@ - - - - - - - - - - - - - - - - - @@ -168,7 +151,7 @@ - + diff --git a/Disco.Services/Plugins/InstallPluginTask.cs b/Disco.Services/Plugins/InstallPluginTask.cs index d6056eee..db794974 100644 --- a/Disco.Services/Plugins/InstallPluginTask.cs +++ b/Disco.Services/Plugins/InstallPluginTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Net; using System.Text; using System.Threading.Tasks; using Disco.Data.Repository; @@ -17,12 +18,38 @@ namespace Disco.Services.Plugins protected override void ExecuteTask() { + string packageUrlPath = (string)this.ExecutionContext.JobDetail.JobDataMap["PackageUrl"]; string packageFilePath = (string)this.ExecutionContext.JobDetail.JobDataMap["PackageFilePath"]; bool DeletePackageAfterInstall = (bool)this.ExecutionContext.JobDetail.JobDataMap["DeletePackageAfterInstall"]; if (!Plugins.PluginsLoaded) throw new InvalidOperationException("Plugins have not been initialized"); + if (!string.IsNullOrEmpty(packageUrlPath)) + { + this.Status.UpdateStatus(0, "Downloading Plugin Package", "Connecting..."); + + if (File.Exists(packageFilePath)) + File.Delete(packageFilePath); + + if (!Directory.Exists(Path.GetDirectoryName(packageFilePath))) + Directory.CreateDirectory(Path.GetDirectoryName(packageFilePath)); + + // Need to Download the Package + WebClient downloader = new WebClient(); + DateTime progressExpires = DateTime.Now; + downloader.DownloadProgressChanged += (sender, e) => + { + Console.WriteLine(e.ProgressPercentage); + if (progressExpires <= DateTime.Now) + { + this.Status.UpdateStatus(e.ProgressPercentage, string.Format("{0} of {1} KB downloaded", e.BytesReceived / 1024, e.TotalBytesToReceive / 1024)); + progressExpires = DateTime.Now.AddMilliseconds(250); + } + }; + downloader.DownloadFileTaskAsync(new Uri(packageUrlPath), packageFilePath).Wait(); + } + this.Status.UpdateStatus(10, "Opening Plugin Package", Path.GetFileName(packageFilePath)); using (var packageStream = File.OpenRead(packageFilePath)) @@ -44,7 +71,7 @@ namespace Disco.Services.Plugins this.Status.UpdateStatus(20, string.Format("{0} [{1} v{2}] by {3}", packageManifest.Name, packageManifest.Id, packageManifest.Version.ToString(4), packageManifest.Author), "Initializing Install Environment"); PluginsLog.LogInstalling(packageManifest); - + lock (Plugins._PluginLock) { if (!Plugins.PluginsLoaded) @@ -58,6 +85,12 @@ namespace Disco.Services.Plugins { string packagePath = Path.Combine(dbContext.DiscoConfiguration.PluginsLocation, packageManifest.Id); + // Check for Compatibility + var compatibilityData = Plugins.LoadCompatibilityData(dbContext); + var pluginCompatibility = compatibilityData.Plugins.FirstOrDefault(i => i.Id.Equals(packageManifest.Id, StringComparison.InvariantCultureIgnoreCase) && packageManifest.Version == Version.Parse(i.Version)); + if (pluginCompatibility != null && !pluginCompatibility.Compatible) + throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] is not compatible: {2}", packageManifest.Id, packageManifest.VersionFormatted, pluginCompatibility.Reason)); + // Force Delete of Existing Folder if (Directory.Exists(packagePath)) { @@ -117,12 +150,16 @@ namespace Disco.Services.Plugins File.Delete(packageFilePath); } - public static ScheduledTaskStatus InstallPlugin(string PackageFilePath, bool DeletePackageAfterInstall) + public static ScheduledTaskStatus InstallLocalPlugin(string PackageFilePath, bool DeletePackageAfterInstall) + { + return InstallPlugin(null, PackageFilePath, DeletePackageAfterInstall); + } + public static ScheduledTaskStatus InstallPlugin(string PackageUrl, string PackageFilePath, bool DeletePackageAfterInstall) { if (ScheduledTasks.GetTaskStatuses(typeof(InstallPluginTask)).Where(s => s.IsRunning).Count() > 0) throw new InvalidOperationException("A plugin is already being Installed"); - JobDataMap taskData = new JobDataMap() { { "PackageFilePath", PackageFilePath }, { "DeletePackageAfterInstall", DeletePackageAfterInstall } }; + JobDataMap taskData = new JobDataMap() { { "PackageUrl", PackageUrl }, { "PackageFilePath", PackageFilePath }, { "DeletePackageAfterInstall", DeletePackageAfterInstall } }; var instance = new InstallPluginTask(); diff --git a/Disco.Services/Plugins/PluginAttribute.cs b/Disco.Services/Plugins/PluginAttribute.cs index f9cbf4bc..70c87bdb 100644 --- a/Disco.Services/Plugins/PluginAttribute.cs +++ b/Disco.Services/Plugins/PluginAttribute.cs @@ -1,16 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Disco.Services.Plugins -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class PluginAttribute : Attribute - { - public string Id { get; set; } - public string Name { get; set; } - public string Author { get; set; } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Services.Plugins +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class PluginAttribute : Attribute + { + public string Id { get; set; } + public string Name { get; set; } + public string Author { get; set; } + public string Url { get; set; } + public string HostVersionMin { get; set; } + public string HostVersionMax { get; set; } + } +} diff --git a/Disco.Services/Plugins/PluginManifest.cs b/Disco.Services/Plugins/PluginManifest.cs index f860b3d9..8b266078 100644 --- a/Disco.Services/Plugins/PluginManifest.cs +++ b/Disco.Services/Plugins/PluginManifest.cs @@ -11,16 +11,27 @@ using System.Web.Mvc; using Disco.Data.Repository; using Disco.Services.Tasks; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Disco.Services.Plugins { public class PluginManifest { + [JsonProperty] public string Id { get; set; } + [JsonProperty] public string Name { get; set; } + [JsonProperty] public string Author { get; set; } + [JsonProperty] + public string Url { get; set; } + [JsonProperty] public Version Version { get; set; } [JsonProperty] + public Version HostVersionMin { get; set; } + [JsonProperty] + public Version HostVersionMax { get; set; } + [JsonProperty] internal string AssemblyPath { get; set; } [JsonProperty] private string TypeName { get; set; } @@ -49,6 +60,16 @@ namespace Disco.Services.Plugins [JsonIgnore] public string StorageLocation { get; private set; } + [JsonIgnore] + public string VersionFormatted + { + get + { + var v = Version; + return string.Format("{0}.{1}.{2:0000}.{3:0000}", v.Major, v.Minor, v.Build, v.Revision); + } + } + [JsonIgnore] private bool environmentInitalized { get; set; } @@ -83,7 +104,7 @@ namespace Disco.Services.Plugins return manifest; } } - + /// /// Deserializes a Json Manifest /// @@ -98,7 +119,7 @@ namespace Disco.Services.Plugins manifestString = manifestStreamReader.ReadToEnd(); } - var manifest = JsonConvert.DeserializeObject(manifestString); + var manifest = JsonConvert.DeserializeObject(manifestString, new VersionConverter()); manifest.PluginLocation = PluginLocation; @@ -129,6 +150,10 @@ namespace Disco.Services.Plugins var pluginId = pluginAttributes.Id; var pluginName = pluginAttributes.Name; var pluginAuthor = pluginAttributes.Author; + var pluginUrl = pluginAttributes.Url; + + var pluginHostVersionMin = pluginAttributes.HostVersionMin == null ? null : Version.Parse(pluginAttributes.HostVersionMin); + var pluginHostVersionMax = pluginAttributes.HostVersionMax == null ? null : Version.Parse(pluginAttributes.HostVersionMax); var pluginVersion = assemblyName.Version; var pluginAssemblyPath = Path.GetFileName(assembly.Location); @@ -167,6 +192,9 @@ namespace Disco.Services.Plugins Name = pluginName, Author = pluginAuthor, Version = pluginVersion, + Url = pluginUrl, + HostVersionMin = pluginHostVersionMin, + HostVersionMax = pluginHostVersionMax, AssemblyPath = pluginAssemblyPath, TypeName = pluginTypeName, AssemblyReferences = pluginAssemblyReferences, @@ -188,7 +216,7 @@ namespace Disco.Services.Plugins public string ToManifestFile() { - return JsonConvert.SerializeObject(this, Formatting.Indented); + return JsonConvert.SerializeObject(this, Formatting.Indented, new VersionConverter()); } private bool InitializePluginEnvironment(DiscoDataContext dbContext) { @@ -362,7 +390,7 @@ namespace Disco.Services.Plugins var fileDateCheck = System.IO.File.GetLastWriteTime(resourcePath); if (fileDateCheck == resourceHash.Item2) #endif - return new Tuple(resourcePath, resourceHash.Item1); + return new Tuple(resourcePath, resourceHash.Item1); } if (!File.Exists(resourcePath)) diff --git a/Disco.Services/Plugins/Plugins.cs b/Disco.Services/Plugins/Plugins.cs index 8456c526..968534ed 100644 --- a/Disco.Services/Plugins/Plugins.cs +++ b/Disco.Services/Plugins/Plugins.cs @@ -7,6 +7,9 @@ using System.Text; using System.Threading.Tasks; using Disco.Data.Repository; using System.IO.Compression; +using Disco.Models.BI.Interop.Community; +using System.Web; +using Newtonsoft.Json; namespace Disco.Services.Plugins { @@ -42,6 +45,15 @@ namespace Disco.Services.Plugins } } + public static bool PluginInstalled(string PluginId) + { + if (_PluginManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + PluginManifest manifest; + return _PluginManifests.TryGetValue(PluginId, out manifest); + } + public static PluginManifest GetPlugin(string PluginId, Type ContainsCategoryType) { if (_PluginManifests == null) @@ -145,6 +157,81 @@ namespace Disco.Services.Plugins throw new InvalidOperationException(string.Format("Unknown Plugin Feature Category Type: [{0}]", FeatureCategoryType.Name)); } + public static string CatalogueFile(DiscoDataContext dbContext) + { + return Path.Combine(dbContext.DiscoConfiguration.PluginPackagesLocation, "Catalogue.json"); + } + public static string CompatibilityFile(DiscoDataContext dbContext) + { + return Path.Combine(dbContext.DiscoConfiguration.PluginPackagesLocation, "Compatibility.json"); + } + + public static PluginLibraryUpdateResponse LoadCatalogue(DiscoDataContext dbContext) + { + var catalogueFile = CatalogueFile(dbContext); + + if (!File.Exists(catalogueFile)) + return null; + + return JsonConvert.DeserializeObject(File.ReadAllText(catalogueFile)); + } + + public static PluginLibraryCompatibilityResponse LoadCompatibilityData(DiscoDataContext dbContext) + { + var pluginAssembly = typeof(Plugins).Assembly; + Version hostVersion = pluginAssembly.GetName().Version; + PluginLibraryCompatibilityResponse Data = null; + var localCompatFile = Path.Combine(Path.GetDirectoryName(pluginAssembly.Location), "ReleasePluginCompatibility.json"); + var serverCompatFile = CompatibilityFile(dbContext); + + if (File.Exists(localCompatFile)) + { + Data = JsonConvert.DeserializeObject(File.ReadAllText(localCompatFile)); + Data.HostVersion = hostVersion.ToString(4); + } + if (File.Exists(serverCompatFile)) + { + var serverData = JsonConvert.DeserializeObject(File.ReadAllText(serverCompatFile)); + if (Version.Parse(serverData.HostVersion) == hostVersion) + { + if (Data == null) + { + // No Local Compatibility File + Data = serverData; + } + else + { + // Join Compatibility Files + var localItems = Data.Plugins; + var localItemVersions = localItems.ToDictionary(i => i, i => Version.Parse(i.Version)); + var joinedItems = localItems.ToList(); + Data.ResponseTimestamp = serverData.ResponseTimestamp; + foreach (var serverItem in serverData.Plugins) + { + var serverItemVersion = Version.Parse(serverItem.Version); + var localItem = localItems.FirstOrDefault(i => i.Id.Equals(serverItem.Id, StringComparison.InvariantCultureIgnoreCase) && serverItemVersion == localItemVersions[i]); + if (localItem != null) + joinedItems.Remove(localItem); + + joinedItems.Add(serverItem); + } + Data.Plugins = joinedItems; + } + } + } + if (Data == null) + { + Data = new PluginLibraryCompatibilityResponse() + { + HostVersion = hostVersion.ToString(4), + Plugins = new List(), + ResponseTimestamp = new DateTime(2011, 7, 1) + }; + } + + return Data; + } + public static void InitalizePlugins(DiscoDataContext dbContext) { if (_PluginManifests == null) @@ -153,6 +240,8 @@ namespace Disco.Services.Plugins { if (_PluginManifests == null) { + Version hostVersion = typeof(Plugins).Assembly.GetName().Version; + var compatibilityData = new Lazy(() => LoadCompatibilityData(dbContext)); Dictionary loadedPlugins = new Dictionary(); PluginPath = dbContext.DiscoConfiguration.PluginsLocation; @@ -185,11 +274,21 @@ namespace Disco.Services.Plugins if (File.Exists(updatePackagePath)) { // Update Plugin - pluginManifest = UpdatePlugin(dbContext, pluginManifest, updatePackagePath); + pluginManifest = UpdatePlugin(dbContext, pluginManifest, updatePackagePath, compatibilityData.Value); } if (pluginManifest != null) { + // Check Version Compatibility + var pluginCompatibility = compatibilityData.Value.Plugins.FirstOrDefault(i => i.Id.Equals(pluginManifest.Id, StringComparison.InvariantCultureIgnoreCase) && pluginManifest.Version == Version.Parse(i.Version)); + if (pluginCompatibility != null && !pluginCompatibility.Compatible) + throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] is not compatible: {2}", pluginManifest.Id, pluginManifest.VersionFormatted, pluginCompatibility.Reason)); + + if (pluginManifest.HostVersionMin != null && pluginManifest.HostVersionMin > hostVersion) + throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] does not support this version of Disco (Requires v{2} or greater)", pluginManifest.Id, pluginManifest.VersionFormatted, pluginManifest.HostVersionMin.ToString())); + if (pluginManifest.HostVersionMax != null && pluginManifest.HostVersionMax < hostVersion) + throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] does not support this version of Disco (Support expired as of v{2})", pluginManifest.Id, pluginManifest.VersionFormatted, pluginManifest.HostVersionMax.ToString())); + pluginManifest.InitializePlugin(dbContext); loadedPlugins[pluginManifest.Id] = pluginManifest; } @@ -251,13 +350,13 @@ namespace Disco.Services.Plugins _PluginAssemblyManifests = _PluginManifests.Values.ToDictionary(p => p.PluginAssembly, p => p); } - public static PluginManifest UpdatePlugin(DiscoDataContext dbContext, PluginManifest ExistingManifest, String UpdatePluginPackageFilePath) + public static PluginManifest UpdatePlugin(DiscoDataContext dbContext, PluginManifest ExistingManifest, String UpdatePluginPackageFilePath, PluginLibraryCompatibilityResponse CompatibilityData = null) { PluginManifest updatedManifest; using (var packageStream = File.OpenRead(UpdatePluginPackageFilePath)) { - updatedManifest = UpdatePlugin(dbContext, ExistingManifest, packageStream); + updatedManifest = UpdatePlugin(dbContext, ExistingManifest, packageStream, CompatibilityData); } // Remove Update after processing @@ -266,7 +365,7 @@ namespace Disco.Services.Plugins return updatedManifest; } - public static PluginManifest UpdatePlugin(DiscoDataContext dbContext, PluginManifest ExistingManifest, Stream UpdatePluginPackage) + public static PluginManifest UpdatePlugin(DiscoDataContext dbContext, PluginManifest ExistingManifest, Stream UpdatePluginPackage, PluginLibraryCompatibilityResponse CompatibilityData = null) { using (MemoryStream packageStream = new MemoryStream()) { @@ -301,6 +400,13 @@ namespace Disco.Services.Plugins throw new InvalidDataException("A newer version of this plugin is already installed"); } + // Check Compatibility + if (CompatibilityData == null) + CompatibilityData = LoadCompatibilityData(dbContext); + var pluginCompatibility = CompatibilityData.Plugins.FirstOrDefault(i => i.Id.Equals(packageManifest.Id, StringComparison.InvariantCultureIgnoreCase) && packageManifest.Version == Version.Parse(i.Version)); + if (pluginCompatibility != null && !pluginCompatibility.Compatible) + throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] is not compatible: {2}", packageManifest.Id, packageManifest.VersionFormatted, pluginCompatibility.Reason)); + string packagePath = Path.Combine(dbContext.DiscoConfiguration.PluginsLocation, packageManifest.Id); // Force Delete of Existing Folder diff --git a/Disco.Services/Properties/AssemblyInfo.cs b/Disco.Services/Properties/AssemblyInfo.cs index bced427e..99c249f0 100644 --- a/Disco.Services/Properties/AssemblyInfo.cs +++ b/Disco.Services/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.2.0207.1727")] -[assembly: AssemblyFileVersion("1.2.0207.1727")] +[assembly: AssemblyVersion("1.2.0212.1702")] +[assembly: AssemblyFileVersion("1.2.0212.1702")] diff --git a/Disco.Web.Extensions/Properties/AssemblyInfo.cs b/Disco.Web.Extensions/Properties/AssemblyInfo.cs index 1f6dccef..c4bb3bd6 100644 --- a/Disco.Web.Extensions/Properties/AssemblyInfo.cs +++ b/Disco.Web.Extensions/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.2.0207.1727")] -[assembly: AssemblyFileVersion("1.2.0207.1727")] +[assembly: AssemblyVersion("1.2.0212.1702")] +[assembly: AssemblyFileVersion("1.2.0212.1702")] diff --git a/Disco.Web/Areas/API/Controllers/PluginController.cs b/Disco.Web/Areas/API/Controllers/PluginController.cs index f65872cc..b9384b0c 100644 --- a/Disco.Web/Areas/API/Controllers/PluginController.cs +++ b/Disco.Web/Areas/API/Controllers/PluginController.cs @@ -4,12 +4,21 @@ using System.IO; using System.Linq; using System.Web; using System.Web.Mvc; +using Disco.BI.Interop.Community; using Disco.Services.Plugins; namespace Disco.Web.Areas.API.Controllers { public partial class PluginController : dbAdminController { + public virtual ActionResult UpdateLibraryCatalogue() + { + var status = PluginLibraryUpdateTask.ScheduleNow(); + + status.SetFinishedUrl(Url.Action(MVC.Config.Plugins.Install())); + + return RedirectToAction(MVC.Config.Logging.TaskStatus(status.SessionId)); + } public virtual ActionResult Uninstall(string id, bool UninstallData) { @@ -23,7 +32,29 @@ namespace Disco.Web.Areas.API.Controllers return RedirectToAction(MVC.Config.Logging.TaskStatus(status.SessionId)); } - public virtual ActionResult Install(HttpPostedFileBase Plugin) + public virtual ActionResult Install(string PluginId) + { + if (string.IsNullOrEmpty(PluginId)) + throw new ArgumentNullException("PluginId", "A PluginId must be supplied"); + + var catalogue = Plugins.LoadCatalogue(dbContext); + var plugin = catalogue.Plugins.FirstOrDefault(p => p.Id.Equals(PluginId)); + + if (plugin == null) + throw new ArgumentNullException("PluginId", "Plugin not found in catalogue"); + + // Already Installed? + if (Plugins.PluginInstalled(plugin.Id)) + throw new InvalidOperationException("This plugin is already installed"); + + var tempPluginLocation = Path.Combine(dbContext.DiscoConfiguration.PluginPackagesLocation, string.Format("{0}.discoPlugin", plugin.Id)); + + var status = InstallPluginTask.InstallPlugin(plugin.LatestDownloadUrl, tempPluginLocation, true); + + return RedirectToAction(MVC.Config.Logging.TaskStatus(status.SessionId)); + } + + public virtual ActionResult InstallLocal(HttpPostedFileBase Plugin) { if (Plugin == null || Plugin.ContentLength <= 0 || string.IsNullOrWhiteSpace(Plugin.FileName)) throw new ArgumentException("A discoPlugin file must be uploaded", "Plugin"); @@ -36,10 +67,9 @@ namespace Disco.Web.Areas.API.Controllers if (System.IO.File.Exists(tempPluginLocation)) System.IO.File.Delete(tempPluginLocation); - Plugin.SaveAs(tempPluginLocation); - var status = InstallPluginTask.InstallPlugin(tempPluginLocation, true); + var status = InstallPluginTask.InstallLocalPlugin(tempPluginLocation, true); return RedirectToAction(MVC.Config.Logging.TaskStatus(status.SessionId)); } diff --git a/Disco.Web/Areas/Config/ConfigAreaRegistration.cs b/Disco.Web/Areas/Config/ConfigAreaRegistration.cs index 48685e89..e9d0537a 100644 --- a/Disco.Web/Areas/Config/ConfigAreaRegistration.cs +++ b/Disco.Web/Areas/Config/ConfigAreaRegistration.cs @@ -1,122 +1,127 @@ -using System.Web.Mvc; - -namespace Disco.Web.Areas.Config -{ - public class ConfigAreaRegistration : AreaRegistration - { - public override string AreaName - { - get - { - return "Config"; - } - } - - public override void RegisterArea(AreaRegistrationContext context) - { - context.MapRoute( - "Config_DeviceModel_GenericComponents", - "Config/DeviceModel/GenericComponents", - new { controller = "DeviceModel", action = "GenericComponents" } - ); - context.MapRoute( - "Config_DeviceModel", - "Config/DeviceModel/{id}", - new { controller = "DeviceModel", action = "Index", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_DeviceBatch_Create", - "Config/DeviceBatch/Create", - new { controller = "DeviceBatch", action = "Create", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_DeviceBatch_Timeline", - "Config/DeviceBatch/Timeline", - new { controller = "DeviceBatch", action = "Timeline" } - ); - context.MapRoute( - "Config_DeviceBatch", - "Config/DeviceBatch/{id}", - new { controller = "DeviceBatch", action = "Index", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_DeviceProfile_Create", - "Config/DeviceProfile/Create", - new { controller = "DeviceProfile", action = "Create" } - ); - context.MapRoute( - "Config_DeviceProfile_Defaults", - "Config/DeviceProfile/Defaults", - new { controller = "DeviceProfile", action = "Defaults" } - ); - context.MapRoute( - "Config_DeviceProfile", - "Config/DeviceProfile/{id}", - new { controller = "DeviceProfile", action = "Index", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_AttachmentType_Create", - "Config/AttachmentType/Create", - new { controller = "AttachmentType", action = "Create" } - ); - context.MapRoute( - "Config_AttachmentType_ExpressionBrowser_Type", - "Config/AttachmentType/ExpressionBrowser/{type}", - new { controller = "AttachmentType", action = "ExpressionBrowser", type = UrlParameter.Optional } - ); - context.MapRoute( - "Config_AttachmentType", - "Config/AttachmentType/{id}", - new { controller = "AttachmentType", action = "Index", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_DocumentTemplate_Create", - "Config/DocumentTemplate/Create", - new { controller = "DocumentTemplate", action = "Create", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_DocumentTemplate_ImportStatus", - "Config/DocumentTemplate/ImportStatus", - new { controller = "DocumentTemplate", action = "ImportStatus", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_DocumentTemplate_UndetectedPages", - "Config/DocumentTemplate/UndetectedPages", - new { controller = "DocumentTemplate", action = "UndetectedPages", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_DocumentTemplate_ExpressionBrowser", - "Config/DocumentTemplate/ExpressionBrowser", - new { controller = "DocumentTemplate", action = "ExpressionBrowser", id = UrlParameter.Optional } - ); - context.MapRoute( - "Config_DocumentTemplate", - "Config/DocumentTemplate/{id}", - new { controller = "DocumentTemplate", action = "Index", id = UrlParameter.Optional } - ); - - context.MapRoute( - "Config_Warranty", - "Config/Warranty/{id}", - new { controller = "Warranty", action = "Index", id = UrlParameter.Optional } - ); - - context.MapRoute( - "Config_Plugins", - "Config/Plugins", - new { controller = "Plugins", action = "Index"} - ); - context.MapRoute( - "Config_Plugins_Configure", - "Config/Plugins/{PluginId}", - new { controller = "Plugins", action = "Configure" } - ); - - context.MapRoute( - "Config_default", - "Config/{controller}/{action}/{id}", - new { controller = "Config", action = "Index", id = UrlParameter.Optional } - ); - } - } -} +using System.Web.Mvc; + +namespace Disco.Web.Areas.Config +{ + public class ConfigAreaRegistration : AreaRegistration + { + public override string AreaName + { + get + { + return "Config"; + } + } + + public override void RegisterArea(AreaRegistrationContext context) + { + context.MapRoute( + "Config_DeviceModel_GenericComponents", + "Config/DeviceModel/GenericComponents", + new { controller = "DeviceModel", action = "GenericComponents" } + ); + context.MapRoute( + "Config_DeviceModel", + "Config/DeviceModel/{id}", + new { controller = "DeviceModel", action = "Index", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_DeviceBatch_Create", + "Config/DeviceBatch/Create", + new { controller = "DeviceBatch", action = "Create", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_DeviceBatch_Timeline", + "Config/DeviceBatch/Timeline", + new { controller = "DeviceBatch", action = "Timeline" } + ); + context.MapRoute( + "Config_DeviceBatch", + "Config/DeviceBatch/{id}", + new { controller = "DeviceBatch", action = "Index", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_DeviceProfile_Create", + "Config/DeviceProfile/Create", + new { controller = "DeviceProfile", action = "Create" } + ); + context.MapRoute( + "Config_DeviceProfile_Defaults", + "Config/DeviceProfile/Defaults", + new { controller = "DeviceProfile", action = "Defaults" } + ); + context.MapRoute( + "Config_DeviceProfile", + "Config/DeviceProfile/{id}", + new { controller = "DeviceProfile", action = "Index", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_AttachmentType_Create", + "Config/AttachmentType/Create", + new { controller = "AttachmentType", action = "Create" } + ); + context.MapRoute( + "Config_AttachmentType_ExpressionBrowser_Type", + "Config/AttachmentType/ExpressionBrowser/{type}", + new { controller = "AttachmentType", action = "ExpressionBrowser", type = UrlParameter.Optional } + ); + context.MapRoute( + "Config_AttachmentType", + "Config/AttachmentType/{id}", + new { controller = "AttachmentType", action = "Index", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_DocumentTemplate_Create", + "Config/DocumentTemplate/Create", + new { controller = "DocumentTemplate", action = "Create", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_DocumentTemplate_ImportStatus", + "Config/DocumentTemplate/ImportStatus", + new { controller = "DocumentTemplate", action = "ImportStatus", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_DocumentTemplate_UndetectedPages", + "Config/DocumentTemplate/UndetectedPages", + new { controller = "DocumentTemplate", action = "UndetectedPages", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_DocumentTemplate_ExpressionBrowser", + "Config/DocumentTemplate/ExpressionBrowser", + new { controller = "DocumentTemplate", action = "ExpressionBrowser", id = UrlParameter.Optional } + ); + context.MapRoute( + "Config_DocumentTemplate", + "Config/DocumentTemplate/{id}", + new { controller = "DocumentTemplate", action = "Index", id = UrlParameter.Optional } + ); + + context.MapRoute( + "Config_Warranty", + "Config/Warranty/{id}", + new { controller = "Warranty", action = "Index", id = UrlParameter.Optional } + ); + + context.MapRoute( + "Config_Plugins", + "Config/Plugins", + new { controller = "Plugins", action = "Index"} + ); + context.MapRoute( + "Config_Plugins_Install", + "Config/Plugins/Install", + new { controller = "Plugins", action = "Install" } + ); + context.MapRoute( + "Config_Plugins_Configure", + "Config/Plugins/{PluginId}", + new { controller = "Plugins", action = "Configure" } + ); + + context.MapRoute( + "Config_default", + "Config/{controller}/{action}/{id}", + new { controller = "Config", action = "Index", id = UrlParameter.Optional } + ); + } + } +} diff --git a/Disco.Web/Areas/Config/Controllers/LoggingController.cs b/Disco.Web/Areas/Config/Controllers/LoggingController.cs index e5e44cc1..b6574702 100644 --- a/Disco.Web/Areas/Config/Controllers/LoggingController.cs +++ b/Disco.Web/Areas/Config/Controllers/LoggingController.cs @@ -1,53 +1,43 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Mvc; -using Disco.Services.Logging; -using Disco.Services.Logging.Models; - -namespace Disco.Web.Areas.Config.Controllers -{ - public partial class LoggingController : dbAdminController - { - // - // GET: /Config/Logs/ - - public virtual ActionResult Index() - { - var m = new Models.Logging.IndexModel() - { - LogModules = new Dictionary>() - }; - foreach (var logModule in LogContext.LogModules.Values) - { - m.LogModules.Add(logModule, logModule.EventTypes.Values.Where(et => et.UsePersist).ToList()); - } - - return View(m); - } - - public virtual ActionResult TaskStatus(string id) - { - - if (string.IsNullOrEmpty(id)) - { - string sessionId; - do - { - System.Threading.Thread.Sleep(100); - sessionId = Disco.Services.Tasks.ScheduledTasks.GetTaskStatuses(typeof(Disco.BI.Interop.ActiveDirectory.ActiveDirectoryUpdateLastNetworkLogonDateJob)).Select(t => t.SessionId).FirstOrDefault(); - } while (sessionId == null); - - return View(new Models.Logging.TaskStatusModel() { SessionId = sessionId }); - } - else - { - var taskStatus = Disco.Services.Tasks.ScheduledTasks.GetTaskStatus(id); - return View(new Models.Logging.TaskStatusModel() { SessionId = taskStatus.SessionId }); - } - - } - - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using Disco.Services.Logging; +using Disco.Services.Logging.Models; + +namespace Disco.Web.Areas.Config.Controllers +{ + public partial class LoggingController : dbAdminController + { + // + // GET: /Config/Logs/ + + public virtual ActionResult Index() + { + var m = new Models.Logging.IndexModel() + { + LogModules = new Dictionary>() + }; + foreach (var logModule in LogContext.LogModules.Values) + { + m.LogModules.Add(logModule, logModule.EventTypes.Values.Where(et => et.UsePersist).ToList()); + } + + return View(m); + } + + public virtual ActionResult TaskStatus(string id) + { + if (string.IsNullOrEmpty(id)) + throw new ArgumentNullException("id", "A Task Status Id is required"); + + var taskStatus = Disco.Services.Tasks.ScheduledTasks.GetTaskStatus(id); + if (taskStatus == null) + return RedirectToAction(MVC.Config.Logging.Index()); + + return View(new Models.Logging.TaskStatusModel() { SessionId = taskStatus.SessionId }); + } + + } +} diff --git a/Disco.Web/Areas/Config/Controllers/PluginsController.cs b/Disco.Web/Areas/Config/Controllers/PluginsController.cs index 5d27c66b..32442b2c 100644 --- a/Disco.Web/Areas/Config/Controllers/PluginsController.cs +++ b/Disco.Web/Areas/Config/Controllers/PluginsController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Web; using System.Web.Mvc; using Disco.Services.Plugins; +using Disco.Services.Tasks; using Disco.Web.Areas.Config.Models.Plugins; namespace Disco.Web.Areas.Config.Controllers @@ -65,5 +66,27 @@ namespace Disco.Web.Areas.Config.Controllers } #endregion + public virtual ActionResult Install() + { + // Check for recent catalogue + + var catalogue = Plugins.LoadCatalogue(dbContext); + + if (catalogue == null || catalogue.ResponseTimestamp < DateTime.Now.AddMinutes(-15)) + { + // Need to Update Catalogue + return RedirectToAction(MVC.API.Plugin.UpdateLibraryCatalogue()); + } + else + { + var model = new Models.Plugins.InstallModel() + { + Catalogue = catalogue + }; + + return View(model); + } + } + } } diff --git a/Disco.Web/Areas/Config/Models/Plugins/InstallModel.cs b/Disco.Web/Areas/Config/Models/Plugins/InstallModel.cs new file mode 100644 index 00000000..8f59c960 --- /dev/null +++ b/Disco.Web/Areas/Config/Models/Plugins/InstallModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Disco.Models.BI.Interop.Community; + +namespace Disco.Web.Areas.Config.Models.Plugins +{ + public class InstallModel + { + public PluginLibraryUpdateResponse Catalogue { get; set; } + } +} \ No newline at end of file diff --git a/Disco.Web/Areas/Config/Models/Plugins/ProviderConfigurationViewModel.cs b/Disco.Web/Areas/Config/Models/Plugins/PluginConfigurationViewModel.cs similarity index 96% rename from Disco.Web/Areas/Config/Models/Plugins/ProviderConfigurationViewModel.cs rename to Disco.Web/Areas/Config/Models/Plugins/PluginConfigurationViewModel.cs index 22965677..2d599287 100644 --- a/Disco.Web/Areas/Config/Models/Plugins/ProviderConfigurationViewModel.cs +++ b/Disco.Web/Areas/Config/Models/Plugins/PluginConfigurationViewModel.cs @@ -1,23 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using Disco.Services.Plugins; - -namespace Disco.Web.Areas.Config.Models.Plugins -{ - public class PluginConfigurationViewModel - { - public PluginManifest Manifest { get; set; } - public Type PluginViewType { get; set; } - public object PluginViewModel { get; set; } - - public PluginConfigurationViewModel(PluginConfigurationHandler.PluginConfigurationHandlerGetResponse response) - { - this.Manifest = response.Manifest; - - this.PluginViewType = response.ViewType; - this.PluginViewModel = response.ViewModel; - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Disco.Services.Plugins; + +namespace Disco.Web.Areas.Config.Models.Plugins +{ + public class PluginConfigurationViewModel + { + public PluginManifest Manifest { get; set; } + public Type PluginViewType { get; set; } + public object PluginViewModel { get; set; } + + public PluginConfigurationViewModel(PluginConfigurationHandler.PluginConfigurationHandlerGetResponse response) + { + this.Manifest = response.Manifest; + + this.PluginViewType = response.ViewType; + this.PluginViewModel = response.ViewModel; + } + } } \ No newline at end of file diff --git a/Disco.Web/Areas/Config/Views/Expressions/Editor.cshtml b/Disco.Web/Areas/Config/Views/Expressions/Editor.cshtml index c682686a..d169b56b 100644 --- a/Disco.Web/Areas/Config/Views/Expressions/Editor.cshtml +++ b/Disco.Web/Areas/Config/Views/Expressions/Editor.cshtml @@ -1,52 +1,52 @@ -@model Disco.Web.Areas.Config.Models.Expressions.EditorModel -@{ - ViewBag.Title = Html.ToBreadcrumb("Configuration", MVC.Config.Config.Index(), "Expressions"); - Html.BundleDeferred("~/ClientScripts/Modules/Disco-ExpressionEditor"); -} -
-
-
-
-

- Parse Error:

-
-
-
- - @if (false) - { } - -
+@model Disco.Web.Areas.Config.Models.Expressions.EditorModel +@{ + ViewBag.Title = Html.ToBreadcrumb("Configuration", MVC.Config.Config.Index(), "Expressions"); + Html.BundleDeferred("~/ClientScripts/Modules/Disco-ExpressionEditor"); +} +
+
+
+
+

+ Parse Error:

+
+
+
+ +@* @if (false) + { }*@ + +
diff --git a/Disco.Web/Areas/Config/Views/Expressions/Editor.generated.cs b/Disco.Web/Areas/Config/Views/Expressions/Editor.generated.cs index 5d26562e..7e777b55 100644 --- a/Disco.Web/Areas/Config/Views/Expressions/Editor.generated.cs +++ b/Disco.Web/Areas/Config/Views/Expressions/Editor.generated.cs @@ -1,169 +1,141 @@ -#pragma warning disable 1591 -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.17929 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Disco.Web.Areas.Config.Views.Expressions -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net; - using System.Text; - using System.Web; - using System.Web.Helpers; - using System.Web.Mvc; - using System.Web.Mvc.Ajax; - using System.Web.Mvc.Html; - using System.Web.Routing; - using System.Web.Security; - using System.Web.UI; - using System.Web.WebPages; - using Disco.BI.Extensions; - using Disco.Models.Repository; - using Disco.Web; - using Disco.Web.Extensions; - - [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] - [System.Web.WebPages.PageVirtualPathAttribute("~/Areas/Config/Views/Expressions/Editor.cshtml")] - public class Editor : System.Web.Mvc.WebViewPage - { - public Editor() - { - } - public override void Execute() - { - - #line 2 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" - - ViewBag.Title = Html.ToBreadcrumb("Configuration", MVC.Config.Config.Index(), "Expressions"); - Html.BundleDeferred("~/ClientScripts/Modules/Disco-ExpressionEditor"); - - - #line default - #line hidden -WriteLiteral("\r\n\r\n \r\n \r\n \r\n

\r\n Parse Error:

\r\n \r\n \r\n \r\n \r\n Validate\r\n"); - - - #line 17 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" - - - #line default - #line hidden - - #line 17 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" - if (false) - { - - #line default - #line hidden -WriteLiteral(" "); - - - #line 18 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" - } - - - #line default - #line hidden -WriteLiteral(" \r\n $(function () {\r\n var initExpression = \'"); - - - #line 21 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" - Write(Model.Expression); - - - #line default - #line hidden -WriteLiteral("\';\r\n var hostSrcUrl = \'"); - - - #line 22 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" - Write(Links.ClientSource.Style.ExpressionEditor_htm); - - - #line default - #line hidden -WriteLiteral(@"'; - var host = $('