├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── BUILD.md ├── LICENSE ├── LibraryStoreMigration ├── LibraryStoreMigration.csproj └── Program.cs ├── README.md ├── SECURITY.md ├── ServerlessLibrary.sln ├── ServerlessLibraryAPI ├── .gitignore ├── CacheService.cs ├── ClientApp │ ├── .env.development │ ├── .env.production │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── actions │ │ ├── actionTypes.js │ │ ├── sampleActions.js │ │ └── userActions.js │ │ ├── assets │ │ ├── contribution.svg │ │ ├── functionapp.svg │ │ ├── github.svg │ │ └── logicapp.svg │ │ ├── components │ │ ├── ContentHeader │ │ │ ├── ContentHeader.css │ │ │ └── ContentHeader.js │ │ ├── Contribute │ │ │ ├── Contribute.js │ │ │ ├── Contribute.scss │ │ │ ├── ContributionForm.js │ │ │ ├── ContributionForm.styles.js │ │ │ ├── ContributionsList.js │ │ │ └── SignInDialog.js │ │ ├── DetailView │ │ │ ├── ActionBar.js │ │ │ ├── ActionBar.scss │ │ │ ├── DetailPageContent.js │ │ │ ├── DetailPageContent.scss │ │ │ ├── DetailPageHeader.js │ │ │ └── DetailView.js │ │ ├── Header │ │ │ ├── AuthControl.js │ │ │ ├── Header.js │ │ │ ├── Header.scss │ │ │ └── UserPersona.js │ │ ├── ItemList │ │ │ ├── ItemList.js │ │ │ └── ItemList.scss │ │ ├── ItemTags │ │ │ ├── ItemTags.css │ │ │ └── ItemTags.js │ │ ├── Main │ │ │ ├── Main.css │ │ │ └── Main.js │ │ ├── MetricBar │ │ │ ├── MetricBar.css │ │ │ └── MetricBar.js │ │ ├── shared │ │ │ ├── Button.styles.js │ │ │ ├── Constants.js │ │ │ ├── PageHeaderWithBackButton.js │ │ │ ├── PageHeaderWithBackButton.scss │ │ │ └── SignInButton.js │ │ └── sidebar │ │ │ ├── SideBar.css │ │ │ └── SideBar.js │ │ ├── helpers │ │ ├── appinsights.js │ │ ├── handle-response.js │ │ ├── index.js │ │ ├── query-param.js │ │ ├── registerIcons.js │ │ └── registerIcons.scss │ │ ├── index.css │ │ ├── index.js │ │ ├── reducers │ │ ├── authenticationReducer.js │ │ ├── index.js │ │ ├── initialState.js │ │ └── sampleReducer.js │ │ └── services │ │ ├── github.service.js │ │ ├── index.js │ │ ├── library.service.js │ │ └── user.service.js ├── Connected Services │ └── Application Insights │ │ └── ConnectedService.json ├── Controllers │ ├── LibraryController.cs │ ├── MetricsController.cs │ └── UsersController.cs ├── CosmosLibraryStore.cs ├── ILibraryStore.cs ├── Models │ ├── GitHubUser.cs │ ├── LibraryItem.cs │ └── SentimentPayload.cs ├── OAuth.GitHub │ ├── GitHubAuthenticationConstants.cs │ ├── GitHubAuthenticationDefaults.cs │ ├── GitHubAuthenticationHandler.cs │ └── GitHubAuthenticationOptions.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ └── _ViewImports.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── ServerlessLibraryAPI.csproj ├── ServerlessLibraryAPI.csproj.user ├── ServerlessLibrarySettings.cs ├── Startup.cs ├── StorageHelper.cs ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ └── items.json ├── ServerlessLibraryFunctionApp ├── .gitignore ├── Properties │ └── PublishProfiles │ │ └── slfunctionapp - Web Deploy.pubxml ├── ServerLessLibraryFunctionApp.csproj ├── UpdateCounts.cs └── host.json └── ServerlessLibraryLogicApp ├── Deploy-AzureResourceGroup.ps1 ├── Deployment.targets ├── LogicApp.json ├── LogicApp.parameters.json └── ServerlessLibraryLogicApp.deployproj /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | If you'd like to submit a Repo to the Azure Serverless Community Library, fill out the following template: 2 | 3 | - Title: What you'd like your sample to be called. 4 | ________ 5 | 6 | - Description: A short block of text (~100 characters max) explaining what your sample does. 7 | ________ 8 | 9 | - Template: The raw path to the ARM template which deploys the application. eg https://raw.githubusercontent.com/anthonychu/azure-functions-openalpr/master/azuredeploy.json For more information, see the [README](https://github.com/Azure/FunctionLibrary/blob/master/README.md). 10 | ________ 11 | 12 | - Repository: The URL of a public git repository. 13 | ________ 14 | 15 | - Language (optional): The language your code is written in (Functions only.) 16 | ________ 17 | 18 | - Technology (optional): Whether your app is a Function App or a Logic App 19 | ________ 20 | 21 | - Solution Area (optional): A broad level categorization of what type of solution you've built (IoT, ML, Data Processing, ...) 22 | ________ 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/ServerlessLibraryAPI/bin/Debug/netcoreapp2.0/ServerlessLibraryAPI.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/ServerlessLibraryAPI", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start ${auto-detect-url}" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceFolder}/Views" 37 | } 38 | }, 39 | { 40 | "name": ".NET Core Attach", 41 | "type": "coreclr", 42 | "request": "attach", 43 | "processId": "${command:pickProcess}" 44 | } 45 | ,] 46 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/ServerlessLibraryAPI/ServerlessLibraryAPI.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # serverless-samples 2 | 3 | ``` cd ServerlessLibraryUI 4 | yarn install 5 | rem yarn serve (to debug locally) 6 | yarn build 7 | ``` 8 | copy dist folder over to ServerlessLibraryAPI\wwwroot 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Microsoft Azure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LibraryStoreMigration/LibraryStoreMigration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LibraryStoreMigration/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.IO; 4 | using ServerlessLibrary.Models; 5 | using System.Collections.Generic; 6 | using Newtonsoft.Json; 7 | using ServerlessLibrary; 8 | using Microsoft.WindowsAzure.Storage.Table; 9 | using Microsoft.WindowsAzure.Storage; 10 | using Microsoft.WindowsAzure.Storage.RetryPolicies; 11 | using System.Threading.Tasks; 12 | using System.Linq; 13 | 14 | namespace LibraryStoreMigration 15 | { 16 | class Program 17 | { 18 | const string tableName = "slitemstats"; 19 | static void Main(string[] args) 20 | { 21 | if (args.Length < 1) 22 | { 23 | Console.WriteLine("Please enter specify operation to be performed (cosmosdb|stats)"); 24 | Console.WriteLine("Please note that connection informations need to be provided as environment variables."); 25 | return; 26 | } 27 | 28 | if (args[0].Equals("cosmosdb", StringComparison.OrdinalIgnoreCase)) 29 | { 30 | MigrateToCosmosDB(); 31 | } 32 | 33 | if (args[0].Equals("stats", StringComparison.OrdinalIgnoreCase)) 34 | { 35 | AddNewStatsColumns(); 36 | } 37 | } 38 | 39 | public static void AddNewStatsColumns() 40 | { 41 | TableRequestOptions tableRequestRetry = new TableRequestOptions { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(2), 3) }; 42 | TableQuery query = new TableQuery(); 43 | TableContinuationToken continuationToken = null; 44 | List entities = new List(); 45 | CloudStorageAccount storageAccount = CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString); 46 | 47 | CloudTableClient tableClient = storageAccount.CreateCloudTableClient(); 48 | CloudTable table = tableClient.GetTableReference(tableName); 49 | var opContext = new OperationContext(); 50 | do 51 | { 52 | TableQuerySegment queryResults = (table).ExecuteQuerySegmentedAsync(query, continuationToken, tableRequestRetry, opContext).Result; 53 | continuationToken = queryResults.ContinuationToken; 54 | entities.AddRange(queryResults.Results); 55 | } while (continuationToken != null); 56 | 57 | Console.WriteLine(entities.Count); 58 | List libraryItems = GetAllLibraryItemsFromFile(); 59 | foreach (var entity in entities) 60 | { 61 | entity.id = libraryItems.FirstOrDefault(l => l.Template == entity.template).Id; 62 | entity.likes = 0; 63 | entity.dislikes = 0; 64 | TableOperation operation = TableOperation.InsertOrMerge(entity); 65 | Task r = table.ExecuteAsync(operation); 66 | TableResult a = r.Result; 67 | } 68 | } 69 | 70 | public static void MigrateToCosmosDB() 71 | { 72 | var libraryItems = GetAllLibraryItemsFromFile(); 73 | Console.WriteLine("Number of samples to be migrated from file to cosmos db: {0}", libraryItems.Count); 74 | CosmosLibraryStore libraryStore = new CosmosLibraryStore(); 75 | 76 | IList libraryItemsInCosmos = libraryStore.GetAllItems().Result; 77 | Console.WriteLine("Number of samples already present in cosmos db: {0}", libraryItemsInCosmos.Count); 78 | 79 | if (libraryItemsInCosmos.Count != libraryItems.Count) 80 | { 81 | foreach (LibraryItem libraryItem in libraryItems) 82 | { 83 | if (!libraryItemsInCosmos.Any(c => c.Id == libraryItem.Id)) 84 | { 85 | Console.WriteLine("Item {0} not present in cosmos db. will be migrated" + libraryItem.Id); 86 | try 87 | { 88 | libraryStore.Add(libraryItem).Wait(); 89 | Console.WriteLine("Migrated sample with id {0}" + libraryItem.Id); 90 | } 91 | catch (Exception ex) 92 | { 93 | Console.WriteLine("Got exception {0}", ex); 94 | throw; 95 | } 96 | } 97 | } 98 | 99 | Console.WriteLine("Samples are successfully migrated to cosmos db"); 100 | } 101 | else 102 | { 103 | Console.WriteLine("Samples are already migrated to cosmos db"); 104 | } 105 | } 106 | 107 | public static List GetAllLibraryItemsFromFile() 108 | { 109 | var assembly = Assembly.GetExecutingAssembly(); 110 | using (Stream stream = assembly.GetManifestResourceStream("LibraryStoreMigration.items.json")) 111 | using (StreamReader reader = new StreamReader(stream)) 112 | { 113 | string result = reader.ReadToEnd(); 114 | return JsonConvert.DeserializeObject>(result); 115 | } 116 | } 117 | } 118 | 119 | public class OldSLItemStats : TableEntity 120 | { 121 | public string template { get; set; } 122 | public int totalDownloads { get; set; } 123 | public int downloadsToday { get; set; } 124 | public int downloadsThisWeek { get; set; } 125 | public int downloadsThisMonth { get; set; } 126 | public DateTime lastUpdated { get; set; } 127 | } 128 | 129 | public class NewSLItemStats : OldSLItemStats 130 | { 131 | public string id { get; set; } 132 | public int likes { get; set; } 133 | public int dislikes { get; set; } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Serverless Community Library 2 | A gallery of samples that people have built with serverless technology on Azure. 3 | 4 | Visit the site [here!](https://www.serverlesslibrary.net/) 5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /ServerlessLibrary.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2037 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessLibraryFunctionApp", "ServerlessLibraryFunctionApp\ServerlessLibraryFunctionApp.csproj", "{95E9DC49-8B38-4D2C-95AD-1BD261A1CBF0}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessLibraryAPI", "ServerlessLibraryAPI\ServerlessLibraryAPI.csproj", "{9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}" 9 | EndProject 10 | Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "ServerlessLibraryLogicApp", "ServerlessLibraryLogicApp\ServerlessLibraryLogicApp.deployproj", "{92DD9A5D-3A93-493C-8F69-23A1C519A1C4}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {95E9DC49-8B38-4D2C-95AD-1BD261A1CBF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {95E9DC49-8B38-4D2C-95AD-1BD261A1CBF0}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {95E9DC49-8B38-4D2C-95AD-1BD261A1CBF0}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {95E9DC49-8B38-4D2C-95AD-1BD261A1CBF0}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {92DD9A5D-3A93-493C-8F69-23A1C519A1C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {92DD9A5D-3A93-493C-8F69-23A1C519A1C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {92DD9A5D-3A93-493C-8F69-23A1C519A1C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {92DD9A5D-3A93-493C-8F69-23A1C519A1C4}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {972ED417-3343-4F74-BD0C-C6BB7515C695} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/.gitignore: -------------------------------------------------------------------------------- 1 | /Properties/launchSettings.json 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | build/ 23 | bld/ 24 | bin/ 25 | Bin/ 26 | obj/ 27 | Obj/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | /wwwroot/dist/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opendb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | *.sap 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | 154 | # Microsoft Azure Build Output 155 | csx/ 156 | *.build.csdef 157 | 158 | # Microsoft Azure Emulator 159 | ecf/ 160 | rcf/ 161 | 162 | # Microsoft Azure ApplicationInsights config file 163 | ApplicationInsights.config 164 | 165 | # Windows Store app package directory 166 | AppPackages/ 167 | BundleArtifacts/ 168 | 169 | # Visual Studio cache files 170 | # files ending in .cache can be ignored 171 | *.[Cc]ache 172 | # but keep track of directories ending in .cache 173 | !*.[Cc]ache/ 174 | 175 | # Others 176 | ClientBin/ 177 | ~$* 178 | *~ 179 | *.dbmdl 180 | *.dbproj.schemaview 181 | *.pfx 182 | *.publishsettings 183 | orleans.codegen.cs 184 | 185 | /node_modules 186 | 187 | # RIA/Silverlight projects 188 | Generated_Code/ 189 | 190 | # Backup & report files from converting an old project file 191 | # to a newer Visual Studio version. Backup files are not needed, 192 | # because we have git ;-) 193 | _UpgradeReport_Files/ 194 | Backup*/ 195 | UpgradeLog*.XML 196 | UpgradeLog*.htm 197 | 198 | # SQL Server files 199 | *.mdf 200 | *.ldf 201 | 202 | # Business Intelligence projects 203 | *.rdl.data 204 | *.bim.layout 205 | *.bim_*.settings 206 | 207 | # Microsoft Fakes 208 | FakesAssemblies/ 209 | 210 | # GhostDoc plugin setting file 211 | *.GhostDoc.xml 212 | 213 | # Node.js Tools for Visual Studio 214 | .ntvs_analysis.dat 215 | 216 | # Visual Studio 6 build log 217 | *.plg 218 | 219 | # Visual Studio 6 workspace options file 220 | *.opt 221 | 222 | # Visual Studio LightSwitch build output 223 | **/*.HTMLClient/GeneratedArtifacts 224 | **/*.DesktopClient/GeneratedArtifacts 225 | **/*.DesktopClient/ModelManifest.xml 226 | **/*.Server/GeneratedArtifacts 227 | **/*.Server/ModelManifest.xml 228 | _Pvt_Extensions 229 | 230 | # Paket dependency manager 231 | .paket/paket.exe 232 | 233 | # FAKE - F# Make 234 | .fake/ 235 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/CacheService.cs: -------------------------------------------------------------------------------- 1 | using ServerlessLibrary.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Caching.Memory; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace ServerlessLibrary 11 | { 12 | public interface ICacheService 13 | { 14 | IList GetCachedItems(); 15 | } 16 | 17 | //https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core 18 | public class CacheService : ICacheService 19 | { 20 | protected readonly IMemoryCache _cache; 21 | private readonly ILibraryStore libraryStore; 22 | private readonly ILogger logger; 23 | 24 | private Task LoadingTask = Task.CompletedTask; 25 | private Timer Timer = null; 26 | private bool LoadingBusy = false; 27 | private bool isCacheLoadedOnce = false; 28 | 29 | public CacheService(IMemoryCache cache, ILibraryStore libraryStore, ILogger logger) 30 | { 31 | this._cache = cache; 32 | this.libraryStore = libraryStore; 33 | this.logger = logger; 34 | InitTimer(); 35 | } 36 | 37 | private void InitTimer() 38 | { 39 | _cache.Set(ServerlessLibrarySettings.CACHE_ENTRY, new LibraryItemsResult() { Result = new List(), IsBusy = true }); 40 | 41 | Timer = new Timer(TimerTickAsync, null, 1000, ServerlessLibrarySettings.SLCacheRefreshIntervalInSeconds * 1000); 42 | } 43 | 44 | public IList GetCachedItems() 45 | { 46 | // Make a blocking call to load cache on first time call. 47 | if (!isCacheLoadedOnce) 48 | { 49 | try 50 | { 51 | logger.LogInformation("Loading initial cache"); 52 | IList items = this.ConstructCache().Result; 53 | _cache.Set(ServerlessLibrarySettings.CACHE_ENTRY, new LibraryItemsResult() { Result = items, IsBusy = false }); 54 | logger.LogInformation("Loaded {0} items into cache", items.Count()); 55 | } 56 | catch (Exception ex) 57 | { 58 | this.logger.LogError(ex, "Failed to load cache in first call"); 59 | } 60 | } 61 | 62 | logger.LogInformation("Successfully loaded initial cache"); 63 | isCacheLoadedOnce = true; 64 | return _cache.Get(ServerlessLibrarySettings.CACHE_ENTRY).Result; 65 | } 66 | 67 | private async void TimerTickAsync(object state) 68 | { 69 | logger.LogInformation("Cache refresh timer fired"); 70 | if (!isCacheLoadedOnce || LoadingBusy) 71 | { 72 | logger.LogWarning("Skipping cache refresh"); 73 | return; 74 | } 75 | 76 | try 77 | { 78 | LoadingBusy = true; 79 | LoadingTask = LoadCaches(); 80 | await LoadingTask; 81 | } 82 | catch 83 | { 84 | // do not crash the app 85 | } 86 | finally 87 | { 88 | LoadingBusy = false; 89 | } 90 | } 91 | private async Task LoadCaches() 92 | { 93 | try 94 | { 95 | logger.LogInformation("Starting cache refresh"); 96 | var items = await ConstructCache(); 97 | _cache.Set(ServerlessLibrarySettings.CACHE_ENTRY, new LibraryItemsResult() { Result = items, IsBusy = false }); 98 | logger.LogInformation("Updated cache with {0} items", items.Count()); 99 | } 100 | catch (Exception ex) 101 | { 102 | this.logger.LogError(ex, "Failed to load cache"); 103 | } 104 | } 105 | private async Task> ConstructCache() 106 | { 107 | logger.LogInformation("Starting ConstructCache"); 108 | IList libraryItems; 109 | IList libraryItemsWithStats = new List(); 110 | libraryItems = await this.libraryStore.GetAllItems(); 111 | logger.LogInformation("Cosmos DB returned {0} results", libraryItems.Count()); 112 | var stats = await StorageHelper.getSLItemRecordsAsync(); 113 | logger.LogInformation("Storage returned {0} results", stats.Count()); 114 | foreach (var storeItem in libraryItems) 115 | { 116 | var item = storeItem.ConvertTo(); 117 | var itemStat = stats.Where(s => s.id == storeItem.Id.ToString()).FirstOrDefault(); 118 | item.TotalDownloads = itemStat != null && itemStat.totalDownloads > 0 ? itemStat.totalDownloads : 1; 119 | item.Likes = itemStat != null && itemStat.likes > 0 ? itemStat.likes : 0; 120 | item.Dislikes = itemStat != null && itemStat.dislikes > 0 ? itemStat.dislikes : 0; 121 | libraryItemsWithStats.Add(item); 122 | } 123 | 124 | logger.LogInformation("ConstructCache returned {0} items", libraryItemsWithStats.Count()); 125 | return libraryItemsWithStats; 126 | } 127 | } 128 | 129 | public class LibraryItemsResult 130 | { 131 | public IList Result { get; set; } 132 | public bool IsBusy { get; set; } 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_INSTRUMENTATION_KEY= -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_INSTRUMENTATION_KEY=d35b5caf-a276-467c-9ac7-f7f7d84ea171 -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ServerlessLibraryAPI", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@uifabric/styling": "^6.47.6", 7 | "markdown-to-jsx": "^6.11.4", 8 | "office-ui-fabric-react": "^6.187.0", 9 | "react": "^16.8.6", 10 | "react-app-polyfill": "^1.0.1", 11 | "react-dom": "^16.8.6", 12 | "react-redux": "^6.0.1", 13 | "react-router-dom": "^4.3.1", 14 | "redux": "^4.0.1", 15 | "rimraf": "^2.6.3" 16 | }, 17 | "devDependencies": { 18 | "react-scripts": "^5.0.1" 19 | }, 20 | "scripts": { 21 | "start": "rimraf ./build && react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "ie 11", 32 | "not dead", 33 | "not op_mini all" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/ServerlessLibrary/22d6af9182763804124758a955b560162d393a6a/ServerlessLibraryAPI/ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 14 | 15 | 24 | Azure Serverless Community Library 25 | 37 | 38 | 39 | 42 |
43 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ServerlessLibraryAPI", 3 | "name": "ServerlessLibraryAPI", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/App.css: -------------------------------------------------------------------------------- 1 | /* #container { background: blue; } */ 2 | #header { 3 | height: 40px; 4 | } 5 | #main { 6 | height: calc(100% - 40px); 7 | } 8 | #container { 9 | overflow-y: auto; 10 | } 11 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Switch, Route, withRouter } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import { Dialog } from "office-ui-fabric-react"; 5 | 6 | import "./App.css"; 7 | 8 | import Main from "./components/Main/Main"; 9 | import Header from "./components/Header/Header"; 10 | import DetailView from "./components/DetailView/DetailView"; 11 | import ContributionsPage from "./components/Contribute/Contribute"; 12 | import { sampleActions } from "./actions/sampleActions"; 13 | import { userActions } from "./actions/userActions"; 14 | import { libraryService, userService } from "./services"; 15 | 16 | const loginErrorMsg = "We were unable to log you in. Please try again later."; 17 | 18 | class App extends Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | showErrorDialog: false 24 | }; 25 | this.onDismissErrorDialog = this.onDismissErrorDialog.bind(this); 26 | } 27 | 28 | componentDidMount() { 29 | libraryService 30 | .getAllSamples() 31 | .then(samples => this.props.getSamplesSuccess(samples)) 32 | .catch(() => { 33 | // do nothing 34 | }); 35 | 36 | this.props.getCurrentUserRequest(); 37 | userService 38 | .getCurrentUser() 39 | .then(user => this.props.getCurrentUserSuccess(user)) 40 | .catch(data => { 41 | this.props.getCurrentUserFailure(); 42 | if (data.status !== 401) { 43 | this.setState({ 44 | showErrorDialog: true 45 | }); 46 | } 47 | }); 48 | } 49 | 50 | onDismissErrorDialog() { 51 | this.setState({ 52 | showErrorDialog: false 53 | }); 54 | } 55 | 56 | render() { 57 | const { showErrorDialog } = this.state; 58 | return ( 59 |
60 | 63 |
64 | 65 | 66 | 67 | 68 | 69 |
70 | {showErrorDialog && ( 71 | 80 | )} 81 |
82 | ); 83 | } 84 | } 85 | 86 | function mapStateToProps(state) { 87 | return {}; 88 | } 89 | 90 | const mapDispatchToProps = { 91 | getSamplesSuccess: sampleActions.getSamplesSuccess, 92 | getCurrentUserRequest: userActions.getCurrentUserRequest, 93 | getCurrentUserSuccess: userActions.getCurrentUserSuccess, 94 | getCurrentUserFailure: userActions.getCurrentUserFailure 95 | }; 96 | const AppContainer = withRouter( 97 | connect( 98 | mapStateToProps, 99 | mapDispatchToProps 100 | )(App) 101 | ); 102 | 103 | export default AppContainer; 104 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const sampleActionTypes = { 2 | GETSAMPLES_SUCCESS: "GETSAMPLES_SUCCESS" 3 | }; 4 | 5 | export const userActionTypes = { 6 | GETCURRENTUSER_REQUEST: "GETCURRENTUSER_REQUEST", 7 | GETCURRENTUSER_SUCCESS: "GETCURRENTUSER_SUCCESS", 8 | GETCURRENTUSER_FAILURE: "GETCURRENTUSER_FAILURE", 9 | LOGOUT: "LOGOUT" 10 | }; 11 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/actions/sampleActions.js: -------------------------------------------------------------------------------- 1 | import { sampleActionTypes } from "./actionTypes"; 2 | 3 | export const sampleActions = { 4 | getSamplesSuccess 5 | }; 6 | 7 | function getSamplesSuccess(samples) { 8 | return { 9 | type: sampleActionTypes.GETSAMPLES_SUCCESS, 10 | samples 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import { userActionTypes } from "./actionTypes"; 2 | 3 | export const userActions = { 4 | getCurrentUserRequest, 5 | getCurrentUserSuccess, 6 | getCurrentUserFailure, 7 | logout 8 | }; 9 | 10 | function getCurrentUserRequest(user) { 11 | return { 12 | type: userActionTypes.GETCURRENTUSER_REQUEST, 13 | user 14 | }; 15 | } 16 | 17 | function getCurrentUserSuccess(user) { 18 | return { 19 | type: userActionTypes.GETCURRENTUSER_SUCCESS, 20 | user 21 | }; 22 | } 23 | 24 | function getCurrentUserFailure(user) { 25 | return { 26 | type: userActionTypes.GETCURRENTUSER_FAILURE, 27 | user 28 | }; 29 | } 30 | 31 | function logout() { 32 | return { 33 | type: userActionTypes.LOGOUT 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/assets/contribution.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/assets/functionapp.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 17 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/assets/logicapp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/ContentHeader/ContentHeader.css: -------------------------------------------------------------------------------- 1 | .content-header-titlewapper { 2 | display: flex; 3 | margin-top: 20px; 4 | margin-bottom: 20px; 5 | } 6 | .content-header-title { 7 | font-size: 18px; 8 | font-weight: bold; 9 | } 10 | .content-header-contributionLink { 11 | text-decoration: none; 12 | } 13 | .contributionLink-content { 14 | font-size: 12px; 15 | display: flex; 16 | text-decoration: none; 17 | } 18 | 19 | .contribution-link-text { 20 | margin-left: 12px; 21 | } 22 | .content-header-sortbywrappper { 23 | display: flex; 24 | margin-top: 20px; 25 | margin-bottom: 5px; 26 | } 27 | 28 | .content-header-count { 29 | text-align: left; 30 | margin-top: auto; 31 | margin-bottom: auto; 32 | color: #000000; 33 | font-size: 16px; 34 | line-height: normal; 35 | } 36 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/ContentHeader/ContentHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { withRouter } from "react-router-dom"; 4 | import { 5 | SearchBox, 6 | Dropdown, 7 | Icon, 8 | Link as FabricLink 9 | } from "office-ui-fabric-react"; 10 | import { Link } from "react-router-dom"; 11 | import { 12 | paramsToQueryString, 13 | queryStringToParams, 14 | trackEvent 15 | } from "../../helpers"; 16 | import "./ContentHeader.css"; 17 | 18 | class ContentHeader extends Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | filterText: this.props.initialSearchText, 23 | sortby: this.props.initialSortBy 24 | }; 25 | } 26 | 27 | filterTextChanged(newValue) { 28 | var params = queryStringToParams(this.props.location.search); 29 | delete params["filtertext"]; 30 | if (newValue && newValue !== "") { 31 | params["filtertext"] = newValue; 32 | } 33 | 34 | this.setState({ filterText: newValue }); 35 | this.props.history.push(paramsToQueryString(params)); 36 | trackEvent("/filter/change/searchtext", newValue); 37 | } 38 | 39 | sortbyChanged(newValue) { 40 | var params = queryStringToParams(this.props.location.search); 41 | delete params["sortby"]; 42 | if (newValue !== "totaldownloads") { 43 | params["sortby"] = newValue; 44 | } 45 | this.setState({ sortby: newValue }); 46 | this.props.history.push(paramsToQueryString(params)); 47 | trackEvent("/sortby/change", newValue); 48 | } 49 | 50 | render() { 51 | const dropdownStyles = () => { 52 | return { 53 | root: { 54 | display: "flex" 55 | }, 56 | label: { 57 | marginRight: "10px", 58 | color: "#000000", 59 | fontSize: "12px" 60 | }, 61 | title: { 62 | color: "#595959;", 63 | border: "1px solid #BCBCBC", 64 | borderRadius: "2px", 65 | fontSize: "12px" 66 | }, 67 | dropdown: { 68 | width: 150 69 | } 70 | }; 71 | }; 72 | 73 | const searchBoxStyles = () => { 74 | return { 75 | root: { 76 | border: "1px solid #BCBCBC", 77 | borderRadius: "3px" 78 | } 79 | }; 80 | }; 81 | 82 | let resultCount = this.props.samples.length; 83 | return ( 84 |
85 |
86 |
87 | Azure serverless community library 88 |
89 |
90 | 95 |
96 | 97 |
Contributions
98 |
99 |
100 |
101 |
102 | this.filterTextChanged(newValue)} 106 | onClear={() => this.filterTextChanged("")} 107 | styles={searchBoxStyles} 108 | /> 109 |
110 |
111 | Displaying {resultCount} {resultCount === 1 ? "result" : "results"} 112 |
113 |
114 | this.sortbyChanged(item.key)} 124 | /> 125 |
126 |
127 |
128 | ); 129 | } 130 | } 131 | 132 | const mapStateToProps = state => ({}); 133 | 134 | const ContentHeaderContainer = connect(mapStateToProps)(ContentHeader); 135 | 136 | export default withRouter(ContentHeaderContainer); 137 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Contribute/Contribute.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ContributionForm from "./ContributionForm"; 3 | import ContributionsList from "./ContributionsList"; 4 | import SignInDialog from "./SignInDialog"; 5 | import PageHeaderWithBackButton from "../shared/PageHeaderWithBackButton"; 6 | 7 | import "./Contribute.scss"; 8 | 9 | class ContributionsPage extends Component { 10 | render() { 11 | return ( 12 |
13 | 14 |

15 | This is where you can see all your existing contributions. You can 16 | also add a new contribution by clicking on the add new contribution 17 | link. 18 |

19 |
20 | 21 | 22 | 23 |
24 | ); 25 | } 26 | } 27 | 28 | export default ContributionsPage; 29 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Contribute/Contribute.scss: -------------------------------------------------------------------------------- 1 | .left-margin { 2 | margin-left: 23px; 3 | } 4 | 5 | .contribute-page-description { 6 | max-width: 785px; 7 | } 8 | 9 | .add-contribution-container { 10 | margin-top: 23px; 11 | margin-bottom: 22px; 12 | } 13 | 14 | .add-contribution-link { 15 | @extend .left-margin; 16 | font-size: 12px; 17 | } 18 | 19 | .contribution-form-container { 20 | background-color: #fbfbfb; 21 | margin-top: 12px; 22 | padding-top: 15px; 23 | padding-left: 23px; 24 | padding-bottom: 28px; 25 | } 26 | .input-container { 27 | display: flex; 28 | flex-direction: row; 29 | flex: 1; 30 | } 31 | .contribution-form-fields-container { 32 | max-width: 430px; 33 | margin-right: 100px; 34 | flex: 1; 35 | } 36 | .input-field-container { 37 | margin-bottom: 10px; 38 | } 39 | 40 | .contribution-form-actions-container { 41 | margin-top: 18px; 42 | } 43 | 44 | .contribution-list-container { 45 | @extend .left-margin; 46 | max-width: 746px; 47 | } 48 | 49 | .empty-contribution-list-container { 50 | font-size: 12px; 51 | margin-top: 6px; 52 | } 53 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Contribute/ContributionForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | Icon, 4 | Link, 5 | TextField, 6 | PrimaryButton, 7 | DefaultButton, 8 | Dropdown, 9 | Dialog 10 | } from "office-ui-fabric-react"; 11 | 12 | import { libraryService } from "../../services"; 13 | import * as commonStyles from "../shared/Button.styles"; 14 | import * as Constants from "../shared/Constants"; 15 | 16 | const initialState = { 17 | showForm: false, 18 | title: "", 19 | description: "", 20 | repository: "", 21 | template: "", 22 | technologies: [], 23 | language: "", 24 | solutionareas: [], 25 | dialogProps: { 26 | isVisible: false, 27 | title: "", 28 | content: {} 29 | } 30 | }; 31 | 32 | class ContributionForm extends Component { 33 | constructor(props) { 34 | super(props); 35 | 36 | this.state = initialState; 37 | 38 | this.onLinkClick = this.onLinkClick.bind(this); 39 | this.onAddButtonClick = this.onAddButtonClick.bind(this); 40 | this.onCancelButtonClick = this.onCancelButtonClick.bind(this); 41 | this.handleInputChange = this.handleInputChange.bind(this); 42 | this.onTitleChange = this.onTitleChange.bind(this); 43 | this.onDescriptionChange = this.onDescriptionChange.bind(this); 44 | this.onDismissDialog = this.onDismissDialog.bind(this); 45 | this.validateForm = this.getFormValidationErrors.bind(this); 46 | } 47 | 48 | technologiesOptionsChanged(newValue) { 49 | let technologies = this.state.technologies; 50 | if (newValue.selected) { 51 | technologies.push(newValue.key); 52 | } else { 53 | technologies = technologies.filter(t => t !== newValue.key); 54 | } 55 | 56 | this.setState({ technologies: technologies }); 57 | } 58 | 59 | solutionAreasOptionsChanged(newValue) { 60 | let solutionAreas = this.state.solutionareas; 61 | if (newValue.selected) { 62 | solutionAreas.push(newValue.key); 63 | } else { 64 | solutionAreas = solutionAreas.filter(t => t !== newValue.key); 65 | } 66 | 67 | this.setState({ solutionareas: solutionAreas }); 68 | } 69 | 70 | languageOptionChanged(newValue) { 71 | this.setState({ language: newValue }); 72 | } 73 | 74 | handleInputChange(event, newValue) { 75 | const target = event.target; 76 | const name = target.name; 77 | this.setState({ [name]: newValue }); 78 | } 79 | 80 | onTitleChange(event, newValue) { 81 | if (!newValue || newValue.length <= 80) { 82 | this.setState({ title: newValue || "" }); 83 | } else { 84 | // this block is needed because of Fabric bug #1350 85 | this.setState({ title: this.state.title }); 86 | } 87 | } 88 | 89 | onDescriptionChange(event, newValue) { 90 | if (!newValue || newValue.length <= 300) { 91 | this.setState({ description: newValue || "" }); 92 | } else { 93 | // this block is needed because of Fabric bug #1350 94 | this.setState({ description: this.state.description }); 95 | } 96 | } 97 | 98 | getFormValidationErrors(sample) { 99 | let errors = []; 100 | if (sample.title.length === 0) { 101 | errors.push("Title cannot be empty"); 102 | } 103 | if (sample.repository.length === 0) { 104 | errors.push("Repository URL cannot be empty"); 105 | } 106 | if (sample.description.length === 0) { 107 | errors.push("Description cannot be empty"); 108 | } 109 | if (sample.technologies.length === 0) { 110 | errors.push("At least one technology must be selected"); 111 | } 112 | if (sample.language.length === 0) { 113 | errors.push("Language must be selected"); 114 | } 115 | if (sample.solutionareas.length === 0) { 116 | errors.push("At least one solution area must be selected"); 117 | } 118 | 119 | return errors; 120 | } 121 | 122 | onLinkClick() { 123 | this.setState({ showForm: true }); 124 | } 125 | 126 | onAddButtonClick() { 127 | const sample = { 128 | title: this.state.title, 129 | description: this.state.description, 130 | repository: this.state.repository, 131 | template: this.state.template, 132 | technologies: this.state.technologies, 133 | language: this.state.language, 134 | solutionareas: this.state.solutionareas 135 | }; 136 | const errors = this.getFormValidationErrors(sample); 137 | if (errors.length > 0) { 138 | this.showDialog("Unable to add sample", errors); 139 | return; 140 | } 141 | 142 | libraryService 143 | .submitNewSample(sample) 144 | .then(sample => { 145 | this.resetForm(); 146 | this.showDialog( 147 | "Thank you!", 148 | "Thank you for your contribution! Your sample has been submitted for approval." 149 | ); 150 | }) 151 | .catch(data => { 152 | this.showDialog("Unable to add sample", data.error); 153 | }); 154 | } 155 | 156 | showDialog(title, content) { 157 | const dialogProps = { 158 | title: title, 159 | content: content, 160 | isVisible: true 161 | }; 162 | this.setState({ dialogProps }); 163 | } 164 | 165 | onDismissDialog() { 166 | const dialogProps = { ...this.state.dialogProps, isVisible: false }; 167 | this.setState({ dialogProps }); 168 | } 169 | 170 | onCancelButtonClick() { 171 | this.resetForm(); 172 | } 173 | 174 | resetForm() { 175 | this.setState(initialState); 176 | } 177 | 178 | render() { 179 | let technologiesOptions = Constants.technologies.map(t => ({ 180 | key: t, 181 | text: t 182 | })); 183 | let languageOptions = Constants.languages.map(l => ({ key: l, text: l })); 184 | languageOptions.push({ 185 | key: Constants.NotApplicableLanguage, 186 | text: "Not applicable" 187 | }); 188 | let solutionAreasOptions = Constants.solutionAreas.map(s => ({ 189 | key: s, 190 | text: s 191 | })); 192 | solutionAreasOptions.push({ 193 | key: Constants.OtherSolutionArea, 194 | text: Constants.OtherSolutionArea 195 | }); 196 | let { showForm, dialogProps } = this.state; 197 | return ( 198 |
199 |
200 | 201 | 202 | Add new contribution 203 | 204 |
205 | {dialogProps.isVisible && ( 206 | 223 | )} 224 | {showForm && ( 225 |
226 |
227 |
228 | 237 | 246 | 258 |
259 |
260 | this.technologiesOptionsChanged(item)} 265 | required={true} 266 | multiSelect 267 | options={technologiesOptions} 268 | /> 269 | this.languageOptionChanged(item.key)} 274 | required={true} 275 | options={languageOptions} 276 | /> 277 | 282 | this.solutionAreasOptionsChanged(item) 283 | } 284 | required={true} 285 | multiSelect 286 | options={solutionAreasOptions} 287 | /> 288 | 296 |
297 |
298 |
299 | 304 | 309 |
310 |
311 | )} 312 |
313 | ); 314 | } 315 | } 316 | 317 | export default ContributionForm; 318 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Contribute/ContributionForm.styles.js: -------------------------------------------------------------------------------- 1 | const labelStyles = { 2 | root: { 3 | fontSize: "13px", 4 | lineHeight: "18px", 5 | height: "21px", 6 | paddingBottom: "1px", 7 | paddingTop: 0 8 | } 9 | }; 10 | 11 | export const textFieldStyles = { 12 | root: { 13 | marginBottom: "10px" 14 | }, 15 | fieldGroup: { 16 | height: "23px" 17 | }, 18 | field: { 19 | fontSize: "12px", 20 | paddingLeft: "7px", 21 | selectors: { 22 | "::placeholder": { 23 | fontSize: "12px" 24 | } 25 | } 26 | }, 27 | subComponentStyles: { 28 | label: labelStyles 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Contribute/ContributionsList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Label } from "office-ui-fabric-react"; 4 | import ItemList from "../../components/ItemList/ItemList"; 5 | 6 | class ContributionsList extends Component { 7 | filteredSamples() { 8 | let { loggedIn, user, samples } = this.props; 9 | let { userName } = user; 10 | if (!loggedIn || !userName) { 11 | return {}; 12 | } 13 | 14 | let filter = new RegExp(userName, "i"); 15 | samples = samples.filter( 16 | el => 17 | el.repository.replace("https://github.com/", "").match(filter) || // this match should be against author 18 | (el.author && el.author.match(filter)) 19 | ); 20 | 21 | return samples; 22 | } 23 | 24 | render() { 25 | const headerLabelStyles = { 26 | root: { 27 | fontSize: "12px", 28 | fontWeight: "bold", 29 | paddingTop: "0px", 30 | paddingBottom: "6px" 31 | } 32 | }; 33 | const filteredSamples = this.filteredSamples(); 34 | return ( 35 |
36 | 37 | {filteredSamples.length > 0 ? ( 38 | 39 | ) : ( 40 |
41 | You currently do not have any samples 42 |
43 | )} 44 |
45 | ); 46 | } 47 | } 48 | 49 | function mapStateToProps(state) { 50 | return { 51 | samples: state.samples, 52 | loggedIn: state.authentication.loggedIn, 53 | user: state.authentication.user 54 | }; 55 | } 56 | 57 | const ContributionsListContainer = connect(mapStateToProps)(ContributionsList); 58 | 59 | export default ContributionsListContainer; 60 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Contribute/SignInDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import { Dialog, DialogFooter, DefaultButton } from "office-ui-fabric-react"; 5 | 6 | import SignInButton from "../shared/SignInButton"; 7 | 8 | class SignInDialog extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.handleHomeButtonClick = this.handleHomeButtonClick.bind(this); 12 | } 13 | 14 | handleHomeButtonClick() { 15 | this.props.history.push("/"); 16 | } 17 | 18 | render() { 19 | const footerStyles = { 20 | actionsRight: { 21 | textAlign: "center", 22 | marginRight: "0px" 23 | } 24 | }; 25 | const buttonStyles = { 26 | root: { 27 | fontSize: "12px", 28 | height: "32px", 29 | minWidth: "40px", 30 | backgroundColor: "white", 31 | border: "1px solid #0078D7", 32 | color: "#0058AD" 33 | }, 34 | rootHovered: { 35 | border: "1px solid #0078D7", 36 | color: "#0058AD" 37 | }, 38 | label: { 39 | fontWeight: "normal" 40 | } 41 | }; 42 | 43 | const { loading, loggedIn } = this.props; 44 | if (loading) { 45 | return null; 46 | } 47 | 48 | return ( 49 |
50 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | const mapStateToProps = state => ({ 75 | loading: state.authentication.loading, 76 | loggedIn: state.authentication.loggedIn 77 | }); 78 | 79 | const SignInDialogContainer = connect(mapStateToProps)(SignInDialog); 80 | 81 | export default withRouter(SignInDialogContainer); 82 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/DetailView/ActionBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Icon, Link as FabricLink } from "office-ui-fabric-react"; 3 | import { trackEvent } from "../../helpers"; 4 | import { libraryService } from "../../services"; 5 | import "./ActionBar.scss"; 6 | 7 | class ActionBar extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.outboundRepoClick = this.outboundRepoClick.bind(this); 11 | this.outboundDeployClick = this.outboundDeployClick.bind(this); 12 | this.openInVSCodeClick = this.openInVSCodeClick.bind(this); 13 | this.trackUserActionEvent = this.trackUserActionEvent.bind(this); 14 | } 15 | 16 | outboundDeployClick() { 17 | this.updateDownloadCount(this.props.id); 18 | this.trackUserActionEvent("/sample/deploy/agree"); 19 | } 20 | 21 | updateDownloadCount(id) { 22 | libraryService 23 | .updateDownloadCount(id) 24 | .then(() => { 25 | // do nothing 26 | }) 27 | .catch(() => { 28 | // do nothing 29 | }); 30 | } 31 | 32 | getDeployLink(template) { 33 | return ( 34 | "https://portal.azure.com/#create/Microsoft.Template/uri/" + 35 | encodeURIComponent(template) 36 | ); 37 | } 38 | 39 | outboundRepoClick() { 40 | this.trackUserActionEvent("/sample/source"); 41 | } 42 | 43 | getOpenInVSCodeLink(repository) { 44 | return "vscode://vscode.git/clone?url=" + encodeURIComponent(repository); 45 | } 46 | 47 | openInVSCodeClick() { 48 | this.updateDownloadCount(this.props.id); 49 | this.trackUserActionEvent("/sample/openinvscode"); 50 | } 51 | 52 | trackUserActionEvent(eventName) { 53 | let eventData = { 54 | id: this.props.id, 55 | repository: this.props.repository, 56 | template: this.props.template 57 | }; 58 | 59 | trackEvent(eventName, eventData); 60 | } 61 | 62 | render() { 63 | const { repository, template } = this.props; 64 | 65 | return ( 66 |
67 |
68 | 73 |
74 | 75 | Edit in VS Code 76 |
77 |
78 |
79 |
80 | 86 |
87 | 88 | Deploy 89 |
90 |
91 |
92 |
93 | 99 |
100 | 101 | Open in Github 102 |
103 |
104 |
105 |
106 | ); 107 | } 108 | } 109 | 110 | export default ActionBar; 111 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/DetailView/ActionBar.scss: -------------------------------------------------------------------------------- 1 | .action-container { 2 | display: flex; 3 | flex-direction: row; 4 | padding-top: 7px; 5 | padding-left: 23px; 6 | } 7 | 8 | .action-item { 9 | margin-right: 16px; 10 | } 11 | 12 | .action-link-wrapper { 13 | height: 100%; 14 | display: flex; 15 | } 16 | 17 | .fabric-icon-link { 18 | font-size: 12px; 19 | width: 13px; 20 | height: 11px; 21 | margin-top: 2px; 22 | } 23 | 24 | .githubicon { 25 | @extend .fabric-icon-link; 26 | fill: currentColor; 27 | } 28 | 29 | .action-link-text { 30 | padding-left: 8px; 31 | font-size: 12px; 32 | margin: auto; 33 | } 34 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/DetailView/DetailPageContent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ReactMarkdown from "markdown-to-jsx"; 3 | import { 4 | Pivot, 5 | PivotItem, 6 | PivotLinkSize, 7 | ScrollablePane 8 | } from "office-ui-fabric-react/lib/index"; 9 | import { githubService } from "../../services"; 10 | import "./DetailPageContent.scss"; 11 | 12 | const defaultLicenseText = 13 | "Each application is licensed to you by its owner (which may or may not be Microsoft) under the agreement which accompanies the application. Microsoft is not responsible for any non-Microsoft code and does not screen for security, compatibility, or performance. The applications are not supported by any Microsoft support program or service. The applications are provided AS IS without warranty of any kind."; 14 | 15 | class DetailPageContent extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | armTemplateText: "", 20 | markdownText: "", 21 | licenseText: "", 22 | selectedKey: "overview" 23 | }; 24 | 25 | this.handleLinkClick = this.handleLinkClick.bind(this); 26 | } 27 | 28 | // This method is used to fetch readme content from repo when valid repository url is received as props 29 | componentDidUpdate(prevProps, prevState) { 30 | if ( 31 | this.props.repository !== prevProps.repository && 32 | prevState.markdownText === "" 33 | ) { 34 | let { repository } = this.props; 35 | githubService 36 | .getReadMe(repository) 37 | .then(data => { 38 | var r = new RegExp( 39 | "https?://Azuredeploy.net/deploybutton.(png|svg)", 40 | "ig" 41 | ); 42 | data = data.replace(r, ""); 43 | this.setState({ markdownText: data }); 44 | }) 45 | .catch(() => { 46 | // do nothing 47 | }); 48 | } 49 | } 50 | 51 | handleLinkClick(pivotItem, ev) { 52 | const selectedKey = pivotItem.props.itemKey; 53 | this.setState({ selectedKey }); 54 | if (selectedKey === "armtemplate" && this.state.armTemplateText === "") { 55 | const { template } = this.props; 56 | if (template) { 57 | githubService 58 | .getArmTemplate(template) 59 | .then(data => 60 | this.setState({ 61 | armTemplateText: data 62 | }) 63 | ) 64 | .catch(() => { 65 | // do nothing 66 | }); 67 | } 68 | } 69 | if (selectedKey === "license" && this.state.licenseText === "") { 70 | const { license, repository } = this.props; 71 | githubService 72 | .getLicense(license, repository) 73 | .then(data => 74 | this.setState({ 75 | licenseText: data 76 | }) 77 | ) 78 | .catch(() => { 79 | // do nothing 80 | }); 81 | } 82 | } 83 | 84 | render() { 85 | const pivotStyles = { 86 | root: { 87 | paddingLeft: "15px" 88 | }, 89 | text: { 90 | color: " #0058AD", 91 | fontSize: "14px" 92 | } 93 | }; 94 | 95 | const { 96 | selectedKey, 97 | markdownText, 98 | licenseText, 99 | armTemplateText 100 | } = this.state; 101 | return ( 102 |
103 | this.handleLinkClick(item, ev)} 108 | > 109 | 110 |
111 |
112 | 113 | {markdownText} 114 | 115 |
116 |
117 |
118 | 119 |
120 |
121 | 122 |
123 |

