├── DevApp ├── custom-pulse-ui │ └── script.html ├── appsettings.json ├── Program.cs ├── DevApp.csproj ├── TestController.cs ├── Startup.cs └── TestHostedService.cs ├── dotnetify.png ├── Demo ├── pulse-demo.gif └── NetCoreService │ ├── appsettings.json │ ├── NetCoreService.csproj │ ├── Worker.cs │ ├── Program.cs │ ├── pulse-ui │ └── section_template.html │ └── Startup.cs ├── DotNetifyLib.Pulse ├── pulse-ui │ ├── style.css │ ├── section.html │ └── index.html ├── DotNetify.Pulse.targets ├── IPulseDataProvider.cs ├── Disposable.cs ├── Logger │ ├── PulseLoggerProvider.cs │ ├── LogItem.cs │ └── LogProvider.cs ├── PulseConfiguration.cs ├── SystemUsage │ ├── ProcessData.cs │ ├── CpuUsageProvider.cs │ └── MemoryUsageProvider.cs ├── PulseVM.cs ├── DotNetifyLib.Pulse.csproj ├── PulseExtensions.cs └── PulseMiddleware.cs ├── LICENSE.md ├── DotNetify.Pulse.sln ├── README.md └── .gitignore /DevApp/custom-pulse-ui/script.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dotnetify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Pulse/HEAD/dotnetify.png -------------------------------------------------------------------------------- /Demo/pulse-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Pulse/HEAD/Demo/pulse-demo.gif -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/pulse-ui/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 14px; 3 | } 4 | 5 | label { 6 | font-size: small; 7 | } 8 | 9 | .card { 10 | padding: 1rem 11 | } 12 | /*style_ext*/ -------------------------------------------------------------------------------- /Demo/NetCoreService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } -------------------------------------------------------------------------------- /DevApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Trace", 5 | "Microsoft": "Information" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "DotNetifyPulse": { 10 | "PushUpdateInterval": 200, 11 | "Providers": { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Demo/NetCoreService/NetCoreService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /DevApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace DevApp 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup(); 16 | } 17 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/DotNetify.Pulse.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DevApp/DevApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | InProcess 6 | DevApp 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Dicky Suryadi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | -------------------------------------------------------------------------------- /Demo/NetCoreService/Worker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace NetCoreService 10 | { 11 | public class Worker : BackgroundService 12 | { 13 | private readonly ILogger _logger; 14 | 15 | public Worker(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | 20 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 21 | { 22 | while (!stoppingToken.IsCancellationRequested) 23 | { 24 | _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); 25 | await Task.Delay(1000, stoppingToken); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/IPulseDataProvider.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Dicky Suryadi 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | using System; 15 | 16 | namespace DotNetify.Pulse 17 | { 18 | public delegate void OnPushUpdate(bool liveUpdate); 19 | 20 | public interface IPulseDataProvider 21 | { 22 | IDisposable Configure(PulseVM pulseVM, out OnPushUpdate onPushUpdate); 23 | } 24 | } -------------------------------------------------------------------------------- /Demo/NetCoreService/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace NetCoreService 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IHostBuilder CreateHostBuilder(string[] args) => 21 | Host.CreateDefaultBuilder(args) 22 | .ConfigureServices((hostContext, services) => 23 | { 24 | services.AddHostedService(); 25 | }) 26 | .ConfigureWebHostDefaults(webBuilder => 27 | { 28 | webBuilder.UseStartup(); 29 | }); 30 | } 31 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/Disposable.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Dicky Suryadi 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | using System; 15 | using System.Linq; 16 | 17 | namespace DotNetify.Pulse 18 | { 19 | public class Disposable : IDisposable 20 | { 21 | private readonly Action _dispose; 22 | 23 | public Disposable(Action dispose) => _dispose = dispose; 24 | 25 | public Disposable(params IDisposable[] args) => _dispose = () => args.ToList().ForEach(x => x.Dispose()); 26 | 27 | public void Dispose() => _dispose?.Invoke(); 28 | } 29 | } -------------------------------------------------------------------------------- /DevApp/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | using System.Collections.Generic; 4 | 5 | namespace DevApp.Controllers 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class TestController : ControllerBase 10 | { 11 | [HttpGet] 12 | public ActionResult Get([FromQuery] string level, [FromServices] ILogger logger) 13 | { 14 | switch (level) 15 | { 16 | case "critical": logger.LogCritical("Critical level is logged"); break; 17 | case "error": logger.LogError("Error level is logged"); break; 18 | case "warning": logger.LogWarning("Warning level is logged"); break; 19 | case "trace": logger.LogTrace("Trace level is logged"); break; 20 | case "debug": logger.LogDebug("Debug level is logged"); break; 21 | default: logger.LogInformation("Info level is logged"); break; 22 | } 23 | return "200 OK"; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/Logger/PulseLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Dicky Suryadi 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | using Microsoft.Extensions.Logging; 15 | 16 | namespace DotNetify.Pulse.Log 17 | { 18 | public class PulseLoggerProvider : ILoggerProvider 19 | { 20 | private readonly ILogger _logger; 21 | 22 | public PulseLoggerProvider(IPulseLogger logger) => _logger = logger as ILogger; 23 | 24 | public ILogger CreateLogger(string categoryName) => _logger; 25 | 26 | public void Dispose() 27 | { 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/Logger/LogItem.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Dicky Suryadi 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | using Microsoft.Extensions.Logging; 15 | using System; 16 | 17 | namespace DotNetify.Pulse.Log 18 | { 19 | public struct LogItem 20 | { 21 | public DateTimeOffset Time { get; } 22 | public string FormattedTime => Time.ToString("yyyy'-'MM'-'dd HH':'mm':'ss.fff"); 23 | public string Level { get; } 24 | public string Message { get; } 25 | 26 | public LogItem(LogLevel level, string message) 27 | { 28 | Level = level.ToString(); 29 | Message = message; 30 | Time = DateTimeOffset.Now; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/PulseConfiguration.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Dicky Suryadi 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | using System; 15 | using Microsoft.Extensions.Configuration; 16 | 17 | namespace DotNetify.Pulse 18 | { 19 | public class PulseConfiguration 20 | { 21 | internal static readonly string SECTION = "DotNetifyPulse"; 22 | 23 | // Absolute path to the folder containing UI static files. 24 | public string UIPath { get; set; } 25 | 26 | // Minimum interval between push updates in milliseconds. 27 | public int PushUpdateInterval { get; set; } = 100; 28 | 29 | public IConfigurationSection Providers { get; set; } 30 | 31 | public T GetProvider(string key) where T : class => Providers?.GetSection(key).Get() ?? Activator.CreateInstance(); 32 | } 33 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/pulse-ui/section.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Demo/NetCoreService/pulse-ui/section_template.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /DevApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using DotNetify; 2 | using DotNetify.Pulse; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using System.IO; 8 | 9 | namespace DevApp 10 | { 11 | public class Startup 12 | { 13 | public IConfiguration Configuration { get; } 14 | 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.AddSignalR(); 23 | services.AddDotNetify(); 24 | services.AddDotNetifyPulse(Configuration); 25 | 26 | services.AddHostedService(); 27 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 28 | } 29 | 30 | public void Configure(IApplicationBuilder app) 31 | { 32 | app.UseWebSockets(); 33 | app.UseSignalR(config => config.MapDotNetifyHub()); 34 | app.UseDotNetify(); 35 | app.UseDotNetifyPulse(config => 36 | { 37 | config.UIPath = $"{Directory.GetCurrentDirectory()}\\custom-pulse-ui"; 38 | }); 39 | 40 | app.UseStaticFiles(); 41 | app.UseMvc(); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Demo/NetCoreService/Startup.cs: -------------------------------------------------------------------------------- 1 | using DotNetify; 2 | using DotNetify.Pulse; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | namespace NetCoreService 10 | { 11 | public class Startup 12 | { 13 | // This method gets called by the runtime. Use this method to add services to the container. 14 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 15 | public void ConfigureServices(IServiceCollection services) 16 | { 17 | services.AddSignalR(); 18 | services.AddDotNetify(); 19 | services.AddDotNetifyPulse(); 20 | } 21 | 22 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 23 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 24 | { 25 | if (env.IsDevelopment()) 26 | { 27 | app.UseDeveloperExceptionPage(); 28 | } 29 | 30 | app.UseWebSockets(); 31 | app.UseDotNetify(); 32 | app.UseDotNetifyPulse(); 33 | 34 | app.UseRouting(); 35 | 36 | app.UseEndpoints(endpoints => 37 | { 38 | endpoints.MapHub("/dotnetify"); 39 | endpoints.MapGet("/", async context => 40 | { 41 | await context.Response.WriteAsync("Hello World!"); 42 | }); 43 | }); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/SystemUsage/ProcessData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace DotNetify.Pulse.SystemUsage 5 | { 6 | public class ProcessData 7 | { 8 | private Process _process; 9 | private TimeSpan? _lastTotalProcessorTime; 10 | private DateTime? _lastTimeStamp; 11 | 12 | public double TotalCpu => CalculateTotalCpu(); 13 | public double GCTotalMemory => GC.GetTotalMemory(false).ToMBytes(); 14 | 15 | public ProcessData(Process process) 16 | { 17 | _process = process; 18 | } 19 | 20 | private double CalculateTotalCpu() 21 | { 22 | _lastTotalProcessorTime = _lastTotalProcessorTime ?? _process.TotalProcessorTime; 23 | _lastTimeStamp = _lastTimeStamp ?? _process.StartTime; 24 | 25 | double totalCpuTimeUsed = _process.TotalProcessorTime.TotalMilliseconds - _lastTotalProcessorTime.Value.TotalMilliseconds; 26 | double cpuTimeElapsed = (DateTime.UtcNow - _lastTimeStamp.Value).TotalMilliseconds * Environment.ProcessorCount; 27 | 28 | _lastTotalProcessorTime = _process.TotalProcessorTime; 29 | _lastTimeStamp = DateTime.UtcNow; 30 | 31 | return (totalCpuTimeUsed / cpuTimeElapsed).ToPercent(); 32 | } 33 | } 34 | 35 | internal static class ProcessDataExtensions 36 | { 37 | private static readonly double MB = Math.Pow(1024, 2); 38 | 39 | public static double ToMBytes(this long number) => Math.Round(number / MB, 2); 40 | 41 | public static double ToPercent(this double number) => Math.Round(number * 100, 2); 42 | } 43 | } -------------------------------------------------------------------------------- /DotNetify.Pulse.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.572 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevApp", "DevApp\DevApp.csproj", "{72C4F7BC-A682-4539-B64A-C6414DB242E2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetifyLib.Pulse", "DotNetifyLib.Pulse\DotNetifyLib.Pulse.csproj", "{6648ED9D-47B0-4FC1-BA3D-CBAE52B0E633}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {72C4F7BC-A682-4539-B64A-C6414DB242E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {72C4F7BC-A682-4539-B64A-C6414DB242E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {72C4F7BC-A682-4539-B64A-C6414DB242E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {72C4F7BC-A682-4539-B64A-C6414DB242E2}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {6648ED9D-47B0-4FC1-BA3D-CBAE52B0E633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {6648ED9D-47B0-4FC1-BA3D-CBAE52B0E633}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {6648ED9D-47B0-4FC1-BA3D-CBAE52B0E633}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {6648ED9D-47B0-4FC1-BA3D-CBAE52B0E633}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {10E50947-D68F-4242-82F5-23AB3AEE58B5} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/PulseVM.cs: -------------------------------------------------------------------------------- 1 | using DotNetify.Elements; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reactive.Linq; 5 | 6 | namespace DotNetify.Pulse 7 | { 8 | public class PulseVM : BaseVM 9 | { 10 | private readonly List _disposables = new List(); 11 | 12 | public ReactiveProperty LiveUpdate { get; } 13 | 14 | public PulseVM(IEnumerable dataSources, PulseConfiguration pulseConfig) 15 | { 16 | LiveUpdate = AddProperty(nameof(LiveUpdate), true) 17 | .WithAttribute(new CheckboxAttribute() { Label = "Live update" }); 18 | 19 | var onPushUpdates = ConfigureDataSource(dataSources); 20 | 21 | // Set minimum interval to push updates. 22 | Observable 23 | .Interval(TimeSpan.FromMilliseconds(pulseConfig.PushUpdateInterval)) 24 | .Subscribe(_ => 25 | { 26 | onPushUpdates.ForEach(x => x(LiveUpdate)); 27 | if (LiveUpdate) 28 | PushUpdates(); 29 | }) 30 | .AddTo(_disposables); 31 | } 32 | 33 | public override void Dispose() 34 | { 35 | _disposables.ForEach(x => x.Dispose()); 36 | base.Dispose(); 37 | } 38 | 39 | private List ConfigureDataSource(IEnumerable dataSources) 40 | { 41 | var onPushUpdates = new List(); 42 | 43 | foreach (var dataSource in dataSources) 44 | { 45 | dataSource 46 | .Configure(this, out OnPushUpdate onPushUpdate) 47 | .AddTo(_disposables); 48 | 49 | if (onPushUpdate != null) 50 | onPushUpdates.Add(onPushUpdate); 51 | } 52 | 53 | return onPushUpdates; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /DevApp/TestHostedService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Diagnostics; 5 | using System.Reactive.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace DevApp 11 | { 12 | public class TestHostedService : IHostedService 13 | { 14 | private readonly ILogger _logger; 15 | private IDisposable _subs; 16 | 17 | public TestHostedService(ILogger logger) => _logger = logger; 18 | 19 | public Task StartAsync(CancellationToken cancellationToken) 20 | { 21 | int count = 0; 22 | var random = new Random(); 23 | _subs = Observable 24 | .Interval(TimeSpan.FromMilliseconds(100)) 25 | .Subscribe(_ => 26 | { 27 | // Simulate logging. 28 | if (random.Next(0, 10) < 5) 29 | { 30 | var level = random.Next(0, 6); 31 | var logLevel = (LogLevel) level; 32 | _logger.Log(logLevel, $"Test log {++count} level {logLevel}"); 33 | } 34 | }); 35 | 36 | // Simulate CPU load. 37 | Task.Run(() => 38 | { 39 | int percentage = 0; 40 | Stopwatch watch = new Stopwatch(); 41 | watch.Start(); 42 | while (!cancellationToken.IsCancellationRequested) 43 | { 44 | if (watch.ElapsedMilliseconds > percentage) 45 | { 46 | Thread.Sleep(100 - percentage); 47 | percentage = random.Next(30, 90); 48 | watch.Reset(); 49 | watch.Start(); 50 | } 51 | } 52 | }); 53 | 54 | return Task.CompletedTask; 55 | } 56 | 57 | public Task StopAsync(CancellationToken cancellationToken) 58 | { 59 | _subs?.Dispose(); 60 | return Task.CompletedTask; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/DotNetifyLib.Pulse.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | DotNetify.Pulse 6 | true 7 | Dicky Suryadi 8 | DotNetify.Pulse 9 | DotNetify.Pulse 10 | Customizable real-time monitoring for .NET services. 11 | 1.0.0 12 | https://github.com/dsuryd/dotNetify-Pulse 13 | https://github.com/dsuryd/dotNetify-Pulse 14 | Copyright 2019-2020 15 | 16 | 17 | LICENSE.md 18 | dotnetify.png 19 | 20 | 21 | 22 | 23 | true 24 | \pulse-ui\ 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | 43 | 44 | 45 | True 46 | 47 | 48 | 49 | True 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/PulseExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Dicky Suryadi 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | using System; 15 | using System.Collections.Generic; 16 | using DotNetify.Pulse.Log; 17 | using DotNetify.Pulse.SystemUsage; 18 | using Microsoft.AspNetCore.Builder; 19 | using Microsoft.Extensions.Configuration; 20 | using Microsoft.Extensions.DependencyInjection; 21 | using Microsoft.Extensions.DependencyInjection.Extensions; 22 | 23 | namespace DotNetify.Pulse 24 | { 25 | public static class PulseExtensions 26 | { 27 | public static IServiceCollection AddDotNetifyPulse(this IServiceCollection services, IConfiguration configuration = null) 28 | { 29 | var pulseConfig = configuration?.GetSection(PulseConfiguration.SECTION).Get() ?? new PulseConfiguration(); 30 | services.TryAdd(ServiceDescriptor.Singleton(_ => pulseConfig)); 31 | 32 | return services 33 | .AddPulseLogger() 34 | .AddPulseMemoryUsage() 35 | .AddPulseCpuUsage(); 36 | } 37 | 38 | public static IServiceCollection ClearPulseDataProvider(this IServiceCollection services) 39 | { 40 | return services.RemoveAll(); 41 | } 42 | 43 | public static IApplicationBuilder UseDotNetifyPulse(this IApplicationBuilder app, Action options = null) 44 | { 45 | var pulseConfig = app.ApplicationServices.GetRequiredService(); 46 | options?.Invoke(pulseConfig); 47 | 48 | VMController.RegisterAssembly(typeof(PulseVM).Assembly); 49 | app.UseMiddleware(); 50 | return app; 51 | } 52 | 53 | public static void AddTo(this T item, List items) 54 | { 55 | items.Add(item); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/pulse-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pulse 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |

