├── .github └── workflows │ └── azure-static-web-apps-mango-field-07b209610.yml ├── .gitignore ├── .vscode ├── demo.code-snippets └── launch.json ├── Api ├── Api.csproj ├── KernelFunctions.cs ├── KernelStore.cs ├── Properties │ ├── launchSettings.json │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── Startup.cs ├── host.json └── local.settings.json ├── Client ├── App.razor ├── Data │ ├── FileSizeFormatExtensions.cs │ └── NotebookService.cs ├── Pages │ ├── Index.razor │ ├── Index.razor.css │ ├── Notebook │ │ ├── Cells.razor │ │ ├── Cells.razor.css │ │ ├── CodeCell.razor │ │ ├── CodeCell.razor.css │ │ ├── FileCell.razor │ │ ├── FileCell.razor.css │ │ ├── NewCell.razor │ │ ├── NewCell.razor.css │ │ ├── Notebook.razor │ │ ├── Notebook.razor.css │ │ ├── OutputDisplays │ │ │ ├── ArrayDisplay.razor │ │ │ ├── ArrayDisplay.razor.css │ │ │ ├── ErrorDisplay.razor │ │ │ ├── ErrorDisplay.razor.css │ │ │ ├── FallbackDisplay.razor │ │ │ ├── FallbackDisplay.razor.css │ │ │ └── OutputDisplay.razor │ │ ├── TextCell.razor │ │ └── TextCell.razor.css │ ├── Notebooks.razor │ └── Notebooks.razor.css ├── Program.cs ├── Properties │ └── launchSettings.json ├── Shared │ ├── MainLayout.razor │ └── Toolbar │ │ ├── MainToolbar.razor │ │ ├── MainToolbar.razor.css │ │ └── NotebookName.razor ├── _Imports.razor ├── blazoract.Client.csproj └── wwwroot │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── css │ └── app.css │ ├── dotnet.png │ ├── favicon.ico │ ├── index.html │ ├── js │ └── notebook-name.js │ ├── routes.json │ └── wave.png ├── Directory.Build.props ├── LICENSE ├── README.md ├── Shared ├── Cell.cs ├── ExecuteRequest.cs ├── ExecuteResult.cs ├── GetCompletionsRequest.cs ├── Notebook.cs └── blazoract.Shared.csproj ├── blazoract.sln ├── nuget.config └── sales-data.csv /.github/workflows/azure-static-web-apps-mango-field-07b209610.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | submodules: true 21 | - name: Build And Deploy 22 | id: builddeploy 23 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 24 | with: 25 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_FIELD_07B209610 }} 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 27 | action: "upload" 28 | ###### Repository/Build Configurations - These values can be configured to match you app requirements. ###### 29 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 30 | app_location: "Client" # App source code path 31 | api_location: "Api" # Api source code path - optional 32 | app_artifact_location: "wwwroot" # Built app content directory - optional 33 | ###### End of Repository/Build Configurations ###### 34 | 35 | close_pull_request_job: 36 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 37 | runs-on: ubuntu-latest 38 | name: Close Pull Request Job 39 | steps: 40 | - name: Close Pull Request 41 | id: closepullrequest 42 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 43 | with: 44 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_FIELD_07B209610 }} 45 | action: "close" 46 | -------------------------------------------------------------------------------- /.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 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd 363 | 364 | .DS_Store -------------------------------------------------------------------------------- /.vscode/demo.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "DEMO: Button style": { 3 | "prefix": "demo-buttonstyle", 4 | "body": ["background-color: #19b5fe;\ncolor: white;\nborder: none;\npadding: 0.4rem 1.2rem;\nborder-radius: 4px;"] 5 | }, 6 | "DEMO: Info background": { 7 | "prefix": "demo-infobackground", 8 | "body": ["background: url('dotnet.png') bottom 10px right 30px / calc(min(200px, 35%)) no-repeat, url('wave.png') bottom center/contain no-repeat;"] 9 | }, 10 | "DEMO: CodeEditor tag": { 11 | "prefix": "demo-codeeditor-tag", 12 | "body": [""] 14 | }, 15 | "DEMO: CodeEditor GetCompletionsAsync": { 16 | "prefix": "demo-codeeditor-getcompletionsasync", 17 | "body": [ 18 | "private async Task GetCompletionsAsync(string value, Position position)", 19 | "{", 20 | " var response = await Http.PostAsJsonAsync(\"api/code/completions\", new GetCompletionsRequest", 21 | " {", 22 | " NotebookId = Cell.NotebookId,", 23 | " Code = value,", 24 | " Column = position.Column,", 25 | " LineNumber = position.LineNumber", 26 | " });", 27 | "", 28 | " return await response.Content.ReadFromJsonAsync();", 29 | "}"] 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach to Blazor WebAssembly App", 9 | "type": "blazorwasm", 10 | "request": "attach", 11 | "cwd": "${workspaceFolder}/Client", 12 | "url": "https://localhost:5001", 13 | "browser": "edge" 14 | }, 15 | { 16 | "name": "Attach to .NET Functions", 17 | "type": "coreclr", 18 | "request": "attach", 19 | "processId": "${command:pickProcess}" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /Api/Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1 4 | v3 5 | blazoract.Api 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | Never 28 | 29 | 30 | -------------------------------------------------------------------------------- /Api/KernelFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.Http; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.DotNet.Interactive.CSharp; 10 | using Microsoft.DotNet.Interactive; 11 | using Microsoft.DotNet.Interactive.Events; 12 | using Microsoft.DotNet.Interactive.Commands; 13 | using blazoract.Shared; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | using System.Net.Http; 17 | using Newtonsoft.Json; 18 | using Microsoft.CodeAnalysis.Text; 19 | 20 | namespace blazoract.Api 21 | { 22 | public class KernelFunction 23 | { 24 | private KernelStore _kernels; 25 | public KernelFunction(KernelStore kernels) 26 | { 27 | _kernels = kernels; 28 | } 29 | 30 | [FunctionName("RunCode")] 31 | public async Task RunCode( 32 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "code/run")] HttpRequest req, 33 | ILogger log) 34 | { 35 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 36 | var executeRequest = JsonConvert.DeserializeObject(requestBody); 37 | var kernel = _kernels.GetKernelForNotebook(executeRequest.NotebookId); 38 | var request = await kernel.SendAsync(new SubmitCode(executeRequest.Code), CancellationToken.None); 39 | var result = new ExecuteResult(); 40 | request.KernelEvents.Subscribe(x => 41 | { 42 | Console.WriteLine($"Received event: {x}"); 43 | switch (x) 44 | { 45 | case DisplayEvent displayEvent: 46 | var value = displayEvent.Value; 47 | result.OutputType = value?.GetType().AssemblyQualifiedName; 48 | result.OutputToString = value?.ToString(); 49 | try 50 | { 51 | result.OutputJson = System.Text.Json.JsonSerializer.Serialize(value); 52 | } 53 | catch 54 | { 55 | // If it's not serializable, the client will just use OutputToString 56 | } 57 | break; 58 | case CommandFailed commandFailed: 59 | result.OutputType = "error"; 60 | result.OutputJson = null; 61 | result.OutputToString = commandFailed.Message; 62 | break; 63 | } 64 | }); 65 | 66 | return new OkObjectResult(result); 67 | } 68 | 69 | [FunctionName("GetCompletions")] 70 | public async Task GetCompletions( 71 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "code/completions")] HttpRequest req, 72 | ILogger log) 73 | { 74 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 75 | var completionRequest = JsonConvert.DeserializeObject(requestBody); 76 | 77 | var kernel = _kernels.GetKernelForNotebook(completionRequest.NotebookId); 78 | var request = await kernel.SendAsync(new RequestCompletions(completionRequest.Code, new LinePosition(completionRequest.LineNumber - 1, completionRequest.Column - 1))); 79 | var result = Array.Empty(); 80 | request.KernelEvents.Subscribe(x => 81 | { 82 | if (x is CompletionsProduced completions) 83 | { 84 | result = completions.Completions.Select(c => new Suggestion 85 | { 86 | Label = c.DisplayText, 87 | InsertText = c.InsertText, 88 | Kind = Enum.TryParse(c.Kind, out var parsedKind) ? parsedKind : CompletionItemKind.Property, 89 | Documentation = c.Documentation, 90 | }).ToArray(); 91 | } 92 | }); 93 | return new OkObjectResult(result); 94 | } 95 | 96 | [FunctionName("UploadFile")] 97 | public async Task UploadFile( 98 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "code/uploadfile")] HttpRequest req, 99 | ILogger log) 100 | { 101 | using var requestData = new MemoryStream(); 102 | await req.Body.CopyToAsync(requestData); 103 | 104 | // TODO: Is there a way to copy data into the kernel without stringification? 105 | var requestDataBase64 = Convert.ToBase64String(new Span(requestData.GetBuffer(), 0, (int)requestData.Length)); 106 | 107 | var notebookId = req.Query["notebookId"].First(); 108 | var variable = req.Query["variable"].First(); 109 | var kernel = _kernels.GetKernelForNotebook(notebookId); 110 | 111 | var code = $"var {variable} = Convert.FromBase64String(\"{requestDataBase64}\");"; 112 | var request = await kernel.SendAsync(new SubmitCode(code), CancellationToken.None); 113 | return new OkResult(); 114 | } 115 | 116 | // TODO: Avoid duplication by changing MonacoRazor to target 3.0 or moving shared types to seperate package. 117 | public enum CompletionItemKind : int 118 | { 119 | Method = 0, 120 | Function = 1, 121 | Constructor = 2, 122 | Field = 3, 123 | Variable = 4, 124 | Class = 5, 125 | Struct = 6, 126 | Interface = 7, 127 | Module = 8, 128 | Property = 9, 129 | Event = 10, 130 | Operator = 11, 131 | Unit = 12, 132 | Value = 13, 133 | Constant = 14, 134 | Enum = 15, 135 | EnumMember = 16, 136 | Keyword = 17, 137 | Text = 18, 138 | Color = 19, 139 | File = 20, 140 | Reference = 21, 141 | Customcolor = 22, 142 | Folder = 23, 143 | TypeParameter = 24, 144 | User = 25, 145 | Issue = 26, 146 | Snippet = 27 147 | } 148 | 149 | public class Suggestion 150 | { 151 | public string Label { get; set; } 152 | public string InsertText { get; set; } 153 | public CompletionItemKind Kind { get; set; } 154 | public string Documentation { get; set; } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Api/KernelStore.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.DotNet.Interactive.CSharp; 3 | using Microsoft.DotNet.Interactive; 4 | using System.Collections.Concurrent; 5 | using Microsoft.Extensions.Caching.Memory; 6 | using System; 7 | 8 | [assembly: FunctionsStartup(typeof(blazoract.Api.Startup))] 9 | 10 | namespace blazoract.Api 11 | { 12 | // To avoid interference across notebooks, we need a separate kernel instance per notebook. 13 | // Set up a cache in memory that holds a certain maximum number of instances, and evicts 14 | // entries if they become idle for longer than a certain period. 15 | 16 | public class KernelStore 17 | { 18 | const int MaxEntries = 1024; 19 | const int MaxIdleMinutes = 15; 20 | 21 | private MemoryCache _kernelsCache = new MemoryCache(new MemoryCacheOptions 22 | { 23 | SizeLimit = MaxEntries 24 | }); 25 | 26 | private ConcurrentDictionary _kernels 27 | = new ConcurrentDictionary(); 28 | 29 | public CompositeKernel GetKernelForNotebook(string notebookId) 30 | { 31 | return _kernelsCache.GetOrCreate(notebookId, entry => 32 | { 33 | entry.SetSlidingExpiration(TimeSpan.FromMinutes(MaxIdleMinutes)); 34 | entry.Size = 1; 35 | return new CompositeKernel() 36 | { 37 | new CSharpKernel().UseDefaultFormatting().UseDotNetVariableSharing().UseNugetDirective() 38 | }; 39 | }); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Api": { 4 | "commandName": "Project", 5 | "commandLineArgs": "start --cors *" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Api/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "storage1": { 4 | "type": "storage", 5 | "connectionId": "AzureWebJobsStorage" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Api/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "storage1": { 4 | "type": "storage.emulator", 5 | "connectionId": "AzureWebJobsStorage" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | [assembly: FunctionsStartup(typeof(blazoract.Api.Startup))] 5 | 6 | namespace blazoract.Api 7 | { 8 | public class Startup : FunctionsStartup 9 | { 10 | public override void Configure(IFunctionsHostBuilder builder) 11 | { 12 | builder.Services.AddSingleton(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingExcludedTypes": "Request", 6 | "samplingSettings": { 7 | "isEnabled": true 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /Api/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=false", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet" 6 | }, 7 | "Host": { 8 | "LocalHttpPort": 7071, 9 | "CORS": "*", 10 | "CORSCredentials": false 11 | } 12 | } -------------------------------------------------------------------------------- /Client/App.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.WebAssembly.Services 2 | @inject LazyAssemblyLoader assemblyLoader 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Sorry, there's nothing at this address.

