This commit is contained in:
Neha Gupta 2019-05-29 13:58:26 -07:00
Родитель e7e3cbb3e6 8231d94886
Коммит 0cdb1a0c0f
136 изменённых файлов: 19450 добавлений и 9961 удалений

Просмотреть файл

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\ServerlessLibraryAPI\CosmosLibraryStore.cs" Link="CosmosLibraryStore.cs" />
<Compile Include="..\ServerlessLibraryAPI\ILibraryStore.cs" Link="ILibraryStore.cs" />
<Compile Include="..\ServerlessLibraryAPI\Models\LibraryItem.cs" Link="LibraryItem.cs" />
<Compile Include="..\ServerlessLibraryAPI\ServerlessLibrarySettings.cs" Link="ServerlessLibrarySettings.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\ServerlessLibraryAPI\wwwroot\items.json" Link="items.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.DocumentDB.Core" Version="2.3.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.5.0" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,135 @@
using System;
using System.Reflection;
using System.IO;
using ServerlessLibrary.Models;
using System.Collections.Generic;
using Newtonsoft.Json;
using ServerlessLibrary;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.RetryPolicies;
using System.Threading.Tasks;
using System.Linq;
namespace LibraryStoreMigration
{
class Program
{
const string tableName = "slitemstats";
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Please enter specify operation to be performed (cosmosdb|stats)");
Console.WriteLine("Please note that connection informations need to be provided as environment variables.");
return;
}
if (args[0].Equals("cosmosdb", StringComparison.OrdinalIgnoreCase))
{
MigrateToCosmosDB();
}
if (args[0].Equals("stats", StringComparison.OrdinalIgnoreCase))
{
AddNewStatsColumns();
}
}
public static void AddNewStatsColumns()
{
TableRequestOptions tableRequestRetry = new TableRequestOptions { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(2), 3) };
TableQuery<NewSLItemStats> query = new TableQuery<NewSLItemStats>();
TableContinuationToken continuationToken = null;
List<NewSLItemStats> entities = new List<NewSLItemStats>();
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString);
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable table = tableClient.GetTableReference(tableName);
var opContext = new OperationContext();
do
{
TableQuerySegment<NewSLItemStats> queryResults = (table).ExecuteQuerySegmentedAsync(query, continuationToken, tableRequestRetry, opContext).Result;
continuationToken = queryResults.ContinuationToken;
entities.AddRange(queryResults.Results);
} while (continuationToken != null);
Console.WriteLine(entities.Count);
List<LibraryItem> libraryItems = GetAllLibraryItemsFromFile();
foreach (var entity in entities)
{
entity.id = libraryItems.FirstOrDefault(l => l.Template == entity.template).Id;
entity.likes = 0;
entity.dislikes = 0;
TableOperation operation = TableOperation.InsertOrMerge(entity);
Task<TableResult> r = table.ExecuteAsync(operation);
TableResult a = r.Result;
}
}
public static void MigrateToCosmosDB()
{
var libraryItems = GetAllLibraryItemsFromFile();
Console.WriteLine("Number of samples to be migrated from file to cosmos db: {0}", libraryItems.Count);
CosmosLibraryStore libraryStore = new CosmosLibraryStore();
IList<LibraryItem> libraryItemsInCosmos = libraryStore.GetAllItems().Result;
Console.WriteLine("Number of samples already present in cosmos db: {0}", libraryItemsInCosmos.Count);
if (libraryItemsInCosmos.Count != libraryItems.Count)
{
foreach (LibraryItem libraryItem in libraryItems)
{
if (!libraryItemsInCosmos.Any(c => c.Id == libraryItem.Id))
{
Console.WriteLine("Item {0} not present in cosmos db. will be migrated" + libraryItem.Id);
try
{
libraryStore.Add(libraryItem).Wait();
Console.WriteLine("Migrated sample with id {0}" + libraryItem.Id);
}
catch (Exception ex)
{
Console.WriteLine("Got exception {0}", ex);
throw;
}
}
}
Console.WriteLine("Samples are successfully migrated to cosmos db");
}
else
{
Console.WriteLine("Samples are already migrated to cosmos db");
}
}
public static List<LibraryItem> GetAllLibraryItemsFromFile()
{
var assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream("LibraryStoreMigration.items.json"))
using (StreamReader reader = new StreamReader(stream))
{
string result = reader.ReadToEnd();
return JsonConvert.DeserializeObject<List<LibraryItem>>(result);
}
}
}
public class OldSLItemStats : TableEntity
{
public string template { get; set; }
public int totalDownloads { get; set; }
public int downloadsToday { get; set; }
public int downloadsThisWeek { get; set; }
public int downloadsThisMonth { get; set; }
public DateTime lastUpdated { get; set; }
}
public class NewSLItemStats : OldSLItemStats
{
public string id { get; set; }
public int likes { get; set; }
public int dislikes { get; set; }
}
}

Просмотреть файл

@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessLibraryFunctionAp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessLibraryAPI", "ServerlessLibraryAPI\ServerlessLibraryAPI.csproj", "{9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}"
EndProject
Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "ServerlessLibraryLogicApp", "ServerlessLibraryLogicApp\ServerlessLibraryLogicApp.deployproj", "{92DD9A5D-3A93-493C-8F69-23A1C519A1C4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -21,6 +23,10 @@ Global
{9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9DB5E7FD-1720-4FEC-B6BF-E91BCA659A54}.Release|Any CPU.Build.0 = Release|Any CPU
{92DD9A5D-3A93-493C-8F69-23A1C519A1C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92DD9A5D-3A93-493C-8F69-23A1C519A1C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92DD9A5D-3A93-493C-8F69-23A1C519A1C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92DD9A5D-3A93-493C-8F69-23A1C519A1C4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

234
ServerlessLibraryAPI/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,234 @@
/Properties/launchSettings.json
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
bin/
Bin/
obj/
Obj/
# Visual Studio 2015 cache/options directory
.vs/
/wwwroot/dist/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Microsoft Azure ApplicationInsights config file
ApplicationInsights.config
# Windows Store app package directory
AppPackages/
BundleArtifacts/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
orleans.codegen.cs
/node_modules
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
# FAKE - F# Make
.fake/

Просмотреть файл

@ -1,92 +1,135 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using ServerlessLibrary.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace ServerlessLibrary
{
public interface ICacheService {
List<LibraryItem> GetCachedItems();
}
//https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core
public class CacheService:ICacheService
public interface ICacheService
{
IList<LibraryItemWithStats> GetCachedItems();
}
//https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core
public class CacheService : ICacheService
{
protected readonly IMemoryCache _cache;
private readonly ILibraryStore libraryStore;
private readonly ILogger logger;
private Task LoadingTask = Task.CompletedTask;
private Timer Timer = null;
private bool LoadingBusy = false;
private bool isCacheLoadedOnce = false;
public CacheService(IMemoryCache cache, ILibraryStore libraryStore, ILogger<CacheService> logger)
{
protected readonly IMemoryCache _cache;
private IHostingEnvironment _env;
public CacheService(IMemoryCache cache, IHostingEnvironment env)
{
this._cache = cache;
this._env = env;
InitTimer();
}
private void InitTimer()
{
_cache.Set<LibraryItemsResult>(ServerlessLibrarySettings.CACHE_ENTRY, new LibraryItemsResult() { Result = new List<LibraryItem>(), IsBusy = true });
Timer = new Timer(TimerTickAsync, null, 1000, ServerlessLibrarySettings.SLCacheRefreshIntervalInSeconds * 1000);
}
public Task LoadingTask = Task.CompletedTask;
public Timer Timer { get; set; }
public bool LoadingBusy = false;
public List<LibraryItem> GetCachedItems() {
return _cache.Get<LibraryItemsResult>(ServerlessLibrarySettings.CACHE_ENTRY).Result;
}
private async void TimerTickAsync(object state)
{
if (LoadingBusy) return;
try
{
LoadingBusy = true;
LoadingTask = LoadCaches();
await LoadingTask;
}
catch
{
// do not crash the app
}
finally
{
LoadingBusy = false;
}
}
private async Task LoadCaches()
{
try
{
var items = await ConstructCache();
_cache.Set<LibraryItemsResult>(ServerlessLibrarySettings.CACHE_ENTRY, new LibraryItemsResult() { Result = items, IsBusy = false });
}
catch { }
}
private async Task<List<LibraryItem>> ConstructCache()
{
var webRoot = _env.WebRootPath;
var file = System.IO.Path.Combine(webRoot, "items.json");
var stats = await StorageHelper.getSLItemRecordsAsync();
var fileContent = JsonConvert.DeserializeObject<List<LibraryItem>>(await System.IO.File.ReadAllTextAsync(file));
foreach (var item in fileContent)
{
var itemStat = item.Template != null ? stats.Where(s => s.template == item.Template.ToString()).FirstOrDefault() : null;
item.TotalDownloads = itemStat != null ? itemStat.totalDownloads : 1;
item.DownloadsThisMonth = itemStat != null ? itemStat.downloadsThisMonth : 1;
item.DownloadsThisWeek = itemStat != null ? itemStat.downloadsThisWeek : 1;
item.DownloadsToday = itemStat != null ? itemStat.downloadsToday : 1;
item.AuthorTypeDesc = (item.AuthorType == "Microsoft" ? "This has been authored by Microsoft" : "This is a community contribution");
}
return fileContent;
}
this._cache = cache;
this.libraryStore = libraryStore;
this.logger = logger;
InitTimer();
}
public class LibraryItemsResult {
public List<LibraryItem> Result { get; set; }
public bool IsBusy{ get; set; }
private void InitTimer()
{
_cache.Set<LibraryItemsResult>(ServerlessLibrarySettings.CACHE_ENTRY, new LibraryItemsResult() { Result = new List<LibraryItemWithStats>(), IsBusy = true });
Timer = new Timer(TimerTickAsync, null, 1000, ServerlessLibrarySettings.SLCacheRefreshIntervalInSeconds * 1000);
}
public IList<LibraryItemWithStats> GetCachedItems()
{
// Make a blocking call to load cache on first time call.
if (!isCacheLoadedOnce)
{
try
{
logger.LogInformation("Loading initial cache");
IList<LibraryItemWithStats> items = this.ConstructCache().Result;
_cache.Set(ServerlessLibrarySettings.CACHE_ENTRY, new LibraryItemsResult() { Result = items, IsBusy = false });
logger.LogInformation("Loaded {0} items into cache", items.Count());
}
catch (Exception ex)
{
this.logger.LogError(ex, "Failed to load cache in first call");
}
}
logger.LogInformation("Successfully loaded initial cache");
isCacheLoadedOnce = true;
return _cache.Get<LibraryItemsResult>(ServerlessLibrarySettings.CACHE_ENTRY).Result;
}
private async void TimerTickAsync(object state)
{
logger.LogInformation("Cache refresh timer fired");
if (!isCacheLoadedOnce || LoadingBusy)
{
logger.LogWarning("Skipping cache refresh");
return;
}
try
{
LoadingBusy = true;
LoadingTask = LoadCaches();
await LoadingTask;
}
catch
{
// do not crash the app
}
finally
{
LoadingBusy = false;
}
}
private async Task LoadCaches()
{
try
{
logger.LogInformation("Starting cache refresh");
var items = await ConstructCache();
_cache.Set<LibraryItemsResult>(ServerlessLibrarySettings.CACHE_ENTRY, new LibraryItemsResult() { Result = items, IsBusy = false });
logger.LogInformation("Updated cache with {0} items", items.Count());
}
catch (Exception ex)
{
this.logger.LogError(ex, "Failed to load cache");
}
}
private async Task<IList<LibraryItemWithStats>> ConstructCache()
{
logger.LogInformation("Starting ConstructCache");
IList<LibraryItem> libraryItems;
IList<LibraryItemWithStats> libraryItemsWithStats = new List<LibraryItemWithStats>();
libraryItems = await this.libraryStore.GetAllItems();
logger.LogInformation("Cosmos DB returned {0} results", libraryItems.Count());
var stats = await StorageHelper.getSLItemRecordsAsync();
logger.LogInformation("Storage returned {0} results", stats.Count());
foreach (var storeItem in libraryItems)
{
var item = storeItem.ConvertTo<LibraryItemWithStats>();
var itemStat = stats.Where(s => s.id == storeItem.Id.ToString()).FirstOrDefault();
item.TotalDownloads = itemStat != null && itemStat.totalDownloads > 0 ? itemStat.totalDownloads : 1;
item.Likes = itemStat != null && itemStat.likes > 0 ? itemStat.likes : 0;
item.Dislikes = itemStat != null && itemStat.dislikes > 0 ? itemStat.dislikes : 0;
libraryItemsWithStats.Add(item);
}
logger.LogInformation("ConstructCache returned {0} items", libraryItemsWithStats.Count());
return libraryItemsWithStats;
}
}
public class LibraryItemsResult
{
public IList<LibraryItemWithStats> Result { get; set; }
public bool IsBusy { get; set; }
}
}

Просмотреть файл

@ -0,0 +1 @@
REACT_APP_INSTRUMENTATION_KEY=

Просмотреть файл

@ -0,0 +1 @@
REACT_APP_INSTRUMENTATION_KEY=d35b5caf-a276-467c-9ac7-f7f7d84ea171

23
ServerlessLibraryAPI/ClientApp/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

Просмотреть файл

@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent 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.
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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

13544
ServerlessLibraryAPI/ClientApp/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,34 @@
{
"name": "ServerlessLibraryAPI",
"version": "0.1.0",
"private": true,
"dependencies": {
"@uifabric/styling": "^6.47.6",
"markdown-to-jsx": "^6.10.1",
"node-sass": "^4.12.0",
"office-ui-fabric-react": "^6.187.0",
"react": "^16.8.6",
"react-app-polyfill": "^1.0.1",
"react-dom": "^16.8.6",
"react-redux": "^6.0.1",
"react-router-dom": "^4.3.1",
"react-scripts": "3.0.0",
"redux": "^4.0.1",
"rimraf": "^2.6.3"
},
"scripts": {
"start": "rimraf ./build && react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"ie 11",
"not dead",
"not op_mini all"
]
}

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 8.0 KiB

После

Ширина:  |  Высота:  |  Размер: 8.0 KiB

Просмотреть файл

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<base href="%PUBLIC_URL%/" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Azure Serverless Community Library</title>
<script type="text/javascript">
var aiInstrumentationKey="%REACT_APP_INSTRUMENTATION_KEY%";
if (aiInstrumentationKey !== "")
{
var appInsights=window.appInsights||function(a){
function b(a){c[a]=function(){var b=arguments;c.queue.push(function(){c[a].apply(c,b)})}}var c={config:a},d=document,e=window;setTimeout(function(){var b=d.createElement("script");b.src=a.url||"https://az416426.vo.msecnd.net/scripts/a/ai.0.js",d.getElementsByTagName("script")[0].parentNode.appendChild(b)});try{c.cookie=d.cookie}catch(a){}c.queue=[];for(var f=["Event","Exception","Metric","PageView","Trace","Dependency"];f.length;)b("track"+f.pop());if(b("setAuthenticatedUserContext"),b("clearAuthenticatedUserContext"),b("startTrackEvent"),b("stopTrackEvent"),b("startTrackPage"),b("stopTrackPage"),b("flush"),!a.disableExceptionTracking){f="onerror",b("_"+f);var g=e[f];e[f]=function(a,b,d,e,h){var i=g&&g(a,b,d,e,h);return!0!==i&&c["_"+f](a,b,d,e,h),i}}return c
}({
instrumentationKey: aiInstrumentationKey
});
window.appInsights=appInsights,appInsights.queue&&0===appInsights.queue.length&&appInsights.trackPageView();
}
</script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Просмотреть файл

@ -0,0 +1,15 @@
{
"short_name": "ServerlessLibraryAPI",
"name": "ServerlessLibraryAPI",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

Просмотреть файл

@ -0,0 +1,10 @@
/* #container { background: blue; } */
#header {
height: 40px;
}
#main {
height: calc(100% - 40px);
}
#container {
overflow-y: auto;
}