{defaultLicenseText}

124 | {licenseText !== "" && ( 125 |

{licenseText}

126 | )} 127 |
128 |
129 |
130 |
131 |
132 | {this.props.template && ( 133 | 134 |
135 |
136 | 137 |
138 |
{armTemplateText}
139 |
140 |
141 |
142 |
143 |
144 | )} 145 |
146 |
147 | ); 148 | } 149 | } 150 | 151 | export default DetailPageContent; 152 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/DetailView/DetailPageContent.scss: -------------------------------------------------------------------------------- 1 | .detail-page-content { 2 | margin-top: 24px; 3 | border-top: 1px solid rgba(105, 130, 155, 0.25); 4 | border-bottom: 1px solid rgba(105, 130, 155, 0.25); 5 | } 6 | .pivot-item-container { 7 | padding-left: 23px; 8 | background-color: #fbfbfb; 9 | } 10 | .scrollablePane-wrapper { 11 | min-height: 60vh; 12 | position: relative; 13 | max-height: inherit; 14 | } 15 | 16 | .armtemplate-content { 17 | overflow-wrap: break-word; 18 | white-space: pre-line; 19 | justify-content: center; 20 | } 21 | 22 | .license-content { 23 | max-width: 750px; 24 | text-align: justify; 25 | white-space: pre-line; 26 | } 27 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/DetailView/DetailPageHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PageHeaderWithBackButton from "../shared/PageHeaderWithBackButton"; 3 | import MetricBar from "../MetricBar/MetricBar"; 4 | 5 | class DetailPageHeader extends Component { 6 | render() { 7 | let { 8 | title, 9 | author, 10 | id, 11 | totaldownloads, 12 | createddate, 13 | description, 14 | likes, 15 | dislikes 16 | } = this.props; 17 | return ( 18 |
19 | 20 | 28 |

{description}

29 |
30 |
31 | ); 32 | } 33 | } 34 | 35 | export default DetailPageHeader; 36 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/DetailView/DetailView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import ActionBar from "./ActionBar"; 4 | import DetailPageContent from "./DetailPageContent"; 5 | import DetailPageHeader from "./DetailPageHeader"; 6 | 7 | import { sampleActions } from "../../actions/sampleActions"; 8 | import { trackEvent } from "../../helpers"; 9 | 10 | class DetailView extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | sample: {} 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | this.setCurrentItemInState(); 20 | } 21 | 22 | componentDidUpdate(prevProps, prevState) { 23 | this.setCurrentItemInState(); 24 | } 25 | 26 | setCurrentItemInState() { 27 | if (!this.state.sample.id && this.props.samples.length > 0) { 28 | const id = this.props.match.params.id; 29 | let currentItem = this.props.samples.filter(s => s.id === id)[0] || {}; 30 | this.setState({ sample: currentItem }); 31 | this.trackPageLoadEvent(currentItem); 32 | } 33 | } 34 | 35 | trackPageLoadEvent(sample) { 36 | let eventData = { 37 | id: sample.id, 38 | repository: sample.repository, 39 | template: sample.template 40 | }; 41 | trackEvent("/sample/detailpage", eventData); 42 | } 43 | 44 | render() { 45 | let likes = this.state.sample.likes ? this.state.sample.likes : 0; 46 | let dislikes = this.state.sample.dislikes ? this.state.sample.dislikes : 0; 47 | return ( 48 |
49 | 59 | 64 | 68 |
69 | ); 70 | } 71 | } 72 | 73 | const mapStateToProps = state => ({ 74 | samples: state.samples 75 | }); 76 | 77 | const mapDispatchToProps = { 78 | getSamplesSuccess: sampleActions.getSamplesSuccess 79 | }; 80 | 81 | const DetailViewContainer = connect( 82 | mapStateToProps, 83 | mapDispatchToProps 84 | )(DetailView); 85 | 86 | export default DetailViewContainer; 87 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Header/AuthControl.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import { ActionButton, ContextualMenuItemType } from "office-ui-fabric-react"; 5 | 6 | import { userService } from "../../services"; 7 | import { userActions } from "../../actions/userActions"; 8 | import SignInButton from "../shared/SignInButton"; 9 | import UserPersona from "./UserPersona"; 10 | 11 | class AuthControl extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this._renderMenuList = this._renderMenuList.bind(this); 16 | this._onSignoutClick = this._onSignoutClick.bind(this); 17 | this._onContributionsClick = this._onContributionsClick.bind(this); 18 | } 19 | 20 | getMenuItems() { 21 | const { user } = this.props; 22 | const userName = 23 | user.fullName && user.fullName !== "" ? user.fullName : user.displayName; 24 | 25 | return [ 26 | { 27 | key: "Header", 28 | itemType: ContextualMenuItemType.Header, 29 | text: "Logged in as: " + userName 30 | }, 31 | { 32 | key: "divider_1", 33 | itemType: ContextualMenuItemType.Divider 34 | }, 35 | { 36 | key: "contributions", 37 | text: "My contributions", 38 | onClick: this._onContributionsClick 39 | }, 40 | { 41 | key: "divider_1", 42 | itemType: ContextualMenuItemType.Divider 43 | }, 44 | { 45 | key: "signOut", 46 | text: "Sign out", 47 | onClick: this._onSignoutClick 48 | } 49 | ]; 50 | } 51 | 52 | _renderMenuIcon() { 53 | return null; 54 | } 55 | 56 | _renderMenuList(menuListProps, defaultRender) { 57 | const { loggedIn } = this.props; 58 | if (loggedIn) { 59 | return
{defaultRender(menuListProps)}
; 60 | } 61 | 62 | return ( 63 |
64 | 65 |
66 | ); 67 | } 68 | 69 | _onContributionsClick() { 70 | this.props.history.push("/contribute"); 71 | } 72 | 73 | _onSignoutClick() { 74 | this.props.logout(); // clear the redux store before making a call to the backend 75 | userService 76 | .logout() 77 | .then(() => { 78 | // do nothing 79 | }) 80 | .catch(() => { 81 | // do nothing 82 | }); 83 | } 84 | 85 | render() { 86 | return ( 87 | 88 |
89 | 97 | 98 | 99 |
100 |
101 | ); 102 | } 103 | } 104 | 105 | const mapStateToProps = state => ({ 106 | loggedIn: state.authentication.loggedIn, 107 | user: state.authentication.user 108 | }); 109 | 110 | const mapDispatchToProps = { 111 | logout: userActions.logout 112 | }; 113 | 114 | const AuthControlContainer = connect( 115 | mapStateToProps, 116 | mapDispatchToProps 117 | )(AuthControl); 118 | 119 | export default withRouter(AuthControlContainer); 120 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Link } from "office-ui-fabric-react"; 4 | import { getTheme } from "office-ui-fabric-react"; 5 | 6 | import "./Header.scss"; 7 | import AuthControl from "./AuthControl"; 8 | 9 | class Header extends Component { 10 | render() { 11 | const theme = getTheme(); 12 | const linkStyles = { 13 | root: { 14 | marginLeft: "15px", 15 | lineHeight: "40px", 16 | fontSize: "14px", 17 | color: theme.palette.white, 18 | selectors: { 19 | "&:active, &:hover, &:active:hover, &:visited": { 20 | color: theme.palette.white 21 | } 22 | } 23 | } 24 | }; 25 | 26 | return ( 27 |
28 | 29 | 34 | Microsoft Azure 35 | 36 | 37 | 38 |
39 | ); 40 | } 41 | } 42 | 43 | const mapStateToProps = state => ({ 44 | loggedIn: state.authentication.loggedIn 45 | }); 46 | 47 | const HeaderContainer = connect(mapStateToProps)(Header); 48 | 49 | export default HeaderContainer; 50 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .headerbar { 2 | background-color: black; 3 | display: inline-block; 4 | width: 100%; 5 | } 6 | 7 | .auth-control { 8 | float: right; 9 | } 10 | 11 | .signin-button-container { 12 | text-align: center; 13 | padding: 20px 16px; 14 | } 15 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Header/UserPersona.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Persona, PersonaSize, Icon } from "office-ui-fabric-react"; 4 | import { getTheme } from "office-ui-fabric-react"; 5 | 6 | class UserPersona extends Component { 7 | _onRenderInitials() { 8 | return ; 9 | } 10 | 11 | render() { 12 | const theme = getTheme(); 13 | const personaStyles = { 14 | root: { 15 | height: "40px", 16 | color: theme.palette.white, 17 | float: "right", 18 | selectors: { 19 | ":hover": { 20 | selectors: { 21 | $primaryText: { 22 | color: theme.palette.white 23 | } 24 | } 25 | } 26 | } 27 | }, 28 | details: { 29 | width: "85px" 30 | }, 31 | primaryText: { 32 | color: theme.palette.white 33 | } 34 | }; 35 | 36 | const { loading, loggedIn, user } = this.props; 37 | if (loading) { 38 | return null; 39 | } 40 | 41 | return loggedIn ? ( 42 | 53 | ) : ( 54 | 61 | ); 62 | } 63 | } 64 | 65 | const mapStateToProps = state => ({ 66 | loading: state.authentication.loading, 67 | loggedIn: state.authentication.loggedIn, 68 | user: state.authentication.user 69 | }); 70 | 71 | const UserPersonaContainer = connect(mapStateToProps)(UserPersona); 72 | 73 | export default UserPersonaContainer; 74 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/ItemList/ItemList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | FocusZone, 4 | FocusZoneDirection, 5 | List, 6 | Link as FabricLink 7 | } from "office-ui-fabric-react"; 8 | import { Link } from "react-router-dom"; 9 | import ItemTags from "../ItemTags/ItemTags"; 10 | import MetricBar from "../MetricBar/MetricBar"; 11 | 12 | import "./ItemList.scss"; 13 | 14 | class ItemList extends Component { 15 | constructor(props) { 16 | super(props); 17 | this._onRenderCell = this._onRenderCell.bind(this); 18 | this.disableHover = this.props.disableHover || false; 19 | } 20 | 21 | _onRenderCell(item, index) { 22 | let likes = item.likes ? item.likes : 0; 23 | let dislikes = item.dislikes ? item.dislikes : 0; 24 | return ( 25 |
26 |
27 |
32 |
33 | 38 | {item.title} 39 | 40 |
41 | 49 |
{item.description}
50 | 56 |
57 |
58 |
59 | ); 60 | } 61 | 62 | render() { 63 | return ( 64 |
65 | 66 | 70 | 71 |
72 | ); 73 | } 74 | } 75 | 76 | export default ItemList; 77 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/ItemList/ItemList.scss: -------------------------------------------------------------------------------- 1 | .libraryitem { 2 | border-bottom: 1px solid #d8d8d8; 3 | padding-top: 12px; 4 | padding-bottom: 15px; 5 | } 6 | 7 | .libraryitem-withhover { 8 | @extend .libraryitem; 9 | &:hover { 10 | border-bottom: none; 11 | } 12 | } 13 | 14 | .libraryitemContainer { 15 | padding-left: 16px; 16 | &:hover { 17 | margin-top: -1px; 18 | margin-bottom: 1px; 19 | background-color: white; 20 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); 21 | } 22 | } 23 | 24 | .title { 25 | font-size: 16px; 26 | line-height: 21px; 27 | color: #0072c6; 28 | &:visited { 29 | text-decoration: none; 30 | } 31 | } 32 | 33 | .titlelink { 34 | text-decoration: none; 35 | } 36 | 37 | .description { 38 | font-size: 14px; 39 | line-height: 19px; 40 | color: #000000; 41 | padding-bottom: 4px; 42 | padding-bottom: 10px; 43 | white-space: nowrap; 44 | overflow: hidden; 45 | text-overflow: ellipsis; 46 | } 47 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/ItemTags/ItemTags.css: -------------------------------------------------------------------------------- 1 | .tagcontainer { 2 | font-size: 14px; 3 | line-height: normal; 4 | vertical-align: sub; 5 | } 6 | 7 | .tag { 8 | margin-left: 5px; 9 | background: #efefef; 10 | border-radius: 10px; 11 | padding: 4px 10px 4px 10px; 12 | font-size: 12px; 13 | line-height: 16px; 14 | display: inline-block; 15 | } 16 | 17 | .svg { 18 | width: 17px; 19 | height: 17px; 20 | vertical-align: baseline; 21 | margin-right: 4px; 22 | } 23 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/ItemTags/ItemTags.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import * as Constants from "../shared/Constants"; 3 | import "./ItemTags.css"; 4 | 5 | class ItemTags extends Component { 6 | render() { 7 | // Tags contain technologies, language, solutionareas and custom tags 8 | let allTags = []; 9 | 10 | if (this.props.technologies) { 11 | allTags.push(...this.props.technologies); 12 | } 13 | if ( 14 | this.props.language !== "" && 15 | this.props.language !== Constants.NotApplicableLanguage 16 | ) { 17 | allTags.push(this.props.language); 18 | } 19 | if (this.props.solutionareas) { 20 | let solutionareas = this.props.solutionareas.filter( 21 | s => s !== Constants.OtherSolutionArea 22 | ); 23 | allTags.push(...solutionareas); 24 | } 25 | 26 | if (this.props.tags) { 27 | allTags.push(...this.props.tags); 28 | } 29 | 30 | return ( 31 |
32 | Tags : 33 | {allTags.map((value, index) => ( 34 | 35 | {value} 36 | 37 | ))} 38 |
39 | ); 40 | } 41 | } 42 | 43 | export default ItemTags; 44 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Main/Main.css: -------------------------------------------------------------------------------- 1 | #mainContainer { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | } 6 | #sidebar { 7 | overflow-y: auto; 8 | height: 100%; 9 | max-width: 15%; 10 | min-width: 175px; 11 | background-color: #f9f9f9; 12 | box-shadow: 0px 0px 1.8px rgba(0, 0, 0, 0.12), 13 | 0px 3.2px 3.6px rgba(0, 0, 0, 0.08); 14 | } 15 | 16 | #content { 17 | display: flex; 18 | flex-direction: column; 19 | overflow: hidden; 20 | flex: 1; 21 | } 22 | 23 | #contentheader{ 24 | max-width: 830px; 25 | padding-left: 40px; 26 | padding-right: 40px; 27 | } 28 | #list { 29 | flex:1; 30 | overflow-y: auto; 31 | padding: 10px; 32 | padding-left: 25px; 33 | } 34 | 35 | .list-container { 36 | max-width: 765px; 37 | } 38 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/Main/Main.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import SideBarContainer from "../../components/sidebar/SideBar"; 5 | import ContentHeaderContainer from "../ContentHeader/ContentHeader"; 6 | import ItemList from "../../components/ItemList/ItemList"; 7 | import "./Main.css"; 8 | import { queryStringToParams } from "../../helpers"; 9 | 10 | class Main extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | initialFilters: this.getFiltersFromQueryParams(), 15 | showContentHeaderShadow: false 16 | }; 17 | 18 | this.onListScrollEvent = this.onListScrollEvent.bind(this); 19 | } 20 | 21 | filteredSamples() { 22 | let samples = this.props.samples; 23 | let currentfilters = this.getFiltersFromQueryParams(); 24 | let filter = new RegExp(currentfilters.filtertext, "i"); 25 | samples = samples.filter( 26 | el => 27 | el.title.match(filter) || 28 | el.description.match(filter) || 29 | el.language.match(filter) || 30 | el.repository.replace("https://github.com/", "").match(filter) || 31 | (el.tags && el.tags.some(x => x.match(filter))) || 32 | (el.technologies && el.technologies.some(x => x.match(filter))) || 33 | (el.solutionareas && el.solutionareas.some(x => x.match(filter))) || 34 | (el.author && el.author.match(filter)) 35 | ); 36 | 37 | if (currentfilters.categories.technologies.length > 0) { 38 | samples = samples.filter(s => 39 | s.technologies.some(t => 40 | currentfilters.categories.technologies.includes(t) 41 | ) 42 | ); 43 | } 44 | 45 | if (currentfilters.categories.languages.length > 0) { 46 | samples = samples.filter(s => 47 | currentfilters.categories.languages.includes(s.language) 48 | ); 49 | } 50 | 51 | if (currentfilters.categories.solutionareas.length > 0) { 52 | samples = samples.filter(s => 53 | s.solutionareas.some(a => 54 | currentfilters.categories.solutionareas.includes(a) 55 | ) 56 | ); 57 | } 58 | 59 | return this.Sort(samples, currentfilters.sortby); 60 | } 61 | 62 | Sort(list, sortby) { 63 | list = list.map(a => a); 64 | if (sortby === "totaldownloads") { 65 | list = list.sort(function(a, b) { 66 | return b.totaldownloads - a.totaldownloads; 67 | }); 68 | } else if (sortby === "createddate") { 69 | list.sort(function(a, b) { 70 | let dateA = new Date(a.createddate), 71 | dateB = new Date(b.createddate); 72 | return dateB - dateA; 73 | }); 74 | } else { 75 | list = list.sort(function(a, b) { 76 | let titleA = a.title.toLowerCase(), 77 | titleB = b.title.toLowerCase(); 78 | if (titleA < titleB) 79 | //sort string ascending 80 | return -1; 81 | if (titleA > titleB) return 1; 82 | return 0; //default return value (no sorting) 83 | }); 84 | } 85 | 86 | return list; 87 | } 88 | 89 | getFiltersFromQueryParams() { 90 | var filter = { 91 | categories: { 92 | technologies: [], 93 | languages: [], 94 | solutionareas: [] 95 | }, 96 | filtertext: "", 97 | sortby: "totaldownloads" 98 | }; 99 | 100 | var params = queryStringToParams(this.props.location.search); 101 | if (params.technology && params.technology.length > 0) { 102 | filter.categories.technologies = params.technology.split(","); 103 | } 104 | 105 | if (params.language && params.language.length > 0) { 106 | filter.categories.languages = params.language.split(","); 107 | } 108 | 109 | if (params.solutionarea && params.solutionarea.length > 0) { 110 | filter.categories.solutionareas = params.solutionarea.split(","); 111 | } 112 | if (params.filtertext && params.filtertext.length > 0) { 113 | filter.filtertext = params.filtertext; 114 | } 115 | if (params.sortby && params.sortby.length > 0) { 116 | filter.sortby = params.sortby; 117 | } 118 | 119 | return filter; 120 | } 121 | 122 | onListScrollEvent(e) { 123 | let element = e.target; 124 | if (element.scrollTop > 0) { 125 | !this.state.showContentHeaderShadow && 126 | this.setState({ showContentHeaderShadow: true }); 127 | } else { 128 | this.state.showContentHeaderShadow && 129 | this.setState({ showContentHeaderShadow: false }); 130 | } 131 | } 132 | render() { 133 | const contentheaderShadowStyle = { 134 | boxShadow: this.state.showContentHeaderShadow 135 | ? "0 4px 6px -6px #222" 136 | : "none" 137 | }; 138 | 139 | return ( 140 |
141 | 146 |
147 |
148 | 153 |
154 |
155 |
156 | 157 |
158 |
159 |
160 |
161 | ); 162 | } 163 | } 164 | 165 | const mapStateToProps = state => ({ 166 | samples: state.samples 167 | }); 168 | 169 | const MainContainer = connect(mapStateToProps)(withRouter(Main)); 170 | 171 | export default MainContainer; 172 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/MetricBar/MetricBar.css: -------------------------------------------------------------------------------- 1 | .metrics { 2 | margin-top: 7px; 3 | margin-bottom: 7px; 4 | font-size: 12px; 5 | line-height: 16px; 6 | color: #555555; 7 | display: flex; 8 | } 9 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/MetricBar/MetricBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { IconButton } from "office-ui-fabric-react"; 3 | import { libraryService } from "../../services"; 4 | 5 | import "./MetricBar.css"; 6 | class MetricBar extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | sentimentAction: "none" 11 | }; 12 | } 13 | 14 | static getDerivedStateFromProps(nextProps, prevState) { 15 | if (nextProps.id && nextProps.id !== prevState.id) { 16 | return { sentimentAction: localStorage.getItem(nextProps.id) }; 17 | } 18 | 19 | return null; 20 | } 21 | 22 | handleLikeClick() { 23 | // If user already liked, then decrement like count and set the sentiment state to none. 24 | // If in past disliked and choose to like the sample, then decrement dislike count and increment like count 25 | // If not action taken ealier, just increment like count and set sentiment state to liked. 26 | if (this.state.sentimentAction === "liked") { 27 | this.updateSentiment("none", -1, 0); 28 | } else if (this.state.sentimentAction === "disliked") { 29 | this.updateSentiment("liked", 1, -1); 30 | } else { 31 | this.updateSentiment("liked", 1, 0); 32 | } 33 | } 34 | 35 | handleDislikeClick() { 36 | if (this.state.sentimentAction === "liked") { 37 | this.updateSentiment("disliked", -1, 1); 38 | } else if (this.state.sentimentAction === "disliked") { 39 | this.updateSentiment("none", 0, -1); 40 | } else { 41 | this.updateSentiment("disliked", 0, 1); 42 | } 43 | } 44 | 45 | updateSentiment(choice, likeChanges, dislikeChanges) { 46 | localStorage.setItem(this.props.id, choice); 47 | this.setState({ sentimentAction: choice }); 48 | 49 | var sentimentPayload = { 50 | Id: this.props.id, 51 | LikeChanges: likeChanges, 52 | DislikeChanges: dislikeChanges 53 | }; 54 | 55 | libraryService 56 | .updateUserSentimentStats(sentimentPayload) 57 | .then(() => { 58 | // do nothing 59 | }) 60 | .catch(() => { 61 | // do nothing 62 | }); 63 | } 64 | 65 | render() { 66 | let { author, downloads, createddate, likes, dislikes } = this.props; 67 | let createdonDate = new Date(createddate); 68 | let createdonLocaleDate = createdonDate.toLocaleDateString(); 69 | 70 | let likeIconName = "Like"; 71 | let likeTitle = "Like"; 72 | let dislikeIconName = "Dislike"; 73 | let dislikeTitle = "Dislike"; 74 | 75 | if (this.state.sentimentAction === "liked") { 76 | likeIconName = "LikeSolid"; 77 | likeTitle = "Liked"; 78 | likes = likes + 1; 79 | } 80 | 81 | if (this.state.sentimentAction === "disliked") { 82 | dislikeIconName = "DislikeSolid"; 83 | dislikeTitle = "Disliked"; 84 | dislikes = dislikes + 1; 85 | } 86 | 87 | const styles = { 88 | button: { 89 | width: 16, 90 | height: 16, 91 | padding: 0, 92 | marginLeft: 7, 93 | marginRight: 7 94 | } 95 | }; 96 | return ( 97 |
98 |
99 | 100 | By: {author} | {downloads}{" "} 101 | {downloads === 1 ? "download" : "downloads"} | Created on:{" "} 102 | {createdonLocaleDate} | 103 | 104 |
105 | 106 | this.handleLikeClick()} 114 | /> 115 |
{likes}
116 | 117 | this.handleDislikeClick()} 125 | /> 126 |
{dislikes}
127 |
128 | ); 129 | } 130 | } 131 | 132 | export default MetricBar; 133 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/shared/Button.styles.js: -------------------------------------------------------------------------------- 1 | import { mergeStyleSets } from "office-ui-fabric-react"; 2 | 3 | export const buttonStyles = { 4 | root: { 5 | fontSize: "12px", 6 | height: "25px", 7 | marginRight: "8px", 8 | minWidth: "0px", 9 | paddingRight: "10px", 10 | paddingLeft: "10px" 11 | }, 12 | label: { 13 | fontWeight: "normal" 14 | } 15 | }; 16 | 17 | const secondaryButtonAdditionalStyles = { 18 | root: { 19 | backgroundColor: "white", 20 | border: "1px solid #0078D7", 21 | color: "#0058AD" 22 | }, 23 | rootHovered: { 24 | // backgroundColor: "white", 25 | border: "1px solid #0078D7", 26 | color: "#0058AD" 27 | } 28 | }; 29 | 30 | export const secondaryButtonStyles = mergeStyleSets( 31 | buttonStyles, 32 | secondaryButtonAdditionalStyles 33 | ); 34 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/shared/Constants.js: -------------------------------------------------------------------------------- 1 | export const technologies = [ 2 | "Functions 3.x", 3 | "Functions 2.x", 4 | "Functions 1.x", 5 | "Logic Apps", 6 | "Blob Storage", 7 | "Storage Queue", 8 | "Cosmos DB", 9 | "Cognitive Services", 10 | "Azure Active Directory", 11 | "App Service", 12 | "Key Vault", 13 | "SQL Server", 14 | "Service Bus Queue", 15 | "Event Grid" 16 | ]; 17 | 18 | export const solutionAreas = [ 19 | "Web API", 20 | "Data Processing", 21 | "Integration", 22 | "Authentication", 23 | "Automation", 24 | "Event Processing", 25 | "Machine Learning", 26 | "Scheduled Jobs", 27 | "Static Website", 28 | "Gaming", 29 | "IoT" 30 | ]; 31 | 32 | export const languages = [ 33 | "JavaScript", 34 | "TypeScript", 35 | "Java", 36 | "C#", 37 | "C# Script", 38 | "F#", 39 | "Python", 40 | "PowerShell" 41 | ]; 42 | 43 | export const NotApplicableLanguage = "na"; 44 | export const OtherSolutionArea = "Other"; 45 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/shared/PageHeaderWithBackButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { IconButton } from "office-ui-fabric-react"; 4 | 5 | import "./PageHeaderWithBackButton.scss"; 6 | 7 | class PageHeaderWithBackButton extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.handleHomeButtonClick = this.handleHomeButtonClick.bind(this); 11 | } 12 | 13 | handleHomeButtonClick() { 14 | this.props.history.push("/"); 15 | } 16 | 17 | render() { 18 | let { title } = this.props; 19 | 20 | const homeButton = { 21 | button: { 22 | width: 17, 23 | height: 18, 24 | marginRight: 12 25 | } 26 | }; 27 | 28 | return ( 29 |
30 |
31 |
32 | this.handleHomeButtonClick()} 38 | /> 39 |
40 |
41 | {title} 42 |
43 |
44 |
{this.props.children}
45 |
46 | ); 47 | } 48 | } 49 | 50 | export default withRouter(PageHeaderWithBackButton); 51 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/shared/PageHeaderWithBackButton.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | padding-top: 20px; 3 | // margin-bottom: 23px; // this cannot be set at the common header control because of pivot control sizes 4 | background: #ffffff; 5 | padding-left: 23px; 6 | } 7 | .page-title-container { 8 | display: flex; 9 | flex-direction: row; 10 | margin-bottom: 11px; 11 | } 12 | 13 | .back-button-icon-container { 14 | margin: auto 0; 15 | height: 18px; 16 | } 17 | 18 | .page-title { 19 | font-size: 18px; 20 | font-weight: bold; 21 | color: black; 22 | margin-bottom: auto; 23 | } 24 | 25 | .page-description-container { 26 | font-size: 12px; 27 | line-height: 14px; 28 | color: #161616; 29 | } 30 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/shared/SignInButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { PrimaryButton } from "office-ui-fabric-react"; 3 | 4 | class SignInButton extends Component { 5 | handleButtonClick() { 6 | const currentLocation = encodeURIComponent(window.location); 7 | window.location = `/api/user/login?returnUrl=${currentLocation}`; 8 | } 9 | 10 | render() { 11 | const buttonStyles = { 12 | root: { 13 | fontSize: "12px", 14 | height: "32px" 15 | }, 16 | label: { 17 | fontWeight: "normal" 18 | } 19 | }; 20 | 21 | return ( 22 | 28 | Sign in with GitHub 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default SignInButton; 35 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/sidebar/SideBar.css: -------------------------------------------------------------------------------- 1 | .sidebar-wrapper{ 2 | padding-left: 24px; 3 | padding-top: 20px; 4 | padding-right: 20px; 5 | } 6 | .filterby-title { 7 | font-size: 18px; 8 | line-height: 24px; 9 | margin-bottom: 12px; 10 | } 11 | 12 | .Filter-list-header{ 13 | font-size: 14px; 14 | line-height: 16px; 15 | color: #555555; 16 | display: inline-block; 17 | } 18 | 19 | .filterset{ 20 | margin-bottom: 23px; 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/components/sidebar/SideBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { withRouter } from "react-router-dom"; 4 | import { Checkbox } from "office-ui-fabric-react/lib/index"; 5 | import { 6 | paramsToQueryString, 7 | queryStringToParams, 8 | trackEvent 9 | } from "../../helpers"; 10 | 11 | import * as Constants from "../shared/Constants"; 12 | import "./SideBar.css"; 13 | 14 | class SideBar extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | filters: this.props.initialFilters 19 | }; 20 | } 21 | 22 | isChecked(category, item) { 23 | return this.state.filters[category].includes(item); 24 | } 25 | 26 | checkboxclicked(ev, checked, category, item) { 27 | var currentFilters = this.state.filters; 28 | var categoryArray = currentFilters[category]; 29 | if (!checked) { 30 | categoryArray = categoryArray.filter(i => i !== item); 31 | } else { 32 | categoryArray.push(item); 33 | } 34 | 35 | currentFilters[category] = categoryArray; 36 | this.setState({ filters: currentFilters }, () => this.ChangeUrl()); 37 | trackEvent(`/filter/change/${category}`, currentFilters); 38 | } 39 | 40 | ChangeUrl() { 41 | var params = queryStringToParams(this.props.location.search); 42 | delete params["technology"]; 43 | delete params["language"]; 44 | delete params["solutionarea"]; 45 | if (this.state.filters.technologies.length > 0) { 46 | params["technology"] = this.state.filters.technologies.join(); 47 | } 48 | if (this.state.filters.languages.length > 0) { 49 | params["language"] = this.state.filters.languages.join(); 50 | } 51 | if (this.state.filters.solutionareas.length > 0) { 52 | params["solutionarea"] = this.state.filters.solutionareas.join(); 53 | } 54 | 55 | this.props.history.push(paramsToQueryString(params)); 56 | } 57 | 58 | render() { 59 | const checkboxStyles = index => { 60 | return { 61 | root: { 62 | marginTop: index === 0 ? "9px" : "0px", 63 | marginBottom: "5px" 64 | } 65 | }; 66 | }; 67 | 68 | return ( 69 |
70 |
Filter by
71 |
72 | Technology 73 | {Constants.technologies.map((technology, index) => ( 74 | 80 | this.checkboxclicked(ev, checked, "technologies", technology) 81 | } 82 | /> 83 | ))} 84 |
85 | 86 |
87 | Language 88 | {Constants.languages.map((language, index) => ( 89 | 95 | this.checkboxclicked(ev, checked, "languages", language) 96 | } 97 | /> 98 | ))} 99 |
100 | 101 |
102 | Solution Area 103 | {Constants.solutionAreas.map((solutionarea, index) => ( 104 | 110 | this.checkboxclicked(ev, checked, "solutionareas", solutionarea) 111 | } 112 | /> 113 | ))} 114 |
115 |
116 | ); 117 | } 118 | } 119 | 120 | const mapStateToProps = state => ({}); 121 | 122 | const mapDispatchToProps = {}; 123 | 124 | const SideBarContainer = connect( 125 | mapStateToProps, 126 | mapDispatchToProps 127 | )(SideBar); 128 | 129 | export default withRouter(SideBarContainer); 130 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/helpers/appinsights.js: -------------------------------------------------------------------------------- 1 | export function trackEvent(eventName, eventData) { 2 | let appInsights = window.appInsights; 3 | if (typeof appInsights !== "undefined") { 4 | appInsights.trackEvent(eventName, eventData); 5 | } 6 | } 7 | 8 | export function trackError(errorString, properties) { 9 | let appInsights = window.appInsights; 10 | if (typeof appInsights !== "undefined") { 11 | appInsights.trackTrace(errorString, properties, 3); 12 | } 13 | } 14 | 15 | export function trackException(exception, properties) { 16 | let appInsights = window.appInsights; 17 | if (typeof appInsights !== "undefined") { 18 | appInsights.trackException(exception, null, properties); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/helpers/handle-response.js: -------------------------------------------------------------------------------- 1 | import { trackError, trackException } from "./appinsights"; 2 | 3 | export function handleResponse(response) { 4 | try { 5 | return response.text().then(text => { 6 | if (response.ok) { 7 | return text; 8 | } 9 | const error = { 10 | status: response.status, 11 | error: text || response.statusText 12 | }; 13 | trackError(error.error, { ...error, url: response.url }); 14 | return Promise.reject(error); 15 | }); 16 | } catch (ex) { 17 | trackException(ex, { url: response.url, method: "handleResponse" }); 18 | return Promise.reject({ 19 | status: -1, 20 | error: "Encountered unexpected exception." 21 | }); 22 | } 23 | } 24 | 25 | export function handleJsonResponse(response) { 26 | return handleResponse(response).then(data => { 27 | try { 28 | return JSON.parse(data); 29 | } catch (ex) { 30 | trackException(ex, { url: response.url, method: "handleJsonResponse" }); 31 | return Promise.reject({ 32 | status: -1, 33 | error: "Encountered unexpected exception." 34 | }); 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from "./handle-response"; 2 | export * from "./query-param"; 3 | export * from "./appinsights"; 4 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/helpers/query-param.js: -------------------------------------------------------------------------------- 1 | export function queryStringToParams(queryString) { 2 | if (queryString.indexOf("?") > -1) { 3 | queryString = queryString.split("?")[1]; 4 | } 5 | var pairs = queryString.split("&"); 6 | var result = {}; 7 | pairs.forEach(function(pair) { 8 | pair = pair.split("="); 9 | if (pair[0] && pair[0].length > 0) { 10 | result[pair[0]] = decodeURIComponent(pair[1] || ""); 11 | } 12 | }); 13 | return result; 14 | } 15 | 16 | export function paramsToQueryString(params) { 17 | var queryString = Object.keys(params) 18 | .map(key => { 19 | return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); 20 | }) 21 | .join("&"); 22 | return "?" + queryString; 23 | } 24 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/helpers/registerIcons.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { registerIcons } from "office-ui-fabric-react"; 3 | import "./registerIcons.scss"; 4 | import { ReactComponent as GithubIconSvg } from "../assets/github.svg"; 5 | import { ReactComponent as ContributionSvg } from "../assets/contribution.svg"; 6 | 7 | export default function registerCustomIcons() { 8 | registerIcons({ 9 | icons: { 10 | "GitHub-12px": , 11 | "GitHub-16px": , 12 | "contribution-svg": 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/helpers/registerIcons.scss: -------------------------------------------------------------------------------- 1 | .icon-16px { 2 | width: 16px; 3 | height: 16px; 4 | vertical-align: baseline; 5 | fill: currentColor; 6 | } 7 | 8 | .icon-12px { 9 | width: 12px; 10 | height: 12px; 11 | vertical-align: baseline; 12 | margin-right: 4px; 13 | fill: currentColor; 14 | } 15 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | overflow: hidden; 6 | } 7 | html, 8 | body, 9 | #root, 10 | #container { 11 | height: 100%; 12 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | } -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/index.js: -------------------------------------------------------------------------------- 1 | // These must be the first lines in src/index.js 2 | import "react-app-polyfill/ie11"; 3 | import "react-app-polyfill/stable"; 4 | 5 | import "./index.css"; 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | import { BrowserRouter } from "react-router-dom"; 9 | import { Provider } from "react-redux"; 10 | import { initializeIcons } from "office-ui-fabric-react"; 11 | import configureStore from "./reducers"; 12 | import registerCustomIcons from "./helpers/registerIcons"; 13 | 14 | import AppContainer from "./App"; 15 | 16 | const store = configureStore(); 17 | registerCustomIcons(); 18 | initializeIcons(); 19 | const rootElement = document.getElementById("root"); 20 | 21 | ReactDOM.render( 22 | 23 | 24 | 25 | 26 | , 27 | rootElement 28 | ); 29 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/reducers/authenticationReducer.js: -------------------------------------------------------------------------------- 1 | import { userActionTypes } from "../actions/actionTypes"; 2 | import initialState from "./initialState"; 3 | 4 | export default function authenticationReducer( 5 | state = initialState.authentication, 6 | action 7 | ) { 8 | switch (action.type) { 9 | case userActionTypes.GETCURRENTUSER_REQUEST: 10 | return { 11 | ...state, 12 | loading: true 13 | }; 14 | case userActionTypes.GETCURRENTUSER_SUCCESS: 15 | return { 16 | ...state, 17 | loading: false, 18 | loggedIn: true, 19 | user: action.user 20 | }; 21 | case userActionTypes.GETCURRENTUSER_FAILURE: 22 | return { 23 | ...state, 24 | loading: false, 25 | loggedIn: false, 26 | user: {} 27 | }; 28 | case userActionTypes.LOGOUT: 29 | return { 30 | ...state, 31 | loggedIn: false, 32 | user: {} 33 | }; 34 | default: 35 | return state; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore } from "redux"; 2 | 3 | import samples from "./sampleReducer"; 4 | import authentication from "./authenticationReducer"; 5 | 6 | const rootReducer = combineReducers({ 7 | samples, 8 | authentication 9 | }); 10 | 11 | export default function configureStore(initialState) { 12 | return createStore(rootReducer, initialState); 13 | } 14 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/reducers/initialState.js: -------------------------------------------------------------------------------- 1 | export default { 2 | samples: [], 3 | authentication: { 4 | loggedIn: false, 5 | user: {}, 6 | loading: false 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/reducers/sampleReducer.js: -------------------------------------------------------------------------------- 1 | import { sampleActionTypes } from "../actions/actionTypes"; 2 | import initialState from "./initialState"; 3 | 4 | export default function sampleReducer(state = initialState.samples, action) { 5 | switch (action.type) { 6 | case sampleActionTypes.GETSAMPLES_SUCCESS: 7 | return action.samples; 8 | default: 9 | return state; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/services/github.service.js: -------------------------------------------------------------------------------- 1 | import { handleResponse } from "../helpers"; 2 | 3 | export const githubService = { 4 | getReadMe, 5 | getLicense, 6 | getArmTemplate 7 | }; 8 | 9 | function getRawContentUrl(repoUrl, fileName) { 10 | let rawUrl = repoUrl 11 | .replace("https://github.com", "https://raw.githubusercontent.com") 12 | .replace("/tree/", "/"); 13 | rawUrl = rawUrl.includes("/master/") ? rawUrl + "/" : rawUrl + "/master/"; 14 | let contentUrl = rawUrl + fileName; 15 | return contentUrl; 16 | } 17 | 18 | function getReadMe(repoUrl) { 19 | const requestOptions = { 20 | method: "GET" 21 | }; 22 | const readMeUrl = getRawContentUrl(repoUrl, "README.md"); 23 | return fetch(readMeUrl, requestOptions).then(handleResponse); 24 | } 25 | 26 | function getLicense(licenseUrl, repoUrl) { 27 | const requestOptions = { 28 | method: "GET" 29 | }; 30 | const contentUrl = licenseUrl || getRawContentUrl(repoUrl, "LICENSE"); 31 | return fetch(contentUrl, requestOptions).then(handleResponse); 32 | } 33 | 34 | function getArmTemplate(templateUrl) { 35 | const requestOptions = { 36 | method: "GET" 37 | }; 38 | return fetch(templateUrl, requestOptions).then(handleResponse); 39 | } 40 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/services/index.js: -------------------------------------------------------------------------------- 1 | export * from "./user.service"; 2 | export * from "./library.service"; 3 | export * from "./github.service"; 4 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/services/library.service.js: -------------------------------------------------------------------------------- 1 | import { handleResponse, handleJsonResponse } from "../helpers"; 2 | import { trackException } from "../helpers/appinsights"; 3 | 4 | export const libraryService = { 5 | getAllSamples, 6 | submitNewSample, 7 | updateUserSentimentStats, 8 | updateDownloadCount 9 | }; 10 | 11 | function getAllSamples() { 12 | const requestOptions = { 13 | method: "GET" 14 | }; 15 | 16 | return fetch("/api/Library", requestOptions).then(handleJsonResponse); 17 | } 18 | 19 | function submitNewSample(item) { 20 | const requestOptions = { 21 | method: "PUT", 22 | body: JSON.stringify(item), 23 | headers: { 24 | "Content-Type": "application/json" 25 | } 26 | }; 27 | return fetch("/api/library", requestOptions) 28 | .then(handleJsonResponse) 29 | .catch(data => { 30 | let error = data.error; 31 | if (data.status === 400) { 32 | try { 33 | error = JSON.parse(data.error); 34 | } catch (ex) { 35 | trackException(ex, { method: "submitNewSample" }); 36 | } 37 | } 38 | return Promise.reject({ 39 | status: data.status, 40 | error: error 41 | }); 42 | }); 43 | } 44 | 45 | function updateUserSentimentStats(sentimentPayload) { 46 | const requestOptions = { 47 | method: "PUT", 48 | body: JSON.stringify(sentimentPayload), 49 | headers: { 50 | "Content-Type": "application/json" 51 | } 52 | }; 53 | return fetch("/api/metrics/sentiment", requestOptions).then(handleResponse); 54 | } 55 | 56 | function updateDownloadCount(id) { 57 | const requestOptions = { 58 | method: "PUT", 59 | body: '"' + id + '"', 60 | headers: { 61 | "Content-Type": "application/json" 62 | } 63 | }; 64 | return fetch("/api/metrics/downloads", requestOptions).then(handleResponse); 65 | } 66 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ClientApp/src/services/user.service.js: -------------------------------------------------------------------------------- 1 | import { handleResponse, handleJsonResponse } from "../helpers"; 2 | 3 | export const userService = { 4 | getCurrentUser, 5 | logout 6 | }; 7 | 8 | function getCurrentUser() { 9 | const requestOptions = { 10 | method: "GET" 11 | }; 12 | return fetch("/api/user", requestOptions).then(handleJsonResponse); 13 | } 14 | 15 | function logout() { 16 | const requestOptions = { 17 | method: "GET" 18 | }; 19 | return fetch("/api/user/logout", requestOptions).then(handleResponse); 20 | } 21 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Connected Services/Application Insights/ConnectedService.json: -------------------------------------------------------------------------------- 1 | { 2 | "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider", 3 | "Version": "8.14.11009.1", 4 | "GettingStartedDocument": { 5 | "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432" 6 | } 7 | } -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Controllers/LibraryController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Text.RegularExpressions; 6 | using ServerlessLibrary.Models; 7 | using Newtonsoft.Json; 8 | using System.Threading.Tasks; 9 | 10 | 11 | // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 12 | 13 | namespace ServerlessLibrary.Controllers 14 | { 15 | [Produces("application/json")] 16 | [Route("api/[controller]")] 17 | public class LibraryController : Controller 18 | { 19 | ICacheService _cacheService; 20 | ILibraryStore _libraryStore; 21 | 22 | public LibraryController(ICacheService cacheService, ILibraryStore libraryStore) 23 | { 24 | this._cacheService = cacheService; 25 | this._libraryStore = libraryStore; 26 | } 27 | 28 | // GET: api/ 29 | [HttpGet] 30 | [ProducesResponseType(typeof(IEnumerable), 200)] 31 | public JsonResult Get(string filterText, string language) 32 | { 33 | //TODO: Add filtering for solution areas and technologies. 34 | var results = _cacheService.GetCachedItems(); 35 | var filteredResults = results.Where( 36 | x => 37 | ( 38 | (string.IsNullOrWhiteSpace(language) || x.Language == language) && 39 | ( 40 | string.IsNullOrWhiteSpace(filterText) 41 | || Regex.IsMatch(x.Title, filterText, RegexOptions.IgnoreCase) 42 | || Regex.IsMatch(x.Description, filterText, RegexOptions.IgnoreCase) 43 | || Regex.IsMatch(x.Repository.Replace("https://github.com/", "", StringComparison.InvariantCulture), filterText, RegexOptions.IgnoreCase) 44 | || (!string.IsNullOrWhiteSpace(x.Author) && Regex.IsMatch(x.Author, filterText, RegexOptions.IgnoreCase)) 45 | || (x.Tags != null && x.Tags.Any(t => Regex.IsMatch(t, filterText, RegexOptions.IgnoreCase))) 46 | || (x.Technologies != null && x.Technologies.Any(t => Regex.IsMatch(t, filterText, RegexOptions.IgnoreCase))) 47 | || (x.SolutionAreas != null && x.SolutionAreas.Any(c => Regex.IsMatch(c, filterText, RegexOptions.IgnoreCase))) 48 | ) 49 | ) 50 | ); 51 | 52 | return new JsonResult(filteredResults); 53 | } 54 | 55 | [HttpPut] 56 | [ProducesResponseType(typeof(LibraryItem), 200)] 57 | public async Task Put([FromBody]LibraryItem libraryItem) 58 | { 59 | if (!User.Identity.IsAuthenticated) 60 | { 61 | return Unauthorized(); 62 | } 63 | 64 | var validationsErrors = ValidateLibraryItem(libraryItem); 65 | if (validationsErrors?.Count > 0) 66 | { 67 | return BadRequest(validationsErrors); 68 | } 69 | 70 | // assign id, created date 71 | libraryItem.Id = Guid.NewGuid().ToString(); 72 | libraryItem.CreatedDate = DateTime.UtcNow; 73 | 74 | // set the author to current authenticated user 75 | GitHubUser user = new GitHubUser(User); 76 | libraryItem.Author = user.UserName; 77 | await StorageHelper.submitContributionForApproval(JsonConvert.SerializeObject(libraryItem)); 78 | return new JsonResult(libraryItem); 79 | } 80 | 81 | private static List ValidateLibraryItem(LibraryItem libraryItem) 82 | { 83 | List errors = new List(); 84 | if (string.IsNullOrWhiteSpace(libraryItem.Title)) 85 | { 86 | errors.Add("Title cannot be empty"); 87 | } 88 | 89 | if (string.IsNullOrWhiteSpace(libraryItem.Repository) || !IsValidUri(libraryItem.Repository)) 90 | { 91 | errors.Add("Repository URL must be a valid GitHub URL"); 92 | } 93 | 94 | if (string.IsNullOrWhiteSpace(libraryItem.Description)) 95 | { 96 | errors.Add("Description cannot be empty"); 97 | } 98 | 99 | if (libraryItem.Technologies.Length == 0) 100 | { 101 | errors.Add("At least one technology must be specified"); 102 | } 103 | 104 | if (string.IsNullOrWhiteSpace(libraryItem.Language)) 105 | { 106 | errors.Add("Language must be specified"); 107 | } 108 | 109 | if (libraryItem.SolutionAreas.Length == 0) 110 | { 111 | errors.Add("At least one solution area must be specified"); 112 | } 113 | 114 | if (!string.IsNullOrWhiteSpace(libraryItem.Template) && !IsValidUri(libraryItem.Template, "raw.githubusercontent.com")) 115 | { 116 | errors.Add("ARM template URL must point to the raw path of the ARM template (https://raw.githubusercontent.com/...)"); 117 | } 118 | 119 | return errors; 120 | } 121 | 122 | private static bool IsValidUri(string uriString, string expectedHostName = null) 123 | { 124 | try 125 | { 126 | var uri = new Uri(uriString); 127 | if (string.IsNullOrWhiteSpace(expectedHostName)) 128 | { 129 | return true; 130 | } 131 | else 132 | { 133 | return string.Equals(expectedHostName, uri.Host, StringComparison.OrdinalIgnoreCase); 134 | } 135 | } 136 | catch 137 | { 138 | return false; 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Controllers/MetricsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | using Newtonsoft.Json; 4 | using ServerlessLibrary.Models; 5 | using System; 6 | 7 | namespace ServerlessLibrary.Controllers 8 | { 9 | [Route("api/[controller]/[action]")] 10 | [ApiController] 11 | public class MetricsController : ControllerBase 12 | { 13 | private readonly ILogger logger; 14 | 15 | public MetricsController(ILogger logger) 16 | { 17 | this.logger = logger; 18 | } 19 | 20 | // PUT api//downloads 21 | [ProducesResponseType(typeof(bool), 200)] 22 | [HttpPut] 23 | public JsonResult Downloads([FromBody]string id) 24 | { 25 | try 26 | { 27 | StorageHelper.updateUserStats(JsonConvert.SerializeObject(new { id, userAction = "download" })).Wait(); 28 | } 29 | catch (Exception ex) 30 | { 31 | this.logger.LogError(ex, "Unable to update download count"); 32 | } 33 | 34 | return new JsonResult(true); 35 | } 36 | 37 | // PUT api//sentiment 38 | [ProducesResponseType(typeof(bool), 200)] 39 | [HttpPut] 40 | public IActionResult Sentiment([FromBody]SentimentPayload sentimentPayload) 41 | { 42 | if (sentimentPayload.LikeChanges < -1 43 | || sentimentPayload.LikeChanges > 1 44 | || sentimentPayload.LikeChanges == sentimentPayload.DislikeChanges) 45 | { 46 | return BadRequest("Invalid values for like or dislike count"); 47 | } 48 | 49 | try 50 | { 51 | StorageHelper.updateUserStats(JsonConvert.SerializeObject(new 52 | { 53 | id = sentimentPayload.Id, 54 | userAction = "Sentiment", 55 | likeChanges = sentimentPayload.LikeChanges, 56 | dislikeChanges = sentimentPayload.DislikeChanges 57 | })).Wait(); 58 | } 59 | catch (Exception ex) 60 | { 61 | this.logger.LogError(ex, "Unable to update sentiments"); 62 | } 63 | 64 | return new JsonResult(true); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Mvc; 4 | using ServerlessLibrary.Models; 5 | 6 | namespace ServerlessLibrary.Controllers 7 | { 8 | [Route("api/[controller]")] 9 | [ApiController] 10 | public class UserController : ControllerBase 11 | { 12 | [HttpGet("login"), HttpPost("login")] 13 | public IActionResult Login(string returnUrl = "/") 14 | { 15 | if (User.Identity.IsAuthenticated) 16 | { 17 | return new RedirectResult(returnUrl); 18 | } 19 | 20 | // Instruct the middleware corresponding to the requested external identity 21 | // provider to redirect the user agent to its own authorization endpoint. 22 | // Note: the authenticationScheme parameter must match the value configured in Startup.cs. 23 | // If no scheme is provided then the DefaultChallengeScheme will be used 24 | return Challenge(new AuthenticationProperties { RedirectUri = returnUrl }); 25 | } 26 | 27 | [HttpGet("logout"), HttpPost("logout")] 28 | public IActionResult Logout() 29 | { 30 | // Instruct the cookies middleware to delete the local cookie which 31 | // was created after a successful authentication flow. 32 | return SignOut( 33 | new AuthenticationProperties { RedirectUri = "/" }, 34 | CookieAuthenticationDefaults.AuthenticationScheme); 35 | } 36 | 37 | [HttpGet] 38 | [ProducesResponseType(typeof(GitHubUser), 200)] 39 | public IActionResult Get() 40 | { 41 | if (User.Identity.IsAuthenticated) 42 | { 43 | GitHubUser user = new GitHubUser(User); 44 | return Ok(user); 45 | } 46 | 47 | return Unauthorized(); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /ServerlessLibraryAPI/CosmosLibraryStore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.Azure.Cosmos; 5 | using Microsoft.Azure.Cosmos.Fluent; 6 | using ServerlessLibrary.Models; 7 | 8 | namespace ServerlessLibrary 9 | { 10 | /// 11 | /// Cosmos db Library store 12 | /// 13 | public class CosmosLibraryStore : ILibraryStore 14 | { 15 | public CosmosLibraryStore() 16 | { 17 | CosmosDBRepository.Initialize(); 18 | } 19 | 20 | public async Task Add(LibraryItem libraryItem) 21 | { 22 | await CosmosDBRepository.CreateItemAsync(libraryItem); 23 | } 24 | 25 | async public Task> GetAllItems() 26 | { 27 | IEnumerable libraryItems = await CosmosDBRepository.GetAllItemsAsync(); 28 | return libraryItems.ToList(); 29 | } 30 | } 31 | 32 | /// 33 | /// Cosmos db APIs 34 | /// 35 | /// 36 | static class CosmosDBRepository where T : class 37 | { 38 | private static readonly string DatabaseId = ServerlessLibrarySettings.Database; 39 | private static readonly string CollectionId = ServerlessLibrarySettings.Collection; 40 | private static Container container; 41 | 42 | public static async Task GetItemAsync(string id) 43 | { 44 | try 45 | { 46 | ItemResponse response = await container.ReadItemAsync(id, PartitionKey.None); 47 | return response.Resource; 48 | } 49 | catch (CosmosException e) 50 | { 51 | if (e.StatusCode == System.Net.HttpStatusCode.NotFound) 52 | { 53 | return null; 54 | } 55 | else 56 | { 57 | throw; 58 | } 59 | } 60 | } 61 | 62 | public static async Task> GetAllItemsAsync() 63 | { 64 | FeedIterator query = container.GetItemQueryIterator( 65 | queryDefinition: null, 66 | requestOptions: new QueryRequestOptions() { MaxItemCount = -1 }); // NOTE: FeedOptions.EnableCrossPartitionQuery is removed in SDK v3 (https://docs.microsoft.com/en-us/azure/cosmos-db/sql/migrate-dotnet-v3?tabs=dotnet-v3#changes-to-feedoptions-queryrequestoptions-in-v30-sdk) 67 | 68 | List results = new List(); 69 | using (query) 70 | { 71 | while (query.HasMoreResults) 72 | { 73 | results.AddRange(await query.ReadNextAsync()); 74 | } 75 | } 76 | 77 | return results; 78 | } 79 | 80 | public static async Task CreateItemAsync(T item) 81 | { 82 | ItemResponse response = await container.CreateItemAsync(item, PartitionKey.None); 83 | return response.Resource; 84 | } 85 | 86 | public static async Task UpdateItemAsync(string id, T item) 87 | { 88 | ItemResponse response = await container.UpsertItemAsync(item, PartitionKey.None); 89 | return response.Resource; 90 | } 91 | 92 | public static async Task DeleteItemAsync(string id) 93 | { 94 | await container.DeleteItemAsync(id, PartitionKey.None); 95 | } 96 | 97 | public static void Initialize() 98 | { 99 | if (container == null) 100 | { 101 | CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder( 102 | ServerlessLibrarySettings.CosmosEndpoint, 103 | ServerlessLibrarySettings.CosmosAuthkey); 104 | CosmosClient client = cosmosClientBuilder.Build(); 105 | 106 | DatabaseResponse databaseResponse = client.CreateDatabaseIfNotExistsAsync(DatabaseId).Result; 107 | Database database = databaseResponse; 108 | 109 | ContainerResponse containerResponse = database.CreateContainerIfNotExistsAsync(id: CollectionId, partitionKeyPath: "/_partitionKey", throughput: 400).Result; 110 | container = containerResponse; 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ILibraryStore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using ServerlessLibrary.Models; 4 | 5 | namespace ServerlessLibrary 6 | { 7 | /// 8 | /// Interface for serverless library store 9 | /// 10 | public interface ILibraryStore 11 | { 12 | /// 13 | /// Add an item to library 14 | /// 15 | /// Library item 16 | Task Add(LibraryItem libraryItem); 17 | 18 | /// 19 | /// Get all items from library 20 | /// 21 | /// 22 | Task> GetAllItems(); 23 | } 24 | } -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Models/GitHubUser.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using static ServerlessLibrary.OAuth.GitHub.GitHubAuthenticationConstants; 3 | 4 | namespace ServerlessLibrary.Models 5 | { 6 | public class GitHubUser 7 | { 8 | public GitHubUser() 9 | { 10 | } 11 | 12 | public GitHubUser(ClaimsPrincipal claimsPrincipal) 13 | { 14 | this.FullName = claimsPrincipal.FindFirstValue(Claims.Name); 15 | this.Email = claimsPrincipal.FindFirstValue(ClaimTypes.Email); 16 | this.AvatarUrl = claimsPrincipal.FindFirstValue(Claims.Avatar); 17 | this.UserName = claimsPrincipal.FindFirstValue(Claims.Login); 18 | } 19 | 20 | public string FullName { get; set; } 21 | 22 | public string Email { get; set; } 23 | 24 | public string AvatarUrl { get; set; } 25 | 26 | public string UserName { get; set; } 27 | 28 | public string DisplayName 29 | { 30 | get 31 | { 32 | if (!string.IsNullOrWhiteSpace(this.FullName)) 33 | { 34 | return this.FullName.Split(' ')?[0]; 35 | } 36 | 37 | if (!string.IsNullOrWhiteSpace(this.UserName)) 38 | { 39 | return this.UserName; 40 | } 41 | 42 | return string.Empty; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Models/LibraryItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace ServerlessLibrary.Models 5 | { 6 | public class LibraryItemWithStats : LibraryItem 7 | { 8 | [JsonProperty(PropertyName = "totaldownloads", DefaultValueHandling = DefaultValueHandling.Include)] 9 | public int TotalDownloads { get; set; } 10 | 11 | [JsonProperty(PropertyName = "likes", DefaultValueHandling = DefaultValueHandling.Include)] 12 | public int Likes { get; set; } 13 | 14 | [JsonProperty(PropertyName = "dislikes", DefaultValueHandling = DefaultValueHandling.Include)] 15 | public int Dislikes { get; set; } 16 | } 17 | 18 | public class LibraryItem 19 | { 20 | [JsonProperty(PropertyName = "id")] 21 | public string Id { get; set; } 22 | 23 | [JsonProperty(PropertyName = "createddate")] 24 | public DateTime CreatedDate { get; set; } 25 | 26 | [JsonProperty(PropertyName = "title", DefaultValueHandling = DefaultValueHandling.Include)] 27 | public string Title { get; set; } 28 | 29 | [JsonProperty(PropertyName = "template", DefaultValueHandling = DefaultValueHandling.Include)] 30 | public string Template { get; set; } 31 | 32 | [JsonProperty(PropertyName = "repository", DefaultValueHandling = DefaultValueHandling.Include)] 33 | public string Repository { get; set; } 34 | 35 | [JsonProperty(PropertyName = "description", DefaultValueHandling = DefaultValueHandling.Include)] 36 | public string Description { get; set; } 37 | 38 | [JsonProperty(PropertyName = "tags", DefaultValueHandling = DefaultValueHandling.Include)] 39 | public string[] Tags { get; set; } 40 | 41 | [JsonProperty(PropertyName = "language", DefaultValueHandling = DefaultValueHandling.Include)] 42 | public string Language { get; set; } 43 | 44 | [JsonProperty(PropertyName = "technologies", DefaultValueHandling = DefaultValueHandling.Include)] 45 | public string[] Technologies { get; set; } 46 | 47 | [JsonProperty(PropertyName = "solutionareas", DefaultValueHandling = DefaultValueHandling.Include)] 48 | public string[] SolutionAreas { get; set; } 49 | 50 | [JsonProperty(PropertyName = "author", DefaultValueHandling = DefaultValueHandling.Include)] 51 | public string Author { get; internal set; } 52 | 53 | internal T ConvertTo() 54 | { 55 | var serializedObj = JsonConvert.SerializeObject(this); 56 | return JsonConvert.DeserializeObject(serializedObj); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Models/SentimentPayload.cs: -------------------------------------------------------------------------------- 1 | namespace ServerlessLibrary.Models 2 | { 3 | public class SentimentPayload 4 | { 5 | public string Id { get; set; } 6 | public int LikeChanges { get; set; } 7 | public int DislikeChanges { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/OAuth.GitHub/GitHubAuthenticationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace ServerlessLibrary.OAuth.GitHub 2 | { 3 | /// 4 | /// Contains constants specific to the . 5 | /// 6 | public static class GitHubAuthenticationConstants 7 | { 8 | public static class Claims 9 | { 10 | public const string Name = "urn:github:name"; 11 | public const string Url = "urn:github:url"; 12 | public const string Login = "urn:github:login"; 13 | public const string Avatar = "urn:github:avatar"; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/OAuth.GitHub/GitHubAuthenticationDefaults.cs: -------------------------------------------------------------------------------- 1 | namespace ServerlessLibrary.OAuth.GitHub 2 | { 3 | /// 4 | /// Default values used by the GitHub authentication middleware. 5 | /// 6 | public static class GitHubAuthenticationDefaults 7 | { 8 | /// 9 | /// Default value for . 10 | /// 11 | public const string AuthenticationScheme = "GitHub"; 12 | 13 | /// 14 | /// Default value for . 15 | /// 16 | public const string DisplayName = "GitHub"; 17 | 18 | /// 19 | /// Default value for . 20 | /// 21 | public const string Issuer = "GitHub"; 22 | 23 | /// 24 | /// Default value for . 25 | /// 26 | public const string CallbackPath = "/signin-github"; 27 | 28 | /// 29 | /// Default value for . 30 | /// 31 | public const string AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; 32 | 33 | /// 34 | /// Default value for . 35 | /// 36 | public const string TokenEndpoint = "https://github.com/login/oauth/access_token"; 37 | 38 | /// 39 | /// Default value for . 40 | /// 41 | public const string UserInformationEndpoint = "https://api.github.com/user"; 42 | 43 | /// 44 | /// Default value for . 45 | /// 46 | public const string UserEmailsEndpoint = "https://api.github.com/user/emails"; 47 | 48 | /// 49 | /// Scope for . 50 | /// 51 | public const string UserInformationScope = "user"; 52 | 53 | /// 54 | /// Scope for . 55 | /// 56 | public const string UserEmailsScope = "user:email"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/OAuth.GitHub/GitHubAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Security.Claims; 5 | using System.Text.Encodings.Web; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authentication.OAuth; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Options; 11 | using Newtonsoft.Json.Linq; 12 | 13 | namespace ServerlessLibrary.OAuth.GitHub 14 | { 15 | public class GitHubAuthenticationHandler : OAuthHandler 16 | { 17 | public GitHubAuthenticationHandler( 18 | IOptionsMonitor options, 19 | ILoggerFactory logger, 20 | UrlEncoder encoder, 21 | ISystemClock clock) 22 | : base(options, logger, encoder, clock) 23 | { 24 | } 25 | 26 | protected override async Task CreateTicketAsync(ClaimsIdentity identity, 27 | AuthenticationProperties properties, OAuthTokenResponse tokens) 28 | { 29 | var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); 30 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 31 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); 32 | 33 | var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); 34 | if (!response.IsSuccessStatusCode) 35 | { 36 | Logger.LogError("An error occurred while retrieving the user profile: the remote server " + 37 | "returned a {Status} response with the following payload: {Headers} {Body}.", 38 | /* Status: */ response.StatusCode, 39 | /* Headers: */ response.Headers.ToString(), 40 | /* Body: */ await response.Content.ReadAsStringAsync()); 41 | 42 | throw new HttpRequestException("An error occurred while retrieving the user profile."); 43 | } 44 | 45 | var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); 46 | 47 | var principal = new ClaimsPrincipal(identity); 48 | var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload); 49 | 50 | context.RunClaimActions(payload); 51 | 52 | // When the email address is not public, retrieve it from 53 | // the emails endpoint if the user:email scope is specified. 54 | if (!string.IsNullOrEmpty(Options.UserEmailsEndpoint) && 55 | !identity.HasClaim(claim => claim.Type == ClaimTypes.Email) && Options.Scope.Contains("user:email")) 56 | { 57 | var address = await GetEmailAsync(tokens); 58 | if (!string.IsNullOrEmpty(address)) 59 | { 60 | identity.AddClaim(new Claim(ClaimTypes.Email, address, ClaimValueTypes.String, Options.ClaimsIssuer)); 61 | } 62 | } 63 | 64 | await Options.Events.CreatingTicket(context); 65 | return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); 66 | } 67 | 68 | protected virtual async Task GetEmailAsync(OAuthTokenResponse tokens) 69 | { 70 | // See https://developer.github.com/v3/users/emails/ for more information about the /user/emails endpoint. 71 | var request = new HttpRequestMessage(HttpMethod.Get, Options.UserEmailsEndpoint); 72 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 73 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); 74 | 75 | // Failed requests shouldn't cause an error: in this case, return null to indicate that the email address cannot be retrieved. 76 | var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); 77 | if (!response.IsSuccessStatusCode) 78 | { 79 | Logger.LogWarning("An error occurred while retrieving the email address associated with the logged in user: " + 80 | "the remote server returned a {Status} response with the following payload: {Headers} {Body}.", 81 | /* Status: */ response.StatusCode, 82 | /* Headers: */ response.Headers.ToString(), 83 | /* Body: */ await response.Content.ReadAsStringAsync()); 84 | 85 | return null; 86 | } 87 | 88 | var payload = JArray.Parse(await response.Content.ReadAsStringAsync()); 89 | 90 | return (from address in payload.AsJEnumerable() 91 | where address.Value("primary") 92 | select address.Value("email")).FirstOrDefault(); 93 | } 94 | 95 | protected override Task HandleAuthenticateAsync() 96 | { 97 | return base.HandleAuthenticateAsync(); 98 | } 99 | 100 | protected override Task HandleRemoteAuthenticateAsync() 101 | { 102 | return base.HandleRemoteAuthenticateAsync(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/OAuth.GitHub/GitHubAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Authentication.OAuth; 4 | using Microsoft.AspNetCore.Http; 5 | using static ServerlessLibrary.OAuth.GitHub.GitHubAuthenticationConstants; 6 | 7 | namespace ServerlessLibrary.OAuth.GitHub 8 | { 9 | /// 10 | /// Defines a set of options used by . 11 | /// 12 | public class GitHubAuthenticationOptions : OAuthOptions 13 | { 14 | public GitHubAuthenticationOptions() 15 | { 16 | ClaimsIssuer = GitHubAuthenticationDefaults.Issuer; 17 | 18 | CallbackPath = new PathString(GitHubAuthenticationDefaults.CallbackPath); 19 | 20 | AuthorizationEndpoint = GitHubAuthenticationDefaults.AuthorizationEndpoint; 21 | TokenEndpoint = GitHubAuthenticationDefaults.TokenEndpoint; 22 | UserInformationEndpoint = GitHubAuthenticationDefaults.UserInformationEndpoint; 23 | //Scope.Add(GitHubAuthenticationDefaults.UserInformationScope); 24 | Scope.Add(GitHubAuthenticationDefaults.UserEmailsScope); 25 | 26 | ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); 27 | ClaimActions.MapJsonKey(ClaimTypes.Name, "login"); 28 | ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); 29 | ClaimActions.MapJsonKey(Claims.Name, "name"); 30 | ClaimActions.MapJsonKey(Claims.Url, "html_url"); 31 | ClaimActions.MapJsonKey(Claims.Login, "login"); 32 | ClaimActions.MapJsonKey(Claims.Avatar, "avatar_url"); 33 | } 34 | 35 | /// 36 | /// Gets or sets the address of the endpoint exposing 37 | /// the email addresses associated with the logged in user. 38 | /// 39 | public string UserEmailsEndpoint { get; set; } = GitHubAuthenticationDefaults.UserEmailsEndpoint; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to Development environment will display more detailed information about the error that occurred. 20 |

21 |

22 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 23 |

24 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | 9 | namespace ServerlessLibraryAPI.Pages 10 | { 11 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 12 | public class ErrorModel : PageModel 13 | { 14 | public string RequestId { get; set; } 15 | 16 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 17 | 18 | public void OnGet() 19 | { 20 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using ServerlessLibraryAPI 2 | @namespace ServerlessLibraryAPI.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.ApplicationInsights; 5 | 6 | namespace ServerlessLibrary 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateWebHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 16 | WebHost.CreateDefaultBuilder(args) 17 | .UseApplicationInsights() 18 | .UseStartup() 19 | .ConfigureLogging( 20 | builder => 21 | { 22 | builder.AddApplicationInsights(); 23 | builder.AddFilter("ServerlessLibrary.Program", LogLevel.Information); 24 | builder.AddFilter("ServerlessLibrary.Startup", LogLevel.Information); 25 | builder.AddFilter("", LogLevel.Information); 26 | } 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:16743/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "IIS Express - API only": { 19 | "commandName": "IISExpress", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development", 23 | "ApiOnly": "true" 24 | } 25 | }, 26 | "ServerLessLibrary": { 27 | "commandName": "Project", 28 | "launchBrowser": true, 29 | "environmentVariables": { 30 | "ASPNETCORE_ENVIRONMENT": "Development" 31 | }, 32 | "applicationUrl": "http://localhost:16744/" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ServerlessLibraryAPI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | true 6 | Latest 7 | false 8 | ClientApp\ 9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 10 | /subscriptions/7c1b7bab-00b2-4cb7-924e-205c4f411810/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/ServerlessLibrary 11 | /subscriptions/7c1b7bab-00b2-4cb7-924e-205c4f411810/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/ServerlessLibrary 12 | 235c2497-239d-47f0-8ea7-af2dd2416d95 13 | ServerlessLibrary 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | %(DistFiles.Identity) 57 | PreserveNewest 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ServerlessLibraryAPI.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | FolderProfile 5 | true 6 | 7 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/ServerlessLibrarySettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading.Tasks; 7 | 8 | namespace ServerlessLibrary 9 | { 10 | public static class ServerlessLibrarySettings 11 | { 12 | private static string config(string @default = null, [CallerMemberName] string key = null) 13 | { 14 | var value = System.Environment.GetEnvironmentVariable(key) ?? ConfigurationManager.AppSettings[key]; 15 | return string.IsNullOrEmpty(value) 16 | ? @default 17 | : value; 18 | } 19 | 20 | public static string SLStorageString { get { return config("UseDevelopmentStorage=true"); } } 21 | public static string SLAppInsightsKey { get { return config(""); } } 22 | public static int SLCacheRefreshIntervalInSeconds { get { return Int32.Parse(config("60")); } } 23 | public static string CACHE_ENTRY = "_CacheEntry"; 24 | public static string CosmosEndpoint { get { return config(); } } 25 | public static string CosmosAuthkey { get { return config(); } } 26 | public static string Database { get { return "serverlesslibrary"; } } 27 | public static string Collection { get { return "contributions"; } } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.Cookies; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using ServerlessLibrary.OAuth.GitHub; 11 | using System.Threading.Tasks; 12 | 13 | namespace ServerlessLibrary 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.AddMemoryCache(); 28 | 29 | services.AddAuthentication(options => 30 | { 31 | options.DefaultChallengeScheme = GitHubAuthenticationDefaults.AuthenticationScheme; 32 | options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 33 | }) 34 | .AddCookie() 35 | .AddOAuth( 36 | GitHubAuthenticationDefaults.AuthenticationScheme, 37 | GitHubAuthenticationDefaults.DisplayName, 38 | options => 39 | { 40 | options.ClientId = Configuration["Authentication:GitHub:ClientId"]; // these settings need to be present in appSettings (or in secrets.json) 41 | options.ClientSecret = Configuration["Authentication:GitHub:ClientSecret"]; 42 | }); 43 | 44 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 45 | 46 | // In production, the React files will be served from this directory 47 | services.AddSpaStaticFiles(configuration => 48 | { 49 | configuration.RootPath = "ClientApp/build"; 50 | }); 51 | 52 | // ToDo: re-enable swagger 53 | //services.AddSwaggerGen(c => 54 | //{ 55 | // c.SwaggerDoc("v1", new Info 56 | // { 57 | // Title = "ASP.NET Core 2.0 Web API", 58 | // Version = "v1" 59 | // }); 60 | //}); 61 | 62 | services.AddSingleton(); 63 | services.AddSingleton(); 64 | } 65 | 66 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 67 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 68 | { 69 | app.UseHsts(); 70 | app.UseHttpsRedirection(); 71 | app.UseDefaultFiles(); 72 | app.UseStaticFiles(); 73 | app.UseSpaStaticFiles(); 74 | 75 | // ToDo: Re-enable swagger 76 | //app.UseSwaggerUI(c => 77 | //{ 78 | // c.SwaggerEndpoint("/swagger/v1/swagger.json", "Serverless library API v1"); 79 | // c.RoutePrefix = "swagger"; 80 | //}); 81 | 82 | app.UseAuthentication(); 83 | 84 | app.UseMvc(routes => 85 | { 86 | routes.MapRoute( 87 | name: "default", 88 | template: "{controller}/{action=Index}/{id?}"); 89 | }); 90 | 91 | app.UseSpa(spa => 92 | { 93 | spa.Options.SourcePath = "ClientApp"; 94 | 95 | if (env.IsDevelopment()) 96 | { 97 | spa.UseReactDevelopmentServer(npmScript: "start"); 98 | } 99 | }); 100 | 101 | app.Use(async (context, next) => 102 | { 103 | context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); 104 | context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); 105 | context.Response.Headers.Add("Strict-Transport-Security", "max-age=600; includeSubDomains; preload"); 106 | context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); 107 | await next(); 108 | }); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/StorageHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.WindowsAzure.Storage; 5 | using Microsoft.WindowsAzure.Storage.Queue; 6 | using Microsoft.WindowsAzure.Storage.RetryPolicies; 7 | using Microsoft.WindowsAzure.Storage.Table; 8 | 9 | namespace ServerlessLibrary 10 | { 11 | /// 12 | /// Summary description for StorageHelper 13 | /// 14 | public class StorageHelper 15 | { 16 | private const string slItemTableName = "slitemstats"; 17 | private const string slContributionRequests = "contribution-requests"; 18 | private static readonly TableRequestOptions tableRequestRetry = 19 | new TableRequestOptions { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(2), 3) }; 20 | 21 | private static CloudTableClient tableClient() 22 | { 23 | // Retrieve storage account from connection string. 24 | CloudStorageAccount storageAccount = 25 | CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString); 26 | 27 | // Create the table client. 28 | return storageAccount.CreateCloudTableClient(); 29 | } 30 | 31 | private static CloudQueueClient cloudQueueClient() 32 | { 33 | // Retrieve storage account from connection string. 34 | CloudStorageAccount storageAccount = 35 | CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString); 36 | 37 | // Create the queue client. 38 | return storageAccount.CreateCloudQueueClient(); 39 | } 40 | 41 | private static async Task getTableReference(string tableName = slItemTableName) 42 | { 43 | CloudTable table = tableClient().GetTableReference(tableName); 44 | await table.CreateIfNotExistsAsync(); 45 | return table; 46 | } 47 | 48 | private static async Task getQueueReference(string queueName) 49 | { 50 | CloudQueue queue = cloudQueueClient().GetQueueReference(queueName); 51 | await queue.CreateIfNotExistsAsync(); 52 | return queue; 53 | } 54 | 55 | public static async Task submitContributionForApproval(string contributionPayload) 56 | { 57 | var message = new CloudQueueMessage(contributionPayload); 58 | await (await getQueueReference(slContributionRequests)).AddMessageAsync(message); 59 | } 60 | 61 | public static async Task updateUserStats(string statsPayload) 62 | { 63 | var message = new CloudQueueMessage(statsPayload); 64 | await (await getQueueReference(slItemTableName)).AddMessageAsync(message); 65 | } 66 | 67 | public static async Task> getSLItemRecordsAsync() 68 | { 69 | TableQuery query = new TableQuery() 70 | .Select(new List { "id", "totalDownloads", "likes", "dislikes" }); 71 | TableContinuationToken continuationToken = null; 72 | List entities = new List(); 73 | var opContext = new OperationContext(); 74 | do 75 | { 76 | TableQuerySegment queryResults = 77 | await (await getTableReference()).ExecuteQuerySegmentedAsync(query, continuationToken, tableRequestRetry, opContext); 78 | continuationToken = queryResults.ContinuationToken; 79 | entities.AddRange(queryResults.Results); 80 | 81 | } while (continuationToken != null); 82 | return entities; 83 | } 84 | 85 | public async Task GetItem(string id) 86 | { 87 | TableOperation operation = TableOperation.Retrieve(id, id); 88 | 89 | TableResult result = await (await getTableReference()).ExecuteAsync(operation); 90 | 91 | return (SLItemStats)(dynamic)result.Result; 92 | } 93 | 94 | } 95 | 96 | public class SLItemStats : TableEntity 97 | { 98 | public string id { get; set; } 99 | public int totalDownloads { get; set; } 100 | public DateTime lastUpdated { get; set; } 101 | public int likes { get; set; } 102 | public int dislikes { get; set; } 103 | 104 | } 105 | 106 | } -------------------------------------------------------------------------------- /ServerlessLibraryAPI/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | }, 9 | "ApplicationInsights": { 10 | "InstrumentationKey": "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ServerlessLibraryAPI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "ApplicationInsights": { 9 | "InstrumentationKey": "d35b5caf-a276-467c-9ac7-f7f7d84ea171" 10 | }, 11 | "Authentication": { 12 | "GitHub": { 13 | "ClientId": "", 14 | "ClientSecret": "" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /ServerlessLibraryFunctionApp/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /ServerlessLibraryFunctionApp/Properties/PublishProfiles/slfunctionapp - Web Deploy.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | $slfunctionapp 9 | MSDeploy 10 | AzureWebSite 11 | Release 12 | Any CPU 13 | http://slfunctionapp.azurewebsites.net 14 | False 15 | False 16 | slfunctionapp.scm.azurewebsites.net:443 17 | slfunctionapp 18 | True 19 | WMSVC 20 | True 21 | <_SavePWD>True 22 | False 23 | /subscriptions/499a4934-1b59-43e4-9472-f5a8f6309fea/resourceGroups/ServerlessLibrary/providers/Microsoft.Web/sites/slfunctionapp 24 | 25 | -------------------------------------------------------------------------------- /ServerlessLibraryFunctionApp/ServerLessLibraryFunctionApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | PreserveNewest 18 | Never 19 | 20 | 21 | -------------------------------------------------------------------------------- /ServerlessLibraryFunctionApp/UpdateCounts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.WindowsAzure.Storage; 7 | using Microsoft.WindowsAzure.Storage.RetryPolicies; 8 | using Microsoft.WindowsAzure.Storage.Table; 9 | using Newtonsoft.Json; 10 | 11 | namespace ServerlessLibraryFunctionApp 12 | { 13 | public static class UpdateCounts 14 | { 15 | private static readonly TableRequestOptions tableRequestRetry = new TableRequestOptions { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(2), 3) }; 16 | 17 | [FunctionName("UpdateCounts")] 18 | [Singleton] 19 | public static async Task Run([QueueTrigger("slitemstats")]string myQueueItemJson, [Table("slitemstats")] CloudTable table, ILogger log) 20 | { 21 | var payload = JsonConvert.DeserializeObject(((dynamic)myQueueItemJson)); 22 | string id = payload.id.ToString(); 23 | 24 | var userActionString = payload.userAction.ToString(); 25 | log.LogInformation($"Id:{id}, UserAction: {userActionString}"); 26 | UserAction userAction; 27 | if (!Enum.TryParse(userActionString, true, out userAction)) 28 | { 29 | log.LogInformation($"Unknown user action received."); 30 | return; 31 | } 32 | 33 | int likeChanges = 0, dislikeChanges = 0; 34 | if (userAction == UserAction.Sentiment) 35 | { 36 | try 37 | { 38 | likeChanges = (int)payload.likeChanges; 39 | dislikeChanges = (int)payload.dislikeChanges; 40 | } 41 | catch (Exception ex) 42 | { 43 | log.LogInformation($"Exception got in casting {ex}"); 44 | } 45 | } 46 | 47 | string mainFilter = TableQuery.GenerateFilterCondition("id", QueryComparisons.Equal, id); 48 | TableQuery query = new TableQuery().Where(mainFilter); 49 | TableContinuationToken continuationToken = null; 50 | List entities = new List(); 51 | var opContext = new OperationContext(); 52 | do 53 | { 54 | TableQuerySegment 55 | queryResults = await (table).ExecuteQuerySegmentedAsync(query, continuationToken, tableRequestRetry, opContext); 56 | continuationToken = queryResults.ContinuationToken; 57 | entities.AddRange(queryResults.Results); 58 | 59 | } while (continuationToken != null); 60 | if (entities.Count == 0) 61 | { 62 | // Create new entry 63 | SLItemStats item = null; 64 | item = new SLItemStats() 65 | { 66 | PartitionKey = Guid.NewGuid().ToString(), 67 | RowKey = Guid.NewGuid().ToString(), 68 | id = id, 69 | totalDownloads = userAction == UserAction.Download ? 1 : 0, 70 | lastUpdated = DateTime.UtcNow, 71 | likes = likeChanges, 72 | dislikes = dislikeChanges 73 | }; 74 | 75 | // Create the TableOperation that inserts the itemStats entity. 76 | TableOperation insertOperation = TableOperation.Insert(item); 77 | 78 | // Execute the insert operation. 79 | await table.ExecuteAsync(insertOperation); 80 | 81 | } 82 | else 83 | { 84 | //Update existing entry 85 | var item = entities[0]; 86 | switch (userAction) 87 | { 88 | case UserAction.Download: 89 | item.totalDownloads += 1; 90 | break; 91 | case UserAction.Sentiment: 92 | item.likes += likeChanges; 93 | item.dislikes += dislikeChanges; 94 | break; 95 | default: 96 | log.LogInformation($"Unexpected user action."); 97 | return; 98 | } 99 | 100 | TableOperation operation = TableOperation.InsertOrMerge(item); 101 | await table.ExecuteAsync(operation); 102 | } 103 | } 104 | } 105 | 106 | enum UserAction 107 | { 108 | Download, 109 | Sentiment 110 | } 111 | 112 | public class SLItemStats : TableEntity 113 | { 114 | public string id { get; set; } 115 | public int totalDownloads { get; set; } 116 | public DateTime lastUpdated { get; set; } 117 | public int likes { get; set; } 118 | public int dislikes { get; set; } 119 | 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /ServerlessLibraryFunctionApp/host.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /ServerlessLibraryLogicApp/Deploy-AzureResourceGroup.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 3.0 2 | 3 | Param( 4 | [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation, 5 | [string] $ResourceGroupName = 'ServerlessLibrary', 6 | [switch] $UploadArtifacts, 7 | [string] $StorageAccountName, 8 | [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts', 9 | [string] $TemplateFile = 'LogicApp.json', 10 | [string] $TemplateParametersFile = 'LogicApp.parameters.json', 11 | [string] $ArtifactStagingDirectory = '.', 12 | [string] $DSCSourceFolder = 'DSC', 13 | [switch] $ValidateOnly 14 | ) 15 | 16 | try { 17 | [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ','_'), '3.0.0') 18 | } catch { } 19 | 20 | $ErrorActionPreference = 'Stop' 21 | Set-StrictMode -Version 3 22 | 23 | function Format-ValidationOutput { 24 | param ($ValidationOutput, [int] $Depth = 0) 25 | Set-StrictMode -Off 26 | return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @(' ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) }) 27 | } 28 | 29 | $OptionalParameters = New-Object -TypeName Hashtable 30 | $TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile)) 31 | $TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile)) 32 | 33 | if ($UploadArtifacts) { 34 | # Convert relative paths to absolute paths if needed 35 | $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory)) 36 | $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder)) 37 | 38 | # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present 39 | $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json 40 | if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) { 41 | $JsonParameters = $JsonParameters.parameters 42 | } 43 | $ArtifactsLocationName = '_artifactsLocation' 44 | $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken' 45 | $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select -Expand $ArtifactsLocationName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore 46 | $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore 47 | 48 | # Create DSC configuration archive 49 | if (Test-Path $DSCSourceFolder) { 50 | $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName}) 51 | foreach ($DSCSourceFilePath in $DSCSourceFilePaths) { 52 | $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip' 53 | Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose 54 | } 55 | } 56 | 57 | # Create a storage account name if none was provided 58 | if ($StorageAccountName -eq '') { 59 | $StorageAccountName = 'stage' + ((Get-AzureRmContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19) 60 | } 61 | 62 | $StorageAccount = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName}) 63 | 64 | # Create the storage account if it doesn't already exist 65 | if ($StorageAccount -eq $null) { 66 | $StorageResourceGroupName = 'ARM_Deploy_Staging' 67 | New-AzureRmResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force 68 | $StorageAccount = New-AzureRmStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation" 69 | } 70 | 71 | # Generate the value for artifacts location if it is not provided in the parameter file 72 | if ($OptionalParameters[$ArtifactsLocationName] -eq $null) { 73 | $OptionalParameters[$ArtifactsLocationName] = $StorageAccount.Context.BlobEndPoint + $StorageContainerName 74 | } 75 | 76 | # Copy files from the local storage staging location to the storage account container 77 | New-AzureStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1 78 | 79 | $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName} 80 | foreach ($SourcePath in $ArtifactFilePaths) { 81 | Set-AzureStorageBlobContent -File $SourcePath -Blob $SourcePath.Substring($ArtifactStagingDirectory.length + 1) ` 82 | -Container $StorageContainerName -Context $StorageAccount.Context -Force 83 | } 84 | 85 | # Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file 86 | if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) { 87 | $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString -AsPlainText -Force ` 88 | (New-AzureStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4)) 89 | } 90 | } 91 | 92 | # Create the resource group only when it doesn't already exist 93 | if ((Get-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue) -eq $null) { 94 | New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force -ErrorAction Stop 95 | } 96 | 97 | if ($ValidateOnly) { 98 | $ErrorMessages = Format-ValidationOutput (Test-AzureRmResourceGroupDeployment -ResourceGroupName $ResourceGroupName ` 99 | -TemplateFile $TemplateFile ` 100 | -TemplateParameterFile $TemplateParametersFile ` 101 | @OptionalParameters) 102 | if ($ErrorMessages) { 103 | Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.' 104 | } 105 | else { 106 | Write-Output '', 'Template is valid.' 107 | } 108 | } 109 | else { 110 | New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) ` 111 | -ResourceGroupName $ResourceGroupName ` 112 | -TemplateFile $TemplateFile ` 113 | -TemplateParameterFile $TemplateParametersFile ` 114 | @OptionalParameters ` 115 | -Force -Verbose ` 116 | -ErrorVariable ErrorMessages 117 | if ($ErrorMessages) { 118 | Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") }) 119 | } 120 | } -------------------------------------------------------------------------------- /ServerlessLibraryLogicApp/Deployment.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | bin\$(Configuration)\ 7 | false 8 | true 9 | false 10 | None 11 | obj\ 12 | $(BaseIntermediateOutputPath)\ 13 | $(BaseIntermediateOutputPath)$(Configuration)\ 14 | $(IntermediateOutputPath)ProjectReferences 15 | $(ProjectReferencesOutputPath)\ 16 | true 17 | 18 | 19 | 20 | false 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Always 33 | 34 | 35 | Never 36 | 37 | 38 | false 39 | Build 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | _GetDeploymentProjectContent; 48 | _CalculateContentOutputRelativePaths; 49 | _GetReferencedProjectsOutput; 50 | _CalculateArtifactStagingDirectory; 51 | _CopyOutputToArtifactStagingDirectory; 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Configuration=$(Configuration);Platform=$(Platform) 69 | 70 | 71 | 75 | 76 | 77 | 78 | $([System.IO.Path]::GetFileNameWithoutExtension('%(ProjectReference.Identity)')) 79 | 80 | 81 | 82 | 83 | 84 | 85 | $(OutDir) 86 | $(OutputPath) 87 | $(ArtifactStagingDirectory)\ 88 | $(ArtifactStagingDirectory)staging\ 89 | $(Build_StagingDirectory) 90 | 91 | 92 | 93 | 94 | 96 | 97 | <_OriginalIdentity>%(DeploymentProjectContentOutput.Identity) 98 | <_RelativePath>$(_OriginalIdentity.Replace('$(MSBuildProjectDirectory)', '')) 99 | 100 | 101 | 102 | 103 | $(_RelativePath) 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | PrepareForRun 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /ServerlessLibraryLogicApp/LogicApp.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "logicAppName": { 6 | "value": "ApprovalWorkflow" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /ServerlessLibraryLogicApp/ServerlessLibraryLogicApp.deployproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 8 | 9 | Release 10 | AnyCPU 11 | 12 | 13 | 14 | 92dd9a5d-3a93-493c-8f69-23a1c519a1c4 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | False 28 | 29 | 30 | 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------