diff --git a/README.md b/README.md index 59a03be..43f059f 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,20 @@ For more information on contributing, please visit the [contributor guide](./CON ## Contributors: - [Akshay Hosur](https://github.com/akshay-online) + +## Web Application + +A new ASP.NET Core project (`ADOGenerator.Web`) exposes minimal API endpoints that leverage the existing services to create projects or generate template artifacts. This application can be deployed to Azure App Service. + +The web app also serves a simple UI that lets you choose a template and create projects. + +### Running locally + +```bash +cd src/ADOGenerator.Web +dotnet run +``` + +### Deploying to Azure + +Publish the project and deploy the generated output to an Azure App Service instance using the Azure portal or the `az webapp` CLI commands. diff --git a/src/ADOGenerator.Web/ADOGenerator.Web.csproj b/src/ADOGenerator.Web/ADOGenerator.Web.csproj new file mode 100644 index 0000000..479819a --- /dev/null +++ b/src/ADOGenerator.Web/ADOGenerator.Web.csproj @@ -0,0 +1,18 @@ + + + net8.0 + disable + enable + + + + + + + + + Templates\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + diff --git a/src/ADOGenerator.Web/Controllers/HomeController.cs b/src/ADOGenerator.Web/Controllers/HomeController.cs new file mode 100644 index 0000000..0b1e418 --- /dev/null +++ b/src/ADOGenerator.Web/Controllers/HomeController.cs @@ -0,0 +1,26 @@ +using ADOGenerator.IServices; +using ADOGenerator.Web.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using System.Linq; + +namespace ADOGenerator.Web.Controllers +{ + public class HomeController : Controller + { + private readonly ITemplateService _templateService; + + public HomeController(ITemplateService templateService) + { + _templateService = templateService; + } + + public IActionResult Index() + { + var vm = new ProjectViewModel(); + vm.TemplateOptions = _templateService.GetAvailableTemplates() + .Select(t => new SelectListItem { Text = t.Name, Value = t.TemplateFolder }); + return View(vm); + } + } +} diff --git a/src/ADOGenerator.Web/Controllers/ProjectsController.cs b/src/ADOGenerator.Web/Controllers/ProjectsController.cs new file mode 100644 index 0000000..608e7cd --- /dev/null +++ b/src/ADOGenerator.Web/Controllers/ProjectsController.cs @@ -0,0 +1,62 @@ +using ADOGenerator.IServices; +using ADOGenerator.Models; +using ADOGenerator.Web.ViewModels; +using Microsoft.AspNetCore.Mvc; + +namespace ADOGenerator.Web.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ProjectsController : Controller + { + private readonly IProjectService _projectService; + private readonly ITemplateService _templateService; + + public ProjectsController(IProjectService projectService, ITemplateService templateService) + { + _projectService = projectService; + _templateService = templateService; + } + + [HttpPost("create")] + public IActionResult Create([FromBody] ProjectViewModel request) + { + var model = new Project + { + id = Guid.NewGuid().ToString(), + accountName = request.Organization, + accessToken = request.AccessToken, + adoAuthScheme = request.AuthScheme, + ProjectName = request.ProjectName, + selectedTemplateFolder = request.TemplateFolder, + TemplateName = request.TemplateName, + isExtensionNeeded = request.InstallExtensions, + isAgreeTerms = request.InstallExtensions + }; + bool created = _projectService.CreateProjectEnvironment(model); + return Ok(new { Success = created }); + } + + [HttpPost("artifacts")] + public IActionResult Artifacts([FromBody] ArtifactRequest request) + { + var model = new Project + { + id = Guid.NewGuid().ToString(), + accountName = request.Organization, + ProjectName = request.ProjectName, + ProjectId = request.ProjectId, + accessToken = request.AccessToken, + adoAuthScheme = request.AuthScheme + }; + var result = _templateService.GenerateTemplateArtifacts(model); + if (result.Item1) + { + return Ok(new { Template = result.Item2, Location = result.Item3 }); + } + return BadRequest(new { Message = "Artifact generation failed" }); + } + } + + public record ArtifactRequest(string Organization, string ProjectName, string ProjectId, string AccessToken, string AuthScheme); +} diff --git a/src/ADOGenerator.Web/Controllers/TemplatesController.cs b/src/ADOGenerator.Web/Controllers/TemplatesController.cs new file mode 100644 index 0000000..aa5acf9 --- /dev/null +++ b/src/ADOGenerator.Web/Controllers/TemplatesController.cs @@ -0,0 +1,25 @@ +using ADOGenerator.IServices; +using Microsoft.AspNetCore.Mvc; +using System.Linq; + +namespace ADOGenerator.Web.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TemplatesController : Controller + { + private readonly ITemplateService _templateService; + public TemplatesController(ITemplateService templateService) + { + _templateService = templateService; + } + + [HttpGet] + public IActionResult Get() + { + var list = _templateService.GetAvailableTemplates() + .Select(t => new { t.Name, t.TemplateFolder, t.Description }); + return Ok(list); + } + } +} diff --git a/src/ADOGenerator.Web/Program.cs b/src/ADOGenerator.Web/Program.cs new file mode 100644 index 0000000..9216645 --- /dev/null +++ b/src/ADOGenerator.Web/Program.cs @@ -0,0 +1,35 @@ +using ADOGenerator.IServices; +using ADOGenerator.Models; +using ADOGenerator.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add configuration and services +builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddControllersWithViews(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.UseStaticFiles(); +app.UseRouting(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseAuthorization(); + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + +app.Run(); diff --git a/src/ADOGenerator.Web/ViewModels/ProjectViewModel.cs b/src/ADOGenerator.Web/ViewModels/ProjectViewModel.cs new file mode 100644 index 0000000..e0b28e0 --- /dev/null +++ b/src/ADOGenerator.Web/ViewModels/ProjectViewModel.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using System.Collections.Generic; + +namespace ADOGenerator.Web.ViewModels +{ + public class ProjectViewModel + { + public string Organization { get; set; } + public string ProjectName { get; set; } + public string TemplateName { get; set; } + public string TemplateFolder { get; set; } + public string AccessToken { get; set; } + public string AuthScheme { get; set; } = "pat"; + public bool InstallExtensions { get; set; } = true; + public IEnumerable TemplateOptions { get; set; } = new List(); + public string ResultMessage { get; set; } + } +} diff --git a/src/ADOGenerator.Web/Views/Home/Index.cshtml b/src/ADOGenerator.Web/Views/Home/Index.cshtml new file mode 100644 index 0000000..8d4462e --- /dev/null +++ b/src/ADOGenerator.Web/Views/Home/Index.cshtml @@ -0,0 +1,42 @@ +@model ADOGenerator.Web.ViewModels.ProjectViewModel + + + + + ADO Generator + + + +

Create Azure DevOps Project

+
+ + + + + + + +
+@if (Model.ResultMessage != null) +{ +
@Model.ResultMessage
+} + + diff --git a/src/ADOGenerator.Web/Views/_ViewImports.cshtml b/src/ADOGenerator.Web/Views/_ViewImports.cshtml new file mode 100644 index 0000000..a757b41 --- /dev/null +++ b/src/ADOGenerator.Web/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/ADOGenerator.Web/Views/_ViewStart.cshtml b/src/ADOGenerator.Web/Views/_ViewStart.cshtml new file mode 100644 index 0000000..9d06493 --- /dev/null +++ b/src/ADOGenerator.Web/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = null; +} diff --git a/src/ADOGenerator.Web/appsettings.json b/src/ADOGenerator.Web/appsettings.json new file mode 100644 index 0000000..ec5d773 --- /dev/null +++ b/src/ADOGenerator.Web/appsettings.json @@ -0,0 +1,46 @@ +{ + "AppSettings": { + "webpages:Version": "2.0.0.0", + "webpages:Enabled": "false", + "PreserveLoginUrl": "true", + "ClientValidationEnabled": "true", + "UnobtrusiveJavaScriptEnabled": "true", + "ProjectCreationVersion": "4.1", + "ProjectPropertyVersion": "4.1-preview", + "RepoVersion": "4.1", + "BuildVersion": "4.1", + "ReleaseVersion": "4.1", + "WikiVersion": "4.1", + "BoardVersion": "4.1", + "WorkItemsVersion": "4.1", + "QueriesVersion": "4.1", + "EndPointVersion": "4.1-preview.1", + "ExtensionVersion": "4.1", + "DashboardVersion": "4.1-preview.2", + "AgentQueueVersion": "4.1-preview", + "GetSourceCodeVersion": "4.1-preview", + "TestPlanVersion": "5.0", + "DefaultHost": "https://dev.azure.com/", + "ReleaseHost": "https://vsrm.dev.azure.com/", + "ExtensionHost": "https://extmgmt.dev.azure.com/", + "GetRelease": "4.1-preview.3", + "BaseAddress": "https://app.vssps.visualstudio.com/", + "GraphAPIHost": "https://vssps.dev.azure.com/", + "ProjectProperties": "4.1-preview.1", + "DefaultTemplate": "SmartHotel360", + "DeloymentGroup": "4.1-preview.1", + "GraphApiVersion": "4.1-preview.1", + "VariableGroupsApiVersion": "5.0-preview.1", + "AnalyticsKey": "", + "clientId": "", + "tenantId": "", + "scopes": "" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "System": "Warning", + "Microsoft": "Warning" + } + } +} diff --git a/src/ADOGenerator/IServices/ITemplateService.cs b/src/ADOGenerator/IServices/ITemplateService.cs index 4e497d7..59c822f 100644 --- a/src/ADOGenerator/IServices/ITemplateService.cs +++ b/src/ADOGenerator/IServices/ITemplateService.cs @@ -7,5 +7,6 @@ public interface ITemplateService bool AnalyzeProject(Project model); bool CheckTemplateExists(Project model); (bool,string,string) GenerateTemplateArtifacts(Project model); + IEnumerable GetAvailableTemplates(); } } diff --git a/src/ADOGenerator/Services/ProjectService.cs b/src/ADOGenerator/Services/ProjectService.cs index dab8f5a..c14ecdb 100644 --- a/src/ADOGenerator/Services/ProjectService.cs +++ b/src/ADOGenerator/Services/ProjectService.cs @@ -183,7 +183,7 @@ public async Task> SelectProject(string accessToken, HttpResponseMe public string GetJsonFilePath(bool IsPrivate, string TemplateFolder, string TemplateName, string FileName = "") { string filePath = string.Empty; - filePath = string.Format(Path.Combine(Directory.GetCurrentDirectory(), "Templates", TemplateName, FileName)); + filePath = Path.Combine(AppContext.BaseDirectory, "Templates", TemplateName, FileName); return filePath; } @@ -366,7 +366,7 @@ public bool CreateProjectEnvironment(Project model) Console.WriteLine("Error in reading project template file:" + ex.Message); } //create team project - string jsonProject = model.ReadJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "Templates", "CreateProject.json")); + string jsonProject = model.ReadJsonFile(Path.Combine(AppContext.BaseDirectory, "Templates", "CreateProject.json")); jsonProject = jsonProject.Replace("$projectName$", model.ProjectName).Replace("$processTemplateId$", processTemplateId); Projects proj = new Projects(_projectCreationVersion); @@ -2260,7 +2260,7 @@ void CreateQueryAndWidgets(Project model, List listQueries, ADOConfigura bool isFolderCreated = false; if (!string.IsNullOrEmpty(teamName)) { - string createQueryFolderJson = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "PreSetting", "CreateQueryFolder.json")); + string createQueryFolderJson = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "PreSetting", "CreateQueryFolder.json")); createQueryFolderJson = createQueryFolderJson.Replace("$TeamName$", teamName); QueryResponse createFolderResponse = _newobjQuery.CreateQuery(model.ProjectName, createQueryFolderJson); isFolderCreated = createFolderResponse.id != null ? true : false; @@ -2787,7 +2787,7 @@ string GetTemplateMessage(string TemplateName) { string groupDetails = ""; TemplateSelection.Templates templates = new TemplateSelection.Templates(); - string templatesPath = ""; templatesPath = Path.Combine(Directory.GetCurrentDirectory(), "Templates"); + string templatesPath = Path.Combine(AppContext.BaseDirectory, "Templates"); if (File.Exists(templatesPath + "TemplateSetting.json")) { groupDetails = File.ReadAllText(templatesPath + "\\TemplateSetting.json"); @@ -2889,7 +2889,7 @@ void CreateVaribaleGroups(Project model, ADOConfiguration _variableGroups) public bool WhereDoseTemplateBelongTo(string templatName) { - string privatePath = Path.Combine(Directory.GetCurrentDirectory(), "PrivateTemplates"); + string privatePath = Path.Combine(AppContext.BaseDirectory, "PrivateTemplates"); string privateTemplate = Path.Combine(privatePath, templatName); if (!Directory.Exists(privatePath)) diff --git a/src/ADOGenerator/Services/TemplateService.cs b/src/ADOGenerator/Services/TemplateService.cs index 608c8b3..36eaeb7 100644 --- a/src/ADOGenerator/Services/TemplateService.cs +++ b/src/ADOGenerator/Services/TemplateService.cs @@ -2,10 +2,13 @@ using ADOGenerator.Models; using ADOGenerator.Services; using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; using RestAPI.Extractor; using RestAPI.ProjectsAndTeams; using RestAPI; using ADOGenerator; +using System.IO; +using System.Linq; public class TemplateService : ITemplateService { @@ -101,6 +104,24 @@ public bool CheckTemplateExists(Project model) } } + public IEnumerable GetAvailableTemplates() + { + var templatesPath = Path.Combine(AppContext.BaseDirectory, "Templates", "TemplateSetting.json"); + if (!File.Exists(templatesPath)) + { + return Enumerable.Empty(); + } + + var json = File.ReadAllText(templatesPath); + var parsed = JsonConvert.DeserializeObject(json); + if (parsed?.GroupwiseTemplates == null) + { + return Enumerable.Empty(); + } + + return parsed.GroupwiseTemplates.SelectMany(g => g.Template); + } + private void LogAnalysisResults(Project model, ExtractorAnalysis analysis) { model.id.AddMessage(Environment.NewLine + "-------------------------------------------------------------------");