Просмотреть файл

@ -0,0 +1,103 @@
import React, { Component } from "react";
import { Switch, Route, withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { Dialog } from "office-ui-fabric-react";
import "./App.css";
import Main from "./components/Main/Main";
import Header from "./components/Header/Header";
import DetailView from "./components/DetailView/DetailView";
import ContributionsPage from "./components/Contribute/Contribute";
import { sampleActions } from "./actions/sampleActions";
import { userActions } from "./actions/userActions";
import { libraryService, userService } from "./services";
const loginErrorMsg = "We were unable to log you in. Please try again later.";
class App extends Component {
constructor(props) {
super(props);
this.state = {
showErrorDialog: false
};
this.onDismissErrorDialog = this.onDismissErrorDialog.bind(this);
}
componentDidMount() {
libraryService
.getAllSamples()
.then(samples => this.props.getSamplesSuccess(samples))
.catch(() => {
// do nothing
});
this.props.getCurrentUserRequest();
userService
.getCurrentUser()
.then(user => this.props.getCurrentUserSuccess(user))
.catch(data => {
this.props.getCurrentUserFailure();
if (data.status !== 401) {
this.setState({
showErrorDialog: true
});
}
});
}
onDismissErrorDialog() {
this.setState({
showErrorDialog: false
});
}
render() {
const { showErrorDialog } = this.state;
return (
<div id="container">
<div id="header">
<Header />
</div>
<div id="main">
<Switch>
<Route exact path="/" component={Main} />
<Route path="/sample/:id" component={DetailView} />
<Route exact path="/contribute" component={ContributionsPage} />
</Switch>
</div>
{showErrorDialog && (
<Dialog
dialogContentProps={{
title: "An error occurred!"
}}
hidden={!showErrorDialog}
onDismiss={this.onDismissErrorDialog}
>
<p>{loginErrorMsg}</p>
</Dialog>
)}
</div>
);
}
}
function mapStateToProps(state) {
return {};
}
const mapDispatchToProps = {
getSamplesSuccess: sampleActions.getSamplesSuccess,
getCurrentUserRequest: userActions.getCurrentUserRequest,
getCurrentUserSuccess: userActions.getCurrentUserSuccess,
getCurrentUserFailure: userActions.getCurrentUserFailure
};
const AppContainer = withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(App)
);
export default AppContainer;

Просмотреть файл

@ -0,0 +1,10 @@
export const sampleActionTypes = {
GETSAMPLES_SUCCESS: "GETSAMPLES_SUCCESS"
};
export const userActionTypes = {
GETCURRENTUSER_REQUEST: "GETCURRENTUSER_REQUEST",
GETCURRENTUSER_SUCCESS: "GETCURRENTUSER_SUCCESS",
GETCURRENTUSER_FAILURE: "GETCURRENTUSER_FAILURE",
LOGOUT: "LOGOUT"
};

Просмотреть файл

@ -0,0 +1,12 @@
import { sampleActionTypes } from "./actionTypes";
export const sampleActions = {
getSamplesSuccess
};
function getSamplesSuccess(samples) {
return {
type: sampleActionTypes.GETSAMPLES_SUCCESS,
samples
};
}

Просмотреть файл

@ -0,0 +1,35 @@
import { userActionTypes } from "./actionTypes";
export const userActions = {
getCurrentUserRequest,
getCurrentUserSuccess,
getCurrentUserFailure,
logout
};
function getCurrentUserRequest(user) {
return {
type: userActionTypes.GETCURRENTUSER_REQUEST,
user
};
}
function getCurrentUserSuccess(user) {
return {
type: userActionTypes.GETCURRENTUSER_SUCCESS,
user
};
}
function getCurrentUserFailure(user) {
return {
type: userActionTypes.GETCURRENTUSER_FAILURE,
user
};
}
function logout() {
return {
type: userActionTypes.LOGOUT
};
}

Просмотреть файл

@ -0,0 +1,3 @@
<svg width="15" height="16" viewBox="0 0 15 16" xmlns="http://www.w3.org/2000/svg">
<path d="M13 6.32812V13.0625L7.5 15.8125L2 13.0625V6.33594L0.601562 2.49219L4.75 4.5625L7.09375 3.39844L8.21094 0.046875L14.1094 3L13 6.32812ZM8.10938 3.5L12.2109 5.54688L12.8906 3.50781L8.78906 1.45312L8.10938 3.5ZM5.86719 5.125L6.90625 5.64062L7.78125 8.05469L11.3828 6.25L7.5 4.3125L5.86719 5.125ZM2.39844 4.50781L2.90625 5.89062L6.60156 7.74219L6.09375 6.35938L2.39844 4.50781ZM3 12.4453L7 14.4453V9.0625L3 7.0625V12.4453ZM12 12.4453V7.0625L8 9.0625V14.4453L12 12.4453Z"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 568 B

Просмотреть файл

@ -0,0 +1,28 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 64 54"
enableBackground="new 0 -10 64 54"
xmlSpace="preserve"
>
<g>
<path
fill="#3999C6"
d="M63.6,32.4c0.6-0.6,0.5-1.7,0-2.3L60.5,27L46.7,13.6c-0.6-0.6-1.5-0.6-2.2,0l0,0c-0.6,0.6-0.8,1.7,0,2.3 L59,30.1c0.6,0.6,0.6,1.7,0,2.3L44.2,47.1c-0.6,0.6-0.6,1.7,0,2.3l0,0c0.6,0.6,1.7,0.5,2.2,0l13.7-13.6c0,0,0,0,0.1-0.1L63.6,32.4z"
/>
<path
fill="#3999C6"
d="M0.4,32.4c-0.6-0.6-0.5-1.7,0-2.3L3.5,27l13.8-13.4c0.6-0.6,1.5-0.6,2.2,0l0,0c0.6,0.6,0.8,1.7,0,2.3 L5.3,30.1c-0.6,0.6-0.6,1.7,0,2.3l14.5,14.7c0.6,0.6,0.6,1.7,0,2.3l0,0c-0.6,0.6-1.7,0.5-2.2,0L3.6,36c0,0,0,0-0.1-0.1L0.4,32.4z"
/>
<polygon
fill="#FCD116"
points="47.6,2.5 28.1,2.5 17.6,32.1 30.4,32.2 20.4,61.5 48,22.4 34.6,22.4 "
/>
<polygon
opacity="0.3"
fill="#FF8C00"
enableBackground="new "
points="34.6,22.4 47.6,2.5 37.4,2.5 26.6,27.1 39.4,27.2 20.4,61.5 48,22.4 "
/>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Просмотреть файл

@ -0,0 +1,10 @@
<svg
width="13"
height="12"
viewBox="0 0 13 12"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.5 0.146484C7.05078 0.146484 7.58203 0.21875 8.09375 0.363281C8.60547 0.503906 9.08203 0.705078 9.52344 0.966797C9.96875 1.22852 10.373 1.54297 10.7363 1.91016C11.1035 2.27344 11.418 2.67773 11.6797 3.12305C11.9414 3.56445 12.1426 4.04102 12.2832 4.55273C12.4277 5.06445 12.5 5.5957 12.5 6.14648C12.5 6.79102 12.4004 7.41211 12.2012 8.00977C12.0059 8.60742 11.7285 9.15625 11.3691 9.65625C11.0098 10.1562 10.5781 10.5957 10.0742 10.9746C9.57031 11.3496 9.01172 11.6387 8.39844 11.8418C8.38672 11.8457 8.36914 11.8496 8.3457 11.8535C8.32227 11.8535 8.30469 11.8535 8.29297 11.8535C8.19922 11.8535 8.125 11.8262 8.07031 11.7715C8.01562 11.7168 7.98828 11.6445 7.98828 11.5547C7.98828 11.2773 7.98828 11.0039 7.98828 10.7344C7.99219 10.4609 7.99414 10.1855 7.99414 9.9082C7.99414 9.70898 7.96484 9.50781 7.90625 9.30469C7.84766 9.10156 7.74219 8.93164 7.58984 8.79492C8.04297 8.74414 8.43945 8.65234 8.7793 8.51953C9.12305 8.38281 9.4082 8.19531 9.63477 7.95703C9.86523 7.71875 10.0371 7.42578 10.1504 7.07812C10.2676 6.72656 10.3262 6.3125 10.3262 5.83594C10.3262 5.53125 10.2754 5.24414 10.1738 4.97461C10.0723 4.70117 9.91797 4.45117 9.71094 4.22461C9.75391 4.11523 9.78516 4.00195 9.80469 3.88477C9.82422 3.76758 9.83398 3.65039 9.83398 3.5332C9.83398 3.38086 9.81641 3.23047 9.78125 3.08203C9.75 2.92969 9.70703 2.78125 9.65234 2.63672C9.63281 2.62891 9.61133 2.625 9.58789 2.625C9.56445 2.625 9.54297 2.625 9.52344 2.625C9.39844 2.625 9.26758 2.64648 9.13086 2.68945C8.99414 2.72852 8.85742 2.7793 8.7207 2.8418C8.58789 2.90039 8.45898 2.9668 8.33398 3.04102C8.20898 3.11523 8.09766 3.18555 8 3.25195C7.51172 3.11523 7.01172 3.04688 6.5 3.04688C5.98828 3.04688 5.48828 3.11523 5 3.25195C4.90234 3.18555 4.79102 3.11523 4.66602 3.04102C4.54102 2.9668 4.41016 2.90039 4.27344 2.8418C4.14062 2.7793 4.00391 2.72852 3.86328 2.68945C3.72656 2.64648 3.59766 2.625 3.47656 2.625C3.45703 2.625 3.43555 2.625 3.41211 2.625C3.38867 2.625 3.36719 2.62891 3.34766 2.63672C3.29297 2.78125 3.24805 2.92969 3.21289 3.08203C3.18164 3.23047 3.16602 3.38086 3.16602 3.5332C3.16602 3.65039 3.17578 3.76758 3.19531 3.88477C3.21484 4.00195 3.24609 4.11523 3.28906 4.22461C3.08203 4.45117 2.92773 4.70117 2.82617 4.97461C2.72461 5.24414 2.67383 5.53125 2.67383 5.83594C2.67383 6.3125 2.73047 6.72461 2.84375 7.07227C2.96094 7.41992 3.13281 7.71484 3.35938 7.95703C3.58984 8.19531 3.875 8.38281 4.21484 8.51953C4.55469 8.65625 4.95117 8.75 5.4043 8.80078C5.29102 8.90234 5.20312 9.02539 5.14062 9.16992C5.08203 9.31055 5.04297 9.45508 5.02344 9.60352C4.91797 9.6543 4.80664 9.69336 4.68945 9.7207C4.57227 9.74805 4.45508 9.76172 4.33789 9.76172C4.08789 9.76172 3.88086 9.70312 3.7168 9.58594C3.55273 9.46875 3.40625 9.30859 3.27734 9.10547C3.23047 9.03125 3.17383 8.95703 3.10742 8.88281C3.04102 8.80859 2.96875 8.74219 2.89062 8.68359C2.8125 8.625 2.72852 8.57812 2.63867 8.54297C2.54883 8.50391 2.45508 8.48438 2.35742 8.48438C2.3418 8.48438 2.31836 8.48633 2.28711 8.49023C2.25586 8.49023 2.22461 8.49414 2.19336 8.50195C2.16602 8.50977 2.14062 8.52148 2.11719 8.53711C2.09375 8.55273 2.08203 8.57227 2.08203 8.5957C2.08203 8.64258 2.10938 8.68945 2.16406 8.73633C2.21875 8.7793 2.26367 8.8125 2.29883 8.83594L2.31641 8.84766C2.40234 8.91406 2.47656 8.97852 2.53906 9.04102C2.60547 9.09961 2.66406 9.16406 2.71484 9.23438C2.76562 9.30078 2.81055 9.375 2.84961 9.45703C2.89258 9.53516 2.9375 9.625 2.98438 9.72656C3.11719 10.0312 3.30273 10.2539 3.54102 10.3945C3.7832 10.5312 4.07031 10.5996 4.40234 10.5996C4.50391 10.5996 4.60547 10.5938 4.70703 10.582C4.80859 10.5664 4.91016 10.5488 5.01172 10.5293V11.5488C5.01172 11.6426 4.98242 11.7168 4.92383 11.7715C4.86914 11.8262 4.79492 11.8535 4.70117 11.8535C4.68945 11.8535 4.67188 11.8535 4.64844 11.8535C4.62891 11.8496 4.61328 11.8457 4.60156 11.8418C3.98828 11.6426 3.42969 11.3555 2.92578 10.9805C2.42188 10.6016 1.99023 10.1621 1.63086 9.66211C1.27148 9.1582 0.992188 8.60742 0.792969 8.00977C0.597656 7.41211 0.5 6.79102 0.5 6.14648C0.5 5.5957 0.570312 5.06445 0.710938 4.55273C0.855469 4.04102 1.05859 3.56445 1.32031 3.12305C1.58203 2.67773 1.89453 2.27344 2.25781 1.91016C2.625 1.54297 3.0293 1.22852 3.4707 0.966797C3.91602 0.705078 4.39453 0.503906 4.90625 0.363281C5.41797 0.21875 5.94922 0.146484 6.5 0.146484Z"
/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 4.3 KiB

Просмотреть файл

@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 51.8">
<g>
<path fill="#59b4d9" d="M26 21.3v-5h-2.5v5c0 .9-.8 1.7-2.1 2.1l-3.4.7c-2.3.7-3.9 2.5-3.9 4.5v5.7h2.5v-5.7c0-.9.8-1.7 2.1-2.1l3.4-.8c2.3-.6 3.9-2.4 3.9-4.4z"></path>
<path fill="#7fba00" d="M19.6 36.9v-4.6c0-1.1-.9-2-2-2H13c-1.1 0-2 .9-2 2v4.6c0 1.1.9 2 2 2h4.6c1.1 0 2-.9 2-2z"></path>
<path fill="#59b4d9" d="M23.2 21.3v-5h2.5v5c0 .9.8 1.7 2.1 2.1l4.2.9c2.3.7 3.9 2.5 3.9 4.5v5.7h-2.5v-5.7c0-.9-.8-1.7-2.1-2.1l-4.2-.9c-2.3-.7-3.9-2.5-3.9-4.5z"></path>
<path fill="#7fba00" d="M30.4 37.1v-4.6c0-1.1.9-2 2-2H37c1.1 0 2 .9 2 2v4.6c0 1.1-.9 2-2 2h-4.6c-1.1 0-2-.9-2-2z"></path>
<path fill="#59b4d9" d="M23.2 16.3H26v4.8h-2.8z"></path>
<path fill="#0072c6" d="M26.5 11.7v3.7h-3.7v-3.7h3.7m.8-2.8H22c-1.1 0-2 .9-2 2v5.3c0 1.1.9 2 2 2h5.3c1.1 0 2-.9 2-2v-5.3c0-1.1-.9-2-2-2z"></path>
<path fill="#59b4d9" d="M7.9 44.4c-2.1 0-3.6-.4-4.5-1.1-.9-.8-1.3-2.1-1.3-4V28.9c0-1.7-.7-2.6-2.1-2.6v-2.6c1.4 0 2.1-.9 2.1-2.7V10.8c0-1.9.4-3.3 1.3-4.1s2.4-1.1 4.5-1.1v2.6c-1.5 0-2.3.8-2.3 2.5v10c0 2.3-.7 3.7-2.2 4.3 1.4.6 2.2 2 2.2 4.3v9.9c0 .9.2 1.6.5 2 .4.4.9.6 1.7.6l.1 2.6c-.1 0 0 0 0 0zM42.1 5.6c2.1 0 3.6.4 4.5 1.1.9.8 1.3 2.1 1.3 4v10.4c0 1.7.7 2.6 2.1 2.6v2.6c-1.4 0-2.1.9-2.1 2.7v10.1c0 1.9-.4 3.3-1.3 4.1-.9.8-2.4 1.2-4.5 1.2v-2.6c1.5 0 2.3-.8 2.3-2.5v-10c0-2.3.7-3.7 2.2-4.3-1.4-.6-2.2-2-2.2-4.3v-9.9c0-.9-.2-1.6-.5-2-.4-.4-.9-.6-1.7-.6l-.1-2.6z"></path>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.5 KiB