11 |
12 |
13 |
14 | 15 | @code { 16 | private async Task OnNavigateAsync(NavigationContext args) 17 | { 18 | if (args.Path.EndsWith("notebooks")) 19 | { 20 | await assemblyLoader.LoadAssembliesAsync( 21 | new List() { "BlazorTable.dll" } 22 | ); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Client/Data/FileSizeFormatExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace blazoract.Client.Data 4 | { 5 | public static class FileSizeFormatExtensions 6 | { 7 | public static string ToDisplayString(this long fileSizeBytes) 8 | { 9 | int scale = 1024; 10 | string[] units = new string[] { "gigabytes", "megabytes", "kilobytes", "bytes" }; 11 | long max = (long)Math.Pow(scale, units.Length - 1); 12 | 13 | foreach (var unit in units) 14 | { 15 | if (fileSizeBytes > max) 16 | return $"{decimal.Divide(fileSizeBytes, max):##.##} {unit}"; 17 | 18 | max /= scale; 19 | } 20 | return "0 bytes"; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Client/Data/NotebookService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using blazoract.Shared; 4 | using Blazored.LocalStorage; 5 | using System.Threading.Tasks; 6 | using System.Net.Http; 7 | using System.Net.Http.Json; 8 | using System.Linq; 9 | 10 | namespace blazoract.Client.Data 11 | { 12 | public class NotebookService 13 | { 14 | public event Action OnChange; 15 | 16 | private ILocalStorageService _storage; 17 | private HttpClient _http; 18 | 19 | private Dictionary> _inMemoryNotebooks = new Dictionary>(); 20 | 21 | public NotebookService(ILocalStorageService storage, HttpClient http) 22 | { 23 | _storage = storage; 24 | _http = http; 25 | } 26 | 27 | public async Task GetInitialContent() 28 | { 29 | return await _http.GetFromJsonAsync("/data/default-notebook.json"); 30 | } 31 | 32 | public Task GetById(string id) 33 | { 34 | 35 | if (!_inMemoryNotebooks.TryGetValue(id, out var result)) 36 | { 37 | result = _storage.GetItemAsync(id).AsTask(); 38 | _inMemoryNotebooks[id] = result; 39 | } 40 | else 41 | { 42 | } 43 | 44 | return result; 45 | } 46 | 47 | public async Task CreateNewNotebook(bool addSample = false) 48 | { 49 | var id = Guid.NewGuid().ToString("N"); 50 | var notebook = new Notebook("New notebook", id); 51 | notebook.Cells = new List(); 52 | 53 | if (addSample) 54 | { 55 | notebook.Cells.Add(new Cell(id, @"int Fibonacci(int n) 56 | { 57 | return n < 2 ? 1 : Fibonacci(n-1) + Fibonacci(n-2); 58 | } 59 | 60 | Fibonacci(5)", 0)); 61 | notebook.Cells.Add(new Cell(id, @"Enumerable.Range(1, 10).Select(Fibonacci).ToArray()", 0)); 62 | } 63 | else 64 | { 65 | notebook.Cells.Add(new Cell(id, "// Type your code here", 0)); 66 | } 67 | 68 | await _storage.SetItemAsync(id, notebook); 69 | _inMemoryNotebooks[id] = Task.FromResult(notebook); 70 | 71 | var notebooks = await _storage.GetItemAsync>("blazoract-notebooks") ?? new List(); 72 | notebooks.Add(id); 73 | await _storage.SetItemAsync("blazoract-notebooks", notebooks); 74 | 75 | return notebook; 76 | } 77 | 78 | public async Task AddCell(string id, string content, CellType type, int position) 79 | { 80 | var notebook = await GetById(id); 81 | notebook.Cells.Insert(position, new Cell(id, content, type)); 82 | await _storage.SetItemAsync(id, notebook); 83 | OnChange.Invoke(); 84 | return notebook; 85 | } 86 | 87 | public async Task Save(Notebook notebook) 88 | { 89 | notebook.Updated = DateTime.Now; 90 | await _storage.SetItemAsync(notebook.NotebookId, notebook); 91 | } 92 | 93 | public async Task> GetNotebooks() 94 | { 95 | var notebooks = await _storage.GetItemAsync>("blazoract-notebooks"); 96 | if (notebooks != null) 97 | { 98 | return await Task.WhenAll(notebooks.Select(async notebookId => await GetById(notebookId))); 99 | } 100 | return new List(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Client/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @inject NotebookService content 3 | @inject NavigationManager navigation 4 | 5 |
6 | 7 | 8 |
9 |

Welcome!

10 |

To get started, create a new notebook or see your existing notebooks.

11 | 12 | 13 |
14 |
15 | 16 | @code { 17 | private async Task CreateNewNotebook() 18 | { 19 | var newNotebook = await content.CreateNewNotebook(addSample: true); 20 | navigation.NavigateTo($"/notebook/{newNotebook.NotebookId}"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Client/Pages/Index.razor.css: -------------------------------------------------------------------------------- 1 | .info { 2 | padding: 2rem 4rem; 3 | flex-grow: 1; 4 | background: url('dotnet.png') bottom 10px right 30px / calc(min(200px, 35%)) no-repeat, url('wave.png') bottom center/contain no-repeat; 5 | } 6 | 7 | button { 8 | background-color: #19b5fe; 9 | color: white; 10 | border: none; 11 | padding: 0.4rem 1.2rem; 12 | border-radius: 4px; 13 | } -------------------------------------------------------------------------------- /Client/Pages/Notebook/Cells.razor: -------------------------------------------------------------------------------- 1 | @inject NotebookService content 2 | @implements IDisposable 3 | 4 |
5 | @if (notebook != null && notebook.Cells != null) 6 | { 7 | for (var index = 0; index < notebook.Cells.Count; index++) 8 | { 9 | var cell = notebook.Cells[index]; 10 | if (cell.Type == CellType.Code) 11 | { 12 | 13 | } 14 | else if (cell.Type == CellType.File) 15 | { 16 | 17 | } 18 | else 19 | { 20 | 21 | } 22 | 23 | 24 | } 25 | } 26 |
27 | 28 | @code { 29 | [Parameter] 30 | public string NotebookId { get; set; } 31 | private blazoract.Shared.Notebook notebook; 32 | 33 | protected override async Task OnInitializedAsync() 34 | { 35 | notebook = await content.GetById(NotebookId); 36 | content.OnChange += StateHasChanged; 37 | } 38 | 39 | private async Task SaveNotebook() 40 | { 41 | notebook = await content.GetById(NotebookId); 42 | await content.Save(notebook); 43 | } 44 | 45 | void IDisposable.Dispose() 46 | { 47 | content.OnChange -= StateHasChanged; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/Cells.razor.css: -------------------------------------------------------------------------------- 1 | .cells { 2 | margin: 0 10px; 3 | } 4 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/CodeCell.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using MonacoRazor 3 | @inject HttpClient Http 4 | 5 |
6 |
7 |
8 | 9 |
10 | 11 | 13 |
14 |
15 | 16 |
17 |
18 | 19 | @code { 20 | [Parameter] 21 | public Cell Cell { get; set; } 22 | 23 | [Parameter] 24 | public Func OnCodeChange { get; set; } 25 | 26 | private ExecuteResult result; 27 | private bool isEvaluating; 28 | 29 | private string CellContent 30 | { 31 | get => Cell.Content; 32 | set 33 | { 34 | Cell.Content = value; 35 | OnCodeChange(); 36 | } 37 | } 38 | 39 | private async Task RunCell() 40 | { 41 | try 42 | { 43 | isEvaluating = true; 44 | var request = new ExecuteRequest(Cell.NotebookId, Cell.Content); 45 | var response = await Http.PostAsJsonAsync("api/code/run", request); 46 | result = await response.Content.ReadFromJsonAsync(); 47 | } 48 | finally 49 | { 50 | isEvaluating = false; 51 | } 52 | } 53 | 54 | private async Task GetCompletionsAsync(string value, Position position) 55 | { 56 | var response = await Http.PostAsJsonAsync("api/code/completions", new GetCompletionsRequest 57 | { 58 | NotebookId = Cell.NotebookId, 59 | Code = value, 60 | Column = position.Column, 61 | LineNumber = position.LineNumber 62 | }); 63 | 64 | return await response.Content.ReadFromJsonAsync(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/CodeCell.razor.css: -------------------------------------------------------------------------------- 1 | .code-cell { 2 | margin: 10px; 3 | border: 1px solid #bfbfbf; 4 | } 5 | 6 | .top { 7 | display: flex; 8 | } 9 | 10 | textarea { 11 | flex-grow: 1; 12 | border: 0; 13 | outline: 0; 14 | font-family: monospace; 15 | min-height: 150px; 16 | } 17 | 18 | ::deep .editor { 19 | flex-grow: 1; 20 | border: none; 21 | margin: 0; 22 | min-height: 150px; 23 | } 24 | 25 | .output { 26 | background-color: #eee; 27 | min-height: 20px; 28 | transition: filter 0.5s ease-in-out; 29 | } 30 | .output > ::deep div { 31 | padding: 10px; 32 | animation-name: yellowfade; 33 | animation-duration: 1s; 34 | background-color: var(--bgcol); 35 | } 36 | 37 | @keyframes yellowfade { 38 | from { 39 | background-color: #f5ea7f; 40 | } 41 | 42 | to { 43 | background-color: var(--bgcol); 44 | } 45 | } 46 | 47 | .loading { 48 | filter: brightness(0.5); 49 | } 50 | 51 | .toolbar { 52 | flex-grow: 0; 53 | padding: 0 10px; 54 | } 55 | 56 | button { 57 | border: none; 58 | background: white; 59 | } 60 | 61 | button:hover { 62 | color: #19b5fe; 63 | } 64 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/FileCell.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Http 2 | 3 |
4 | @Cell.Content: 5 | 6 | 7 | 8 |
@status
9 |
10 | 11 | @code { 12 | [Parameter] 13 | public blazoract.Shared.Cell Cell { get; set; } 14 | 15 | private string status = null; 16 | 17 | private async Task HandleFileChosenAsync(InputFileChangeEventArgs eventArgs) 18 | { 19 | status = $"Sending {eventArgs.File.Size.ToDisplayString()}..."; 20 | 21 | // Stream the data directly to the backend API 22 | using var fileStream = eventArgs.File.OpenReadStream(maxAllowedSize: 5*1024*1024); 23 | var url = $"api/code/uploadfile?notebookId={Cell.NotebookId}&variable={Cell.Content}"; 24 | await Http.PostAsync(url, new StreamContent(fileStream)); 25 | 26 | status = $"Finished sending {eventArgs.File.Size.ToDisplayString()}"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/FileCell.razor.css: -------------------------------------------------------------------------------- 1 | .file { 2 | display: table; 3 | background-color: #eee; 4 | padding: 1em 2em; 5 | margin: 1em auto; 6 | } 7 | 8 | span { 9 | font-family: monospace; 10 | } 11 | 12 | ::deep input[type=file] { 13 | margin-left: 3em; 14 | } 15 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/NewCell.razor: -------------------------------------------------------------------------------- 1 | @inject NotebookService content 2 | 3 |
4 | 11 | 18 | 28 |
29 | 30 | @code { 31 | [Parameter] 32 | public int Position { get; set; } 33 | 34 | [Parameter] 35 | public blazoract.Shared.Notebook Notebook { get; set; } 36 | 37 | public async Task AddCodeCell() 38 | { 39 | await content.AddCell(Notebook.NotebookId, "", CellType.Code, Position + 1); 40 | } 41 | 42 | public async Task AddTextCell() 43 | { 44 | await content.AddCell(Notebook.NotebookId, "Add text here...", CellType.Text, Position + 1); 45 | } 46 | 47 | public async Task AddFileCell() 48 | { 49 | var fileIndex = ChooseAvailableFileVariableName(); 50 | await content.AddCell(Notebook.NotebookId, fileIndex, CellType.File, Position + 1); 51 | } 52 | 53 | private string ChooseAvailableFileVariableName() 54 | { 55 | var existingFileVariables = Notebook.Cells.Where(c => c.Type == CellType.File).ToDictionary(c => c.Content, c => c); 56 | 57 | for (var i = 1; ; i++) 58 | { 59 | var candidateName = $"file{i}"; 60 | if (!existingFileVariables.ContainsKey(candidateName)) 61 | { 62 | return candidateName; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/NewCell.razor.css: -------------------------------------------------------------------------------- 1 | button { 2 | color: #bfbfbf; 3 | background: none; 4 | border: none; 5 | outline: none; 6 | text-align: center; 7 | padding: 0 4px; 8 | 9 | } 10 | 11 | button:hover svg { 12 | transform: scale(1.4); 13 | } 14 | 15 | .new-cell-toolbar { 16 | text-align: center; 17 | } -------------------------------------------------------------------------------- /Client/Pages/Notebook/Notebook.razor: -------------------------------------------------------------------------------- 1 | @page "/notebook/{NotebookId}" 2 | 3 |
4 | 5 | 6 |
7 | 8 | @code { 9 | [Parameter] 10 | public string NotebookId { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/Notebook.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | padding-bottom: 15px; 3 | } -------------------------------------------------------------------------------- /Client/Pages/Notebook/OutputDisplays/ArrayDisplay.razor: -------------------------------------------------------------------------------- 1 | 
2 | 3 | 4 | @if (entry is Array row) // 2D array 5 | { 6 | 7 | @foreach (var cell in row) 8 | { 9 | 10 | } 11 | 12 | } 13 | else // Any other case 14 | { 15 | 16 | 17 | 18 | } 19 | 20 |
@cell
@entry
21 |
22 | 23 | @code { 24 | [Parameter] public ICollection Data { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/OutputDisplays/ArrayDisplay.razor.css: -------------------------------------------------------------------------------- 1 | .scroller { 2 | max-height: 300px; 3 | overflow-y: auto; 4 | } 5 | 6 | table { 7 | width: 100%; 8 | table-layout: fixed; 9 | } 10 | 11 | td { 12 | border: 1px solid gray; 13 | padding: 4px 8px; 14 | overflow: hidden; 15 | white-space: nowrap; 16 | text-overflow: ellipsis; 17 | } 18 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/OutputDisplays/ErrorDisplay.razor: -------------------------------------------------------------------------------- 1 | 
@Result.OutputToString
2 | 3 | @code { 4 | [Parameter] public ExecuteResult Result { get; set; } 5 | } 6 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/OutputDisplays/ErrorDisplay.razor.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: white; 3 | white-space: pre; 4 | font-family: monospace; 5 | --bgcol: red; 6 | } 7 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/OutputDisplays/FallbackDisplay.razor: -------------------------------------------------------------------------------- 1 | @switch (OutputToString) 2 | { 3 | case null: 4 |
(no output)
5 | break; 6 | case "": 7 |
(empty string)
8 | break; 9 | default: 10 |
@OutputToString
11 | break; 12 | } 13 | 14 | @code { 15 | [Parameter] public string OutputToString { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/OutputDisplays/FallbackDisplay.razor.css: -------------------------------------------------------------------------------- 1 | div { 2 | white-space: pre-wrap; 3 | } 4 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/OutputDisplays/OutputDisplay.razor: -------------------------------------------------------------------------------- 1 | @if (Result != null) 2 | { 3 | if (Result.OutputType == "error") 4 | { 5 | 6 | } 7 | else if (_deserializedOutput is string str) 8 | { 9 | 10 | } 11 | else if (_deserializedOutput is ICollection objectCollection) 12 | { 13 | 14 | } 15 | else if (_deserializedOutput is IEnumerable enumerable) 16 | { 17 | 18 | } 19 | else 20 | { 21 | 22 | } 23 | } 24 | 25 | @code { 26 | [Parameter] public ExecuteResult Result { get; set; } 27 | 28 | private object _deserializedOutput; 29 | 30 | protected override void OnInitialized() 31 | { 32 | if (Result?.OutputJson != null && Result.OutputType != "error") 33 | { 34 | try 35 | { 36 | var type = Type.GetType(Result.OutputType); 37 | _deserializedOutput = System.Text.Json.JsonSerializer.Deserialize(Result.OutputJson, type); 38 | } 39 | catch 40 | { 41 | // If we can't deserialize it, we'll just use the string fallback 42 | } 43 | } 44 | } 45 | 46 | // We create a new instance of this component for each new ExecuteResult, 47 | // so there's no need for existing instances to re-render 48 | protected override bool ShouldRender() 49 | => false; 50 | } 51 | -------------------------------------------------------------------------------- /Client/Pages/Notebook/TextCell.razor: -------------------------------------------------------------------------------- 1 |
2 | @if (Cell != null) 3 | { 4 | @Cell.Content 5 | } else { 6 |

Enter your text here...

7 | } 8 |
9 | 10 | @code { 11 | [Parameter] 12 | public Cell Cell { get; set; } 13 | } -------------------------------------------------------------------------------- /Client/Pages/Notebook/TextCell.razor.css: -------------------------------------------------------------------------------- 1 | .text-cell { 2 | display: flex; 3 | flex: 0 0 100%; 4 | flex-wrap: wrap; 5 | margin: 10px; 6 | padding: 0 10px; 7 | border: 0; 8 | } 9 | -------------------------------------------------------------------------------- /Client/Pages/Notebooks.razor: -------------------------------------------------------------------------------- 1 | @page "/notebooks" 2 | @using BlazorTable 3 | @inject NavigationManager navigation 4 | @inject NotebookService content 5 | 6 |
7 | 8 | 9 | 10 | 12 | 14 | 16 |
17 |
18 | 19 | @code { 20 | private IEnumerable notebooks; 21 | 22 | protected override async Task OnInitializedAsync() 23 | { 24 | notebooks = await content.GetNotebooks(); 25 | } 26 | 27 | private void ShowNotebook(blazoract.Shared.Notebook item) 28 | { 29 | navigation.NavigateTo($"notebook/{item.NotebookId}"); 30 | } 31 | } -------------------------------------------------------------------------------- /Client/Pages/Notebooks.razor.css: -------------------------------------------------------------------------------- 1 | .page ::deep .table-responsive tr:hover { 2 | background-color: rgba(0,123,255,.5); 3 | } -------------------------------------------------------------------------------- /Client/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using System.Text; 6 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using blazoract.Client.Data; 11 | using Blazored.LocalStorage; 12 | 13 | namespace blazoract.Client 14 | { 15 | public class Program 16 | { 17 | public static async Task Main(string[] args) 18 | { 19 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 20 | builder.RootComponents.Add("#app"); 21 | 22 | // Configure HTTP client to send requests to the local function server 23 | var baseAddress = builder.Configuration["BaseAddress"] ?? builder.HostEnvironment.BaseAddress; 24 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(baseAddress) }); 25 | 26 | // Services for managing local notebook storage 27 | builder.Services.AddBlazoredLocalStorage(); 28 | builder.Services.AddScoped(); 29 | 30 | await builder.Build().RunAsync(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:65139", 7 | "sslPort": 44369 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "blazoract": { 21 | "commandName": "Project", 22 | "dotnetRunMessages": "true", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | @Body 4 | -------------------------------------------------------------------------------- /Client/Shared/Toolbar/MainToolbar.razor: -------------------------------------------------------------------------------- 1 | @inject NotebookService content 2 | @inject NavigationManager navigation 3 | 4 |
5 |

blazoract

6 | @if (!string.IsNullOrEmpty(NotebookId)) 7 | { 8 | 9 | } 10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 | @code { 18 | [Parameter] public string NotebookId { get; set; } 19 | 20 | private async Task NewNotebook() 21 | { 22 | var newNotebook = await content.CreateNewNotebook(); 23 | navigation.NavigateTo($"/notebook/{newNotebook.NotebookId}"); 24 | } 25 | 26 | private async Task SaveNotebook() 27 | { 28 | if (NotebookId != null) 29 | { 30 | var notebook = await content.GetById(NotebookId); 31 | await content.Save(notebook); 32 | } 33 | } 34 | 35 | private void GoToNotebooks() 36 | { 37 | navigation.NavigateTo("/notebooks"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Client/Shared/Toolbar/MainToolbar.razor.css: -------------------------------------------------------------------------------- 1 | .main-toolbar { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: space-between; 5 | padding: 0.5em 1.2em; 6 | background-color: #e8e8e8; 7 | align-items: center; 8 | } 9 | 10 | h2 { 11 | margin: 0; 12 | } 13 | 14 | .right { 15 | float: right; 16 | } 17 | 18 | button { 19 | background: #bfbfbf; 20 | border: none; 21 | text-decoration: none; 22 | color: black; 23 | padding: 0 12px; 24 | } 25 | 26 | button:hover { 27 | background-color: #19b5fe; 28 | color: white; 29 | font-weight: bold; 30 | } 31 | 32 | a { 33 | color: #19b5fe; 34 | } 35 | -------------------------------------------------------------------------------- /Client/Shared/Toolbar/NotebookName.razor: -------------------------------------------------------------------------------- 1 | @inject IJSRuntime JSRuntime 2 | @inject NotebookService content 3 | @implements IAsyncDisposable 4 | 5 |
6 | @notebook?.Title 7 |
8 | 9 | @code { 10 | [Parameter] 11 | public string NotebookId { get; set; } 12 | 13 | private Notebook notebook; 14 | private ElementReference nameDiv; 15 | private IJSObjectReference module; 16 | private DotNetObjectReference thisReference; 17 | 18 | protected override async Task OnParametersSetAsync() 19 | { 20 | notebook = null; 21 | notebook = await content.GetById(NotebookId); 22 | } 23 | 24 | protected override async Task OnAfterRenderAsync(bool firstRender) 25 | { 26 | if (firstRender) 27 | { 28 | module = await JSRuntime.InvokeAsync( 29 | "import", "./js/notebook-name.js"); 30 | 31 | thisReference = DotNetObjectReference.Create(this); 32 | await module.InvokeAsync("registerListener", nameDiv, thisReference); 33 | } 34 | } 35 | 36 | [JSInvokable] 37 | public async Task RenameNotebook(string newName) 38 | { 39 | notebook.Title = newName; 40 | await content.Save(notebook); 41 | } 42 | 43 | async ValueTask IAsyncDisposable.DisposeAsync() 44 | { 45 | thisReference.Dispose(); 46 | await module.DisposeAsync(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Collections 2 | @using System.Net.Http 3 | @using System.Net.Http.Json 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using blazoract 10 | @using blazoract.Client.Shared 11 | @using blazoract.Client.Pages.Notebook.OutputDisplays 12 | @using blazoract.Shared 13 | @using blazoract.Client.Shared.Toolbar 14 | @using System.Threading 15 | @using blazoract.Client.Data 16 | @using Blazored.LocalStorage 17 | @using System.Threading.Tasks 18 | -------------------------------------------------------------------------------- /Client/blazoract.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Client/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | }, 10 | "BaseAddress": "http://localhost:7071/" 11 | } 12 | -------------------------------------------------------------------------------- /Client/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Client/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body, #app, .page { 8 | font-family: 'Source Sans Pro', sans-serif; 9 | margin: 0; 10 | height: 100%; 11 | } 12 | 13 | #blazor-error-ui { 14 | display: none; 15 | } 16 | 17 | .page { 18 | position: relative; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | body .table-hover tbody tr:hover { 24 | cursor: pointer; 25 | background-color: #fea; 26 | } 27 | -------------------------------------------------------------------------------- /Client/wwwroot/dotnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captainsafia/blazoract/6afab2108fe546fb8c69c03b1b5e0ececd38575e/Client/wwwroot/dotnet.png -------------------------------------------------------------------------------- /Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captainsafia/blazoract/6afab2108fe546fb8c69c03b1b5e0ececd38575e/Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | blazoract 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 |
Loading...
17 | 18 |
19 | An unhandled error has occurred. 20 | Reload 21 | 🗙 22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Client/wwwroot/js/notebook-name.js: -------------------------------------------------------------------------------- 1 | const registerListener = (element, component) => { 2 | element.addEventListener("input", function (event) { 3 | component.invokeMethodAsync('RenameNotebook', element.innerText); 4 | }); 5 | }; 6 | 7 | export { 8 | registerListener 9 | } -------------------------------------------------------------------------------- /Client/wwwroot/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/*", 5 | "serve": "/index.html", 6 | "statusCode": 200 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /Client/wwwroot/wave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captainsafia/blazoract/6afab2108fe546fb8c69c03b1b5e0ececd38575e/Client/wwwroot/wave.png -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5.0.0 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Safia Abdalla 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blazoract 2 | 3 | Blazoract is an interactive notebook user interface implemented in Blazor WebAssembly. It combines some features in Blazor released in .NET 5, such as virtualization and CSS isolation, and includes a kernel backend powered by [.NET Interactive](https://github.com/dotnet/interactive). 4 | 5 | ## Development Setup 6 | 7 | Before starting development, be sure that you have the following installed: 8 | 9 | - [.NET Core](https://dotnet.microsoft.com/download) 10 | - [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) 11 | 12 | 1. Fork and clone this repository locally using Git. 13 | 14 | ``` 15 | $ git clone https://github.com/{yourusername}/blazoract 16 | ``` 17 | 18 | 2. Restore the project's dependencies by running `dotnet restore` in the root. 19 | 20 | 3. Open a terminal and run the following to launch a local instance of the Azure Functions for this app. You will need to the Azure Functions Core Tools mentioned above to enable this. 21 | 22 | ``` 23 | $ cd Api 24 | $ func start --build 25 | ``` 26 | 27 | 4. In another terminal window, run `dotnet run --project Client` to start the client application. 28 | 29 | 5. Navigate to https://localhost:5001 where the contents of the default notebook should load. 30 | 31 | ![image](https://user-images.githubusercontent.com/1857993/98490947-18f87f00-21e8-11eb-889f-db78c79b5a9b.png) 32 | 33 | ## License 34 | 35 | [MIT](https://choosealicense.com/licenses/mit/) 36 | -------------------------------------------------------------------------------- /Shared/Cell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace blazoract.Shared 4 | { 5 | public class Cell 6 | { 7 | public Cell(string notebookId, string content, CellType type = CellType.Code) 8 | { 9 | NotebookId = notebookId; 10 | Content = content; 11 | Type = type; 12 | } 13 | 14 | public string NotebookId { get; set; } 15 | 16 | public string Content { get; set; } 17 | 18 | public CellType Type { get; set; } 19 | } 20 | 21 | public enum CellType 22 | { 23 | Code, 24 | Text, 25 | File, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Shared/ExecuteRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace blazoract.Shared 4 | { 5 | public class ExecuteRequest 6 | { 7 | public ExecuteRequest(string notebookId, string code) 8 | { 9 | NotebookId = notebookId; 10 | Code = code; 11 | } 12 | 13 | public string NotebookId { get; set; } 14 | 15 | public string Code { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Shared/ExecuteResult.cs: -------------------------------------------------------------------------------- 1 | namespace blazoract.Shared 2 | { 3 | public class ExecuteResult 4 | { 5 | // Tell the client which kind of object we're sending, so it can try to JSON-deserialize it as that type 6 | public string OutputType { get; set; } 7 | public string OutputJson { get; set; } 8 | 9 | // In case the client can't deserialize this type (or the server can't serialize it), fall back on a string representation 10 | public string OutputToString { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Shared/GetCompletionsRequest.cs: -------------------------------------------------------------------------------- 1 | namespace blazoract.Shared 2 | { 3 | public class GetCompletionsRequest 4 | { 5 | public string NotebookId { get; set; } 6 | public string Code { get; set; } 7 | public int LineNumber { get; set; } 8 | public int Column { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Shared/Notebook.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace blazoract.Shared 5 | { 6 | public class Notebook 7 | { 8 | public Notebook(string title, string notebookId) 9 | { 10 | Title = title; 11 | NotebookId = notebookId; 12 | Created = DateTime.Now; 13 | Updated = Created; 14 | } 15 | public string Title { get; set; } 16 | 17 | public string NotebookId { get; set; } 18 | 19 | public List Cells { get; set; } 20 | 21 | public DateTime Created { get; set; } 22 | 23 | public DateTime Updated { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Shared/blazoract.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /blazoract.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.0.0 4 | MinimumVisualStudioVersion = 16.0.0.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "blazoract.Client", "Client\blazoract.Client.csproj", "{01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "blazoract.Shared", "Shared\blazoract.Shared.csproj", "{66E0E229-7728-4270-AAF9-659086B73ED4}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Api", "Api\Api.csproj", "{5492C197-0F52-485B-A334-6120D4D8ED03}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Debug|x64 = Debug|x64 15 | Debug|x86 = Debug|x86 16 | Release|Any CPU = Release|Any CPU 17 | Release|x64 = Release|x64 18 | Release|x86 = Release|x86 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Debug|x64.ActiveCfg = Debug|Any CPU 24 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Debug|x64.Build.0 = Debug|Any CPU 25 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Debug|x86.ActiveCfg = Debug|Any CPU 26 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Debug|x86.Build.0 = Debug|Any CPU 27 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Release|x64.ActiveCfg = Release|Any CPU 30 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Release|x64.Build.0 = Release|Any CPU 31 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Release|x86.ActiveCfg = Release|Any CPU 32 | {01966C52-0B55-4B0C-AABE-EC5DE3C11C1B}.Release|x86.Build.0 = Release|Any CPU 33 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Debug|x64.ActiveCfg = Debug|Any CPU 36 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Debug|x64.Build.0 = Debug|Any CPU 37 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Debug|x86.ActiveCfg = Debug|Any CPU 38 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Debug|x86.Build.0 = Debug|Any CPU 39 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Release|x64.ActiveCfg = Release|Any CPU 42 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Release|x64.Build.0 = Release|Any CPU 43 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Release|x86.ActiveCfg = Release|Any CPU 44 | {66E0E229-7728-4270-AAF9-659086B73ED4}.Release|x86.Build.0 = Release|Any CPU 45 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Debug|x64.ActiveCfg = Debug|Any CPU 48 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Debug|x64.Build.0 = Debug|Any CPU 49 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Debug|x86.Build.0 = Debug|Any CPU 51 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Release|x64.ActiveCfg = Release|Any CPU 54 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Release|x64.Build.0 = Release|Any CPU 55 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Release|x86.ActiveCfg = Release|Any CPU 56 | {5492C197-0F52-485B-A334-6120D4D8ED03}.Release|x86.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(ExtensibilityGlobals) = postSolution 62 | SolutionGuid = {73F74FAB-142F-4C17-A4CA-DEC002EC9C99} 63 | EndGlobalSection 64 | EndGlobal 65 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------