Pulse

39 |
40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/SystemUsage/CpuUsageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Subjects; 5 | using DotNetify.Elements; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace DotNetify.Pulse.SystemUsage 9 | { 10 | public class CpuUsageProvider : IPulseDataProvider 11 | { 12 | private readonly ReplaySubject _dataStream; 13 | private readonly ProcessData _process = new ProcessData(Process.GetCurrentProcess()); 14 | private readonly int _interval = 1000; 15 | 16 | public CpuUsageProvider(PulseConfiguration pulseConfig) 17 | { 18 | _dataStream = new ReplaySubject(); 19 | } 20 | 21 | public IDisposable Configure(PulseVM pulseVM, out OnPushUpdate onPushUpdate) 22 | { 23 | // Property names. 24 | string TotalCpu; 25 | string TotalCpuTrend; 26 | 27 | int tick = 0; 28 | 29 | pulseVM.AddProperty(nameof(TotalCpu)) 30 | .WithAttribute(new { Label = "Total CPU", Unit = "%" }) 31 | .SubscribeTo(_dataStream.Select(x => x.TotalCpu)) 32 | .SubscribedBy(value => pulseVM.AddList(nameof(TotalCpuTrend), ToChartData(++tick, value)), out IDisposable totalCpuTrendSubs); 33 | 34 | pulseVM.AddProperty(nameof(TotalCpuTrend), new string[][] { ToChartData(tick, _process.TotalCpu) }) 35 | .WithAttribute(new ChartAttribute 36 | { 37 | Title = "Total CPU", 38 | XAxisLabel = "Time", 39 | YAxisLabel = "%", 40 | MaxDataSize = 60, 41 | YAxisMin = 0, 42 | YAxisMax = 100 43 | }); 44 | 45 | onPushUpdate = _ => 46 | { 47 | }; 48 | 49 | var intervalSubs = Observable 50 | .Interval(TimeSpan.FromMilliseconds(_interval)) 51 | .Subscribe(_ => _dataStream.OnNext(_process)); 52 | 53 | return new Disposable(totalCpuTrendSubs, intervalSubs); 54 | } 55 | 56 | private string[] ToChartData(int tick, double value) => new string[] { $"{tick}", $"{value}" }; 57 | } 58 | 59 | public static class PulseCpuUsageExtensions 60 | { 61 | public static IServiceCollection AddPulseCpuUsage(this IServiceCollection services) 62 | { 63 | return services.AddSingleton(); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/SystemUsage/MemoryUsageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Subjects; 5 | using DotNetify.Elements; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace DotNetify.Pulse.SystemUsage 9 | { 10 | public class MemoryUsageProvider : IPulseDataProvider 11 | { 12 | private readonly ReplaySubject _dataStream; 13 | private readonly ProcessData _process = new ProcessData(Process.GetCurrentProcess()); 14 | private readonly int _interval = 1000; 15 | 16 | public MemoryUsageProvider(PulseConfiguration pulseConfig) 17 | { 18 | _dataStream = new ReplaySubject(); 19 | } 20 | 21 | public IDisposable Configure(PulseVM pulseVM, out OnPushUpdate onPushUpdate) 22 | { 23 | // Property names. 24 | string GCTotalMemory; 25 | string GCTotalMemoryTrend; 26 | 27 | int tick = 0; 28 | 29 | pulseVM.AddProperty(nameof(GCTotalMemory)) 30 | .WithAttribute(new { Label = "GC Total Memory", Unit = "MB" }) 31 | .SubscribeTo(_dataStream.Select(x => x.GCTotalMemory)) 32 | .SubscribedBy(value => pulseVM.AddList(nameof(GCTotalMemoryTrend), ToChartData(++tick, value)), out IDisposable totalMemoryTrendSubs); 33 | 34 | pulseVM.AddProperty(nameof(GCTotalMemoryTrend), new string[][] { ToChartData(tick, _process.GCTotalMemory) }) 35 | .WithAttribute(new ChartAttribute 36 | { 37 | Title = "GC Total Memory", 38 | XAxisLabel = "Time", 39 | YAxisLabel = "MBytes", 40 | MaxDataSize = 60, 41 | YAxisMin = 0, 42 | YAxisMax = 100 43 | }); 44 | 45 | onPushUpdate = _ => 46 | { 47 | }; 48 | 49 | var intervalSubs = Observable 50 | .Interval(TimeSpan.FromMilliseconds(_interval)) 51 | .Subscribe(_ => _dataStream.OnNext(_process)); 52 | 53 | return new Disposable(totalMemoryTrendSubs, intervalSubs); 54 | } 55 | 56 | private string[] ToChartData(int tick, double value) => new string[] { $"{tick}", $"{value}" }; 57 | } 58 | 59 | public static class PulseMemoryUsageExtensions 60 | { 61 | public static IServiceCollection AddPulseMemoryUsage(this IServiceCollection services) 62 | { 63 | return services.AddSingleton(); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/PulseMiddleware.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Dicky Suryadi 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | using Microsoft.AspNetCore.Http; 15 | using System.IO; 16 | using System.Reflection; 17 | using System.Threading.Tasks; 18 | 19 | namespace DotNetify.Pulse 20 | { 21 | internal class PulseMiddleware 22 | { 23 | private static readonly string DEFAULT_UI_PATH = $"{Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)}{Path.DirectorySeparatorChar}pulse-ui"; 24 | 25 | private readonly RequestDelegate _next; 26 | private readonly PulseConfiguration _config; 27 | 28 | public PulseMiddleware(RequestDelegate next, PulseConfiguration config) 29 | { 30 | _next = next; 31 | _config = config; 32 | } 33 | 34 | public async Task InvokeAsync(HttpContext httpContext) 35 | { 36 | var uiPath = (_config.UIPath ?? DEFAULT_UI_PATH).TrimEnd(new char[] { '/', '\\' }); 37 | var requestPath = httpContext.Request.Path.ToString(); 38 | 39 | if (requestPath.EndsWith("/pulse")) 40 | { 41 | string index = ReadFile("index.html", uiPath, DEFAULT_UI_PATH); 42 | string style = ReadFile("style.css", uiPath, DEFAULT_UI_PATH); 43 | string styleExt = ReadFile("style_ext.css", uiPath, DEFAULT_UI_PATH); 44 | string script = ReadFile("script.html", uiPath, DEFAULT_UI_PATH); 45 | string section = ReadFile("section.html", uiPath, DEFAULT_UI_PATH); 46 | string sectionExt = ReadFile("section_ext.html", uiPath, DEFAULT_UI_PATH); 47 | 48 | await httpContext.Response.WriteAsync(index 49 | .Replace("/*style*/", style) 50 | .Replace("/*style_ext*/", styleExt) 51 | .Replace("", script) 52 | .Replace("", section) 53 | .Replace("", sectionExt) 54 | ); 55 | } 56 | else 57 | await _next(httpContext); 58 | } 59 | 60 | private string ReadFile(string fileName, string path, string defaultPath) 61 | { 62 | char directorySeparatorChar = Path.DirectorySeparatorChar; 63 | string separatorWithFileName = $"{directorySeparatorChar}{fileName}"; 64 | string filePath = path + separatorWithFileName; 65 | string defaultFilePath = defaultPath + separatorWithFileName; 66 | 67 | string validPath = File.Exists(filePath) ? filePath : File.Exists(defaultFilePath) ? defaultFilePath : null; 68 | if (validPath == null) 69 | return string.Empty; 70 | 71 | return File.ReadAllText(validPath); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /DotNetifyLib.Pulse/Logger/LogProvider.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Dicky Suryadi 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | using System; 15 | using System.Collections.Generic; 16 | using System.Linq; 17 | using System.Reactive.Subjects; 18 | using DotNetify.Elements; 19 | using Microsoft.Extensions.DependencyInjection; 20 | using Microsoft.Extensions.DependencyInjection.Extensions; 21 | using Microsoft.Extensions.Logging; 22 | 23 | namespace DotNetify.Pulse.Log 24 | { 25 | public interface IPulseLogger { } 26 | 27 | public class LogProvider : ILogger, IPulseLogger, IPulseDataProvider 28 | { 29 | private readonly LoggerExternalScopeProvider _scopeProvider; 30 | private readonly ReplaySubject _logStream; 31 | private readonly LogConfiguration _logConfig; 32 | 33 | public class LogConfiguration 34 | { 35 | // How many last log items to cache. 36 | public int Buffer { get; set; } = 5; 37 | 38 | // Number of rows in the log data grid. 39 | public int Rows { get; set; } = 10; 40 | } 41 | 42 | public LogProvider(LoggerExternalScopeProvider scopeProvider, PulseConfiguration pulseConfig) 43 | { 44 | _scopeProvider = scopeProvider; 45 | _logConfig = pulseConfig.GetProvider(nameof(LogProvider)); 46 | _logStream = new ReplaySubject(_logConfig.Buffer); 47 | } 48 | 49 | #region ILogger Methods 50 | 51 | public IDisposable BeginScope(TState state) 52 | { 53 | return _scopeProvider.Push(state); 54 | } 55 | 56 | public bool IsEnabled(LogLevel logLevel) 57 | { 58 | if (logLevel == LogLevel.None) 59 | return false; 60 | 61 | return true; 62 | } 63 | 64 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 65 | { 66 | _logStream.OnNext(new LogItem(logLevel, formatter(state, exception))); 67 | } 68 | 69 | #endregion ILogger Methods 70 | 71 | public IDisposable Configure(PulseVM pulseVM, out OnPushUpdate onPushUpdate) 72 | { 73 | // Property names. 74 | string Logs; 75 | string SelectedLog; 76 | 77 | var selectedLog = pulseVM.AddProperty(nameof(SelectedLog)); 78 | 79 | pulseVM.AddProperty(nameof(Logs), new LogItem[] { new LogItem(LogLevel.None, "Pulse logger started") }) 80 | .WithItemKey(nameof(LogItem.Time)) 81 | .WithAttribute( 82 | new DataGridAttribute 83 | { 84 | RowKey = nameof(LogItem.Time), 85 | Columns = new DataGridColumn[] 86 | { 87 | new DataGridColumn(nameof(LogItem.FormattedTime), "Time") { Sortable = true }, 88 | new DataGridColumn(nameof(LogItem.Level), "Level") { Sortable = true }, 89 | new DataGridColumn(nameof(LogItem.Message), "Message") 90 | }, 91 | Rows = _logConfig.Rows 92 | } 93 | .CanSelect(DataGridAttribute.Selection.Single, selectedLog) 94 | ); 95 | 96 | var cachedLogs = new List(); 97 | 98 | onPushUpdate = liveUpdate => 99 | { 100 | lock (cachedLogs) 101 | { 102 | if (liveUpdate && cachedLogs.Count > 0) 103 | { 104 | pulseVM.AddList(nameof(Logs), cachedLogs.ToArray()); 105 | selectedLog.Value = cachedLogs.Last().Time; 106 | } 107 | cachedLogs.Clear(); 108 | } 109 | }; 110 | 111 | return _logStream.Subscribe(log => 112 | { 113 | lock (cachedLogs) 114 | { 115 | cachedLogs.Add(log); 116 | } 117 | }); 118 | } 119 | } 120 | 121 | public static class PulseLoggerExtensions 122 | { 123 | public static IServiceCollection AddPulseLogger(this IServiceCollection services) 124 | { 125 | services.TryAddTransient(); 126 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 127 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 128 | services.TryAddSingleton(provider => (IPulseLogger) provider.GetServices().First(x => x is LogProvider)); 129 | return services; 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | [![NuGet version](https://badge.fury.io/nu/DotNetify.Pulse.svg)](https://badge.fury.io/nu/DotNetify.Pulse) 4 | 5 | ## DotNetify-Pulse 6 | 7 | Adds an endpoint to any .NET Core service that opens a customizable web view to monitor the service's log activities and resource usage in real-time. 8 | 9 | ### How to Install 10 | 11 | [[Link to demo project]](https://github.com/dsuryd/dotNetify-Pulse/tree/master/Demo/NetCoreService) 12 | 13 | ##### 1. Install the nuget package: 14 | ``` 15 | dotnet add package DotNetify.Pulse 16 | ``` 17 | 18 | ##### 2. Configure the services and the pipeline in `Startup.cs`: 19 | ```csharp 20 | using DotNetify; 21 | using DotNetify.Pulse; 22 | ... 23 | 24 | public void ConfigureServices(IServiceCollection services) 25 | { 26 | services.AddSignalR(); 27 | services.AddDotNetify(); 28 | services.AddDotNetifyPulse(); 29 | } 30 | 31 | public void Configure(IApplicationBuilder app) 32 | { 33 | app.UseWebSockets(); 34 | app.UseDotNetify(); 35 | app.UseDotNetifyPulse(); 36 | 37 | // .NET Core 2.x only: 38 | app.UseSignalR(config => config.MapDotNetifyHub()); 39 | 40 | // .NET Core 3.x only: 41 | app.UseRouting(); 42 | app.UseEndpoints(endpoints => endpoints.MapHub("/dotnetify")); 43 | } 44 | ``` 45 | 46 | ##### 3. Build, then open your web browser to `/pulse`. You should see this page: 47 | > *Internet connection is required for loading the UI scripts from public CDN.* 48 | 49 | 50 | 51 | ##### 4. If you plan to use `dotnet publish`: 52 | - copy the folder 'pulse-ui' from the build output to your project. 53 | - set the build action of the files in the folder to `Content` and `Copy if newer`. 54 | 55 | ### How to Customize 56 | 57 | #### Overview 58 | Before you proceed, let's first do a bit of a dive on how this thing works. 59 | 60 | This library uses: 61 | - SignalR to push data from your service to the web browser. 62 | - [DotNetify](https://dotnetify.net) to write the code using MVVM + Reactive programming. 63 | - [DotNetify-Elements](https://dotnetify.net/elements) to provide HTML5 web components for the view. 64 | 65 | There is a dotNetify view model in this repo named `PulseVM`. This class is the one that pushes data to the browser view, and it only does that when the page is opened. 66 | 67 | When it's instantiated, it will look for service objects that implements *IPulseDataProvider* and passes its own instance to the interface's `Configure` method so that service object can add properties for the data stream. The view model then regularly checks for data updates on those properties and push them to the browser. 68 | 69 | On the browser side, when it sends the `/pulse` HTTP request, this library's middleware intercepts it and returns `index.html`. You can find it and other static files in your service's output directory under `pulse-ui` folder. The HTML markup uses highly specialized web components from dotNetify-Elements to display data grid and charts and for layout. These components are designed so that they can be configured from the server-side view model and maintain connection with the data properties to auto-update, without requiring client-side scripting. 70 | 71 | #### Steps 72 | 73 | ##### 1. Create your custom data provider class that implements _IPulseDataProvider_. 74 | 75 | For example, let's create a simple clock provider: 76 | - Use `AddProperty` to add a new observable property named "Clock" to the Pulse view model, with an initial value. 77 | - Create a timer to emit new value every second. 78 | 79 | ```csharp 80 | using DotNetify.Pulse; 81 | using System.Reactive.Linq; 82 | ... 83 | public class ClockProvider : IPulseDataProvider 84 | { 85 | public IDisposable Configure(PulseVM pulseVM, out OnPushUpdate onPushUpdate) 86 | { 87 | var clockProperty = pulseVM.AddProperty("Clock", DateTime.Now.ToString("hh:mm:ss")); 88 | 89 | onPushUpdate = _ => { }; // No op. 90 | 91 | return Observable 92 | .Interval(TimeSpan.FromSeconds(1)) 93 | .Subscribe(_ => clockProperty.OnNext(DateTime.Now.ToString("hh:mm:ss"))); 94 | } 95 | } 96 | ``` 97 | 98 | ##### 2. Register the provider in the startup's _ConfigureServices_. 99 | 100 | ```csharp 101 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 102 | ``` 103 | 104 | ##### 3. Add a web component to the static HTML page and associate it with the property. 105 | 106 | To do this, you will override the default HTML fragment file called _"section.html"_. Notice that when you build your service, the library creates in your project a folder called _"pulse-ui"_ which contains _"section_template.html"_. 107 | 108 | - Copy and paste this folder to a new one and name it *_"custom-pulse-ui"_*. 109 | - Rename _"section_template.html"_ to *_"section.html"_*. 110 | - Right-click on _"section.html"_, select Properties, and set the "Copy to Output Directory" to *_"Copy if newer"_*. 111 | - Edit the html file and insert the following: 112 | ```html 113 | ... 114 | 115 |
116 | 117 |
118 | ... 119 | ``` 120 | > Read the [dotNetify-Elements documentation](https://dotnetify.net/elements) for info on all the available web components. 121 | 122 | ##### 4. Configure the location of the custom UI folder in the startup's _Configure_. 123 | 124 | ```csharp 125 | app.UseDotNetifyPulse(config => config.UIPath = Directory.GetCurrentDirectory() + "\\custom-pulse-ui"); 126 | ``` 127 | 128 | ##### 5. (Optional) Add to application settings. 129 | 130 | If you want to pass application settings through `appsettings.json`, you can include your custom configuration in the _"DotNetifyPulse"_ configuration section, under `"Providers"`. For example: 131 | ```json 132 | { 133 | "DotNetifyPulse": { 134 | "Providers": { 135 | "ClockProvider": { 136 | "TimeFormat": "hh:mm:ss" 137 | } 138 | } 139 | } 140 | } 141 | ``` 142 | To read the settings, inject `PulseConfiguration` type in your constructor, and use the `GetProvider` method: 143 | ```csharp 144 | internal class ClockSettings 145 | { 146 | public string TimeFormat { get; set; } 147 | } 148 | 149 | public ClockProvider(PulseConfiguration config) 150 | { 151 | var settings = config.GetProvider("ClockProvider"); 152 | } 153 | ``` 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | --------------------------------------------------------------------------------