Просмотреть файл

@ -0,0 +1,35 @@
.content-header-titlewapper {
display: flex;
margin-top: 20px;
margin-bottom: 20px;
}
.content-header-title {
font-size: 18px;
font-weight: bold;
}
.content-header-contributionLink {
text-decoration: none;
}
.contributionLink-content {
font-size: 12px;
display: flex;
text-decoration: none;
}
.contribution-link-text {
margin-left: 12px;
}
.content-header-sortbywrappper {
display: flex;
margin-top: 20px;
margin-bottom: 5px;
}
.content-header-count {
text-align: left;
margin-top: auto;
margin-bottom: auto;
color: #000000;
font-size: 16px;
line-height: normal;
}

Просмотреть файл

@ -0,0 +1,136 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import {
SearchBox,
Dropdown,
Icon,
Link as FabricLink
} from "office-ui-fabric-react";
import { Link } from "react-router-dom";
import {
paramsToQueryString,
queryStringToParams,
trackEvent
} from "../../helpers";
import "./ContentHeader.css";
class ContentHeader extends Component {
constructor(props) {
super(props);
this.state = {
filterText: this.props.initialSearchText,
sortby: this.props.initialSortBy
};
}
filterTextChanged(newValue) {
var params = queryStringToParams(this.props.location.search);
delete params["filtertext"];
if (newValue && newValue !== "") {
params["filtertext"] = newValue;
}
this.setState({ filterText: newValue });
this.props.history.push(paramsToQueryString(params));
trackEvent("/filter/change/searchtext", newValue);
}
sortbyChanged(newValue) {
var params = queryStringToParams(this.props.location.search);
delete params["sortby"];
if (newValue !== "totaldownloads") {
params["sortby"] = newValue;
}
this.setState({ sortby: newValue });
this.props.history.push(paramsToQueryString(params));
trackEvent("/sortby/change", newValue);
}
render() {
const dropdownStyles = () => {
return {
root: {
display: "flex"
},
label: {
marginRight: "10px",
color: "#000000",
fontSize: "12px"
},
title: {
color: "#595959;",
border: "1px solid #BCBCBC",
borderRadius: "2px",
fontSize: "12px"
},
dropdown: {
width: 150
}
};
};
const searchBoxStyles = () => {
return {
root: {
border: "1px solid #BCBCBC",
borderRadius: "3px"
}
};
};
let resultCount = this.props.samples.length;
return (
<div className="content-header">
<div className="content-header-titlewapper">
<div className="content-header-title">
Azure serverless community library
</div>
<div style={{ marginLeft: "auto" }}>
<FabricLink
as={Link}
to="/contribute"
className="content-header-contributionLink"
>
<div className="contributionLink-content">
<Icon iconName="contribution-svg" />
<div className="contribution-link-text">Contributions</div>
</div>
</FabricLink>
</div>
</div>
<SearchBox
placeholder="Search"
value={this.state.filterText}
onSearch={newValue => this.filterTextChanged(newValue)}
onClear={() => this.filterTextChanged("")}
styles={searchBoxStyles}
/>
<div className="content-header-sortbywrappper">
<div className="content-header-count">
Displaying {resultCount} {resultCount === 1 ? "result" : "results"}
</div>
<div style={{ marginLeft: "auto" }}>
<Dropdown
defaultSelectedKey={this.state.sortby}
options={[
{ key: "totaldownloads", text: "Most downloads" },
{ key: "atoz", text: "A to Z" },
{ key: "createddate", text: "Most recent" }
]}
label="Sort By"
styles={dropdownStyles}
onChange={(ev, item) => this.sortbyChanged(item.key)}
/>
</div>
</div>
</div>
);
}
}
const mapStateToProps = state => ({});
const ContentHeaderContainer = connect(mapStateToProps)(ContentHeader);
export default withRouter(ContentHeaderContainer);

Просмотреть файл

@ -0,0 +1,28 @@
import React, { Component } from "react";
import ContributionForm from "./ContributionForm";
import ContributionsList from "./ContributionsList";
import SignInDialog from "./SignInDialog";
import PageHeaderWithBackButton from "../shared/PageHeaderWithBackButton";
import "./Contribute.scss";
class ContributionsPage extends Component {
render() {
return (
<div className="contribute-page-container">
<PageHeaderWithBackButton title="Contributions">
<p className="contribute-page-description">
This is where you can see all your existing contributions. You can
also add a new contribution by clicking on the add new contribution
link.
</p>
</PageHeaderWithBackButton>
<ContributionForm />
<ContributionsList />
<SignInDialog />
</div>
);
}
}
export default ContributionsPage;

Просмотреть файл

@ -0,0 +1,52 @@
.left-margin {
margin-left: 23px;
}
.contribute-page-description {
max-width: 785px;
}
.add-contribution-container {
margin-top: 23px;
margin-bottom: 22px;
}
.add-contribution-link {
@extend .left-margin;
font-size: 12px;
}
.contribution-form-container {
background-color: #fbfbfb;
margin-top: 12px;
padding-top: 15px;
padding-left: 23px;
padding-bottom: 28px;
}
.input-container {
display: flex;
flex-direction: row;
flex: 1;
}
.contribution-form-fields-container {
max-width: 430px;
margin-right: 100px;
flex: 1;
}
.input-field-container {
margin-bottom: 10px;
}
.contribution-form-actions-container {
margin-top: 18px;
}
.contribution-list-container {
@extend .left-margin;
max-width: 746px;
}
.empty-contribution-list-container {
font-size: 12px;
margin-top: 6px;
}

Просмотреть файл

@ -0,0 +1,317 @@
import React, { Component } from "react";
import {
Icon,
Link,
TextField,
PrimaryButton,
DefaultButton,
Dropdown,
Dialog
} from "office-ui-fabric-react";
import { libraryService } from "../../services";
import * as commonStyles from "../shared/Button.styles";
import * as Constants from "../shared/Constants";
const initialState = {
showForm: false,
title: "",
description: "",
repository: "",
template: "",
technologies: [],
language: "",
solutionareas: [],
dialogProps: {
isVisible: false,
title: "",
content: {}
}
};
class ContributionForm extends Component {
constructor(props) {
super(props);
this.state = initialState;
this.onLinkClick = this.onLinkClick.bind(this);
this.onAddButtonClick = this.onAddButtonClick.bind(this);
this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.onTitleChange = this.onTitleChange.bind(this);
this.onDescriptionChange = this.onDescriptionChange.bind(this);
this.onDismissDialog = this.onDismissDialog.bind(this);
this.validateForm = this.getFormValidationErrors.bind(this);
}
technologiesOptionsChanged(newValue) {
let technologies = this.state.technologies;
if (newValue.selected) {
technologies.push(newValue.key);
} else {
technologies = technologies.filter(t => t !== newValue.key);
}
this.setState({ technologies: technologies });
}
solutionAreasOptionsChanged(newValue) {
let solutionAreas = this.state.solutionareas;
if (newValue.selected) {
solutionAreas.push(newValue.key);
} else {
solutionAreas = solutionAreas.filter(t => t !== newValue.key);
}
this.setState({ solutionareas: solutionAreas });
}
languageOptionChanged(newValue) {
this.setState({ language: newValue });
}
handleInputChange(event, newValue) {
const target = event.target;
const name = target.name;
this.setState({ [name]: newValue });
}
onTitleChange(event, newValue) {
if (!newValue || newValue.length <= 80) {
this.setState({ title: newValue || "" });
} else {
// this block is needed because of Fabric bug #1350
this.setState({ title: this.state.title });
}
}
onDescriptionChange(event, newValue) {
if (!newValue || newValue.length <= 300) {
this.setState({ description: newValue || "" });
} else {
// this block is needed because of Fabric bug #1350
this.setState({ description: this.state.description });
}
}
getFormValidationErrors(sample) {
let errors = [];
if (sample.title.length === 0) {
errors.push("Title cannot be empty");
}
if (sample.repository.length === 0) {
errors.push("Repository URL cannot be empty");
}
if (sample.description.length === 0) {
errors.push("Description cannot be empty");
}
if (sample.technologies.length === 0) {
errors.push("At least one technology must be selected");
}
if (sample.language.length === 0) {
errors.push("Language must be selected");
}
if (sample.solutionareas.length === 0) {
errors.push("At least one solution area must be selected");
}
return errors;
}
onLinkClick() {
this.setState({ showForm: true });
}
onAddButtonClick() {
const sample = {
title: this.state.title,
description: this.state.description,
repository: this.state.repository,
template: this.state.template,
technologies: this.state.technologies,
language: this.state.language,
solutionareas: this.state.solutionareas
};
const errors = this.getFormValidationErrors(sample);
if (errors.length > 0) {
this.showDialog("Unable to add sample", errors);
return;
}
libraryService
.submitNewSample(sample)
.then(sample => {
this.resetForm();
this.showDialog(
"Thank you!",
"Thank you for your contribution! Your sample has been submitted for approval."
);
})
.catch(data => {
this.showDialog("Unable to add sample", data.error);
});
}
showDialog(title, content) {
const dialogProps = {
title: title,
content: content,
isVisible: true
};
this.setState({ dialogProps });
}
onDismissDialog() {
const dialogProps = { ...this.state.dialogProps, isVisible: false };
this.setState({ dialogProps });
}
onCancelButtonClick() {
this.resetForm();
}
resetForm() {
this.setState(initialState);
}
render() {
let technologiesOptions = Constants.technologies.map(t => ({
key: t,
text: t
}));
let languageOptions = Constants.languages.map(l => ({ key: l, text: l }));
languageOptions.push({
key: Constants.NotApplicableLanguage,
text: "Not applicable"
});
let solutionAreasOptions = Constants.solutionAreas.map(s => ({
key: s,
text: s
}));
solutionAreasOptions.push({
key: Constants.OtherSolutionArea,
text: Constants.OtherSolutionArea
});
let { showForm, dialogProps } = this.state;
return (
<div className="add-contribution-container">
<div className="add-contribution-link">
<Link onClick={this.onLinkClick}>
<Icon iconName="Edit" style={{ marginRight: "5px" }} />
Add new contribution
</Link>
</div>
{dialogProps.isVisible && (
<Dialog
dialogContentProps={{
title: dialogProps.title
}}
hidden={!dialogProps.isVisible}
onDismiss={this.onDismissDialog}
>
{Array.isArray(dialogProps.content) ? (
<ul>
{dialogProps.content.map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
) : (
<p>{String(dialogProps.content)}</p>
)}
</Dialog>
)}
{showForm && (
<div className="contribution-form-container">
<div className="input-container">
<div className="contribution-form-fields-container">
<TextField
className="input-field-container"
name="title"
label="Title"
required={true}
placeholder="Enter the title of your sample"
value={this.state.title}
onChange={this.onTitleChange}
/>
<TextField
className="input-field-container"
name="repository"
label="URL"
required={true}
placeholder="Enter the URL of the GitHub repo that hosts your sample "
value={this.state.repository}
onChange={this.handleInputChange}
/>
<TextField
className="input-field-container"
name="description"
label="Description"
required={true}
resizable={false}
rows={4}
multiline
placeholder="Briefly describe your sample (300 characters maximum)"
value={this.state.description}
onChange={this.onDescriptionChange}
/>
</div>
<div className="contribution-form-fields-container">
<Dropdown
className="input-field-container"
placeholder="Select which technologies are used by this sample"
label="Technologies"
onChange={(ev, item) => this.technologiesOptionsChanged(item)}
required={true}
multiSelect
options={technologiesOptions}
/>
<Dropdown
className="input-field-container"
placeholder="Select the language used by the Azure Functions involved"
label="Language"
onChange={(ev, item) => this.languageOptionChanged(item.key)}
required={true}
options={languageOptions}
/>
<Dropdown
className="input-field-container"
placeholder="Select categories for your sample"
label="Solution Area"
onChange={(ev, item) =>
this.solutionAreasOptionsChanged(item)
}
required={true}
multiSelect
options={solutionAreasOptions}
/>
<TextField
className="input-field-container"
name="template"
label="ARM template URL"
placeholder="Enter the URL of the ARM template to use to deploy your sample"
value={this.state.template}
onChange={this.handleInputChange}
/>
</div>
</div>
<div className="contribution-form-actions-container">
<PrimaryButton
styles={commonStyles.buttonStyles}
text="Add"
onClick={this.onAddButtonClick}
/>
<DefaultButton
styles={commonStyles.secondaryButtonStyles}
text="Cancel"
onClick={this.onCancelButtonClick}
/>
</div>
</div>
)}
</div>
);
}
}
export default ContributionForm;

Просмотреть файл

@ -0,0 +1,30 @@
const labelStyles = {
root: {
fontSize: "13px",
lineHeight: "18px",
height: "21px",
paddingBottom: "1px",
paddingTop: 0
}
};
export const textFieldStyles = {
root: {
marginBottom: "10px"
},
fieldGroup: {
height: "23px"
},
field: {
fontSize: "12px",
paddingLeft: "7px",
selectors: {
"::placeholder": {
fontSize: "12px"
}
}
},
subComponentStyles: {
label: labelStyles
}
};

Просмотреть файл

@ -0,0 +1,59 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Label } from "office-ui-fabric-react";
import ItemList from "../../components/ItemList/ItemList";
class ContributionsList extends Component {
filteredSamples() {
let { loggedIn, user, samples } = this.props;
let { userName } = user;
if (!loggedIn || !userName) {
return {};
}
let filter = new RegExp(userName, "i");
samples = samples.filter(
el =>
el.repository.replace("https://github.com/", "").match(filter) || // this match should be against author
(el.author && el.author.match(filter))
);
return samples;
}
render() {
const headerLabelStyles = {
root: {
fontSize: "12px",
fontWeight: "bold",
paddingTop: "0px",
paddingBottom: "6px"
}
};
const filteredSamples = this.filteredSamples();
return (
<div className="contribution-list-container">
<Label styles={headerLabelStyles}>My samples</Label>
{filteredSamples.length > 0 ? (
<ItemList filteredSamples={filteredSamples} disableHover={true} />
) : (
<div className="empty-contribution-list-container">
<i>You currently do not have any samples</i>
</div>
)}
</div>
);
}
}
function mapStateToProps(state) {
return {
samples: state.samples,
loggedIn: state.authentication.loggedIn,
user: state.authentication.user
};
}
const ContributionsListContainer = connect(mapStateToProps)(ContributionsList);
export default ContributionsListContainer;

Просмотреть файл

@ -0,0 +1,81 @@
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { Dialog, DialogFooter, DefaultButton } from "office-ui-fabric-react";
import SignInButton from "../shared/SignInButton";
class SignInDialog extends Component {
constructor(props) {
super(props);
this.handleHomeButtonClick = this.handleHomeButtonClick.bind(this);
}
handleHomeButtonClick() {
this.props.history.push("/");
}
render() {
const footerStyles = {
actionsRight: {
textAlign: "center",
marginRight: "0px"
}
};
const buttonStyles = {
root: {
fontSize: "12px",
height: "32px",
minWidth: "40px",
backgroundColor: "white",
border: "1px solid #0078D7",
color: "#0058AD"
},
rootHovered: {
border: "1px solid #0078D7",
color: "#0058AD"
},
label: {
fontWeight: "normal"
}
};
const { loading, loggedIn } = this.props;
if (loading) {
return null;
}
return (
<div>
<Dialog
hidden={loggedIn}
dialogContentProps={{
title: "Please sign in",
subText: "Please sign in to continue."
}}
modalProps={{
isBlocking: true
}}
>
<DialogFooter styles={footerStyles}>
<SignInButton />
<DefaultButton
styles={buttonStyles}
text="Home"
onClick={this.handleHomeButtonClick}
/>
</DialogFooter>
</Dialog>
</div>
);
}
}
const mapStateToProps = state => ({
loading: state.authentication.loading,
loggedIn: state.authentication.loggedIn
});
const SignInDialogContainer = connect(mapStateToProps)(SignInDialog);
export default withRouter(SignInDialogContainer);

Просмотреть файл

@ -0,0 +1,110 @@
import React, { Component } from "react";
import { Icon, Link as FabricLink } from "office-ui-fabric-react";
import { trackEvent } from "../../helpers";
import { libraryService } from "../../services";
import "./ActionBar.scss";
class ActionBar extends Component {
constructor(props) {
super(props);
this.outboundRepoClick = this.outboundRepoClick.bind(this);
this.outboundDeployClick = this.outboundDeployClick.bind(this);
this.openInVSCodeClick = this.openInVSCodeClick.bind(this);
this.trackUserActionEvent = this.trackUserActionEvent.bind(this);
}
outboundDeployClick() {
this.updateDownloadCount(this.props.id);
this.trackUserActionEvent("/sample/deploy/agree");
}
updateDownloadCount(id) {
libraryService
.updateDownloadCount(id)
.then(() => {
// do nothing
})
.catch(() => {
// do nothing
});
}
getDeployLink(template) {
return (
"https://portal.azure.com/#create/Microsoft.Template/uri/" +
encodeURIComponent(template)
);
}
outboundRepoClick() {
this.trackUserActionEvent("/sample/source");
}
getOpenInVSCodeLink(repository) {
return "vscode://vscode.git/clone?url=" + encodeURIComponent(repository);
}
openInVSCodeClick() {
this.updateDownloadCount(this.props.id);
this.trackUserActionEvent("/sample/openinvscode");
}
trackUserActionEvent(eventName) {
let eventData = {
id: this.props.id,
repository: this.props.repository,
template: this.props.template
};
trackEvent(eventName, eventData);
}
render() {
const { repository, template } = this.props;
return (
<div className="action-container">
<div className="action-item">
<FabricLink
href={this.getOpenInVSCodeLink(repository)}
disabled={!repository}
onClick={this.openInVSCodeClick}
>
<div className="action-link-wrapper">
<Icon iconName="Edit" className="fabric-icon-link" />
<span className="action-link-text">Edit in VS Code</span>
</div>
</FabricLink>
</div>
<div className="action-item">
<FabricLink
href={this.getDeployLink(template)}
disabled={!template}
target="_blank"
onClick={this.outboundDeployClick}
>
<div className="action-link-wrapper">
<Icon iconName="Deploy" className="fabric-icon-link" />
<span className="action-link-text">Deploy</span>
</div>
</FabricLink>
</div>
<div className="action-item">
<FabricLink
href={repository}
disabled={!repository}
target="_blank"
onClick={this.outboundRepoClick}
>
<div className="action-link-wrapper">
<Icon iconName="GitHub-12px" className="githubicon" />
<span className="action-link-text">Open in Github</span>
</div>
</FabricLink>
</div>
</div>
);
}
}
export default ActionBar;

Просмотреть файл

@ -0,0 +1,33 @@
.action-container {
display: flex;
flex-direction: row;
padding-top: 7px;
padding-left: 23px;
}
.action-item {
margin-right: 16px;
}
.action-link-wrapper {
height: 100%;
display: flex;
}
.fabric-icon-link {
font-size: 12px;
width: 13px;
height: 11px;
margin-top: 2px;
}
.githubicon {
@extend .fabric-icon-link;
fill: currentColor;
}
.action-link-text {
padding-left: 8px;
font-size: 12px;
margin: auto;
}

Просмотреть файл

@ -0,0 +1,151 @@
import React, { Component } from "react";
import ReactMarkdown from "markdown-to-jsx";
import {
Pivot,
PivotItem,
PivotLinkSize,
ScrollablePane
} from "office-ui-fabric-react/lib/index";
import { githubService } from "../../services";
import "./DetailPageContent.scss";
const defaultLicenseText =
"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.";
class DetailPageContent extends Component {
constructor(props) {
super(props);
this.state = {
armTemplateText: "",
markdownText: "",
licenseText: "",
selectedKey: "overview"
};
this.handleLinkClick = this.handleLinkClick.bind(this);
}
// This method is used to fetch readme content from repo when valid repository url is received as props
componentDidUpdate(prevProps, prevState) {
if (
this.props.repository !== prevProps.repository &&
prevState.markdownText === ""
) {
let { repository } = this.props;
githubService
.getReadMe(repository)
.then(data => {
var r = new RegExp(
"https?://Azuredeploy.net/deploybutton.(png|svg)",
"ig"
);
data = data.replace(r, "");
this.setState({ markdownText: data });
})
.catch(() => {
// do nothing
});
}
}
handleLinkClick(pivotItem, ev) {
const selectedKey = pivotItem.props.itemKey;
this.setState({ selectedKey });
if (selectedKey === "armtemplate" && this.state.armTemplateText === "") {
const { template } = this.props;
if (template) {
githubService
.getArmTemplate(template)
.then(data =>
this.setState({
armTemplateText: data
})
)
.catch(() => {
// do nothing
});
}
}
if (selectedKey === "license" && this.state.licenseText === "") {
const { license, repository } = this.props;
githubService
.getLicense(license, repository)
.then(data =>
this.setState({
licenseText: data
})
)
.catch(() => {
// do nothing
});
}
}
render() {
const pivotStyles = {
root: {
paddingLeft: "15px"
},
text: {
color: " #0058AD",
fontSize: "14px"
}
};
const {
selectedKey,
markdownText,
licenseText,
armTemplateText
} = this.state;
return (
<div className="detail-page-content">
<Pivot
styles={pivotStyles}
selectedKey={selectedKey}
linkSize={PivotLinkSize.large}
onLinkClick={(item, ev) => this.handleLinkClick(item, ev)}
>
<PivotItem headerText="Overview" itemKey="overview">
<div className="pivot-item-container">
<div className="scrollablePane-wrapper">
<ScrollablePane>
<ReactMarkdown>{markdownText}</ReactMarkdown>
</ScrollablePane>
</div>
</div>
</PivotItem>
<PivotItem headerText="License" itemKey="license">
<div className="pivot-item-container">
<div className="scrollablePane-wrapper">
<ScrollablePane>
<div className="license-content">
<p>{defaultLicenseText}</p>
{licenseText !== "" && (
<p style={{ borderTop: "2px outset" }}>{licenseText}</p>
)}
</div>
</ScrollablePane>
</div>
</div>
</PivotItem>
{this.props.template && (
<PivotItem headerText="ARM template" itemKey="armtemplate">
<div className="pivot-item-container">
<div className="scrollablePane-wrapper">
<ScrollablePane>
<div className="armtemplate-content">
<pre>{armTemplateText}</pre>
</div>
</ScrollablePane>
</div>
</div>
</PivotItem>
)}
</Pivot>
</div>
);
}
}
export default DetailPageContent;

Просмотреть файл

@ -0,0 +1,26 @@
.detail-page-content {
margin-top: 24px;
border-top: 1px solid rgba(105, 130, 155, 0.25);
border-bottom: 1px solid rgba(105, 130, 155, 0.25);
}
.pivot-item-container {
padding-left: 23px;
background-color: #fbfbfb;
}
.scrollablePane-wrapper {
min-height: 60vh;
position: relative;
max-height: inherit;
}
.armtemplate-content {
overflow-wrap: break-word;
white-space: pre-line;
justify-content: center;
}
.license-content {
max-width: 750px;
text-align: justify;
white-space: pre-line;
}

Просмотреть файл

@ -0,0 +1,35 @@
import React, { Component } from "react";
import PageHeaderWithBackButton from "../shared/PageHeaderWithBackButton";
import MetricBar from "../MetricBar/MetricBar";
class DetailPageHeader extends Component {
render() {
let {
title,
author,
id,
totaldownloads,
createddate,
description,
likes,
dislikes
} = this.props;
return (
<div>
<PageHeaderWithBackButton title={title}>
<MetricBar
likes={likes}
dislikes={dislikes}
author={author}
id={id}
downloads={totaldownloads}
createddate={createddate}
/>
<p>{description}</p>
</PageHeaderWithBackButton>
</div>
);
}
}
export default DetailPageHeader;

Просмотреть файл

@ -0,0 +1,86 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import ActionBar from "./ActionBar";
import DetailPageContent from "./DetailPageContent";
import DetailPageHeader from "./DetailPageHeader";
import { sampleActions } from "../../actions/sampleActions";
import { trackEvent } from "../../helpers";
class DetailView extends Component {
constructor(props) {
super(props);
this.state = {
sample: {}
};
}
componentDidMount() {
this.setCurrentItemInState();
}
componentDidUpdate(prevProps, prevState) {
this.setCurrentItemInState();
}
setCurrentItemInState() {
if (!this.state.sample.id && this.props.samples.length > 0) {
const id = this.props.match.params.id;
let currentItem = this.props.samples.filter(s => s.id === id)[0] || {};
this.setState({ sample: currentItem });
this.trackPageLoadEvent(currentItem);
}
}
trackPageLoadEvent(sample) {
let eventData = {
id: sample.id,
repository: sample.repository,
template: sample.template
};
trackEvent("/sample/detailpage", eventData);
}
render() {
let likes = this.state.sample.likes ? this.state.sample.likes : 0;
let dislikes = this.state.sample.dislikes ? this.state.sample.dislikes : 0;
return (
<div>
<DetailPageHeader
title={this.state.sample.title}
author={this.state.sample.author}
id={this.state.sample.id}
totaldownloads={this.state.sample.totaldownloads}
createddate={this.state.sample.createddate}
description={this.state.sample.description}
likes={likes}
dislikes={dislikes}
/>
<ActionBar
id={this.state.sample.id}
template={this.state.sample.template}
repository={this.state.sample.repository}
/>
<DetailPageContent
template={this.state.sample.template}
repository={this.state.sample.repository}
/>
</div>
);
}
}
const mapStateToProps = state => ({
samples: state.samples
});
const mapDispatchToProps = {
getSamplesSuccess: sampleActions.getSamplesSuccess
};
const DetailViewContainer = connect(
mapStateToProps,
mapDispatchToProps
)(DetailView);
export default DetailViewContainer;

Просмотреть файл

@ -0,0 +1,119 @@
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { ActionButton, ContextualMenuItemType } from "office-ui-fabric-react";
import { userService } from "../../services";
import { userActions } from "../../actions/userActions";
import SignInButton from "../shared/SignInButton";
import UserPersona from "./UserPersona";
class AuthControl extends Component {
constructor(props) {
super(props);
this._renderMenuList = this._renderMenuList.bind(this);
this._onSignoutClick = this._onSignoutClick.bind(this);
this._onContributionsClick = this._onContributionsClick.bind(this);
}
getMenuItems() {
const { user } = this.props;
const userName =
user.fullName && user.fullName !== "" ? user.fullName : user.displayName;
return [
{
key: "Header",
itemType: ContextualMenuItemType.Header,
text: "Logged in as: " + userName
},
{
key: "divider_1",
itemType: ContextualMenuItemType.Divider
},
{
key: "contributions",
text: "My contributions",
onClick: this._onContributionsClick
},
{
key: "divider_1",
itemType: ContextualMenuItemType.Divider
},
{
key: "signOut",
text: "Sign out",
onClick: this._onSignoutClick
}
];
}
_renderMenuIcon() {
return null;
}
_renderMenuList(menuListProps, defaultRender) {
const { loggedIn } = this.props;
if (loggedIn) {
return <div>{defaultRender(menuListProps)}</div>;
}
return (
<div className="signin-button-container">
<SignInButton />
</div>
);
}
_onContributionsClick() {
this.props.history.push("/contribute");
}
_onSignoutClick() {
this.props.logout(); // clear the redux store before making a call to the backend
userService
.logout()
.then(() => {
// do nothing
})
.catch(() => {
// do nothing
});
}
render() {
return (
<span>
<div className="auth-control">
<ActionButton
onRenderMenuIcon={this._renderMenuIcon}
menuProps={{
onRenderMenuList: this._renderMenuList,
shouldFocusOnMount: true,
items: this.getMenuItems()
}}
>
<UserPersona />
</ActionButton>
</div>
</span>
);
}
}
const mapStateToProps = state => ({
loggedIn: state.authentication.loggedIn,
user: state.authentication.user
});
const mapDispatchToProps = {
logout: userActions.logout
};
const AuthControlContainer = connect(
mapStateToProps,
mapDispatchToProps
)(AuthControl);
export default withRouter(AuthControlContainer);

Просмотреть файл

@ -0,0 +1,49 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Link } from "office-ui-fabric-react";
import { getTheme } from "office-ui-fabric-react";
import "./Header.scss";
import AuthControl from "./AuthControl";
class Header extends Component {
render() {
const theme = getTheme();
const linkStyles = {
root: {
marginLeft: "15px",
lineHeight: "40px",
fontSize: "14px",
color: theme.palette.white,
selectors: {
"&:active, &:hover, &:active:hover, &:visited": {
color: theme.palette.white
}
}
}
};
return (
<div className="headerbar">
<span>
<Link
styles={linkStyles}
href="https://azure.microsoft.com/"
target="_blank"
>
Microsoft Azure
</Link>
<AuthControl />
</span>
</div>
);
}
}
const mapStateToProps = state => ({
loggedIn: state.authentication.loggedIn
});
const HeaderContainer = connect(mapStateToProps)(Header);
export default HeaderContainer;

Просмотреть файл

@ -0,0 +1,14 @@
.headerbar {
background-color: black;
display: inline-block;
width: 100%;
}
.auth-control {
float: right;
}
.signin-button-container {
text-align: center;
padding: 20px 16px;
}

Просмотреть файл

@ -0,0 +1,73 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Persona, PersonaSize, Icon } from "office-ui-fabric-react";
import { getTheme } from "office-ui-fabric-react";
class UserPersona extends Component {
_onRenderInitials() {
return <Icon iconName="Contact" />;
}
render() {
const theme = getTheme();
const personaStyles = {
root: {
height: "40px",
color: theme.palette.white,
float: "right",
selectors: {
":hover": {
selectors: {
$primaryText: {
color: theme.palette.white
}
}
}
}
},
details: {
width: "85px"
},
primaryText: {
color: theme.palette.white
}
};
const { loading, loggedIn, user } = this.props;
if (loading) {
return null;
}
return loggedIn ? (
<Persona
styles={personaStyles}
text={user.displayName}
imageUrl={user.avatarUrl}
imageAlt={
user.fullName && user.fullName !== ""
? user.fullName
: user.displayName
}
size={PersonaSize.size28}
/>
) : (
<Persona
styles={personaStyles}
text="Guest"
imageAlt="Guest"
size={PersonaSize.size28}
onRenderInitials={this._onRenderInitials}
/>
);
}
}
const mapStateToProps = state => ({
loading: state.authentication.loading,
loggedIn: state.authentication.loggedIn,
user: state.authentication.user
});
const UserPersonaContainer = connect(mapStateToProps)(UserPersona);
export default UserPersonaContainer;

Просмотреть файл

@ -0,0 +1,76 @@
import React, { Component } from "react";
import {
FocusZone,
FocusZoneDirection,
List,
Link as FabricLink
} from "office-ui-fabric-react";
import { Link } from "react-router-dom";
import ItemTags from "../ItemTags/ItemTags";
import MetricBar from "../MetricBar/MetricBar";
import "./ItemList.scss";
class ItemList extends Component {
constructor(props) {
super(props);
this._onRenderCell = this._onRenderCell.bind(this);
this.disableHover = this.props.disableHover || false;
}
_onRenderCell(item, index) {
let likes = item.likes ? item.likes : 0;
let dislikes = item.dislikes ? item.dislikes : 0;
return (
<article>
<div className={this.disableHover ? "" : "libraryitemContainer"}>
<div
className={
this.disableHover ? "libraryitem" : "libraryitem-withhover"
}
>
<div className="title">
<FabricLink
as={Link}
className="titlelink"
to={`/sample/${item.id}`}
>
{item.title}
</FabricLink>
</div>
<MetricBar
likes={likes}
dislikes={dislikes}
author={item.author}
id={item.id}
downloads={item.totaldownloads}
createddate={item.createddate}
/>
<div className="description">{item.description}</div>
<ItemTags
technologies={item.technologies}
language={item.language}
tags={item.tags}
solutionareas={item.solutionareas}
/>
</div>
</div>
</article>
);
}
render() {
return (
<div>
<FocusZone direction={FocusZoneDirection.vertical}>
<List
items={this.props.filteredSamples}
onRenderCell={this._onRenderCell}
/>
</FocusZone>
</div>
);
}
}
export default ItemList;

Просмотреть файл

@ -0,0 +1,46 @@
.libraryitem {
border-bottom: 1px solid #d8d8d8;
padding-top: 12px;
padding-bottom: 15px;
}
.libraryitem-withhover {
@extend .libraryitem;
&:hover {
border-bottom: none;
}
}
.libraryitemContainer {
padding-left: 16px;
&:hover {
margin-top: -1px;
margin-bottom: 1px;
background-color: white;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
}
}
.title {
font-size: 16px;
line-height: 21px;
color: #0072c6;
&:visited {
text-decoration: none;
}
}
.titlelink {
text-decoration: none;
}
.description {
font-size: 14px;
line-height: 19px;
color: #000000;
padding-bottom: 4px;
padding-bottom: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

Просмотреть файл

@ -0,0 +1,22 @@
.tagcontainer {
font-size: 14px;
line-height: normal;
vertical-align: sub;
}
.tag {
margin-left: 5px;
background: #efefef;
border-radius: 10px;
padding: 4px 10px 4px 10px;
font-size: 12px;
line-height: 16px;
display: inline-block;
}
.svg {
width: 17px;
height: 17px;
vertical-align: baseline;
margin-right: 4px;
}

Просмотреть файл

@ -0,0 +1,43 @@
import React, { Component } from "react";
import * as Constants from "../shared/Constants";
import "./ItemTags.css";
class ItemTags extends Component {
render() {
// Tags contain technologies, language, solutionareas and custom tags
let allTags = [];
if (this.props.technologies) {
allTags.push(...this.props.technologies);
}
if (
this.props.language !== "" &&
this.props.language !== Constants.NotApplicableLanguage
) {
allTags.push(this.props.language);
}
if (this.props.solutionareas) {
let solutionareas = this.props.solutionareas.filter(
s => s !== Constants.OtherSolutionArea
);
allTags.push(...solutionareas);
}
if (this.props.tags) {
allTags.push(...this.props.tags);
}
return (
<div className="tagcontainer">
Tags :
{allTags.map((value, index) => (
<span className="tag" key={index}>
{value}
</span>
))}
</div>
);
}
}
export default ItemTags;

Просмотреть файл

@ -0,0 +1,37 @@
#mainContainer {
width: 100%;
height: 100%;
display: flex;
}
#sidebar {
overflow-y: auto;
height: 100%;
max-width: 15%;
min-width: 175px;
background-color: #f9f9f9;
box-shadow: 0px 0px 1.8px rgba(0, 0, 0, 0.12),
0px 3.2px 3.6px rgba(0, 0, 0, 0.08);
}
#content {
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1;
}
#contentheader{
max-width: 830px;
padding-left: 40px;
padding-right: 40px;
}
#list {
flex:1;
overflow-y: auto;
padding: 10px;
padding-left: 25px;
}
.list-container {
max-width: 765px;
}

Просмотреть файл

@ -0,0 +1,171 @@
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import SideBarContainer from "../../components/sidebar/SideBar";
import ContentHeaderContainer from "../ContentHeader/ContentHeader";
import ItemList from "../../components/ItemList/ItemList";
import "./Main.css";
import { queryStringToParams } from "../../helpers";
class Main extends Component {
constructor(props) {
super(props);
this.state = {
initialFilters: this.getFiltersFromQueryParams(),
showContentHeaderShadow: false
};
this.onListScrollEvent = this.onListScrollEvent.bind(this);
}
filteredSamples() {
let samples = this.props.samples;
let currentfilters = this.getFiltersFromQueryParams();
let filter = new RegExp(currentfilters.filtertext, "i");
samples = samples.filter(
el =>
el.title.match(filter) ||
el.description.match(filter) ||
el.language.match(filter) ||
el.repository.replace("https://github.com/", "").match(filter) ||
(el.tags && el.tags.some(x => x.match(filter))) ||
(el.technologies && el.technologies.some(x => x.match(filter))) ||
(el.solutionareas && el.solutionareas.some(x => x.match(filter))) ||
(el.author && el.author.match(filter))
);
if (currentfilters.categories.technologies.length > 0) {
samples = samples.filter(s =>
s.technologies.some(t =>
currentfilters.categories.technologies.includes(t)
)
);
}
if (currentfilters.categories.languages.length > 0) {
samples = samples.filter(s =>
currentfilters.categories.languages.includes(s.language)
);
}
if (currentfilters.categories.solutionareas.length > 0) {
samples = samples.filter(s =>
s.solutionareas.some(a =>
currentfilters.categories.solutionareas.includes(a)
)
);
}
return this.Sort(samples, currentfilters.sortby);
}
Sort(list, sortby) {
list = list.map(a => a);
if (sortby === "totaldownloads") {
list = list.sort(function(a, b) {
return b.totaldownloads - a.totaldownloads;
});
} else if (sortby === "createddate") {
list.sort(function(a, b) {
let dateA = new Date(a.createddate),
dateB = new Date(b.createddate);
return dateB - dateA;
});
} else {
list = list.sort(function(a, b) {
let titleA = a.title.toLowerCase(),
titleB = b.title.toLowerCase();
if (titleA < titleB)
//sort string ascending
return -1;
if (titleA > titleB) return 1;
return 0; //default return value (no sorting)
});
}
return list;
}
getFiltersFromQueryParams() {
var filter = {
categories: {
technologies: [],
languages: [],
solutionareas: []
},
filtertext: "",
sortby: "totaldownloads"
};
var params = queryStringToParams(this.props.location.search);
if (params.technology && params.technology.length > 0) {
filter.categories.technologies = params.technology.split(",");
}
if (params.language && params.language.length > 0) {
filter.categories.languages = params.language.split(",");
}
if (params.solutionarea && params.solutionarea.length > 0) {
filter.categories.solutionareas = params.solutionarea.split(",");
}
if (params.filtertext && params.filtertext.length > 0) {
filter.filtertext = params.filtertext;
}
if (params.sortby && params.sortby.length > 0) {
filter.sortby = params.sortby;
}
return filter;
}
onListScrollEvent(e) {
let element = e.target;
if (element.scrollTop > 0) {
!this.state.showContentHeaderShadow &&
this.setState({ showContentHeaderShadow: true });
} else {
this.state.showContentHeaderShadow &&
this.setState({ showContentHeaderShadow: false });
}
}
render() {
const contentheaderShadowStyle = {
boxShadow: this.state.showContentHeaderShadow
? "0 4px 6px -6px #222"
: "none"
};
return (
<div id="mainContainer">
<div id="sidebar">
<SideBarContainer
initialFilters={this.state.initialFilters.categories}
/>
</div>
<div id="content">
<div id="contentheader" style={contentheaderShadowStyle}>
<ContentHeaderContainer
initialSearchText={this.state.initialFilters.filtertext}
initialSortBy={this.state.initialFilters.sortby}
samples={this.filteredSamples()}
/>
</div>
<div id="list" onScroll={this.onListScrollEvent}>
<div className="list-container">
<ItemList filteredSamples={this.filteredSamples()} />
</div>
</div>
</div>
</div>
);
}
}
const mapStateToProps = state => ({
samples: state.samples
});
const MainContainer = connect(mapStateToProps)(withRouter(Main));
export default MainContainer;

Просмотреть файл

@ -0,0 +1,8 @@
.metrics {
margin-top: 7px;
margin-bottom: 7px;
font-size: 12px;
line-height: 16px;
color: #555555;
display: flex;
}

Просмотреть файл

@ -0,0 +1,132 @@
import React, { Component } from "react";
import { IconButton } from "office-ui-fabric-react";
import { libraryService } from "../../services";
import "./MetricBar.css";
class MetricBar extends Component {
constructor(props) {
super(props);
this.state = {
sentimentAction: "none"
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.id && nextProps.id !== prevState.id) {
return { sentimentAction: localStorage.getItem(nextProps.id) };
}
return null;
}
handleLikeClick() {
// If user already liked, then decrement like count and set the sentiment state to none.
// If in past disliked and choose to like the sample, then decrement dislike count and increment like count
// If not action taken ealier, just increment like count and set sentiment state to liked.
if (this.state.sentimentAction === "liked") {
this.updateSentiment("none", -1, 0);
} else if (this.state.sentimentAction === "disliked") {
this.updateSentiment("liked", 1, -1);
} else {
this.updateSentiment("liked", 1, 0);
}
}
handleDislikeClick() {
if (this.state.sentimentAction === "liked") {
this.updateSentiment("disliked", -1, 1);
} else if (this.state.sentimentAction === "disliked") {
this.updateSentiment("none", 0, -1);
} else {
this.updateSentiment("disliked", 0, 1);
}
}
updateSentiment(choice, likeChanges, dislikeChanges) {
localStorage.setItem(this.props.id, choice);
this.setState({ sentimentAction: choice });
var sentimentPayload = {
Id: this.props.id,
LikeChanges: likeChanges,
DislikeChanges: dislikeChanges
};
libraryService
.updateUserSentimentStats(sentimentPayload)
.then(() => {
// do nothing
})
.catch(() => {
// do nothing
});
}
render() {
let { author, downloads, createddate, likes, dislikes } = this.props;
let createdonDate = new Date(createddate);
let createdonLocaleDate = createdonDate.toLocaleDateString();
let likeIconName = "Like";
let likeTitle = "Like";
let dislikeIconName = "Dislike";
let dislikeTitle = "Dislike";
if (this.state.sentimentAction === "liked") {
likeIconName = "LikeSolid";
likeTitle = "Liked";
likes = likes + 1;
}
if (this.state.sentimentAction === "disliked") {
dislikeIconName = "DislikeSolid";
dislikeTitle = "Disliked";
dislikes = dislikes + 1;
}
const styles = {
button: {
width: 16,
height: 16,
padding: 0,
marginLeft: 7,
marginRight: 7
}
};
return (
<div className="metrics">
<div>
<span>
By: {author} | {downloads}{" "}
{downloads === 1 ? "download" : "downloads"} | Created on:{" "}
{createdonLocaleDate} |
</span>
</div>
<IconButton
style={styles.button}
iconStyle={styles.icon}
tooltipStyles={styles.tooltip}
iconProps={{ iconName: likeIconName }}
title={likeTitle}
ariaLabel={likeTitle}
onClick={() => this.handleLikeClick()}
/>
<div>{likes}</div>
<IconButton
style={styles.button}
iconStyle={styles.icon}
tooltipStyles={styles.tooltip}
iconProps={{ iconName: dislikeIconName }}
title={dislikeTitle}
ariaLabel={dislikeTitle}
onClick={() => this.handleDislikeClick()}
/>
<div>{dislikes}</div>
</div>
);
}
}
export default MetricBar;

Просмотреть файл

@ -0,0 +1,33 @@
import { mergeStyleSets } from "office-ui-fabric-react";
export const buttonStyles = {
root: {
fontSize: "12px",
height: "25px",
marginRight: "8px",
minWidth: "0px",
paddingRight: "10px",
paddingLeft: "10px"
},
label: {
fontWeight: "normal"
}
};
const secondaryButtonAdditionalStyles = {
root: {
backgroundColor: "white",
border: "1px solid #0078D7",
color: "#0058AD"
},
rootHovered: {
// backgroundColor: "white",
border: "1px solid #0078D7",
color: "#0058AD"
}
};
export const secondaryButtonStyles = mergeStyleSets(
buttonStyles,
secondaryButtonAdditionalStyles
);

Просмотреть файл

@ -0,0 +1,43 @@
export const technologies = [
"Functions 2.x",
"Functions 1.x",
"Logic Apps",
"Blob Storage",
"Storage Queue",
"Cosmos DB",
"Cognitive Services",
"Azure Active Directory",
"App Service",
"Key Vault",
"SQL Server",
"Service Bus Queue",
"Event Grid"
];
export const solutionAreas = [
"Web API",
"Data Processing",
"Integration",
"Authentication",
"Automation",
"Event Processing",
"Machine Learning",
"Scheduled Jobs",
"Static Website",
"Gaming",
"IoT"
];
export const languages = [
"JavaScript",
"TypeScript",
"Java",
"C#",
"C# Script",
"F#",
"Python",
"PowerShell"
];
export const NotApplicableLanguage = "na";
export const OtherSolutionArea = "Other";

Просмотреть файл

@ -0,0 +1,50 @@
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import { IconButton } from "office-ui-fabric-react";
import "./PageHeaderWithBackButton.scss";
class PageHeaderWithBackButton extends Component {
constructor(props) {
super(props);
this.handleHomeButtonClick = this.handleHomeButtonClick.bind(this);
}
handleHomeButtonClick() {
this.props.history.push("/");
}
render() {
let { title } = this.props;
const homeButton = {
button: {
width: 17,
height: 18,
marginRight: 12
}
};
return (
<div className="page-header">
<div className="page-title-container">
<div className="back-button-icon-container">
<IconButton
iconProps={{ iconName: "Home" }}
title="Home"
ariaLabel="Home"
style={homeButton.button}
onClick={() => this.handleHomeButtonClick()}
/>
</div>
<div className="page-title">
<span>{title}</span>
</div>
</div>
<div className="page-description-container">{this.props.children}</div>
</div>
);
}
}
export default withRouter(PageHeaderWithBackButton);

Просмотреть файл

@ -0,0 +1,29 @@
.page-header {
padding-top: 20px;
// margin-bottom: 23px; // this cannot be set at the common header control because of pivot control sizes
background: #ffffff;
padding-left: 23px;
}
.page-title-container {
display: flex;
flex-direction: row;
margin-bottom: 11px;
}
.back-button-icon-container {
margin: auto 0;
height: 18px;
}
.page-title {
font-size: 18px;
font-weight: bold;
color: black;
margin-bottom: auto;
}
.page-description-container {
font-size: 12px;
line-height: 14px;
color: #161616;
}

Просмотреть файл

@ -0,0 +1,34 @@
import React, { Component } from "react";
import { PrimaryButton } from "office-ui-fabric-react";
class SignInButton extends Component {
handleButtonClick() {
const currentLocation = encodeURIComponent(window.location);
window.location = `/api/user/login?returnUrl=${currentLocation}`;
}
render() {
const buttonStyles = {
root: {
fontSize: "12px",
height: "32px"
},
label: {
fontWeight: "normal"
}
};
return (
<PrimaryButton
styles={buttonStyles}
primary={true}
iconProps={{ iconName: "GitHub-16px" }}
onClick={this.handleButtonClick}
>
Sign in with GitHub
</PrimaryButton>
);
}
}
export default SignInButton;

Просмотреть файл

@ -0,0 +1,23 @@
.sidebar-wrapper{
padding-left: 24px;
padding-top: 20px;
padding-right: 20px;
}
.filterby-title {
font-size: 18px;
line-height: 24px;
margin-bottom: 12px;
}
.Filter-list-header{
font-size: 14px;
line-height: 16px;
color: #555555;
display: inline-block;
}
.filterset{
margin-bottom: 23px;
}

Просмотреть файл

@ -0,0 +1,129 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import { Checkbox } from "office-ui-fabric-react/lib/index";
import {
paramsToQueryString,
queryStringToParams,
trackEvent
} from "../../helpers";
import * as Constants from "../shared/Constants";
import "./SideBar.css";
class SideBar extends Component {
constructor(props) {
super(props);
this.state = {
filters: this.props.initialFilters
};
}
isChecked(category, item) {
return this.state.filters[category].includes(item);
}
checkboxclicked(ev, checked, category, item) {
var currentFilters = this.state.filters;
var categoryArray = currentFilters[category];
if (!checked) {
categoryArray = categoryArray.filter(i => i !== item);
} else {
categoryArray.push(item);
}
currentFilters[category] = categoryArray;
this.setState({ filters: currentFilters }, () => this.ChangeUrl());
trackEvent(`/filter/change/${category}`, currentFilters);
}
ChangeUrl() {
var params = queryStringToParams(this.props.location.search);
delete params["technology"];
delete params["language"];
delete params["solutionarea"];
if (this.state.filters.technologies.length > 0) {
params["technology"] = this.state.filters.technologies.join();
}
if (this.state.filters.languages.length > 0) {
params["language"] = this.state.filters.languages.join();
}
if (this.state.filters.solutionareas.length > 0) {
params["solutionarea"] = this.state.filters.solutionareas.join();
}
this.props.history.push(paramsToQueryString(params));
}
render() {
const checkboxStyles = index => {
return {
root: {
marginTop: index === 0 ? "9px" : "0px",
marginBottom: "5px"
}
};
};
return (
<div className="sidebar-wrapper">
<div className="filterby-title">Filter by</div>
<div className="filterset">
<span className="Filter-list-header">Technology</span>
{Constants.technologies.map((technology, index) => (
<Checkbox
styles={checkboxStyles(index)}
label={technology}
key={technology}
defaultChecked={this.isChecked("technologies", technology)}
onChange={(ev, checked) =>
this.checkboxclicked(ev, checked, "technologies", technology)
}
/>
))}
</div>
<div className="filterset">
<span className="Filter-list-header">Language</span>
{Constants.languages.map((language, index) => (
<Checkbox
styles={checkboxStyles(index)}
label={language}
key={language}
defaultChecked={this.isChecked("languages", language)}
onChange={(ev, checked) =>
this.checkboxclicked(ev, checked, "languages", language)
}
/>
))}
</div>
<div className="filterset">
<span className="Filter-list-header">Solution Area</span>
{Constants.solutionAreas.map((solutionarea, index) => (
<Checkbox
styles={checkboxStyles(index)}
label={solutionarea}
key={solutionarea}
defaultChecked={this.isChecked("solutionareas", solutionarea)}
onChange={(ev, checked) =>
this.checkboxclicked(ev, checked, "solutionareas", solutionarea)
}
/>
))}
</div>
</div>
);
}
}
const mapStateToProps = state => ({});
const mapDispatchToProps = {};
const SideBarContainer = connect(
mapStateToProps,
mapDispatchToProps
)(SideBar);
export default withRouter(SideBarContainer);

Просмотреть файл

@ -0,0 +1,20 @@
export function trackEvent(eventName, eventData) {
let appInsights = window.appInsights;
if (typeof appInsights !== "undefined") {
appInsights.trackEvent(eventName, eventData);
}
}
export function trackError(errorString, properties) {
let appInsights = window.appInsights;
if (typeof appInsights !== "undefined") {
appInsights.trackTrace(errorString, properties, 3);
}
}
export function trackException(exception, properties) {
let appInsights = window.appInsights;
if (typeof appInsights !== "undefined") {
appInsights.trackException(exception, null, properties);
}
}

Просмотреть файл

@ -0,0 +1,37 @@
import { trackError, trackException } from "./appinsights";
export function handleResponse(response) {
try {
return response.text().then(text => {
if (response.ok) {
return text;
}
const error = {
status: response.status,
error: text || response.statusText
};
trackError(error.error, { ...error, url: response.url });
return Promise.reject(error);
});
} catch (ex) {
trackException(ex, { url: response.url, method: "handleResponse" });
return Promise.reject({
status: -1,
error: "Encountered unexpected exception."
});
}
}
export function handleJsonResponse(response) {
return handleResponse(response).then(data => {
try {
return JSON.parse(data);
} catch (ex) {
trackException(ex, { url: response.url, method: "handleJsonResponse" });
return Promise.reject({
status: -1,
error: "Encountered unexpected exception."
});
}
});
}

Просмотреть файл

@ -0,0 +1,3 @@
export * from "./handle-response";
export * from "./query-param";
export * from "./appinsights";

Просмотреть файл

@ -0,0 +1,23 @@
export function queryStringToParams(queryString) {
if (queryString.indexOf("?") > -1) {
queryString = queryString.split("?")[1];
}
var pairs = queryString.split("&");
var result = {};
pairs.forEach(function(pair) {
pair = pair.split("=");
if (pair[0] && pair[0].length > 0) {
result[pair[0]] = decodeURIComponent(pair[1] || "");
}
});
return result;
}
export function paramsToQueryString(params) {
var queryString = Object.keys(params)
.map(key => {
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
})
.join("&");
return "?" + queryString;
}

Просмотреть файл

@ -0,0 +1,15 @@
import React from "react";
import { registerIcons } from "office-ui-fabric-react";
import "./registerIcons.scss";
import { ReactComponent as GithubIconSvg } from "../assets/github.svg";
import { ReactComponent as ContributionSvg } from "../assets/contribution.svg";
export default function registerCustomIcons() {
registerIcons({
icons: {
"GitHub-12px": <GithubIconSvg className="icon-12px" />,
"GitHub-16px": <GithubIconSvg className="icon-16px" />,
"contribution-svg": <ContributionSvg className="icon-16px" />
}
});
}

Просмотреть файл

@ -0,0 +1,14 @@
.icon-16px {
width: 16px;
height: 16px;
vertical-align: baseline;
fill: currentColor;
}
.icon-12px {
width: 12px;
height: 12px;
vertical-align: baseline;
margin-right: 4px;
fill: currentColor;
}

Просмотреть файл

@ -0,0 +1,17 @@
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
html,
body,
#root,
#container {
height: 100%;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
* {
box-sizing: border-box;
}

Просмотреть файл

@ -0,0 +1,28 @@
// These must be the first lines in src/index.js
import "react-app-polyfill/ie11";
import "react-app-polyfill/stable";
import "./index.css";
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { initializeIcons } from "office-ui-fabric-react";
import configureStore from "./reducers";
import registerCustomIcons from "./helpers/registerIcons";
import AppContainer from "./App";
const store = configureStore();
registerCustomIcons();
initializeIcons();
const rootElement = document.getElementById("root");
ReactDOM.render(
<BrowserRouter>
<Provider store={store}>
<AppContainer />
</Provider>
</BrowserRouter>,
rootElement
);

Просмотреть файл

@ -0,0 +1,37 @@
import { userActionTypes } from "../actions/actionTypes";
import initialState from "./initialState";
export default function authenticationReducer(
state = initialState.authentication,
action
) {
switch (action.type) {
case userActionTypes.GETCURRENTUSER_REQUEST:
return {
...state,
loading: true
};
case userActionTypes.GETCURRENTUSER_SUCCESS:
return {
...state,
loading: false,
loggedIn: true,
user: action.user
};
case userActionTypes.GETCURRENTUSER_FAILURE:
return {
...state,
loading: false,
loggedIn: false,
user: {}
};
case userActionTypes.LOGOUT:
return {
...state,
loggedIn: false,
user: {}
};
default:
return state;
}
}

Просмотреть файл

@ -0,0 +1,13 @@
import { combineReducers, createStore } from "redux";
import samples from "./sampleReducer";
import authentication from "./authenticationReducer";
const rootReducer = combineReducers({
samples,
authentication
});
export default function configureStore(initialState) {
return createStore(rootReducer, initialState);
}

Просмотреть файл

@ -0,0 +1,8 @@
export default {
samples: [],
authentication: {
loggedIn: false,
user: {},
loading: false
}
};

Просмотреть файл

@ -0,0 +1,11 @@
import { sampleActionTypes } from "../actions/actionTypes";
import initialState from "./initialState";
export default function sampleReducer(state = initialState.samples, action) {
switch (action.type) {
case sampleActionTypes.GETSAMPLES_SUCCESS:
return action.samples;
default:
return state;
}
}

Просмотреть файл

@ -0,0 +1,39 @@
import { handleResponse } from "../helpers";
export const githubService = {
getReadMe,
getLicense,
getArmTemplate
};
function getRawContentUrl(repoUrl, fileName) {
let rawUrl = repoUrl
.replace("https://github.com", "https://raw.githubusercontent.com")
.replace("/tree/", "/");
rawUrl = rawUrl.includes("/master/") ? rawUrl + "/" : rawUrl + "/master/";
let contentUrl = rawUrl + fileName;
return contentUrl;
}
function getReadMe(repoUrl) {
const requestOptions = {
method: "GET"
};
const readMeUrl = getRawContentUrl(repoUrl, "README.md");
return fetch(readMeUrl, requestOptions).then(handleResponse);
}
function getLicense(licenseUrl, repoUrl) {
const requestOptions = {
method: "GET"
};
const contentUrl = licenseUrl || getRawContentUrl(repoUrl, "LICENSE");
return fetch(contentUrl, requestOptions).then(handleResponse);
}
function getArmTemplate(templateUrl) {
const requestOptions = {
method: "GET"
};
return fetch(templateUrl, requestOptions).then(handleResponse);
}

Просмотреть файл

@ -0,0 +1,3 @@
export * from "./user.service";
export * from "./library.service";
export * from "./github.service";

Просмотреть файл

@ -0,0 +1,65 @@
import { handleResponse, handleJsonResponse } from "../helpers";
import { trackException } from "../helpers/appinsights";
export const libraryService = {
getAllSamples,
submitNewSample,
updateUserSentimentStats,
updateDownloadCount
};
function getAllSamples() {
const requestOptions = {
method: "GET"
};
return fetch("/api/Library", requestOptions).then(handleJsonResponse);
}
function submitNewSample(item) {
const requestOptions = {
method: "PUT",
body: JSON.stringify(item),
headers: {
"Content-Type": "application/json"
}
};
return fetch("/api/library", requestOptions)
.then(handleJsonResponse)
.catch(data => {
let error = data.error;
if (data.status === 400) {
try {
error = JSON.parse(data.error);
} catch (ex) {
trackException(ex, { method: "submitNewSample" });
}
}
return Promise.reject({
status: data.status,
error: error
});
});
}
function updateUserSentimentStats(sentimentPayload) {
const requestOptions = {
method: "PUT",
body: JSON.stringify(sentimentPayload),
headers: {
"Content-Type": "application/json"
}
};
return fetch("/api/metrics/sentiment", requestOptions).then(handleResponse);
}
function updateDownloadCount(id) {
const requestOptions = {
method: "PUT",
body: '"' + id + '"',
headers: {
"Content-Type": "application/json"
}
};
return fetch("/api/metrics/downloads", requestOptions).then(handleResponse);
}

Просмотреть файл

@ -0,0 +1,20 @@
import { handleResponse, handleJsonResponse } from "../helpers";
export const userService = {
getCurrentUser,
logout
};
function getCurrentUser() {
const requestOptions = {
method: "GET"
};
return fetch("/api/user", requestOptions).then(handleJsonResponse);
}
function logout() {
const requestOptions = {
method: "GET"
};
return fetch("/api/user/logout", requestOptions).then(handleResponse);
}

Просмотреть файл

@ -1,6 +1,6 @@
{
"ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider",
"Version": "8.11.10402.2",
"Version": "8.14.11009.1",
"GettingStartedDocument": {
"Uri": "https://go.microsoft.com/fwlink/?LinkID=798432"
}

Просмотреть файл

@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using System.Text.RegularExpressions;
using ServerlessLibrary.Models;
using Newtonsoft.Json;
using System.Threading.Tasks;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace ServerlessLibrary.Controllers
{
[Produces("application/json")]
[Route("api/[controller]")]
public class LibraryController : Controller
{
ICacheService _cacheService;
ILibraryStore _libraryStore;
public LibraryController(ICacheService cacheService, ILibraryStore libraryStore)
{
this._cacheService = cacheService;
this._libraryStore = libraryStore;
}
// GET: api/<controller>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<LibraryItemWithStats>), 200)]
public JsonResult Get(string filterText, string language)
{
//TODO: Add filtering for solution areas and technologies.
var results = _cacheService.GetCachedItems();
var filteredResults = results.Where(
x =>
(
(string.IsNullOrWhiteSpace(language) || x.Language == language) &&
(
string.IsNullOrWhiteSpace(filterText)
|| Regex.IsMatch(x.Title, filterText, RegexOptions.IgnoreCase)
|| Regex.IsMatch(x.Description, filterText, RegexOptions.IgnoreCase)
|| Regex.IsMatch(x.Repository.Replace("https://github.com/", "", StringComparison.InvariantCulture), filterText, RegexOptions.IgnoreCase)
|| (!string.IsNullOrWhiteSpace(x.Author) && Regex.IsMatch(x.Author, filterText, RegexOptions.IgnoreCase))
|| (x.Tags != null && x.Tags.Any(t => Regex.IsMatch(t, filterText, RegexOptions.IgnoreCase)))
|| (x.Technologies != null && x.Technologies.Any(t => Regex.IsMatch(t, filterText, RegexOptions.IgnoreCase)))
|| (x.SolutionAreas != null && x.SolutionAreas.Any(c => Regex.IsMatch(c, filterText, RegexOptions.IgnoreCase)))
)
)
);
return new JsonResult(filteredResults);
}
[HttpPut]
[ProducesResponseType(typeof(LibraryItem), 200)]
public async Task<IActionResult> Put([FromBody]LibraryItem libraryItem)
{
if (!User.Identity.IsAuthenticated)
{
return Unauthorized();
}
var validationsErrors = ValidateLibraryItem(libraryItem);
if (validationsErrors?.Count > 0)
{
return BadRequest(validationsErrors);
}
// assign id, created date
libraryItem.Id = Guid.NewGuid().ToString();
libraryItem.CreatedDate = DateTime.UtcNow;
// set the author to current authenticated user
GitHubUser user = new GitHubUser(User);
libraryItem.Author = user.UserName;
await StorageHelper.submitContributionForApproval(JsonConvert.SerializeObject(libraryItem));
return new JsonResult(libraryItem);
}
private static List<string> ValidateLibraryItem(LibraryItem libraryItem)
{
List<string> errors = new List<string>();
if (string.IsNullOrWhiteSpace(libraryItem.Title))
{
errors.Add("Title cannot be empty");
}
if (string.IsNullOrWhiteSpace(libraryItem.Repository) || !IsValidUri(libraryItem.Repository))
{
errors.Add("Repository URL must be a valid GitHub URL");
}
if (string.IsNullOrWhiteSpace(libraryItem.Description))
{
errors.Add("Description cannot be empty");
}
if (libraryItem.Technologies.Length == 0)
{
errors.Add("At least one technology must be specified");
}
if (string.IsNullOrWhiteSpace(libraryItem.Language))
{
errors.Add("Language must be specified");
}
if (libraryItem.SolutionAreas.Length == 0)
{
errors.Add("At least one solution area must be specified");
}
if (!string.IsNullOrWhiteSpace(libraryItem.Template) && !IsValidUri(libraryItem.Template, "raw.githubusercontent.com"))
{
errors.Add("ARM template URL must point to the raw path of the ARM template (https://raw.githubusercontent.com/...)");
}
return errors;
}
private static bool IsValidUri(string uriString, string expectedHostName = null)
{
try
{
var uri = new Uri(uriString);
if (string.IsNullOrWhiteSpace(expectedHostName))
{
return true;
}
else
{
return string.Equals(expectedHostName, uri.Host, StringComparison.OrdinalIgnoreCase);
}
}
catch
{
return false;
}
}
}
}

Просмотреть файл

@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using ServerlessLibrary.Models;
using System;
namespace ServerlessLibrary.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class MetricsController : ControllerBase
{
private readonly ILogger<MetricsController> logger;
public MetricsController(ILogger<MetricsController> logger)
{
this.logger = logger;
}
// PUT api/<controller>/downloads
[ProducesResponseType(typeof(bool), 200)]
[HttpPut]
public JsonResult Downloads([FromBody]string id)
{
try
{
StorageHelper.updateUserStats(JsonConvert.SerializeObject(new { id, userAction = "download" })).Wait();
}
catch (Exception ex)
{
this.logger.LogError(ex, "Unable to update download count");
}
return new JsonResult(true);
}
// PUT api/<controller>/sentiment
[ProducesResponseType(typeof(bool), 200)]
[HttpPut]
public IActionResult Sentiment([FromBody]SentimentPayload sentimentPayload)
{
if (sentimentPayload.LikeChanges < -1
|| sentimentPayload.LikeChanges > 1
|| sentimentPayload.LikeChanges == sentimentPayload.DislikeChanges)
{
return BadRequest("Invalid values for like or dislike count");
}
try
{
StorageHelper.updateUserStats(JsonConvert.SerializeObject(new
{
id = sentimentPayload.Id,
userAction = "Sentiment",
likeChanges = sentimentPayload.LikeChanges,
dislikeChanges = sentimentPayload.DislikeChanges
})).Wait();
}
catch (Exception ex)
{
this.logger.LogError(ex, "Unable to update sentiments");
}
return new JsonResult(true);
}
}
}

Просмотреть файл

@ -0,0 +1,50 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using ServerlessLibrary.Models;
namespace ServerlessLibrary.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
[HttpGet("login"), HttpPost("login")]
public IActionResult Login(string returnUrl = "/")
{
if (User.Identity.IsAuthenticated)
{
return new RedirectResult(returnUrl);
}
// Instruct the middleware corresponding to the requested external identity
// provider to redirect the user agent to its own authorization endpoint.
// Note: the authenticationScheme parameter must match the value configured in Startup.cs.
// If no scheme is provided then the DefaultChallengeScheme will be used
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl });
}
[HttpGet("logout"), HttpPost("logout")]
public IActionResult Logout()
{
// Instruct the cookies middleware to delete the local cookie which
// was created after a successful authentication flow.
return SignOut(
new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
[HttpGet]
[ProducesResponseType(typeof(GitHubUser), 200)]
public IActionResult Get()
{
if (User.Identity.IsAuthenticated)
{
GitHubUser user = new GitHubUser(User);
return Ok(user);
}
return Unauthorized();
}
}
}

Просмотреть файл

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Linq;
using ServerlessLibrary.Models;
namespace ServerlessLibrary
{
/// <summary>
/// Cosmos db Library store
/// </summary>
public class CosmosLibraryStore : ILibraryStore
{
public CosmosLibraryStore()
{
CosmosDBRepository<LibraryItem>.Initialize();
}
public async Task Add(LibraryItem libraryItem)
{
await CosmosDBRepository<LibraryItem>.CreateItemAsync(libraryItem);
}
async public Task<IList<LibraryItem>> GetAllItems()
{
IEnumerable<LibraryItem> libraryItems = await CosmosDBRepository<LibraryItem>.GetAllItemsAsync();
return libraryItems.ToList();
}
}
/// <summary>
/// Cosmos db APIs
/// </summary>
/// <typeparam name="T"></typeparam>
static class CosmosDBRepository<T> where T : class
{
private static readonly string DatabaseId = ServerlessLibrarySettings.Database;
private static readonly string CollectionId = ServerlessLibrarySettings.Collection;
private static DocumentClient client;
public static async Task<T> GetItemAsync(string id)
{
try
{
Document document = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id));
return (T)(dynamic)document;
}
catch (DocumentClientException e)
{
if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
else
{
throw;
}
}
}
public static async Task<List<T>> GetAllItemsAsync()
{
IDocumentQuery<T> query = client.CreateDocumentQuery<T>(
UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
new FeedOptions { MaxItemCount = -1, EnableCrossPartitionQuery = true })
.AsDocumentQuery();
List<T> results = new List<T>();
while (query.HasMoreResults)
{
results.AddRange(await query.ExecuteNextAsync<T>());
}
return results;
}
public static async Task<Document> CreateItemAsync(T item)
{
return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), item);
}
public static async Task<Document> UpdateItemAsync(string id, T item)
{
return await client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id), item);
}
public static async Task DeleteItemAsync(string id)
{
await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id));
}
public static void Initialize()
{
if (client == null)
{
client = new DocumentClient(new Uri(ServerlessLibrarySettings.CosmosEndpoint), ServerlessLibrarySettings.CosmosAuthkey);
CreateDatabaseIfNotExistsAsync().Wait();
CreateCollectionIfNotExistsAsync().Wait();
}
}
private static async Task CreateDatabaseIfNotExistsAsync()
{
try
{
await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
}
catch (DocumentClientException e)
{
if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
{
await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
}
else
{
throw;
}
}
}
private static async Task CreateCollectionIfNotExistsAsync()
{
try
{
await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
}
catch (DocumentClientException e)
{
if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
{
await client.CreateDocumentCollectionAsync(
UriFactory.CreateDatabaseUri(DatabaseId),
new DocumentCollection { Id = CollectionId },
new RequestOptions { OfferThroughput = 400 });
}
else
{
throw;
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ServerlessLibrary.Models;
namespace ServerlessLibrary
{
/// <summary>
/// Interface for serverless library store
/// </summary>
public interface ILibraryStore
{
/// <summary>
/// Add an item to library
/// </summary>
/// <param name="libraryItem">Library item </param>
Task Add(LibraryItem libraryItem);
/// <summary>
/// Get all items from library
/// </summary>
/// <returns></returns>
Task<IList<LibraryItem>> GetAllItems();
}
}

Просмотреть файл

@ -1,67 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json;
using System.Threading;
using System.Net;
using System.Text.RegularExpressions;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace ServerlessLibrary
{
[Produces("application/json")]
[Route("api/[controller]")]
public class LibraryController : Controller
{
ICacheService _cacheService;
public LibraryController(ICacheService cacheService)
{
this._cacheService = cacheService;
}
// GET: api/<controller>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<LibraryItem>), 200)]
public JsonResult Get(string filterText, string type, string language)
{
var results = _cacheService.GetCachedItems();
var filteredResults = results.Where(
x =>
(
(string.IsNullOrWhiteSpace(language) || x.Language == language) &&
(string.IsNullOrWhiteSpace(type) || x.Type == type) &&
(
string.IsNullOrWhiteSpace(filterText)
|| Regex.IsMatch(x.Title, filterText, RegexOptions.IgnoreCase)
|| Regex.IsMatch(x.Description, filterText, RegexOptions.IgnoreCase)
|| Regex.IsMatch(x.AuthorType, filterText, RegexOptions.IgnoreCase)
|| Regex.IsMatch(x.Repository.AbsoluteUri.Replace("https://github.com/", "", StringComparison.InvariantCulture), filterText, RegexOptions.IgnoreCase)
|| (x.RuntimeVersion != null && Regex.IsMatch(x.RuntimeVersion, filterText, RegexOptions.IgnoreCase))
|| (x.Tags != null && x.Tags.Any(t => Regex.IsMatch(t,filterText, RegexOptions.IgnoreCase)))
)
)
);
return new JsonResult(filteredResults);
}
// PUT api/<controller>/
[ProducesResponseType(typeof(bool), 200)]
[HttpPut()]
public JsonResult Put([FromBody]string template)
{
StorageHelper.updateDownloadCount(JsonConvert.SerializeObject( new { template = WebUtility.UrlDecode(template) }));
return new JsonResult(true);
}
}
}

Просмотреть файл

@ -1,66 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ServerlessLibrary
{
public class LibraryItem
{
[JsonProperty(PropertyName = "title", DefaultValueHandling = DefaultValueHandling.Include)]
public string Title { get; set; }
[JsonProperty(PropertyName = "template", DefaultValueHandling = DefaultValueHandling.Include)]
public Uri Template { get; set; }
[JsonProperty(PropertyName = "repository", DefaultValueHandling = DefaultValueHandling.Include)]
public Uri Repository { get; set; }
[JsonProperty(PropertyName = "description", DefaultValueHandling = DefaultValueHandling.Include)]
public string Description { get; set; }
[JsonProperty(PropertyName = "tags", DefaultValueHandling = DefaultValueHandling.Include)]
public string[] Tags { get; set; }
[JsonProperty(PropertyName = "language", DefaultValueHandling = DefaultValueHandling.Include)]
public string Language { get; set; }
[JsonProperty(PropertyName = "type", DefaultValueHandling = DefaultValueHandling.Include)]
public string Type { get; set; }
[JsonProperty(PropertyName = "author", DefaultValueHandling = DefaultValueHandling.Include)]
public string Author { get; private set; }
[JsonProperty(PropertyName = "authortype", DefaultValueHandling = DefaultValueHandling.Include)]
public string AuthorType { get; set; }
[JsonProperty(PropertyName = "authortypedesc", DefaultValueHandling = DefaultValueHandling.Include)]
public string AuthorTypeDesc { get; set; }
[JsonProperty(PropertyName = "totaldownloads", DefaultValueHandling = DefaultValueHandling.Include)]
public int TotalDownloads { get; set; }
[JsonProperty(PropertyName = "downloadsthismonth", DefaultValueHandling = DefaultValueHandling.Include)]
public int DownloadsThisMonth { get; set; }
[JsonProperty(PropertyName = "downloadsthisweek", DefaultValueHandling = DefaultValueHandling.Include)]
public int DownloadsThisWeek { get; set; }
[JsonProperty(PropertyName = "downloadstoday", DefaultValueHandling = DefaultValueHandling.Include)]
public int DownloadsToday { get; set; }
[JsonProperty(PropertyName = "runtimeversion", DefaultValueHandling = DefaultValueHandling.Include)]
public string RuntimeVersion { get; set; }
public LibraryItem(string title, string template, string repository, string description, string language,
string type)
{
this.Title = title;
this.Template = string.IsNullOrWhiteSpace(template)? null : new Uri(template);
this.Repository = new Uri(repository);
this.Description = description;
this.Language = language;
this.Type = type;
}
}
}

Просмотреть файл

@ -0,0 +1,46 @@
using System.Security.Claims;
using static ServerlessLibrary.OAuth.GitHub.GitHubAuthenticationConstants;
namespace ServerlessLibrary.Models
{
public class GitHubUser
{
public GitHubUser()
{
}
public GitHubUser(ClaimsPrincipal claimsPrincipal)
{
this.FullName = claimsPrincipal.FindFirstValue(Claims.Name);
this.Email = claimsPrincipal.FindFirstValue(ClaimTypes.Email);
this.AvatarUrl = claimsPrincipal.FindFirstValue(Claims.Avatar);
this.UserName = claimsPrincipal.FindFirstValue(Claims.Login);
}
public string FullName { get; set; }
public string Email { get; set; }
public string AvatarUrl { get; set; }
public string UserName { get; set; }
public string DisplayName
{
get
{
if (!string.IsNullOrWhiteSpace(this.FullName))
{
return this.FullName.Split(' ')?[0];
}
if (!string.IsNullOrWhiteSpace(this.UserName))
{
return this.UserName;
}
return string.Empty;
}
}
}
}

Просмотреть файл

@ -0,0 +1,59 @@
using System;
using Newtonsoft.Json;
namespace ServerlessLibrary.Models
{
public class LibraryItemWithStats : LibraryItem
{
[JsonProperty(PropertyName = "totaldownloads", DefaultValueHandling = DefaultValueHandling.Include)]
public int TotalDownloads { get; set; }
[JsonProperty(PropertyName = "likes", DefaultValueHandling = DefaultValueHandling.Include)]
public int Likes { get; set; }
[JsonProperty(PropertyName = "dislikes", DefaultValueHandling = DefaultValueHandling.Include)]
public int Dislikes { get; set; }
}
public class LibraryItem
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonProperty(PropertyName = "createddate")]
public DateTime CreatedDate { get; set; }
[JsonProperty(PropertyName = "title", DefaultValueHandling = DefaultValueHandling.Include)]
public string Title { get; set; }
[JsonProperty(PropertyName = "template", DefaultValueHandling = DefaultValueHandling.Include)]
public string Template { get; set; }
[JsonProperty(PropertyName = "repository", DefaultValueHandling = DefaultValueHandling.Include)]
public string Repository { get; set; }
[JsonProperty(PropertyName = "description", DefaultValueHandling = DefaultValueHandling.Include)]
public string Description { get; set; }
[JsonProperty(PropertyName = "tags", DefaultValueHandling = DefaultValueHandling.Include)]
public string[] Tags { get; set; }
[JsonProperty(PropertyName = "language", DefaultValueHandling = DefaultValueHandling.Include)]
public string Language { get; set; }
[JsonProperty(PropertyName = "technologies", DefaultValueHandling = DefaultValueHandling.Include)]
public string[] Technologies { get; set; }
[JsonProperty(PropertyName = "solutionareas", DefaultValueHandling = DefaultValueHandling.Include)]
public string[] SolutionAreas { get; set; }
[JsonProperty(PropertyName = "author", DefaultValueHandling = DefaultValueHandling.Include)]
public string Author { get; internal set; }
internal T ConvertTo<T>()
{
var serializedObj = JsonConvert.SerializeObject(this);
return JsonConvert.DeserializeObject<T>(serializedObj);
}
}
}

Просмотреть файл

@ -0,0 +1,9 @@
namespace ServerlessLibrary.Models
{
public class SentimentPayload
{
public string Id { get; set; }
public int LikeChanges { get; set; }
public int DislikeChanges { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,16 @@
namespace ServerlessLibrary.OAuth.GitHub
{
/// <summary>
/// Contains constants specific to the <see cref="GitHubAuthenticationHandler"/>.
/// </summary>
public static class GitHubAuthenticationConstants
{
public static class Claims
{
public const string Name = "urn:github:name";
public const string Url = "urn:github:url";
public const string Login = "urn:github:login";
public const string Avatar = "urn:github:avatar";
}
}
}

Просмотреть файл

@ -0,0 +1,58 @@
namespace ServerlessLibrary.OAuth.GitHub
{
/// <summary>
/// Default values used by the GitHub authentication middleware.
/// </summary>
public static class GitHubAuthenticationDefaults
{
/// <summary>
/// Default value for <see cref="AuthenticationScheme.Name"/>.
/// </summary>
public const string AuthenticationScheme = "GitHub";
/// <summary>
/// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
/// </summary>
public const string DisplayName = "GitHub";
/// <summary>
/// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
/// </summary>
public const string Issuer = "GitHub";
/// <summary>
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
/// </summary>
public const string CallbackPath = "/signin-github";
/// <summary>
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
/// </summary>
public const string AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
/// <summary>
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
/// </summary>
public const string TokenEndpoint = "https://github.com/login/oauth/access_token";
/// <summary>
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
public const string UserInformationEndpoint = "https://api.github.com/user";
/// <summary>
/// Default value for <see cref="GitHubAuthenticationOptions.UserEmailsEndpoint"/>.
/// </summary>
public const string UserEmailsEndpoint = "https://api.github.com/user/emails";
/// <summary>
/// Scope for <see cref="UserInformationEndpoint"/>.
/// </summary>
public const string UserInformationScope = "user";
/// <summary>
/// Scope for <see cref="UserEmailsEndpoint"/>.
/// </summary>
public const string UserEmailsScope = "user:email";
}
}

Просмотреть файл

@ -0,0 +1,105 @@
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
namespace ServerlessLibrary.OAuth.GitHub
{
public class GitHubAuthenticationHandler : OAuthHandler<GitHubAuthenticationOptions>
{
public GitHubAuthenticationHandler(
IOptionsMonitor<GitHubAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity,
AuthenticationProperties properties, OAuthTokenResponse tokens)
{
var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
"returned a {Status} response with the following payload: {Headers} {Body}.",
/* Status: */ response.StatusCode,
/* Headers: */ response.Headers.ToString(),
/* Body: */ await response.Content.ReadAsStringAsync());
throw new HttpRequestException("An error occurred while retrieving the user profile.");
}
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
var principal = new ClaimsPrincipal(identity);
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload);
context.RunClaimActions(payload);
// When the email address is not public, retrieve it from
// the emails endpoint if the user:email scope is specified.
if (!string.IsNullOrEmpty(Options.UserEmailsEndpoint) &&
!identity.HasClaim(claim => claim.Type == ClaimTypes.Email) && Options.Scope.Contains("user:email"))
{
var address = await GetEmailAsync(tokens);
if (!string.IsNullOrEmpty(address))
{
identity.AddClaim(new Claim(ClaimTypes.Email, address, ClaimValueTypes.String, Options.ClaimsIssuer));
}
}
await Options.Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
}
protected virtual async Task<string> GetEmailAsync(OAuthTokenResponse tokens)
{
// See https://developer.github.com/v3/users/emails/ for more information about the /user/emails endpoint.
var request = new HttpRequestMessage(HttpMethod.Get, Options.UserEmailsEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
// Failed requests shouldn't cause an error: in this case, return null to indicate that the email address cannot be retrieved.
var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
Logger.LogWarning("An error occurred while retrieving the email address associated with the logged in user: " +
"the remote server returned a {Status} response with the following payload: {Headers} {Body}.",
/* Status: */ response.StatusCode,
/* Headers: */ response.Headers.ToString(),
/* Body: */ await response.Content.ReadAsStringAsync());
return null;
}
var payload = JArray.Parse(await response.Content.ReadAsStringAsync());
return (from address in payload.AsJEnumerable()
where address.Value<bool>("primary")
select address.Value<string>("email")).FirstOrDefault();
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return base.HandleAuthenticateAsync();
}
protected override Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
{
return base.HandleRemoteAuthenticateAsync();
}
}
}

Просмотреть файл

@ -0,0 +1,41 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http;
using static ServerlessLibrary.OAuth.GitHub.GitHubAuthenticationConstants;
namespace ServerlessLibrary.OAuth.GitHub
{
/// <summary>
/// Defines a set of options used by <see cref="GitHubAuthenticationHandler"/>.
/// </summary>
public class GitHubAuthenticationOptions : OAuthOptions
{
public GitHubAuthenticationOptions()
{
ClaimsIssuer = GitHubAuthenticationDefaults.Issuer;
CallbackPath = new PathString(GitHubAuthenticationDefaults.CallbackPath);
AuthorizationEndpoint = GitHubAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = GitHubAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = GitHubAuthenticationDefaults.UserInformationEndpoint;
//Scope.Add(GitHubAuthenticationDefaults.UserInformationScope);
Scope.Add(GitHubAuthenticationDefaults.UserEmailsScope);
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
ClaimActions.MapJsonKey(Claims.Name, "name");
ClaimActions.MapJsonKey(Claims.Url, "html_url");
ClaimActions.MapJsonKey(Claims.Login, "login");
ClaimActions.MapJsonKey(Claims.Avatar, "avatar_url");
}
/// <summary>
/// Gets or sets the address of the endpoint exposing
/// the email addresses associated with the logged in user.
/// </summary>
public string UserEmailsEndpoint { get; set; } = GitHubAuthenticationDefaults.UserEmailsEndpoint;
}
}

Просмотреть файл

@ -0,0 +1,23 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>Development environment should not be enabled in deployed applications</strong>, 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 <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
</p>

Просмотреть файл

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ServerlessLibraryAPI.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}

Просмотреть файл

@ -0,0 +1,3 @@
@using ServerlessLibraryAPI
@namespace ServerlessLibraryAPI.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Просмотреть файл

@ -1,12 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.ApplicationInsights;
namespace ServerlessLibrary
{
@ -14,14 +9,21 @@ namespace ServerlessLibrary
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHost BuildWebHost(string[] args) =>
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseApplicationInsights()
.UseStartup<Startup>()
.Build();
.ConfigureLogging(
builder =>
{
builder.AddApplicationInsights();
builder.AddFilter<ApplicationInsightsLoggerProvider>("ServerlessLibrary.Program", LogLevel.Information);
builder.AddFilter<ApplicationInsightsLoggerProvider>("ServerlessLibrary.Startup", LogLevel.Information);
builder.AddFilter<ApplicationInsightsLoggerProvider>("", LogLevel.Information);
}
);
}
}

Просмотреть файл

@ -15,6 +15,14 @@
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express - API only": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ApiOnly": "true"
}
},
"ServerLessLibrary": {
"commandName": "Project",
"launchBrowser": true,

Просмотреть файл

@ -1,43 +1,64 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>
<SpaRoot>ClientApp\</SpaRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
<ApplicationInsightsResourceId>/subscriptions/7c1b7bab-00b2-4cb7-924e-205c4f411810/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/ServerlessLibrary</ApplicationInsightsResourceId>
<ApplicationInsightsAnnotationResourceId>/subscriptions/7c1b7bab-00b2-4cb7-924e-205c4f411810/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/ServerlessLibrary</ApplicationInsightsAnnotationResourceId>
<Company>Microsoft</Company>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PlatformTarget>AnyCPU</PlatformTarget>
<UserSecretsId>235c2497-239d-47f0-8ea7-af2dd2416d95</UserSecretsId>
<RootNamespace>ServerlessLibrary</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.5.1" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.6.1" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="2.1.1" />
<PackageReference Include="Microsoft.Azure.DocumentDB.Core" Version="2.2.3" />
<PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.9.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="4.0.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.5.0" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
</ItemGroup>
<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list -->
<Content Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Connected Services" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)build\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

Просмотреть файл

@ -21,7 +21,10 @@ namespace ServerlessLibrary
public static string SLAppInsightsKey { get { return config(""); } }
public static int SLCacheRefreshIntervalInSeconds { get { return Int32.Parse(config("60")); } }
public static string CACHE_ENTRY = "_CacheEntry";
public static string CosmosEndpoint { get { return config(); } }
public static string CosmosAuthkey { get { return config(); } }
public static string Database { get { return "serverlesslibrary"; } }
public static string Collection { get { return "contributions"; } }
}
}

Просмотреть файл

@ -1,25 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Swagger;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ServerlessLibrary.OAuth.GitHub;
using Swashbuckle.AspNetCore.Swagger;
using System.Threading.Tasks;
namespace ServerlessLibrary
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddMvc();
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = GitHubAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOAuth<GitHubAuthenticationOptions, GitHubAuthenticationHandler>(
GitHubAuthenticationDefaults.AuthenticationScheme,
GitHubAuthenticationDefaults.DisplayName,
options => {
options.ClientId = Configuration["Authentication:GitHub:ClientId"]; // these settings need to be present in appSettings (or in secrets.json)
options.ClientSecret = Configuration["Authentication:GitHub:ClientSecret"];
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
services.AddSwaggerGen(c =>
{
@ -29,28 +57,44 @@ namespace ServerlessLibrary
Version = "v1"
});
});
//#region snippet_ConfigureApiBehaviorOptions
//services.Configure<ApiBehaviorOptions>(options =>
//{
// options.SuppressConsumesConstraintForFormFileParameters = true;
// options.SuppressInferBindingSourcesForParameters = true;
// options.SuppressModelStateInvalidFilter = true;
//});
//#endregion
services.AddSingleton<ICacheService, CacheService>();
services.AddSingleton<ILibraryStore, CosmosLibraryStore>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHsts();
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Serverless library API v1");
c.RoutePrefix = "swagger";
});
app.UseMvc();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
@ -59,7 +103,6 @@ namespace ServerlessLibrary
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
await next();
});
}
}
}

Просмотреть файл

@ -1,69 +1,80 @@
using Microsoft.WindowsAzure.Storage;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
using Microsoft.WindowsAzure.Storage.RetryPolicies;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage.Queue;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
namespace ServerlessLibrary {
namespace ServerlessLibrary
{
/// <summary>
/// Summary description for StorageHelper
/// </summary>
public class StorageHelper
{
private const string slItemTableName = "slitemstats";
private static readonly TableRequestOptions tableRequestRetry = new TableRequestOptions { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(2), 3) };
private const string slContributionRequests = "contribution-requests";
private static readonly TableRequestOptions tableRequestRetry =
new TableRequestOptions { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(2), 3) };
private static CloudTableClient tableClient()
{
// Retrieve storage account from connection string.
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString);
CloudStorageAccount storageAccount =
CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString);
// Create the table client.
return storageAccount.CreateCloudTableClient();
}
private static CloudQueueClient cloudQueueClient()
{
// Retrieve storage account from connection string.
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString);
CloudStorageAccount storageAccount =
CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString);
// Create the queue client.
return storageAccount.CreateCloudQueueClient();
}
private static async Task<CloudTable> getTableReference(string tableName = slItemTableName)
{
CloudTable table = tableClient().GetTableReference(tableName);
await table.CreateIfNotExistsAsync();
return table;
}
private static async Task<CloudQueue> getQueueReference(string queueName = slItemTableName)
private static async Task<CloudQueue> getQueueReference(string queueName)
{
CloudQueue queue = cloudQueueClient().GetQueueReference(queueName);
await queue.CreateIfNotExistsAsync();
return queue;
}
public static async void updateDownloadCount(string templateName)
public static async Task submitContributionForApproval(string contributionPayload)
{
var message = new CloudQueueMessage(templateName);
message.SetMessageContent(templateName);
await (await getQueueReference()).AddMessageAsync(message);
var message = new CloudQueueMessage(contributionPayload);
await (await getQueueReference(slContributionRequests)).AddMessageAsync(message);
}
public static async Task updateUserStats(string statsPayload)
{
var message = new CloudQueueMessage(statsPayload);
await (await getQueueReference(slItemTableName)).AddMessageAsync(message);
}
public static async Task<IEnumerable<SLItemStats>> getSLItemRecordsAsync()
{
TableQuery<SLItemStats> query = new TableQuery<SLItemStats>().Select(new List<string> { "template", "totalDownloads"
,"downloadsToday","downloadsThisWeek","downloadsThisMonth"});
TableQuery<SLItemStats> query = new TableQuery<SLItemStats>()
.Select(new List<string> { "id", "totalDownloads", "likes", "dislikes" });
TableContinuationToken continuationToken = null;
List<SLItemStats> entities = new List<SLItemStats>();
var opContext = new OperationContext();
do
{
TableQuerySegment<SLItemStats>
queryResults = await (await getTableReference()).ExecuteQuerySegmentedAsync<SLItemStats>(query, continuationToken, tableRequestRetry, opContext);
TableQuerySegment<SLItemStats> queryResults =
await (await getTableReference()).ExecuteQuerySegmentedAsync<SLItemStats>(query, continuationToken, tableRequestRetry, opContext);
continuationToken = queryResults.ContinuationToken;
entities.AddRange(queryResults.Results);
@ -71,25 +82,24 @@ namespace ServerlessLibrary {
return entities;
}
public async Task<SLItemStats> GetItem(string template)
public async Task<SLItemStats> GetItem(string id)
{
TableOperation operation = TableOperation.Retrieve<SLItemStats>(template, template);
TableOperation operation = TableOperation.Retrieve<SLItemStats>(id, id);
TableResult result = await (await getTableReference()).ExecuteAsync(operation);
return (SLItemStats)(dynamic)result.Result;
}
}
}
public class SLItemStats : TableEntity
{
public string template { get; set; }
public string id { get; set; }
public int totalDownloads { get; set; }
public int downloadsToday { get; set; }
public int downloadsThisWeek { get; set; }
public int downloadsThisMonth { get; set; }
public DateTime lastUpdated { get; set; }
public int likes { get; set; }
public int dislikes { get; set; }
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"ApplicationInsights": {
"InstrumentationKey": ""
}
}

Просмотреть файл

@ -1,5 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ApplicationInsights": {
"InstrumentationKey": "d35b5caf-a276-467c-9ac7-f7f7d84ea171"
},
"Authentication": {
"GitHub": {
"ClientId": "",
"ClientSecret": ""
}
}
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,11 +0,0 @@
<!DOCTYPE html><html class=no-js lang=en dir=ltr><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><meta http-equiv=x-ua-compatible content="ie=edge"><meta name=description content="Azure Functions and Logic Apps Samples"><meta name=keywords content="Serverless Functions Logic Apps Azure"><meta name=author content="Microsoft Corporation"><title>Azure Serverless Community Library</title><link rel=stylesheet type=text/css href=//assets.onestore.ms/cdnfiles/external/mwf/long/v1/v1.26.0/css/mwf-west-european-default.min.css><script>var aiInstrumentationKey="d35b5caf-a276-467c-9ac7-f7f7d84ea171";
if (aiInstrumentationKey !== "")
{
var appInsights=window.appInsights||function(a){
function b(a){c[a]=function(){var b=arguments;c.queue.push(function(){c[a].apply(c,b)})}}var c={config:a},d=document,e=window;setTimeout(function(){var b=d.createElement("script");b.src=a.url||"https://az416426.vo.msecnd.net/scripts/a/ai.0.js",d.getElementsByTagName("script")[0].parentNode.appendChild(b)});try{c.cookie=d.cookie}catch(a){}c.queue=[];for(var f=["Event","Exception","Metric","PageView","Trace","Dependency"];f.length;)b("track"+f.pop());if(b("setAuthenticatedUserContext"),b("clearAuthenticatedUserContext"),b("startTrackEvent"),b("stopTrackEvent"),b("startTrackPage"),b("stopTrackPage"),b("flush"),!a.disableExceptionTracking){f="onerror",b("_"+f);var g=e[f];e[f]=function(a,b,d,e,h){var i=g&&g(a,b,d,e,h);return!0!==i&&c["_"+f](a,b,d,e,h),i}}return c
}({
instrumentationKey: aiInstrumentationKey
});
window.appInsights=appInsights,appInsights.queue&&0===appInsights.queue.length&&appInsights.trackPageView();
}</script><link as=style href=/css/app.97755866.css rel=preload><link as=script href=/js/app.e39284d8.js rel=preload><link as=script href=/js/chunk-vendors.540697dd.js rel=preload><link href=/css/app.97755866.css rel=stylesheet></head><body><a class=m-skip-to-main href=#mainContent tabindex=0>Skip to main content</a><noscript><strong>We're sorry but functions-samples doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app class=f-background-neutral-20></div><script src=/js/chunk-vendors.540697dd.js></script><script src=/js/app.e39284d8.js></script></body><script src=//assets.onestore.ms/cdnfiles/external/mwf/short/v1/latest/scripts/mwf-auto-init-main.var.min.js></script></html>

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше