moved sources from https://github.com/scale-tone/DurableFunctionsMonitor
|
@ -1,350 +1 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
.vs
|
150
README.md
|
@ -1,14 +1,148 @@
|
|||
# Project
|
||||
![logo](https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/main-page.png)
|
||||
# Durable Functions Monitor
|
||||
|
||||
> This repo has been populated by an initial template to help get you started. Please
|
||||
> make sure to update the content to build a great experience for community-building.
|
||||
A monitoring/debugging UI tool for Azure Durable Functions
|
||||
|
||||
As the maintainer of this project, please make a few updates:
|
||||
[Azure Durable Functions](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview) provide an easy and elegant way of building cloud-native Reliable Stateful Services in the Serverless world. The only thing that's missing so far is a UI for monitoring, managing and debugging your orchestration instances. This project tries to bridge the gap.
|
||||
|
||||
- Improving this README.MD file to provide a great experience
|
||||
- Updating SUPPORT.MD with content about this project's support experience
|
||||
- Understanding the security reporting process in SECURITY.MD
|
||||
- Remove this section from the README
|
||||
[<img alt="Nuget" src="https://img.shields.io/nuget/v/DurableFunctionsMonitor.DotNetBackend?label=current%20version">](https://www.nuget.org/profiles/durablefunctionsmonitor) <img src="https://dev.azure.com/kolepes/DurableFunctionsMonitor/_apis/build/status/DurableFunctionsMonitor-CI-from-yml?branchName=master"/> <img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/scale-tone/DurableFunctionsMonitor?style=flat">
|
||||
|
||||
[<img alt="Visual Studio Marketplace Installs" src="https://img.shields.io/visual-studio-marketplace/i/DurableFunctionsMonitor.DurableFunctionsMonitor?label=VsCode%20Extension%20Installs">](https://marketplace.visualstudio.com/items?itemName=DurableFunctionsMonitor.durablefunctionsmonitor) [<img src="https://img.shields.io/docker/pulls/scaletone/durablefunctionsmonitor"/>](https://hub.docker.com/r/scaletone/durablefunctionsmonitor) [<img alt="Nuget" src="https://img.shields.io/nuget/dt/DurableFunctionsMonitor.DotNetBackend?label=nuget%20downloads">](https://www.nuget.org/profiles/durablefunctionsmonitor)
|
||||
|
||||
# Prerequisites
|
||||
To run this on your devbox you need to have [Azure Functions Core Tools](https://www.npmjs.com/package/azure-functions-core-tools) **globally** installed (which is normally already the case, if you're working with Azure Functions - just ensure that you have the latest version of it).
|
||||
|
||||
**OR**
|
||||
|
||||
[Docker Desktop](https://www.docker.com/products/docker-desktop), if you prefer to run it locally [as a container](https://hub.docker.com/r/scaletone/durablefunctionsmonitor).
|
||||
|
||||
# How to run
|
||||
|
||||
As a [VsCode Extension](https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/durablefunctionsmonitor-vscodeext/README.md#durable-functions-monitor-as-a-vscode-extension).
|
||||
* Install it [from the Marketplace](https://marketplace.visualstudio.com/items?itemName=DurableFunctionsMonitor.durablefunctionsmonitor) or from [a VSIX-file](https://github.com/scale-tone/DurableFunctionsMonitor/releases).
|
||||
* (if you have [Azure Functions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) extension also installed) Goto **Azure Functions** <img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-functions-view-container-icon.png" width="32"> View Container, observe all your TaskHubs under **DURABLE FUNCTIONS** tab and click on them to connect.
|
||||
* (if not) Type `Durable Functions Monitor` in your Command Palette and then confirm or provide Storage Connection String and Hub Name.
|
||||
|
||||
**OR**
|
||||
|
||||
[As a standalone service](https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/durablefunctionsmonitor.dotnetbackend/README.md#durablefunctionsmonitordotnetbackend), either running locally on your devbox or deployed into Azure: [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fscale-tone%2FDurableFunctionsMonitor%2Fmaster%2Fdurablefunctionsmonitor.dotnetbackend%2Farm-template.json)
|
||||
|
||||
**OR**
|
||||
|
||||
[Install it as a NuGet package](https://www.nuget.org/packages/DurableFunctionsMonitor.DotNetBackend) into your own Functions project (.Net Core only).
|
||||
|
||||
# Features
|
||||
## 1. View the list of your Orchestrations and/or Durable Entities, with sorting, infinite scrolling and auto-refresh:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/orchestrations.png" width="882">
|
||||
|
||||
## 2. Filter by time range and column values:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/orchestrations-filtered.png" width="882">
|
||||
|
||||
## 3. Visualize the filtered list of instances as a Time Histogram or as a Gantt chart:
|
||||
<img src="https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/readme/screenshots/time-histogram.png" width="700">
|
||||
|
||||
## 4. Start new orchestration instances:
|
||||
<img width="300px" src="https://user-images.githubusercontent.com/5447190/131139060-eb06ef4d-2cc2-48ff-932c-c227f28f1f36.png"/>
|
||||
<img width="300px" src="https://user-images.githubusercontent.com/5447190/130657962-c1c32575-c82c-4e29-ad88-3951eb821fe8.png"/>
|
||||
<img width="500px" src="https://user-images.githubusercontent.com/5447190/130658737-e51e259d-e7ec-43a2-902b-79907936fb82.png"/>
|
||||
|
||||
## 5. Monitor the status of a certain instance:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/orchestration-details.png" width="882">
|
||||
|
||||
## 6. Quickly navigate to a certain instance by its ID:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/goto-instance.png" width="400">
|
||||
|
||||
## 7. Observe Sequence Diagrams and Gantt Charts for orchestrations:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestration-diagram.png" width="400">
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/gantt-chart.png" width="650">
|
||||
|
||||
## 8. Restart, Purge, Rewind, Terminate, Raise Events, Set Custom Status:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/orchestration-raise-event.png" width="440">
|
||||
|
||||
## 9. Purge Orchestration/Entity instances history:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/purge-history-menu.png" width="390">
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/purge-history-dialog.png" width="683">
|
||||
|
||||
## 10. Clean deleted Durable Entities:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/clean-entity-storage-menu.png" width="390">
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/clean-entity-storage-dialog.png" width="580">
|
||||
|
||||
## 11. Create custom Orchestration/Entity status tabs with [Liquid Templates](https://shopify.github.io/liquid/):
|
||||
1. Create a [Liquid](https://shopify.github.io/liquid/) template file and name it like `[My Custom Tab Name].[orchestration-or-entity-name].liquid` or just `[My Custom Tab Name].liquid` (this one will be applied to any kind of entity).
|
||||
2. In the same Storage Account (the account where your Durable Functions run in) create a Blob container called `durable-functions-monitor`.
|
||||
3. Put your template file into a `tab-templates` virtual folder in that container (the full path should look like `/durable-functions-monitor/tab-templates/[My Custom Tab Name].[orchestration-or-entity-name].liquid`).
|
||||
4. Restart Durable Functions Monitor.
|
||||
5. Observe the newly appeared `My Custom Tab Name` tab on the Orchestration/Entity Details page:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/custom-liquid-tab.png" width="390">
|
||||
|
||||
Sample Liquid Template:
|
||||
```
|
||||
<h2>These people were invited:</h2>
|
||||
<ul>
|
||||
{% for participant in Input.Participants %}
|
||||
<li><h3>{{participant}}<h3></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
```
|
||||
|
||||
You can have multiple templates for each Orchestration/Entity type, and also multiple 'common' (applied to any Orchestration/Entity) templates.
|
||||
Here is [a couple](https://gist.github.com/scale-tone/13956ec804a70f5f66200c6ec97db673) [of more](https://github.com/scale-tone/repka-durable-func/blob/master/Repka%20Status.the-saga-of-repka.liquid) sample templates.
|
||||
|
||||
NOTE1: [this .Net object](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.durableorchestrationstatus?view=azure-dotnet) is passed to your templates as a parameter. Mind the property names and their casing.
|
||||
|
||||
NOTE2: code inside your templates is still subject to these [Content Security Policies](https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/durablefunctionsmonitor.react/public/index.html#L8), so no external scripts, sorry.
|
||||
|
||||
## 12. Connect to different Durable Function Hubs and Azure Storage Accounts:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/manage-connection.png" width="609">
|
||||
|
||||
## 13. Monitor non-default Storage Providers (Netherite, Microsoft SQL, etc.):
|
||||
|
||||
For that you can use Durable Functions Monitor in 'injected' mode, aka added as a [NuGet package](https://www.nuget.org/profiles/durablefunctionsmonitor) to *your* project.
|
||||
|
||||
1. Create a .Net Core Function App project, that is [configured to use an alternative Storage Provider](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-storage-providers#azure-storage) and make sure it compiles and starts.
|
||||
2. Add [DurableFunctionsMonitor.DotNetBackend](https://www.nuget.org/profiles/durablefunctionsmonitor) package to it:
|
||||
```
|
||||
dotnet add package DurableFunctionsMonitor.DotNetBackend
|
||||
```
|
||||
|
||||
4. Add mandatory initialization code, that needs to run at your Function's startup:
|
||||
```
|
||||
[assembly: WebJobsStartup(typeof(StartupNs.Startup))]
|
||||
namespace StartupNs
|
||||
{
|
||||
public class Startup : IWebJobsStartup
|
||||
{
|
||||
public void Configure(IWebJobsBuilder builder)
|
||||
{
|
||||
DfmEndpoint.Setup();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Find more details on programmatic configuration options in the [package readme](https://www.nuget.org/packages/DurableFunctionsMonitor.DotNetBackend/).
|
||||
|
||||
6. Run the project:
|
||||
```
|
||||
func start
|
||||
```
|
||||
|
||||
8. Navigate to `http://localhost:7071/api`.
|
||||
You can customize the endpoint address as needed, as described [here](https://www.nuget.org/packages/DurableFunctionsMonitor.DotNetBackend/).
|
||||
|
||||
## 14. Visualize your Azure Function projects in form of an interactive graph:
|
||||
|
||||
This functionality is powered by [az-func-as-a-graph](https://github.com/scale-tone/az-func-as-a-graph/blob/main/README.md) tool, but now it is also fully integrated into Durable Functions Monitor:
|
||||
![image](https://user-images.githubusercontent.com/5447190/127571400-f83c7f96-55bc-4714-8323-04d26f3be74f.png)
|
||||
|
||||
When running Durable Functions Monitor as [VsCode Extension](https://marketplace.visualstudio.com/items?itemName=DurableFunctionsMonitor.durablefunctionsmonitor), the **Functions Graph** tab should appear automatically, once you have the relevant Functions project opened.
|
||||
|
||||
When running in [standalone/injected mode](https://github.com/scale-tone/DurableFunctionsMonitor/tree/master/durablefunctionsmonitor.dotnetbackend#how-to-run) you'll need to generate and upload an intermediate Functions Map JSON file.
|
||||
1. Generate it with [az-func-as-a-graph CLI](https://github.com/scale-tone/az-func-as-a-graph/blob/main/README.md#how-to-run-as-part-of-azure-devops-build-pipeline). Specify `dfm-func-map.<my-task-hub-name>.json` (will be applied to that particular Task Hub only) or just `dfm-func-map.json` (will be applied to all Task Hubs) as the output name.
|
||||
2. Upload this generated JSON file to `function-maps` virtual folder inside `durable-functions-monitor` BLOB container in the underlying Storage Account (the full path should look like `/durable-functions-monitor/function-maps/dfm-func-map.<my-task-hub-name>.json`).
|
||||
3. Restart Durable Functions Monitor.
|
||||
4. Observe the newly appeared **Functions Graph** tab.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
16
SUPPORT.md
|
@ -1,25 +1,11 @@
|
|||
# TODO: The maintainer of this repo has not yet edited this file
|
||||
|
||||
**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
|
||||
|
||||
- **No CSS support:** Fill out this template with information about how to file issues and get help.
|
||||
- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport).
|
||||
- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide.
|
||||
|
||||
*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
|
||||
|
||||
# Support
|
||||
|
||||
## How to file issues and get help
|
||||
|
||||
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
|
||||
This project uses [GitHub Issues](https://github.com/microsoft/DurableFunctionsMonitor/issues) to track bugs and feature requests. Please search the existing
|
||||
issues before filing new issues to avoid duplicates. For new issues, file your bug or
|
||||
feature request as a new Issue.
|
||||
|
||||
For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
|
||||
FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
|
||||
CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
|
||||
|
||||
## Microsoft Support Policy
|
||||
|
||||
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
pool:
|
||||
name: Azure Pipelines
|
||||
vmImage: 'ubuntu-18.04'
|
||||
demands: npm
|
||||
|
||||
steps:
|
||||
- task: Npm@1
|
||||
displayName: 'npm install durablefunctionsmonitor.react'
|
||||
inputs:
|
||||
workingDir: durablefunctionsmonitor.react
|
||||
verbose: false
|
||||
|
||||
- task: Npm@1
|
||||
displayName: 'npm build durablefunctionsmonitor.react'
|
||||
inputs:
|
||||
command: custom
|
||||
workingDir: durablefunctionsmonitor.react
|
||||
verbose: false
|
||||
customCommand: 'run build'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'copy statics to durablefunctionsmonitor.dotnetbackend/DfmStatics'
|
||||
inputs:
|
||||
SourceFolder: durablefunctionsmonitor.react/build
|
||||
Contents: |
|
||||
static/**
|
||||
index.html
|
||||
favicon.png
|
||||
logo.svg
|
||||
service-worker.js
|
||||
manifest.json
|
||||
TargetFolder: durablefunctionsmonitor.dotnetbackend/DfmStatics
|
||||
CleanTargetFolder: true
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'copy durablefunctionsmonitor.dotnetbackend to ArtifactStagingDirectory'
|
||||
inputs:
|
||||
SourceFolder: durablefunctionsmonitor.dotnetbackend
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/durablefunctionsmonitor.dotnetbackend'
|
||||
OverWrite: true
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'dotnet test tests/durablefunctionsmonitor.dotnetbackend.tests'
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: 'tests/durablefunctionsmonitor.dotnetbackend.tests/*.csproj'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'dotnet publish durablefunctionsmonitor.dotnetbackend'
|
||||
inputs:
|
||||
command: publish
|
||||
publishWebProjects: false
|
||||
projects: durablefunctionsmonitor.dotnetbackend
|
||||
arguments: '--output $(Build.ArtifactStagingDirectory)/output'
|
||||
zipAfterPublish: false
|
||||
modifyOutputPath: false
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'copy dotnetbackend to durablefunctionsmonitor-vscodeext/backend'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.ArtifactStagingDirectory)/output'
|
||||
Contents: |
|
||||
**
|
||||
!logo.svg
|
||||
TargetFolder: 'durablefunctionsmonitor-vscodeext/backend'
|
||||
CleanTargetFolder: true
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'copy custom-backends to durablefunctionsmonitor-vscodeext/custom-backends'
|
||||
inputs:
|
||||
SourceFolder: 'custom-backends'
|
||||
Contents: |
|
||||
**
|
||||
!*.md
|
||||
TargetFolder: 'durablefunctionsmonitor-vscodeext/custom-backends'
|
||||
CleanTargetFolder: true
|
||||
|
||||
- task: Npm@1
|
||||
displayName: 'npm install durablefunctionsmonitor-vscodeext'
|
||||
inputs:
|
||||
workingDir: 'durablefunctionsmonitor-vscodeext'
|
||||
verbose: false
|
||||
|
||||
- task: Npm@1
|
||||
displayName: 'package durablefunctionsmonitor-vscodeext to VSIX-file'
|
||||
inputs:
|
||||
command: custom
|
||||
workingDir: 'durablefunctionsmonitor-vscodeext'
|
||||
verbose: false
|
||||
customCommand: 'run package'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'copy VSIX-file to ArtifactStagingDirectory'
|
||||
inputs:
|
||||
SourceFolder: 'durablefunctionsmonitor-vscodeext'
|
||||
Contents: 'durablefunctionsmonitor*.vsix'
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)'
|
||||
OverWrite: true
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'copy LICENSE to output'
|
||||
inputs:
|
||||
Contents: |
|
||||
LICENSE
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/output'
|
||||
OverWrite: true
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'package dotnetbackend into a Nuget package'
|
||||
inputs:
|
||||
command: 'pack'
|
||||
packagesToPack: '$(Build.ArtifactStagingDirectory)/output/nuspec.nuspec'
|
||||
packDestination: '$(Build.ArtifactStagingDirectory)'
|
||||
versioningScheme: 'off'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: drop'
|
|
@ -0,0 +1,7 @@
|
|||
# Custom backends for Durable Functions Monitor
|
||||
|
||||
These are Azure Function projects with Durable Functions Monitor 'injected' as a [NuGet package](https://www.nuget.org/profiles/durablefunctionsmonitor). To be used for e.g. monitoring [custom storage providers](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-storage-providers).
|
||||
|
||||
* [netcore21](https://github.com/scale-tone/DurableFunctionsMonitor/tree/master/custom-backends/netcore21) - (legacy) Durable Functions Monitor backend, that runs on .Net Core 2.1.
|
||||
* [netcore31](https://github.com/scale-tone/DurableFunctionsMonitor/tree/master/custom-backends/netcore31) - Durable Functions Monitor backend, that runs on .Net Core 3.1.
|
||||
* [mssql](https://github.com/scale-tone/DurableFunctionsMonitor/tree/master/custom-backends/mssql) - Durable Functions Monitor backend to be used with [Durable Task SQL Provider](https://microsoft.github.io/durabletask-mssql/#/).
|
|
@ -0,0 +1,267 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# nuget.exe
|
||||
nuget.exe
|
||||
|
||||
# Azure Functions localsettings file
|
||||
local.settings.json
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="3.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.12" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.6.0" />
|
||||
<PackageReference Include="Microsoft.DurableTask.SqlServer.AzureFunctions" Version="0.10.1-beta" />
|
||||
<PackageReference Include="durablefunctionsmonitor.dotnetbackend" Version="5.1.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="host.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,39 @@
|
|||
# Durable Functions Monitor for MSSQL storage provider
|
||||
|
||||
Custom Durable Functions Monitor backend project to be used with [Durable Task SQL Provider](https://microsoft.github.io/durabletask-mssql/#/).
|
||||
|
||||
## How to run locally
|
||||
|
||||
* Clone this repo.
|
||||
* In the project's folder create a `local.settings.json` file, which should look like this:
|
||||
|
||||
```
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"AzureWebJobsSecretStorageType": "files",
|
||||
"DFM_SQL_CONNECTION_STRING": "your-mssql-connection-string",
|
||||
"DFM_HUB_NAME": "mssql",
|
||||
"DFM_NONCE": "i_sure_know_what_i_am_doing",
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
|
||||
},
|
||||
"Host": {
|
||||
"LocalHttpPort": 7072
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Go to the project's folder with your command prompt and type the following:
|
||||
|
||||
```
|
||||
func start
|
||||
```
|
||||
|
||||
* Navigate to http://localhost:7072
|
||||
|
||||
|
||||
# How to deploy to Azure
|
||||
|
||||
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fscale-tone%2FDurableFunctionsMonitor%2Fmaster%2Fcustom-backends%2Fmssql%2Farm-template.json)
|
||||
|
||||
The above button will deploy *these sources* into *your newly created* Function App instance.
|
|
@ -0,0 +1,144 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DurableFunctionsMonitor.DotNetBackend;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
|
||||
using Microsoft.Azure.WebJobs.Hosting;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
[assembly: WebJobsStartup(typeof(Dfm.MsSql.Startup))]
|
||||
namespace Dfm.MsSql
|
||||
{
|
||||
public class Startup : IWebJobsStartup
|
||||
{
|
||||
public void Configure(IWebJobsBuilder builder)
|
||||
{
|
||||
DfmEndpoint.Setup(null, new DfmExtensionPoints { GetInstanceHistoryRoutine = GetInstanceHistory });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom routine for fetching orchestration history
|
||||
/// </summary>
|
||||
public static IEnumerable<HistoryEvent> GetInstanceHistory(IDurableClient durableClient, string connName, string hubName, string instanceId)
|
||||
{
|
||||
string sql =
|
||||
@"SELECT
|
||||
IIF(h2.TaskID IS NULL, h.Timestamp, h2.Timestamp) as Timestamp,
|
||||
IIF(h2.TaskID IS NULL, h.EventType, h2.EventType) as EventType,
|
||||
h.TaskID as EventId,
|
||||
h.Name as Name,
|
||||
IIF(h2.TaskID IS NULL, NULL, h.Timestamp) as ScheduledTime,
|
||||
p.Text as Result,
|
||||
p.Reason as Details,
|
||||
cih.InstanceID as SubOrchestrationId
|
||||
FROM
|
||||
dt.Instances i
|
||||
INNER JOIN
|
||||
dt.History h
|
||||
ON
|
||||
(i.InstanceID = h.InstanceID AND i.ExecutionID = h.ExecutionID)
|
||||
LEFT JOIN
|
||||
dt.History h2
|
||||
ON
|
||||
(
|
||||
h.EventType IN ('TaskScheduled', 'SubOrchestrationInstanceCreated')
|
||||
AND
|
||||
h2.EventType IN ('SubOrchestrationInstanceCompleted', 'SubOrchestrationInstanceFailed', 'TaskCompleted', 'TaskFailed')
|
||||
AND
|
||||
h.InstanceID = h2.InstanceID AND h.ExecutionID = h2.ExecutionID AND h.TaskID = h2.TaskID AND h.SequenceNumber != h2.SequenceNumber
|
||||
)
|
||||
LEFT JOIN
|
||||
dt.Payloads p
|
||||
ON
|
||||
p.PayloadID = h2.DataPayloadID
|
||||
LEFT JOIN
|
||||
(
|
||||
select
|
||||
cii.ParentInstanceID,
|
||||
cii.InstanceID,
|
||||
chh.TaskID
|
||||
from
|
||||
dt.Instances cii
|
||||
INNER JOIN
|
||||
dt.History chh
|
||||
ON
|
||||
(chh.InstanceID = cii.InstanceID AND chh.EventType = 'ExecutionStarted')
|
||||
) cih
|
||||
ON
|
||||
(cih.ParentInstanceID = h.InstanceID AND cih.TaskID = h.TaskID)
|
||||
WHERE
|
||||
h.EventType IN
|
||||
(
|
||||
'ExecutionStarted', 'ExecutionCompleted', 'ExecutionFailed', 'ExecutionTerminated', 'TaskScheduled', 'SubOrchestrationInstanceCreated',
|
||||
'ContinueAsNew', 'TimerCreated', 'TimerFired', 'EventRaised', 'EventSent'
|
||||
)
|
||||
AND
|
||||
i.InstanceID = @OrchestrationInstanceId
|
||||
|
||||
ORDER BY
|
||||
h.SequenceNumber";
|
||||
|
||||
|
||||
string sqlConnectionString = Environment.GetEnvironmentVariable("DFM_SQL_CONNECTION_STRING");
|
||||
|
||||
using (var conn = new SqlConnection(sqlConnectionString))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
using (var cmd = new SqlCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@OrchestrationInstanceId", instanceId);
|
||||
|
||||
using (SqlDataReader reader = cmd.ExecuteReader())
|
||||
{
|
||||
// Memorizing 'ExecutionStarted' event, to further correlate with 'ExecutionCompleted'
|
||||
DateTimeOffset? executionStartedTimestamp = null;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
var evt = ToHistoryEvent(reader, executionStartedTimestamp);
|
||||
|
||||
if (evt.EventType == "ExecutionStarted")
|
||||
{
|
||||
executionStartedTimestamp = evt.Timestamp;
|
||||
}
|
||||
|
||||
yield return evt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HistoryEvent ToHistoryEvent(SqlDataReader reader, DateTimeOffset? executionStartTime)
|
||||
{
|
||||
var evt = new HistoryEvent
|
||||
{
|
||||
Timestamp = ((DateTime)reader["Timestamp"]).ToUniversalTime(),
|
||||
EventType = reader["EventType"].ToString(),
|
||||
EventId = reader["EventId"] is DBNull ? null : (int?)reader["EventId"],
|
||||
Name = reader["Name"].ToString(),
|
||||
Result = reader["Result"].ToString(),
|
||||
Details = reader["Details"].ToString(),
|
||||
SubOrchestrationId = reader["SubOrchestrationId"].ToString(),
|
||||
};
|
||||
|
||||
var rawScheduledTime = reader["ScheduledTime"];
|
||||
if (!(rawScheduledTime is DBNull))
|
||||
{
|
||||
evt.ScheduledTime = ((DateTime)rawScheduledTime).ToUniversalTime();
|
||||
}
|
||||
else if(evt.EventType == "ExecutionCompleted")
|
||||
{
|
||||
evt.ScheduledTime = executionStartTime?.ToUniversalTime();
|
||||
}
|
||||
|
||||
if (evt.ScheduledTime.HasValue)
|
||||
{
|
||||
evt.DurationInMs = (evt.Timestamp - evt.ScheduledTime.Value).TotalMilliseconds;
|
||||
}
|
||||
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"functionAppName": {
|
||||
"type": "string",
|
||||
"defaultValue": "[concat('dfm-',uniqueString(resourceGroup().id))]",
|
||||
"metadata": {
|
||||
"description": "Name for the Function App, that will host your DFM instance. NOTE: there will be a NEW app created, and it will be different from the one that hosts your Durable Functions."
|
||||
}
|
||||
},
|
||||
"storageConnectionString": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Storage Connection String to the Storage your Durable Functions reside in. Copy it from your Durable Functions App Settings."
|
||||
}
|
||||
},
|
||||
"sqlConnectionString": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Connection String for the database your MSSQL storage provider is using. Copy it from your Durable Functions App Settings."
|
||||
}
|
||||
},
|
||||
"taskHubName": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Task Hub name to be monitored"
|
||||
}
|
||||
},
|
||||
"aadAppClientId": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "In Azure Portal->Azure Active Directory->App Registrations create a new AAD App. Set its 'Redirect URI' setting to 'https://[your-function-app].azurewebsites.net/.auth/login/aad/callback'. Then on 'Authentication' page enable ID Tokens. Then copy that AAD App's ClientId into here."
|
||||
}
|
||||
},
|
||||
"aadAppTenantId": {
|
||||
"type": "string",
|
||||
"defaultValue": "[subscription().tenantId]",
|
||||
"metadata": {
|
||||
"description": "Put your AAD TenantId here (you can find it on Azure Portal->Azure Active Directory page), or leave as default to use the current subscription's TenantId."
|
||||
}
|
||||
},
|
||||
"allowedUserNames": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Comma-separated list of users (emails), that will be allowed to access this DFM instance. Specify at least yourself here."
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"apiVersion": "2016-09-01",
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"sku": {
|
||||
"name": "Y1",
|
||||
"tier": "Dynamic"
|
||||
},
|
||||
"properties": {
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"computeMode": "Dynamic"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"apiVersion": "2018-11-01",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"kind": "functionapp",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]"
|
||||
],
|
||||
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "2015-08-01",
|
||||
"name": "web",
|
||||
"type": "sourcecontrols",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"RepoUrl": "https://github.com/scale-tone/DurableFunctionsMonitor",
|
||||
"branch": "master",
|
||||
"IsManualIntegration": true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "[concat(parameters('functionAppName'), '/authsettings')]",
|
||||
"apiVersion": "2018-11-01",
|
||||
"type": "Microsoft.Web/sites/config",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": true,
|
||||
"unauthenticatedClientAction": "RedirectToLoginPage",
|
||||
"tokenStoreEnabled": true,
|
||||
"defaultProvider": "AzureActiveDirectory",
|
||||
"clientId": "[parameters('aadAppClientId')]",
|
||||
"issuer": "[concat('https://login.microsoftonline.com/', parameters('aadAppTenantId'), '/v2.0')]"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"properties": {
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]",
|
||||
"siteConfig": {
|
||||
"appSettings": [
|
||||
{
|
||||
"name": "DFM_SQL_CONNECTION_STRING",
|
||||
"value": "[parameters('sqlConnectionString')]"
|
||||
},
|
||||
{
|
||||
"name": "DFM_HUB_NAME",
|
||||
"value": "[parameters('taskHubName')]"
|
||||
},
|
||||
{
|
||||
"name": "DFM_ALLOWED_USER_NAMES",
|
||||
"value": "[parameters('allowedUserNames')]"
|
||||
},
|
||||
{
|
||||
"name": "AzureWebJobsStorage",
|
||||
"value": "[parameters('storageConnectionString')]"
|
||||
},
|
||||
{
|
||||
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
|
||||
"value": "[parameters('storageConnectionString')]"
|
||||
},
|
||||
{
|
||||
"name": "WEBSITE_CONTENTSHARE",
|
||||
"value": "[toLower(parameters('functionAppName'))]"
|
||||
},
|
||||
{
|
||||
"name": "FUNCTIONS_EXTENSION_VERSION",
|
||||
"value": "~3"
|
||||
},
|
||||
{
|
||||
"name": "FUNCTIONS_WORKER_RUNTIME",
|
||||
"value": "dotnet"
|
||||
},
|
||||
{
|
||||
"name": "Project",
|
||||
"value": "custom-backends/mssql"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"extensions": {
|
||||
"http": {
|
||||
"routePrefix": ""
|
||||
},
|
||||
|
||||
"durableTask": {
|
||||
"hubName": "%DFM_HUB_NAME%",
|
||||
"extendedSessionsEnabled": "true",
|
||||
"UseGracefulShutdown": "true",
|
||||
"storageProvider": {
|
||||
"type": "mssql",
|
||||
"connectionStringName": "DFM_SQL_CONNECTION_STRING",
|
||||
"taskEventLockTimeout": "00:02:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# nuget.exe
|
||||
nuget.exe
|
||||
|
||||
# Azure Functions localsettings file
|
||||
local.settings.json
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.38" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.5.1" />
|
||||
<PackageReference Include="durablefunctionsmonitor.dotnetbackend" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="host.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,9 @@
|
|||
# Durable Functions Monitor on .Net Core 2.1
|
||||
|
||||
Custom Durable Functions Monitor backend project, configured to run on .Net Core 2.1.
|
||||
|
||||
# How to deploy to Azure
|
||||
|
||||
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fscale-tone%2FDurableFunctionsMonitor%2Fmaster%2Fcustom-backends%2Fnetcore21%2Farm-template.json)
|
||||
|
||||
The above button will deploy *these sources* into *your newly created* Function App instance.
|
|
@ -0,0 +1,15 @@
|
|||
using DurableFunctionsMonitor.DotNetBackend;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Hosting;
|
||||
|
||||
[assembly: WebJobsStartup(typeof(Dfm.NetCore21.Startup))]
|
||||
namespace Dfm.NetCore21
|
||||
{
|
||||
public class Startup : IWebJobsStartup
|
||||
{
|
||||
public void Configure(IWebJobsBuilder builder)
|
||||
{
|
||||
DfmEndpoint.Setup();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"functionAppName": {
|
||||
"type": "string",
|
||||
"defaultValue": "[concat('dfm-',uniqueString(resourceGroup().id))]",
|
||||
"metadata": {
|
||||
"description": "Name for the Function App, that will host your DFM instance. NOTE: there will be a NEW app created, and it will be different from the one that hosts your Durable Functions."
|
||||
}
|
||||
},
|
||||
"storageConnectionString": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Storage Connection String to the Storage your Durable Functions reside in. Copy it from your Durable Functions App Settings."
|
||||
}
|
||||
},
|
||||
"taskHubName": {
|
||||
"type": "string",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "(optional) Comma-separated list of Task Hub names to be monitored. WARNING: if not set, this instance will expose ALL Task Hubs in your Storage account!"
|
||||
}
|
||||
},
|
||||
"aadAppClientId": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "In Azure Portal->Azure Active Directory->App Registrations create a new AAD App. Set its 'Redirect URI' setting to 'https://[your-function-app].azurewebsites.net/.auth/login/aad/callback'. Then on 'Authentication' page enable ID Tokens. Then copy that AAD App's ClientId into here."
|
||||
}
|
||||
},
|
||||
"aadAppTenantId": {
|
||||
"type": "string",
|
||||
"defaultValue": "[subscription().tenantId]",
|
||||
"metadata": {
|
||||
"description": "Put your AAD TenantId here (you can find it on Azure Portal->Azure Active Directory page), or leave as default to use the current subscription's TenantId."
|
||||
}
|
||||
},
|
||||
"allowedUserNames": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Comma-separated list of users (emails), that will be allowed to access this DFM instance. Specify at least yourself here."
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"apiVersion": "2016-09-01",
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"sku": {
|
||||
"name": "Y1",
|
||||
"tier": "Dynamic"
|
||||
},
|
||||
"properties": {
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"computeMode": "Dynamic"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"apiVersion": "2018-11-01",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"kind": "functionapp",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]"
|
||||
],
|
||||
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "2015-08-01",
|
||||
"name": "web",
|
||||
"type": "sourcecontrols",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"RepoUrl": "https://github.com/scale-tone/DurableFunctionsMonitor",
|
||||
"branch": "master",
|
||||
"IsManualIntegration": true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "[concat(parameters('functionAppName'), '/authsettings')]",
|
||||
"apiVersion": "2018-11-01",
|
||||
"type": "Microsoft.Web/sites/config",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": true,
|
||||
"unauthenticatedClientAction": "RedirectToLoginPage",
|
||||
"tokenStoreEnabled": true,
|
||||
"defaultProvider": "AzureActiveDirectory",
|
||||
"clientId": "[parameters('aadAppClientId')]",
|
||||
"issuer": "[concat('https://login.microsoftonline.com/', parameters('aadAppTenantId'), '/v2.0')]"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"properties": {
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]",
|
||||
"siteConfig": {
|
||||
"appSettings": [
|
||||
{
|
||||
"name": "DFM_HUB_NAME",
|
||||
"value": "[parameters('taskHubName')]"
|
||||
},
|
||||
{
|
||||
"name": "DFM_ALLOWED_USER_NAMES",
|
||||
"value": "[parameters('allowedUserNames')]"
|
||||
},
|
||||
{
|
||||
"name": "AzureWebJobsStorage",
|
||||
"value": "[parameters('storageConnectionString')]"
|
||||
},
|
||||
{
|
||||
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
|
||||
"value": "[parameters('storageConnectionString')]"
|
||||
},
|
||||
{
|
||||
"name": "WEBSITE_CONTENTSHARE",
|
||||
"value": "[toLower(parameters('functionAppName'))]"
|
||||
},
|
||||
{
|
||||
"name": "FUNCTIONS_EXTENSION_VERSION",
|
||||
"value": "~2"
|
||||
},
|
||||
{
|
||||
"name": "FUNCTIONS_WORKER_RUNTIME",
|
||||
"value": "dotnet"
|
||||
},
|
||||
{
|
||||
"name": "Project",
|
||||
"value": "custom-backends/netcore21"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"extensions": {
|
||||
"http": {
|
||||
"routePrefix": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# nuget.exe
|
||||
nuget.exe
|
||||
|
||||
# Azure Functions localsettings file
|
||||
local.settings.json
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.12" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.5.0" />
|
||||
<PackageReference Include="durablefunctionsmonitor.dotnetbackend" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="host.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,9 @@
|
|||
# Durable Functions Monitor on .Net Core 3.1
|
||||
|
||||
Custom Durable Functions Monitor backend project, configured to run on .Net Core 3.1.
|
||||
|
||||
# How to deploy to Azure
|
||||
|
||||
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fscale-tone%2FDurableFunctionsMonitor%2Fmaster%2Fcustom-backends%2Fnetcore31%2Farm-template.json)
|
||||
|
||||
The above button will deploy *these sources* into *your newly created* Function App instance.
|
|
@ -0,0 +1,15 @@
|
|||
using DurableFunctionsMonitor.DotNetBackend;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Hosting;
|
||||
|
||||
[assembly: WebJobsStartup(typeof(Dfm.NetCore31.Startup))]
|
||||
namespace Dfm.NetCore31
|
||||
{
|
||||
public class Startup : IWebJobsStartup
|
||||
{
|
||||
public void Configure(IWebJobsBuilder builder)
|
||||
{
|
||||
DfmEndpoint.Setup();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"functionAppName": {
|
||||
"type": "string",
|
||||
"defaultValue": "[concat('dfm-',uniqueString(resourceGroup().id))]",
|
||||
"metadata": {
|
||||
"description": "Name for the Function App, that will host your DFM instance. NOTE: there will be a NEW app created, and it will be different from the one that hosts your Durable Functions."
|
||||
}
|
||||
},
|
||||
"storageConnectionString": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Storage Connection String to the Storage your Durable Functions reside in. Copy it from your Durable Functions App Settings."
|
||||
}
|
||||
},
|
||||
"taskHubName": {
|
||||
"type": "string",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "(optional) Comma-separated list of Task Hub names to be monitored. WARNING: if not set, this instance will expose ALL Task Hubs in your Storage account!"
|
||||
}
|
||||
},
|
||||
"aadAppClientId": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "In Azure Portal->Azure Active Directory->App Registrations create a new AAD App. Set its 'Redirect URI' setting to 'https://[your-function-app].azurewebsites.net/.auth/login/aad/callback'. Then on 'Authentication' page enable ID Tokens. Then copy that AAD App's ClientId into here."
|
||||
}
|
||||
},
|
||||
"aadAppTenantId": {
|
||||
"type": "string",
|
||||
"defaultValue": "[subscription().tenantId]",
|
||||
"metadata": {
|
||||
"description": "Put your AAD TenantId here (you can find it on Azure Portal->Azure Active Directory page), or leave as default to use the current subscription's TenantId."
|
||||
}
|
||||
},
|
||||
"allowedUserNames": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Comma-separated list of users (emails), that will be allowed to access this DFM instance. Specify at least yourself here."
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"apiVersion": "2016-09-01",
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"sku": {
|
||||
"name": "Y1",
|
||||
"tier": "Dynamic"
|
||||
},
|
||||
"properties": {
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"computeMode": "Dynamic"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"apiVersion": "2018-11-01",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"name": "[parameters('functionAppName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"kind": "functionapp",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]"
|
||||
],
|
||||
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "2015-08-01",
|
||||
"name": "web",
|
||||
"type": "sourcecontrols",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"RepoUrl": "https://github.com/scale-tone/DurableFunctionsMonitor",
|
||||
"branch": "master",
|
||||
"IsManualIntegration": true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "[concat(parameters('functionAppName'), '/authsettings')]",
|
||||
"apiVersion": "2018-11-01",
|
||||
"type": "Microsoft.Web/sites/config",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": true,
|
||||
"unauthenticatedClientAction": "RedirectToLoginPage",
|
||||
"tokenStoreEnabled": true,
|
||||
"defaultProvider": "AzureActiveDirectory",
|
||||
"clientId": "[parameters('aadAppClientId')]",
|
||||
"issuer": "[concat('https://login.microsoftonline.com/', parameters('aadAppTenantId'), '/v2.0')]"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"properties": {
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]",
|
||||
"siteConfig": {
|
||||
"appSettings": [
|
||||
{
|
||||
"name": "DFM_HUB_NAME",
|
||||
"value": "[parameters('taskHubName')]"
|
||||
},
|
||||
{
|
||||
"name": "DFM_ALLOWED_USER_NAMES",
|
||||
"value": "[parameters('allowedUserNames')]"
|
||||
},
|
||||
{
|
||||
"name": "AzureWebJobsStorage",
|
||||
"value": "[parameters('storageConnectionString')]"
|
||||
},
|
||||
{
|
||||
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
|
||||
"value": "[parameters('storageConnectionString')]"
|
||||
},
|
||||
{
|
||||
"name": "WEBSITE_CONTENTSHARE",
|
||||
"value": "[toLower(parameters('functionAppName'))]"
|
||||
},
|
||||
{
|
||||
"name": "FUNCTIONS_EXTENSION_VERSION",
|
||||
"value": "~3"
|
||||
},
|
||||
{
|
||||
"name": "FUNCTIONS_WORKER_RUNTIME",
|
||||
"value": "dotnet"
|
||||
},
|
||||
{
|
||||
"name": "Project",
|
||||
"value": "custom-backends/netcore31"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"extensions": {
|
||||
"http": {
|
||||
"routePrefix": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
# build output
|
||||
*.vsix
|
||||
|
||||
# DurableFunctionsMonitor.Functions build output
|
||||
backend
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TypeScript output
|
||||
dist
|
||||
out
|
||||
|
||||
# Azure Functions artifacts
|
||||
bin
|
||||
obj
|
||||
appsettings.json
|
||||
local.settings.json
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: watch"
|
||||
},
|
||||
{
|
||||
"name": "Extension Tests",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/test/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: watch"
|
||||
},
|
||||
{
|
||||
"name": "Local Process with Kubernetes (Preview)",
|
||||
"type": "dev-spaces-connect-configuration",
|
||||
"request": "launch"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
"out": false // set this to true to hide the "out" folder with the compiled JS files
|
||||
},
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/test/**
|
||||
src/**
|
||||
.gitignore
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/tslint.json
|
||||
**/*.map
|
||||
**/*.ts
|
|
@ -0,0 +1,199 @@
|
|||
# Change Log
|
||||
|
||||
## Version 5.1.0
|
||||
|
||||
- Instance execution history can now be filtered by time and other field values:
|
||||
![image](https://user-images.githubusercontent.com/5447190/140803804-84ef440b-bce7-432d-aaf9-4b663f2ef5cd.png)
|
||||
|
||||
- 'In' and 'Not In' filter operators. Filter values should be comma-separated or in form of a JSON array.
|
||||
- Backend migrated to .Net Core 3.1.
|
||||
- Direct requests that DfMon makes against Azure Table Storage now contain custom **User-Agent** header: `DurableFunctionsMonitor-Standalone`, `DurableFunctionsMonitor-VsCodeExt` or `DurableFunctionsMonitor-Injected`. Note that the majority of calls is still done via DurableClient, and those cannot be instrumented like this yet.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 5.0.0
|
||||
|
||||
- UI improvements for instance filter and in some other places.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.8.2
|
||||
|
||||
- Minor hotfix (DfMon's View Container might become unresponsive after a debug session).
|
||||
|
||||
## Version 4.8.1
|
||||
|
||||
- Workaround for https://github.com/Azure/azure-functions-durable-extension/issues/1926 (being unable to execute .Reset() and .StartNew() against a Task Hub named 'TestHubName').
|
||||
|
||||
## Version 4.8
|
||||
|
||||
- 'Start New Orchestration Instance' feature:
|
||||
<img width="200px" src="https://user-images.githubusercontent.com/5447190/130657962-c1c32575-c82c-4e29-ad88-3951eb821fe8.png"/>
|
||||
<img width="400px" src="https://user-images.githubusercontent.com/5447190/130658737-e51e259d-e7ec-43a2-902b-79907936fb82.png"/>
|
||||
|
||||
- Should now work seamlessly in [GitHub Codespaces](https://github.com/features/codespaces).
|
||||
- Full support for [Microsoft SQL storage provider](https://github.com/microsoft/durabletask-mssql).
|
||||
- Latest [az-func-as-a-graph](https://github.com/scale-tone/az-func-as-a-graph) integrated.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.7.1
|
||||
|
||||
- Hotfix for incompatibility with Storage Emulator ([#112](https://github.com/scale-tone/DurableFunctionsMonitor/issues/112)).
|
||||
|
||||
## Version 4.7
|
||||
|
||||
- Latest [az-func-as-a-graph](https://github.com/scale-tone/az-func-as-a-graph) integrated, and it is now used as yet another visualization tab for both search results and instance details, with instance counts and statuses rendered on top of it. So it now acts as an *animated* code map of your project:
|
||||
![image](https://user-images.githubusercontent.com/5447190/127571400-f83c7f96-55bc-4714-8323-04d26f3be74f.png)
|
||||
|
||||
- 'Open XXXInstances/XXXHistory in Storage Explorer' menu items for Task Hubs:
|
||||
<img src="https://user-images.githubusercontent.com/5447190/127571803-4502d249-9963-4f70-9c4e-8aa1397bf06e.png" width="300">
|
||||
|
||||
- Long JSON (or just long error messages) can now be viewed in a popup window ([#109](https://github.com/scale-tone/DurableFunctionsMonitor/issues/109)).
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.6
|
||||
|
||||
- Added a sortable **Duration** column to the list of results. Now you can quickly find quickest and longest instances.
|
||||
- Gantt charts are now interactive (lines are clickable).
|
||||
- Custom backends: you can now switch to a .Net Core 3.1 backend, or even to your own customized one:
|
||||
|
||||
![image](https://user-images.githubusercontent.com/5447190/123545702-c3aeb500-d759-11eb-9d6d-7c69db167ca2.png)
|
||||
|
||||
- (Limited) support for [Microsoft SQL storage provider](https://github.com/microsoft/durabletask-mssql). When you open a project that uses it, the relevant Task Hub should appear in the **DURABLE FUNCTIONS** view container:
|
||||
|
||||
![image](https://user-images.githubusercontent.com/5447190/123545989-281e4400-d75b-11eb-865e-b8aa3cee690a.png)
|
||||
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.5
|
||||
|
||||
- Time can now be shown in local time zone. **File->Preferences->Settings->Durable Functions Monitor->Show Time As**.
|
||||
- F# support for Functions Graphs.
|
||||
- Instance Details tab is now integrated with Functions Graph. If relevant Functions project is currently open, the Details tab will allow navigating to Functions Graph and to Orchestration/Entity/Activity source code.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.4
|
||||
|
||||
- Now you can get a quick overview of _any_ Azure Functions project in form of a graph. **Command Palette -> Visualize Functions as a Graph...**. For Durable Functions/Durable Entities the tool also tries to infer and show their relationships. Function nodes are clickable and lead to function's code.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.3
|
||||
|
||||
- Fixed time ranges ('Last Minute', 'Last Hour' etc.).
|
||||
- Multiple choice for filtering by instance status ('Running', 'Completed' etc.).
|
||||
- 'Not Equals', 'Not Starts With' and 'Not Contains' filter operators.
|
||||
- Performance improvements.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.2
|
||||
|
||||
- Orchestrations/Entities are now also visualized as a time histogram and as a Gantt chart. Time histogram is interactive, you can zoom it in/out with your mouse.
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/time-histogram.png" width="400">
|
||||
|
||||
- 'Send Signal' button for Durable Entities.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.1
|
||||
|
||||
- Dark color mode.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 4.0
|
||||
|
||||
- It is now one backend per Storage Account, not per each Task Hub. Works faster and consumes less resources.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 3.9
|
||||
|
||||
- Gantt Charts for orchestrations (in addition to Sequence Diagrams).
|
||||
- 'Go to instanceId...' feature to quickly navigate to an orchestration/entity instance by its id (with autocomplete supported). **Right-click on a Task Hub->Go to instanceId...**.
|
||||
- DotLiquid replaced with [Fluid](https://github.com/sebastienros/fluid) for rendering custom status tabs. [Fluid](https://github.com/sebastienros/fluid) looks much more mature (most of [Liquid](https://shopify.github.io/liquid/) seems to be supported) and more alive library.
|
||||
- 'Save as .SVG' button for diagrams.
|
||||
- Status tabs now refresh much smoother.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 3.8
|
||||
|
||||
- WebViews are now persistent (do not reload every time you switch between them) and even persist their state (filters, sorting etc.) across restarts.
|
||||
- 'Restart' button for orchestrations (triggers the new [.RestartAsync()](https://github.com/Azure/azure-functions-durable-extension/pull/1545) method).
|
||||
- Sequence diagrams now show some timing (start times and durations).
|
||||
- 'Detach from all Task Hubs...' button for quickly killing all backends.
|
||||
- All logs (when enabled) now go to 'Durable Functions Monitor' output channel.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 3.7
|
||||
|
||||
- Now settings are stored in VsCode's settings.json. **File->Preferences->Settings->Durable Functions Monitor**:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-settings.png" width="400">
|
||||
|
||||
- Local Storage Emulator, Azure Government and other exotic Storage Account types are now supported. If your Local Storage Emulator is running and there're some TaskHubs in it - they will appear automatically on your Azure Functions View Container (if not, try to modify the 'Storage Emulator Connection String' parameter on the Settings page).
|
||||
|
||||
- Long-awaited 'Cancel' button on the Orchestrations page.
|
||||
|
||||
- Now you can hide the columns you're not interested in:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/hide-columns.png" width="350">
|
||||
|
||||
- Minor other UI improvements.
|
||||
|
||||
## Version 3.6
|
||||
|
||||
- 'Clear Entity Storage...' menu item for doing garbage collection of deleted Durable Entities. Executes the recently added [IDurableEntityClient.CleanEntityStorageAsync()](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.idurableentityclient.cleanentitystorageasync?view=azure-dotnet) method.
|
||||
|
||||
- Custom status visualisation for orchestrations/entities in form of [Liquid templates](https://shopify.github.io/liquid/).
|
||||
1. Create a [DotLiquid](https://github.com/dotliquid/dotliquid) template file.
|
||||
2. Name it like `[My Custom Tab Name].[orchestration-or-entity-name].liquid` or just `[My Custom Tab Name].liquid` (this one will be applied to any kind of entity).
|
||||
3. In the same Storage Account create a container called `durable-functions-monitor`.
|
||||
4. Put your template file into a `tab-templates` virtual folder in that container (the full path should look like `/durable-functions-monitor/tab-templates/[My Custom Tab Name].[orchestration-or-entity-name].liquid`).
|
||||
5. Restart Durable Functions Monitor.
|
||||
6. Observe the newly appeared `My Custom Tab Name` tab on the Orchestration/Entity Details page.
|
||||
|
||||
- Performance improvements for loading the list of Orchestrations/Entities.
|
||||
|
||||
## Version 3.5
|
||||
|
||||
- Now the **Orchestration Details** page features a nice [mermaid](https://www.npmjs.com/package/mermaid)-based sequence diagram:
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestration-diagram-small.png">
|
||||
- Also it's now possible to navigate to suborchestrations from the history list on the **Orchestration Details** page.
|
||||
|
||||
## Version 3.4
|
||||
|
||||
- Now integrated with [Azure Account](https://marketplace.visualstudio.com/items?itemName=ms-vscode.azure-account) extension, so once logged in to Azure, you can now see and connect to all your TaskHubs. It is also still possible to connect with connection strings, as before. NOTE1: only filtered Azure Subscriptions are shown, so make sure your filter is set correctly with [Azure: Select Subscriptions](https://docs.microsoft.com/en-us/azure/governance/policy/how-to/extension-for-vscode#select-subscriptions) command. NOTE2: many things can go wrong when fetching the list of TaskHubs, so to investigate those problems you can [enable logging](https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/durablefunctionsmonitor-vscodeext/CHANGELOG.md#version-21) and then check the 'Durable Functions Monitor' output channel.
|
||||
|
||||
## Version 3.3
|
||||
|
||||
- customStatus value of your orchestration instances can now be changed with 'Set Custom Status' button.
|
||||
- Minor bugfixes.
|
||||
|
||||
## Version 3.2
|
||||
|
||||
- You can now delete unused Task Hubs with 'Delete Task Hub...' context menu item.
|
||||
- Better (non-native) DateTime pickers.
|
||||
|
||||
## Version 3.1
|
||||
|
||||
- Minor security improvements.
|
||||
- List of existing Task Hubs is now loaded from your Storage Account and shown to you, when connecting to a Task Hub.
|
||||
|
||||
## Version 3.0
|
||||
|
||||
- A 'DURABLE FUNCTIONS' TreeView added to Azure Functions View Container. It displays all currently attached Task Hubs, allows to connect to multiple Task Hubs and switch between them. You need to have [Azure Functions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) extension installed to see it (which is typically the case if you work with Azure Functions in VSCode).
|
||||
|
||||
## Version 2.2
|
||||
|
||||
- Bulk purge for Durable Entities as well.
|
||||
- Prettified JSON on instance details page.
|
||||
|
||||
## Version 2.1
|
||||
|
||||
- Instances list sort order is now persisted as well.
|
||||
- Whenever backend initialization fails, its error message is now being shown immediately (instead of a generic 'timeout' message as before).
|
||||
- A complete backend output can now be logged into a file for debugging purposes. Open the **settings.json** file in extension's folder and set the **logging** setting to **true**. That will produce a **backend/backend-37072.log** file with full console output from func.exe.
|
||||
|
||||
## Version 2.0
|
||||
|
||||
- More native support for Durable Entities.
|
||||
- Backend migrated to Microsoft.Azure.WebJobs.Extensions.DurableTask 2.0.0. Please, ensure you have the latest Azure Functions Core Tools installed globally, otherwise the backend might fail to start.
|
||||
- Now displaying connection info (storage account name/hub name) in the tab title.
|
||||
|
||||
## Version 1.3
|
||||
|
||||
- Implemented purging orchestration instance history. Type 'Purge Durable Functions History...' in your Command Palette.
|
||||
- Added a context menu over a **host.json** file.
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 scale-tone
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
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.
|
|
@ -0,0 +1,37 @@
|
|||
# Durable Functions Monitor as a VsCode Extension
|
||||
|
||||
List/monitor/debug your Azure Durable Functions inside VsCode.
|
||||
|
||||
**Command Palette -> Durable Functions Monitor**, or (if you have [Azure Functions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) extension also installed) **Azure Functions View Container -> DURABLE FUNCTIONS**, or right-click on your **host.json** file and use the context menu.
|
||||
|
||||
## Features
|
||||
|
||||
* Get a bird's eye view of any Azure Functions project in form of a graph - **Command Palette -> Visualize Functions as a Graph...**.
|
||||
* List your Orchestrations and/or Durable Entities, with sorting, infinite scrolling and auto-refresh.
|
||||
* Monitor the status of a certain Orchestration/Durable Entity. Restart, Purge, Rewind, Terminate, Raise Events.
|
||||
* Start new orchestration instances - **Azure Functions View Container -> DURABLE FUNCTIONS -> [right-click on your TaskHub] -> Start New Orchestration Instance...**
|
||||
* Quickly navigate to an Orchestration/Entity instance by its ID - **Command Palette -> Durable Functions Monitor: Go to instanceId...** or **Azure Functions View Container -> DURABLE FUNCTIONS -> [right-click on your TaskHub] -> Go to instanceId...**
|
||||
* Purge Orchestrations/Durable Entities history - **Command Palette -> Durable Functions Monitor: Purge History...**
|
||||
* Cleanup deleted Durable Entities - **Command Palette -> Durable Functions Monitor: Clean Entity Storage...**
|
||||
* Observe all Task Hubs in your Azure Subscription and connect to them - **Azure Functions View Container -> DURABLE FUNCTIONS**
|
||||
* Delete Task Hubs - **Command Palette -> Delete Task Hub...**
|
||||
|
||||
## Pictures
|
||||
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-command-palette.png" width="624">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestrations.png" width="768">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestration.png" width="843">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/gantt-chart.png" width="843">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestration-diagram.png" width="600">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/function-graph.png" width="800">
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have the latest [Azure Functions Core Tools](https://www.npmjs.com/package/azure-functions-core-tools) globally installed on your devbox.
|
||||
|
||||
More info and sources on [the github repo](https://github.com/scale-tone/DurableFunctionsMonitor#features).
|
После Ширина: | Высота: | Размер: 6.0 KiB |
|
@ -0,0 +1,324 @@
|
|||
{
|
||||
"name": "durablefunctionsmonitor",
|
||||
"displayName": "Durable Functions Monitor",
|
||||
"description": "Monitoring/debugging UI tool for Azure Durable Functions. View->Command Palette...->Durable Functions Monitor",
|
||||
"version": "5.1.0",
|
||||
"engines": {
|
||||
"vscode": "^1.39.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other",
|
||||
"Debuggers"
|
||||
],
|
||||
"homepage": "https://github.com/scale-tone/DurableFunctionsMonitor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/scale-tone/DurableFunctionsMonitor"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/scale-tone/DurableFunctionsMonitor/issues"
|
||||
},
|
||||
"icon": "logo.png",
|
||||
"keywords": [
|
||||
"Azure Durable Functions",
|
||||
"Azure Durable Entities",
|
||||
"Azure Functions",
|
||||
"Serverless",
|
||||
"Azure"
|
||||
],
|
||||
"publisher": "DurableFunctionsMonitor",
|
||||
"license": "MIT",
|
||||
"activationEvents": [
|
||||
"onView:durableFunctionsMonitorTreeView",
|
||||
"onCommand:extension.durableFunctionsMonitor",
|
||||
"onCommand:extension.durableFunctionsMonitorPurgeHistory",
|
||||
"onCommand:extension.durableFunctionsMonitorCleanEntityStorage",
|
||||
"onCommand:durableFunctionsMonitorTreeView.attachToAnotherTaskHub",
|
||||
"onCommand:extension.durableFunctionsMonitorGotoInstanceId",
|
||||
"onCommand:extension.durableFunctionsMonitorVisualizeAsGraph",
|
||||
"onCommand:durableFunctionsMonitorTreeView.startNewInstance",
|
||||
"onDebug"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"views": {
|
||||
"azure": [
|
||||
{
|
||||
"id": "durableFunctionsMonitorTreeView",
|
||||
"name": "Durable Functions"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitor",
|
||||
"title": "Durable Functions Monitor"
|
||||
},
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitorPurgeHistory",
|
||||
"title": "Durable Functions Monitor: Purge History..."
|
||||
},
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitorCleanEntityStorage",
|
||||
"title": "Durable Functions Monitor: Clean Entity Storage..."
|
||||
},
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitorGotoInstanceId",
|
||||
"title": "Durable Functions Monitor: Go to instanceId..."
|
||||
},
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitorVisualizeAsGraph",
|
||||
"title": "Visualize Functions as a Graph..."
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.attachToTaskHub",
|
||||
"title": "Attach"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.detachFromTaskHub",
|
||||
"title": "Detach"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.openInstancesInStorageExplorer",
|
||||
"title": "Open *Instances table in Storage Explorer"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.openHistoryInStorageExplorer",
|
||||
"title": "Open *History table in Storage Explorer"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.deleteTaskHub",
|
||||
"title": "Delete Task Hub..."
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.refresh",
|
||||
"title": "Refresh",
|
||||
"icon": {
|
||||
"light": "resources/light/refresh.svg",
|
||||
"dark": "resources/dark/refresh.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.attachToAnotherTaskHub",
|
||||
"title": "Attach to Task Hub...",
|
||||
"icon": {
|
||||
"light": "resources/light/plug.svg",
|
||||
"dark": "resources/dark/plug.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.detachFromAllTaskHubs",
|
||||
"title": "Detach from all Task Hubs...",
|
||||
"icon": {
|
||||
"light": "resources/light/unplug.svg",
|
||||
"dark": "resources/dark/unplug.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.purgeHistory",
|
||||
"title": "Purge History..."
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.cleanEntityStorage",
|
||||
"title": "Clean Entity Storage..."
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.gotoInstanceId",
|
||||
"title": "Go to instanceId..."
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.startNewInstance",
|
||||
"title": "Start New Orchestration Instance..."
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"explorer/context": [
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitor",
|
||||
"when": "resourceFilename == host.json",
|
||||
"group": "DurableFunctionMonitorGroup@1"
|
||||
},
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitorPurgeHistory",
|
||||
"when": "resourceFilename == host.json",
|
||||
"group": "DurableFunctionMonitorGroup@2"
|
||||
},
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitorCleanEntityStorage",
|
||||
"when": "resourceFilename == host.json",
|
||||
"group": "DurableFunctionMonitorGroup@3"
|
||||
},
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitorGotoInstanceId",
|
||||
"when": "resourceFilename == host.json",
|
||||
"group": "DurableFunctionMonitorGroup@4"
|
||||
},
|
||||
{
|
||||
"command": "extension.durableFunctionsMonitorVisualizeAsGraph",
|
||||
"when": "resourceFilename == host.json",
|
||||
"group": "DurableFunctionMonitorGroup@5"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.refresh",
|
||||
"when": "view == durableFunctionsMonitorTreeView",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.detachFromAllTaskHubs",
|
||||
"when": "view == durableFunctionsMonitorTreeView",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.attachToAnotherTaskHub",
|
||||
"when": "view == durableFunctionsMonitorTreeView",
|
||||
"group": "navigation@3"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.gotoInstanceId",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
|
||||
"group": "2_purge_history@3"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.cleanEntityStorage",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
|
||||
"group": "2_purge_history@2"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.purgeHistory",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
|
||||
"group": "2_purge_history@1"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.startNewInstance",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
|
||||
"group": "2_purge_history@0"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.deleteTaskHub",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
|
||||
"group": "3_delete_task_hub@1"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.attachToTaskHub",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-detached",
|
||||
"group": "1_attach_detach@1"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.detachFromTaskHub",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == storageAccount-attached"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.openInstancesInStorageExplorer",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached || viewItem == taskHub-detached",
|
||||
"group": "4_storage_explorer@1"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.openHistoryInStorageExplorer",
|
||||
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached || viewItem == taskHub-detached",
|
||||
"group": "4_storage_explorer@2"
|
||||
}
|
||||
],
|
||||
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.openInstancesInStorageExplorer",
|
||||
"when": "never"
|
||||
},
|
||||
{
|
||||
"command": "durableFunctionsMonitorTreeView.openHistoryInStorageExplorer",
|
||||
"when": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Durable Functions Monitor",
|
||||
"properties": {
|
||||
"durableFunctionsMonitor.backendBaseUrl": {
|
||||
"type": "string",
|
||||
"default": "http://localhost:{portNr}/a/p/i",
|
||||
"description": "URL the backend(s) to be started on. You might want e.g. to change 'localhost' to '127.0.0.1', if you're observing firewall issues. Also it is possible to lock the port number here, if needed (by default it is automatically chosen from the range 37072-38000)."
|
||||
},
|
||||
"durableFunctionsMonitor.backendVersionToUse": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Default",
|
||||
".Net Core 3.1",
|
||||
".Net Core 2.1"
|
||||
],
|
||||
"default": "Default",
|
||||
"description": "Choose which backend binaries to use when starting a backend. Currently 'Default' backend targets .Net Core 2.1, but you can try other ones, if 'Default' doesn't work for you."
|
||||
},
|
||||
"durableFunctionsMonitor.customPathToBackendBinaries": {
|
||||
"type": "string",
|
||||
"description": "Put local path to a custom backend implementation to use. Overrides 'Backend Version to Use' when set."
|
||||
},
|
||||
"durableFunctionsMonitor.backendTimeoutInSeconds": {
|
||||
"type": "number",
|
||||
"default": "60",
|
||||
"description": "Number of seconds to wait for the backend to start."
|
||||
},
|
||||
"durableFunctionsMonitor.storageEmulatorConnectionString": {
|
||||
"type": "string",
|
||||
"default": "AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;",
|
||||
"description": "Connection String to talk to local Storage Emulator. The AccountKey here is a well-known AccountKey. Customize endpoint URLs when needed."
|
||||
},
|
||||
"durableFunctionsMonitor.enableLogging": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable extensive logging and output logs into 'Durable Functions Monitor' output channel"
|
||||
},
|
||||
"durableFunctionsMonitor.showTimeAs": {
|
||||
"type": "string",
|
||||
"default": "UTC",
|
||||
"enum": [
|
||||
"UTC",
|
||||
"Local"
|
||||
],
|
||||
"description": "In which time zone time values should be displayed"
|
||||
},
|
||||
"durableFunctionsMonitor.showWhenDebugSessionStarts": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Show Durable Functions Monitor when you start debugging a Durable Functions project"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"pretest": "npm run compile",
|
||||
"test": "node ./out/test/runTest.js",
|
||||
"package": "node ./node_modules/vsce/out/vsce package"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/node": "^10.12.21",
|
||||
"@types/vscode": "^1.39.0",
|
||||
"glob": "^7.1.4",
|
||||
"mocha": "^8.2.0",
|
||||
"tslint": "^5.12.1",
|
||||
"typescript": "^3.3.1",
|
||||
"vsce": "^1.88.0",
|
||||
"vscode-test": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/arm-storage": "^15.1.0",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
"@types/rimraf": "^3.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"axios": "^0.21.2",
|
||||
"crypto-js": "^4.0.0",
|
||||
"portscanner": "^2.2.0",
|
||||
"tree-kill": "^1.2.2"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"ms-vscode.azure-account"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<svg id="bc18bade-5481-447e-a959-659d72346474"
|
||||
xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<defs>
|
||||
<radialGradient id="a445c717-9d75-44c7-ba6b-0d8f2383e560" cx="-36.63" cy="17.12" r="11.18" gradientTransform="translate(41.88 -7.4) scale(0.94 0.94)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.27" stop-color="#ffd70f"/>
|
||||
<stop offset="0.49" stop-color="#ffcb12"/>
|
||||
<stop offset="0.88" stop-color="#feac19"/>
|
||||
<stop offset="1" stop-color="#fea11b"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<title>Icon-general-2</title>
|
||||
<path id="e3d1e58c-f78e-4fb5-9857-0c9331da9979" d="M13.56,7.19a2.07,2.07,0,0,0,0-2.93h0L10,.69a2.06,2.06,0,0,0-2.92,0h0L3.52,4.26a2.09,2.09,0,0,0,0,2.93l3,3a.61.61,0,0,1,.17.41v5.52a.7.7,0,0,0,.2.5l1.35,1.35a.45.45,0,0,0,.66,0l1.31-1.31h0l.77-.77a.26.26,0,0,0,0-.38l-.55-.56a.29.29,0,0,1,0-.42l.55-.56a.26.26,0,0,0,0-.38L10.4,13a.28.28,0,0,1,0-.41L11,12a.26.26,0,0,0,0-.38l-.77-.78v-.28Zm-5-5.64A1.18,1.18,0,1,1,7.37,2.73,1.17,1.17,0,0,1,8.54,1.55Z" fill="url(#a445c717-9d75-44c7-ba6b-0d8f2383e560)"/>
|
||||
<path id="a21a8f7a-61cc-4035-8449-e5c8fe4d4d5e" d="M7.62,16.21h0A.25.25,0,0,0,8,16V11.55a.27.27,0,0,0-.11-.22h0a.25.25,0,0,0-.39.22V16A.27.27,0,0,0,7.62,16.21Z" fill="#ff9300" opacity="0.75"/>
|
||||
<rect id="ecd3189c-fb1e-4a0e-a2b6-ba2f11dda484" x="5.69" y="5.45" width="5.86" height="0.69" rx="0.32" fill="#ff9300" opacity="0.75"/>
|
||||
<rect id="a1949a3c-4818-4bd1-b236-0d970b92fc62" x="5.69" y="6.57" width="5.86" height="0.69" rx="0.32" fill="#ff9300" opacity="0.75"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.6 KiB |
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="plug.svg"
|
||||
id="svg4"
|
||||
version="1.1"
|
||||
viewBox="0 0 14 16"
|
||||
height="16"
|
||||
width="14">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg4"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-x="85"
|
||||
inkscape:cy="8"
|
||||
inkscape:cx="7"
|
||||
inkscape:zoom="45.9375"
|
||||
showgrid="false"
|
||||
id="namedview6"
|
||||
inkscape:window-height="1470"
|
||||
inkscape:window-width="2160"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<path
|
||||
style="fill:#c8c8c8;fill-opacity:1"
|
||||
id="path2"
|
||||
d="M14 6V5h-4V3H8v1H6c-1.03 0-1.77.81-2 2L3 7c-1.66 0-3 1.34-3 3v2h1v-2c0-1.11.89-2 2-2l1 1c.25 1.16.98 2 2 2h2v1h2v-2h4V9h-4V6h4z"
|
||||
fill-rule="evenodd" />
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.6 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.56253 2.5158C3.46348 3.45013 2 5.55417 2 8.00002C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8.00002C14 5.32522 12.2497 3.05922 9.83199 2.28485L9.52968 3.23835C11.5429 3.88457 13 5.77213 13 8.00002C13 10.7614 10.7614 13 8 13C5.23858 13 3 10.7614 3 8.00002C3 6.31107 3.83742 4.8177 5.11969 3.91248L5.56253 2.5158Z" fill="#C5C5C5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3H2V2H5.5L6 2.5V6H5V3Z" fill="#C5C5C5"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 586 B |
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="14"
|
||||
height="16"
|
||||
viewBox="0 0 14 16"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="unplug.svg"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
inkscape:document-rotation="0"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2160"
|
||||
inkscape:window-height="1470"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="45.9375"
|
||||
inkscape:cx="7"
|
||||
inkscape:cy="8"
|
||||
inkscape:window-x="85"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M14 6V5h-4V3H8v1H6c-1.03 0-1.77.81-2 2L3 7c-1.66 0-3 1.34-3 3v2h1v-2c0-1.11.89-2 2-2l1 1c.25 1.16.98 2 2 2h2v1h2v-2h4V9h-4V6h4z"
|
||||
id="path2"
|
||||
style="opacity:0.3;fill:#c8c8c8;fill-opacity:1" />
|
||||
<g
|
||||
style="opacity:1;fill:#c8c8c8;fill-opacity:1"
|
||||
transform="matrix(0.02347216,0,0,0.02347525,-1.8775739,4.5933482)"
|
||||
id="XMLID_1_">
|
||||
<path
|
||||
style="fill:#c8c8c8;fill-opacity:1"
|
||||
d="m 387,-131 c -141,0 -256,115 -256,256 0,141 115,256 256,256 141,0 256,-115 256,-256 0,-141 -115,-256 -256,-256 z m 0,47.3 c 47.3,0 91.4,15.8 126.8,42.5 l -292.2,293 C 194.8,216.4 178.3,172.3 178.3,125 178.3,10 272,-83.7 387,-83.7 Z m 0,417.4 c -47.3,0 -91.4,-15.8 -126.8,-42.5 L 552.4,-1.8 C 579.2,33.6 595,77.7 595,125 595.7,240 502,333.7 387,333.7 Z"
|
||||
id="XMLID_6_" />
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 2.3 KiB |
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="plug.svg"
|
||||
id="svg4"
|
||||
version="1.1"
|
||||
viewBox="0 0 14 16"
|
||||
height="16"
|
||||
width="14">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg4"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-x="85"
|
||||
inkscape:cy="8"
|
||||
inkscape:cx="7"
|
||||
inkscape:zoom="45.9375"
|
||||
showgrid="false"
|
||||
id="namedview6"
|
||||
inkscape:window-height="1470"
|
||||
inkscape:window-width="2160"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<path
|
||||
style="opacity:0.8"
|
||||
id="path2"
|
||||
d="M14 6V5h-4V3H8v1H6c-1.03 0-1.77.81-2 2L3 7c-1.66 0-3 1.34-3 3v2h1v-2c0-1.11.89-2 2-2l1 1c.25 1.16.98 2 2 2h2v1h2v-2h4V9h-4V6h4z"
|
||||
fill-rule="evenodd" />
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.6 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.56253 2.5158C3.46348 3.45013 2 5.55417 2 8.00002C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8.00002C14 5.32522 12.2497 3.05922 9.83199 2.28485L9.52968 3.23835C11.5429 3.88457 13 5.77213 13 8.00002C13 10.7614 10.7614 13 8 13C5.23858 13 3 10.7614 3 8.00002C3 6.31107 3.83742 4.8177 5.11969 3.91248L5.56253 2.5158Z" fill="#424242"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3H2V2H5.5L6 2.5V6H5V3Z" fill="#424242"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 586 B |
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="unplug.svg"
|
||||
id="svg4"
|
||||
version="1.1"
|
||||
viewBox="0 0 14 16"
|
||||
height="16"
|
||||
width="14">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg4"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-x="85"
|
||||
inkscape:cy="8"
|
||||
inkscape:cx="7"
|
||||
inkscape:zoom="45.9375"
|
||||
showgrid="false"
|
||||
id="namedview6"
|
||||
inkscape:window-height="1470"
|
||||
inkscape:window-width="2160"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
inkscape:document-rotation="0" />
|
||||
<path
|
||||
style="opacity:0.3"
|
||||
id="path2"
|
||||
d="M14 6V5h-4V3H8v1H6c-1.03 0-1.77.81-2 2L3 7c-1.66 0-3 1.34-3 3v2h1v-2c0-1.11.89-2 2-2l1 1c.25 1.16.98 2 2 2h2v1h2v-2h4V9h-4V6h4z"
|
||||
fill-rule="evenodd" />
|
||||
<g
|
||||
id="XMLID_1_"
|
||||
transform="matrix(0.02347216,0,0,0.02347525,-1.8775739,4.5933482)"
|
||||
style="opacity:0.7">
|
||||
<path
|
||||
id="XMLID_6_"
|
||||
d="m 387,-131 c -141,0 -256,115 -256,256 0,141 115,256 256,256 141,0 256,-115 256,-256 0,-141 -115,-256 -256,-256 z m 0,47.3 c 47.3,0 91.4,15.8 126.8,42.5 l -292.2,293 C 194.8,216.4 178.3,172.3 178.3,125 178.3,10 272,-83.7 387,-83.7 Z m 0,417.4 c -47.3,0 -91.4,-15.8 -126.8,-42.5 L 552.4,-1.8 C 579.2,33.6 595,77.7 595,125 595.7,240 502,333.7 387,333.7 Z" />
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 2.2 KiB |
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 17.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="50px" height="50px" viewBox="-0.5 0.5 50 50" enable-background="new -0.5 0.5 50 50" xml:space="preserve">
|
||||
<path fill="#0072C6" d="M5.757,7.288v36.111c0,3.749,8.392,6.789,18.743,6.789v-42.9C24.5,7.288,5.757,7.288,5.757,7.288z"/>
|
||||
<path fill="#0072C6" d="M24.243,50.187H24.5c10.351,0,18.743-3.038,18.743-6.788V7.288h-19V50.187z"/>
|
||||
<path opacity="0.15" fill="#FFFFFF" d="M24.243,50.187H24.5c10.351,0,18.743-3.038,18.743-6.788V7.288h-19V50.187z"/>
|
||||
<path fill="#FFFFFF" d="M43.243,7.288c0,3.749-8.392,6.788-18.743,6.788S5.757,11.037,5.757,7.288S14.149,0.5,24.5,0.5
|
||||
S43.243,3.539,43.243,7.288"/>
|
||||
<path fill="#7FBA00" d="M39.411,6.897c0,2.475-6.676,4.479-14.911,4.479S9.588,9.372,9.588,6.897c0-2.474,6.677-4.479,14.912-4.479
|
||||
S39.411,4.423,39.411,6.897"/>
|
||||
<path fill="#B8D432" d="M36.287,9.634c1.952-0.757,3.125-1.705,3.125-2.735c0-2.475-6.676-4.48-14.912-4.48
|
||||
c-8.235,0-14.911,2.005-14.911,4.48c0,1.03,1.173,1.978,3.125,2.735C15.44,8.576,19.7,7.893,24.5,7.893
|
||||
C29.301,7.893,33.559,8.576,36.287,9.634"/>
|
||||
<path fill="#FFFFFF" d="M18.547,32.354c0,1.122-0.407,1.991-1.221,2.607c-0.814,0.616-1.938,0.924-3.373,0.924
|
||||
c-1.221,0-2.241-0.22-3.061-0.66v-2.64c0.946,0.803,1.988,1.205,3.126,1.205c0.55,0,0.975-0.11,1.275-0.33s0.45-0.511,0.45-0.875
|
||||
c0-0.357-0.144-0.668-0.433-0.932s-0.876-0.605-1.761-1.023c-1.804-0.846-2.706-2.002-2.706-3.464c0-1.061,0.393-1.912,1.18-2.553
|
||||
c0.786-0.64,1.831-0.961,3.134-0.961c1.155,0,2.111,0.152,2.871,0.454v2.466c-0.797-0.55-1.705-0.825-2.722-0.825
|
||||
c-0.511,0-0.915,0.108-1.212,0.325c-0.297,0.218-0.445,0.508-0.445,0.87c0,0.374,0.119,0.681,0.359,0.92
|
||||
c0.239,0.239,0.73,0.535,1.472,0.887c1.106,0.523,1.893,1.053,2.364,1.592C18.312,30.881,18.547,31.552,18.547,32.354z"/>
|
||||
<path fill="#FFFFFF" d="M31.274,29.682c0,1.391-0.317,2.599-0.949,3.621c-0.633,1.023-1.523,1.74-2.672,2.153l3.431,3.176H27.62
|
||||
l-2.45-2.747c-1.05-0.038-1.998-0.316-2.842-0.833c-0.844-0.516-1.496-1.225-1.955-2.124s-0.689-1.902-0.689-3.007
|
||||
c0-1.226,0.249-2.319,0.746-3.279c0.498-0.96,1.197-1.698,2.099-2.215c0.902-0.516,1.935-0.775,3.102-0.775
|
||||
c1.088,0,2.063,0.25,2.924,0.751c0.86,0.5,1.528,1.212,2.004,2.136C31.036,27.463,31.274,28.511,31.274,29.682z M28.47,29.831
|
||||
c0-1.199-0.261-2.146-0.784-2.842s-1.237-1.044-2.145-1.044c-0.924,0-1.663,0.349-2.219,1.047c-0.555,0.699-0.833,1.628-0.833,2.788
|
||||
c0,1.155,0.272,2.077,0.816,2.767c0.545,0.69,1.267,1.035,2.169,1.035c0.919,0,1.647-0.334,2.186-1.002
|
||||
C28.2,31.913,28.47,30.996,28.47,29.831z"/>
|
||||
<polygon fill="#FFFFFF" points="40.273,35.679 33.229,35.679 33.229,23.851 35.893,23.851 35.893,33.518 40.273,33.518 "/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 2.9 KiB |
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="50px"
|
||||
height="50px"
|
||||
viewBox="-0.5 0.5 50 50"
|
||||
enable-background="new -0.5 0.5 50 50"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="mssqlAttached.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)"><metadata
|
||||
id="metadata25"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs23" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1437"
|
||||
inkscape:window-height="978"
|
||||
id="namedview21"
|
||||
showgrid="false"
|
||||
inkscape:zoom="14.7"
|
||||
inkscape:cx="25"
|
||||
inkscape:cy="25"
|
||||
inkscape:window-x="56"
|
||||
inkscape:window-y="-6"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" />
|
||||
<path
|
||||
fill="#0072C6"
|
||||
d="M5.757,7.288v36.111c0,3.749,8.392,6.789,18.743,6.789v-42.9C24.5,7.288,5.757,7.288,5.757,7.288z"
|
||||
id="path2" />
|
||||
<path
|
||||
fill="#0072C6"
|
||||
d="M24.243,50.187H24.5c10.351,0,18.743-3.038,18.743-6.788V7.288h-19V50.187z"
|
||||
id="path4" />
|
||||
<path
|
||||
opacity="0.15"
|
||||
fill="#FFFFFF"
|
||||
d="M24.243,50.187H24.5c10.351,0,18.743-3.038,18.743-6.788V7.288h-19V50.187z"
|
||||
id="path6" />
|
||||
<path
|
||||
fill="#FFFFFF"
|
||||
d="M43.243,7.288c0,3.749-8.392,6.788-18.743,6.788S5.757,11.037,5.757,7.288S14.149,0.5,24.5,0.5 S43.243,3.539,43.243,7.288"
|
||||
id="path8" />
|
||||
<path
|
||||
fill="#7FBA00"
|
||||
d="M39.411,6.897c0,2.475-6.676,4.479-14.911,4.479S9.588,9.372,9.588,6.897c0-2.474,6.677-4.479,14.912-4.479 S39.411,4.423,39.411,6.897"
|
||||
id="path10" />
|
||||
<path
|
||||
fill="#B8D432"
|
||||
d="M36.287,9.634c1.952-0.757,3.125-1.705,3.125-2.735c0-2.475-6.676-4.48-14.912-4.48 c-8.235,0-14.911,2.005-14.911,4.48c0,1.03,1.173,1.978,3.125,2.735C15.44,8.576,19.7,7.893,24.5,7.893 C29.301,7.893,33.559,8.576,36.287,9.634"
|
||||
id="path12" />
|
||||
<path
|
||||
fill="#FFFFFF"
|
||||
d="M18.547,32.354c0,1.122-0.407,1.991-1.221,2.607c-0.814,0.616-1.938,0.924-3.373,0.924 c-1.221,0-2.241-0.22-3.061-0.66v-2.64c0.946,0.803,1.988,1.205,3.126,1.205c0.55,0,0.975-0.11,1.275-0.33s0.45-0.511,0.45-0.875 c0-0.357-0.144-0.668-0.433-0.932s-0.876-0.605-1.761-1.023c-1.804-0.846-2.706-2.002-2.706-3.464c0-1.061,0.393-1.912,1.18-2.553 c0.786-0.64,1.831-0.961,3.134-0.961c1.155,0,2.111,0.152,2.871,0.454v2.466c-0.797-0.55-1.705-0.825-2.722-0.825 c-0.511,0-0.915,0.108-1.212,0.325c-0.297,0.218-0.445,0.508-0.445,0.87c0,0.374,0.119,0.681,0.359,0.92 c0.239,0.239,0.73,0.535,1.472,0.887c1.106,0.523,1.893,1.053,2.364,1.592C18.312,30.881,18.547,31.552,18.547,32.354z"
|
||||
id="path14" />
|
||||
<path
|
||||
fill="#FFFFFF"
|
||||
d="M31.274,29.682c0,1.391-0.317,2.599-0.949,3.621c-0.633,1.023-1.523,1.74-2.672,2.153l3.431,3.176H27.62 l-2.45-2.747c-1.05-0.038-1.998-0.316-2.842-0.833c-0.844-0.516-1.496-1.225-1.955-2.124s-0.689-1.902-0.689-3.007 c0-1.226,0.249-2.319,0.746-3.279c0.498-0.96,1.197-1.698,2.099-2.215c0.902-0.516,1.935-0.775,3.102-0.775 c1.088,0,2.063,0.25,2.924,0.751c0.86,0.5,1.528,1.212,2.004,2.136C31.036,27.463,31.274,28.511,31.274,29.682z M28.47,29.831 c0-1.199-0.261-2.146-0.784-2.842s-1.237-1.044-2.145-1.044c-0.924,0-1.663,0.349-2.219,1.047c-0.555,0.699-0.833,1.628-0.833,2.788 c0,1.155,0.272,2.077,0.816,2.767c0.545,0.69,1.267,1.035,2.169,1.035c0.919,0,1.647-0.334,2.186-1.002 C28.2,31.913,28.47,30.996,28.47,29.831z"
|
||||
id="path16" />
|
||||
<polygon
|
||||
fill="#FFFFFF"
|
||||
points="40.273,35.679 33.229,35.679 33.229,23.851 35.893,23.851 35.893,33.518 40.273,33.518 "
|
||||
id="polygon18" />
|
||||
<g
|
||||
id="shape20-11-3"
|
||||
transform="matrix(0.26020584,0,0,0.23675394,8.3683429,-32.192055)"
|
||||
style="fill:#ff0000;fill-opacity:1"><title
|
||||
id="title32-4">Sheet.20</title><path
|
||||
d="m 0,200.91 h 18.19 l 16.25,-0.87 12.34,-44.32 c 0.65,-2.61 1.95,-7.83 7.15,-7.83 5.2,0 5.85,5.22 5.85,7.83 l 0.65,73.88 9.09,-25.21 c 0.65,-2.61 2.6,-3.48 3.9,-3.48 h 52.63 c 2.6,0 4.55,3.48 4.55,6.09 0,2.61 -1.95,7.82 -4.55,7.82 H 78.62 l -16.89,45.2 c -1.3,4.34 -2.6,6.08 -6.5,5.21 -1.95,-0.87 -3.9,-4.34 -3.9,-6.95 l -0.65,-67.8 -5.2,18.26 c -0.65,2.6 -1.95,6.08 -3.9,6.08 H 25.99 5.85 L 2.6,207.87 Z"
|
||||
class="st4"
|
||||
id="path34-3"
|
||||
style="fill:#ff0000;fill-opacity:1" /></g></svg>
|
После Ширина: | Высота: | Размер: 4.7 KiB |
|
@ -0,0 +1,15 @@
|
|||
<svg id="f2f04349-8aee-4413-84c9-a9053611b319" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<defs>
|
||||
<linearGradient id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74" x1="9" y1="15.83" x2="9" y2="5.79" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b3b3b3"/>
|
||||
<stop offset="0.26" stop-color="#c1c1c1"/>
|
||||
<stop offset="1" stop-color="#e6e6e6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<title>Icon-storage-86</title>
|
||||
<path d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z" fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"/>
|
||||
<path d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z" fill="#37c2b1"/>
|
||||
<path d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z" fill="#fff"/>
|
||||
<path d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z" fill="#37c2b1"/>
|
||||
<path d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z" fill="#258277"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.2 KiB |
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="f2f04349-8aee-4413-84c9-a9053611b319"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
version="1.1"
|
||||
sodipodi:docname="storageAccountAttached.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)">
|
||||
<metadata
|
||||
id="metadata26">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2160"
|
||||
inkscape:window-height="1470"
|
||||
id="namedview24"
|
||||
showgrid="false"
|
||||
inkscape:zoom="40.833333"
|
||||
inkscape:cx="9"
|
||||
inkscape:cy="9"
|
||||
inkscape:window-x="85"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="f2f04349-8aee-4413-84c9-a9053611b319" />
|
||||
<defs
|
||||
id="defs9">
|
||||
<linearGradient
|
||||
id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74"
|
||||
x1="9"
|
||||
y1="15.83"
|
||||
x2="9"
|
||||
y2="5.79"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#b3b3b3"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="0.26"
|
||||
stop-color="#c1c1c1"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#e6e6e6"
|
||||
id="stop6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<title
|
||||
id="title11">Icon-storage-86</title>
|
||||
<path
|
||||
d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z"
|
||||
fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"
|
||||
id="path13" />
|
||||
<path
|
||||
d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z"
|
||||
fill="#37c2b1"
|
||||
id="path15" />
|
||||
<path
|
||||
d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z"
|
||||
fill="#fff"
|
||||
id="path17" />
|
||||
<path
|
||||
d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z"
|
||||
fill="#37c2b1"
|
||||
id="path19" />
|
||||
<path
|
||||
d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z"
|
||||
fill="#258277"
|
||||
id="path21" />
|
||||
<g
|
||||
id="shape20-11-3"
|
||||
transform="matrix(0.08637583,0,0,0.08390308,3.4299469,-8.1252329)"
|
||||
style="fill:#ff0000;fill-opacity:1">
|
||||
<title
|
||||
id="title32-4">Sheet.20</title>
|
||||
<path
|
||||
d="m 0,200.91 h 18.19 l 16.25,-0.87 12.34,-44.32 c 0.65,-2.61 1.95,-7.83 7.15,-7.83 5.2,0 5.85,5.22 5.85,7.83 l 0.65,73.88 9.09,-25.21 c 0.65,-2.61 2.6,-3.48 3.9,-3.48 h 52.63 c 2.6,0 4.55,3.48 4.55,6.09 0,2.61 -1.95,7.82 -4.55,7.82 H 78.62 l -16.89,45.2 c -1.3,4.34 -2.6,6.08 -6.5,5.21 -1.95,-0.87 -3.9,-4.34 -3.9,-6.95 l -0.65,-67.8 -5.2,18.26 c -0.65,2.6 -1.95,6.08 -3.9,6.08 H 25.99 5.85 L 2.6,207.87 Z"
|
||||
class="st4"
|
||||
id="path34-3"
|
||||
style="fill:#ff0000;fill-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 3.5 KiB |
|
@ -0,0 +1,113 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="f2f04349-8aee-4413-84c9-a9053611b319"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
version="1.1"
|
||||
sodipodi:docname="storageAccountV2.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)">
|
||||
<metadata
|
||||
id="metadata26">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1437"
|
||||
inkscape:window-height="978"
|
||||
id="namedview24"
|
||||
showgrid="false"
|
||||
inkscape:zoom="40.833333"
|
||||
inkscape:cx="9"
|
||||
inkscape:cy="9"
|
||||
inkscape:window-x="56"
|
||||
inkscape:window-y="-6"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="f2f04349-8aee-4413-84c9-a9053611b319" />
|
||||
<defs
|
||||
id="defs9">
|
||||
<linearGradient
|
||||
id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74"
|
||||
x1="9"
|
||||
y1="15.83"
|
||||
x2="9"
|
||||
y2="5.79"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#b3b3b3"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="0.26"
|
||||
stop-color="#c1c1c1"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#e6e6e6"
|
||||
id="stop6" />
|
||||
</linearGradient>
|
||||
<rect
|
||||
x="32.433697"
|
||||
y="-249.47739"
|
||||
width="16.053041"
|
||||
height="14.25117"
|
||||
id="rect37" />
|
||||
<rect
|
||||
x="32.433697"
|
||||
y="-249.47739"
|
||||
width="16.053041"
|
||||
height="14.25117"
|
||||
id="rect65" />
|
||||
</defs>
|
||||
<title
|
||||
id="title11">Icon-storage-86</title>
|
||||
<path
|
||||
d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z"
|
||||
fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"
|
||||
id="path13" />
|
||||
<path
|
||||
d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z"
|
||||
fill="#37c2b1"
|
||||
id="path15" />
|
||||
<path
|
||||
d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z"
|
||||
fill="#fff"
|
||||
id="path17" />
|
||||
<path
|
||||
d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z"
|
||||
fill="#37c2b1"
|
||||
id="path19" />
|
||||
<path
|
||||
d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z"
|
||||
fill="#258277"
|
||||
id="path21" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text35"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect37);fill:#ffffff;fill-opacity:1;stroke:none;"
|
||||
transform="matrix(0.83442828,0,0,0.8035861,-20.993044,208.24026)"><tspan
|
||||
x="32.433594"
|
||||
y="-239.8211"><tspan
|
||||
style="fill:#ffffff">V2</tspan></tspan></text>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 3.5 KiB |
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="f2f04349-8aee-4413-84c9-a9053611b319"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
version="1.1"
|
||||
sodipodi:docname="storageAccountV2Attached.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)">
|
||||
<metadata
|
||||
id="metadata26">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1437"
|
||||
inkscape:window-height="978"
|
||||
id="namedview24"
|
||||
showgrid="false"
|
||||
inkscape:zoom="40.833333"
|
||||
inkscape:cx="9"
|
||||
inkscape:cy="9"
|
||||
inkscape:window-x="56"
|
||||
inkscape:window-y="-6"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="f2f04349-8aee-4413-84c9-a9053611b319"
|
||||
inkscape:document-rotation="0" />
|
||||
<defs
|
||||
id="defs9">
|
||||
<linearGradient
|
||||
id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74"
|
||||
x1="9"
|
||||
y1="15.83"
|
||||
x2="9"
|
||||
y2="5.79"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#b3b3b3"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="0.26"
|
||||
stop-color="#c1c1c1"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#e6e6e6"
|
||||
id="stop6" />
|
||||
</linearGradient>
|
||||
<rect
|
||||
x="32.433697"
|
||||
y="-249.47739"
|
||||
width="16.053041"
|
||||
height="14.25117"
|
||||
id="rect37" />
|
||||
<rect
|
||||
x="32.433697"
|
||||
y="-249.47739"
|
||||
width="16.053041"
|
||||
height="14.25117"
|
||||
id="rect100" />
|
||||
</defs>
|
||||
<title
|
||||
id="title11">Icon-storage-86</title>
|
||||
<path
|
||||
d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z"
|
||||
fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"
|
||||
id="path13" />
|
||||
<path
|
||||
d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z"
|
||||
fill="#37c2b1"
|
||||
id="path15" />
|
||||
<path
|
||||
d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z"
|
||||
fill="#fff"
|
||||
id="path17" />
|
||||
<path
|
||||
d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z"
|
||||
fill="#37c2b1"
|
||||
id="path19" />
|
||||
<path
|
||||
d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z"
|
||||
fill="#258277"
|
||||
id="path21" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text35"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect37);fill:#ffffff;fill-opacity:1;stroke:none;"
|
||||
transform="matrix(0.83442828,0,0,0.8035861,-21.099162,208.19127)"><tspan
|
||||
x="32.433594"
|
||||
y="-239.8211"><tspan
|
||||
style="fill:#ffffff">V2</tspan></tspan></text>
|
||||
<g
|
||||
id="shape20-11-3"
|
||||
transform="matrix(0.08637583,0,0,0.08390308,2.56374,-8.4993302)"
|
||||
style="fill:#ff0000;fill-opacity:1">
|
||||
<title
|
||||
id="title32-4">Sheet.20</title>
|
||||
<path
|
||||
d="m 0,200.91 h 18.19 l 16.25,-0.87 12.34,-44.32 c 0.65,-2.61 1.95,-7.83 7.15,-7.83 5.2,0 5.85,5.22 5.85,7.83 l 0.65,73.88 9.09,-25.21 c 0.65,-2.61 2.6,-3.48 3.9,-3.48 h 52.63 c 2.6,0 4.55,3.48 4.55,6.09 0,2.61 -1.95,7.82 -4.55,7.82 H 78.62 l -16.89,45.2 c -1.3,4.34 -2.6,6.08 -6.5,5.21 -1.95,-0.87 -3.9,-4.34 -3.9,-6.95 l -0.65,-67.8 -5.2,18.26 c -0.65,2.6 -1.95,6.08 -3.9,6.08 H 25.99 5.85 L 2.6,207.87 Z"
|
||||
class="st4"
|
||||
id="path34-3"
|
||||
style="fill:#ff0000;fill-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 4.2 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-folder{fill:#dcb67a}.icon-vs-fg{fill:#f0eff1}.icon-vs-bg{fill:#424242}</style><path class="icon-canvas-transparent" d="M0 0h16v16H0V0z" id="canvas"/><path class="icon-vs-out" d="M14.996 9.418V10H16v1.352l-1.004.96v.188c0 .827-.673 1.5-1.5 1.5h-.266l-2.092 2H9.441l.961-2H1.5C.673 14 0 13.327 0 12.5v-10C0 1.673.673 1 1.5 1h8.11l1 2h2.886c.827 0 1.5.673 1.5 1.5V7H16v1.414l-1.004 1.004z" id="outline" style="display: none;"/><path class="icon-vs-fg" d="M2 3h6.374l.5 1H2V3z" id="iconBg" style="display: none;"/><g id="iconFg"><path class="icon-vs-bg" d="M12 8l-2 4h2.5L11 15l4-4h-3l3-3z"/><path class="icon-folder" d="M13.996 7V4.5a.5.5 0 0 0-.5-.5H9.992l-1-2H1.5a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h6.882l3-6h2.614zM2 4V3h6.374l.5 1H2z"/></g></svg>
|
После Ширина: | Высота: | Размер: 894 B |
|
@ -0,0 +1,191 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 64 64"
|
||||
enable-background="new 0 0 64 64"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="taskHub.svg"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
|
||||
id="metadata17"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs15" /><sodipodi:namedview
|
||||
inkscape:document-rotation="0"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2160"
|
||||
inkscape:window-height="1470"
|
||||
id="namedview13"
|
||||
showgrid="false"
|
||||
inkscape:zoom="11.484375"
|
||||
inkscape:cx="32"
|
||||
inkscape:cy="32"
|
||||
inkscape:window-x="85"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" />
|
||||
<g
|
||||
id="Layer_1-5"
|
||||
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
|
||||
style="fill:#b4b4b4;fill-opacity:1"><g
|
||||
id="Azure"
|
||||
style="fill:#b4b4b4;fill-opacity:1" /><g
|
||||
transform="translate(37.3575,-2500.2)"
|
||||
id="BizTalk_Services"
|
||||
style="fill:#b4b4b4;fill-opacity:1" /><g
|
||||
id="Key_Vault"
|
||||
style="fill:#b4b4b4;fill-opacity:1" /><g
|
||||
id="Mobile_Engagement"
|
||||
style="fill:#b4b4b4;fill-opacity:1" /><g
|
||||
id="Office_subscription"
|
||||
style="fill:#b4b4b4;fill-opacity:1"><g
|
||||
id="Office_subscription_1_"
|
||||
style="fill:#b4b4b4;fill-opacity:1" /></g></g><g
|
||||
id="paths"
|
||||
transform="matrix(1.9931036,0,0,1.9931036,-119.22001,-85.548973)"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"><g
|
||||
id="Access_control"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Azure_active_directory"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="API_Management"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Azure_automation"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Azure_SQL_database"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Azure_subscription"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Backup_service"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Bitbucket_code_source"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Azure_cache"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Content_delivery_network__x28_CDN_x29_"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Cloud_service"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="CodePlex"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Dropbox_code_source"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Express_route"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Git_repository"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="GitHub_code"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="HD_Insight"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Health_monitoring"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Healthy"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="BizTalk_hybrid_connection"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Hybrid_connection_manager_for_BizTalk_hybrid_connection"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Hyper-V_recovery_manager"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Machine_learning"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Media_services"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Microsoft_account"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Mobile_services"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Multi-factor_authentication"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="MySQL_database"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Notification_hub"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Notification_topic"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Cloud_Office_365"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Office_365"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="OS_image"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Remote_app"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Task_scheduler"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Azure_SDK"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Service_bus"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Service_bus_queue"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Service_bus_relay"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Service_bus_topic"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Service_endpoint"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Custom_create"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="SQL_data_sync"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="SQL_reporting"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Startup_task"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Windows_Azure_storage"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Storage_blob"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
|
||||
id="Storage_table"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"><path
|
||||
id="path56"
|
||||
d="m 85.5,42.3 h -19 L 57,58.7 66.5,75.1 h 18.9 l 9.5,-16.4 z m -10.4,8.6 h 4.4 v 4.4 h -4.4 z m 0,5.5 h 4.4 v 4.4 h -4.4 z m 0,5.5 h 4.4 v 4.4 h -4.4 z m -5.5,-11 H 74 v 4.4 h -4.4 z m 0,5.5 H 74 v 4.4 h -4.4 z m 0,5.5 H 74 v 4.4 h -4.4 z m 15.3,6.8 H 67 V 50.8 h 1.2 v 16.7 0 0 H 84.9 Z M 85,66.3 H 80.6 V 61.9 H 85 Z m 0,-5.5 H 80.6 V 56.4 H 85 Z m 0,-5.5 H 80.6 V 50.9 H 85 Z"
|
||||
fill="#0078d7"
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002" /></g></g><g
|
||||
id="Layer_57"
|
||||
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
|
||||
style="fill:#b4b4b4;fill-opacity:1" /><g
|
||||
id="Ibiza_Symbols"
|
||||
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
|
||||
style="fill:#b4b4b4;fill-opacity:1"><g
|
||||
id="SQL_Database_Premium"
|
||||
style="fill:#b4b4b4;fill-opacity:1" /></g><g
|
||||
id="g10">
|
||||
<path
|
||||
fill="#3999C6"
|
||||
d="M63.6,32.4c0.6-0.6,0.5-1.7,0-2.3L60.5,27L46.7,13.6c-0.6-0.6-1.5-0.6-2.2,0l0,0c-0.6,0.6-0.8,1.7,0,2.3 L59,30.1c0.6,0.6,0.6,1.7,0,2.3L44.2,47.1c-0.6,0.6-0.6,1.7,0,2.3l0,0c0.6,0.6,1.7,0.5,2.2,0l13.7-13.6c0,0,0,0,0.1-0.1L63.6,32.4z "
|
||||
id="path2" />
|
||||
<path
|
||||
fill="#3999C6"
|
||||
d="M0.4,32.4c-0.6-0.6-0.5-1.7,0-2.3L3.5,27l13.8-13.4c0.6-0.6,1.5-0.6,2.2,0l0,0c0.6,0.6,0.8,1.7,0,2.3 L5.3,30.1c-0.6,0.6-0.6,1.7,0,2.3l14.5,14.7c0.6,0.6,0.6,1.7,0,2.3l0,0c-0.6,0.6-1.7,0.5-2.2,0L3.6,36c0,0,0,0-0.1-0.1L0.4,32.4z"
|
||||
id="path4" />
|
||||
<polygon
|
||||
fill="#FCD116"
|
||||
points="47.6,2.5 28.1,2.5 17.6,32.1 30.4,32.2 20.4,61.5 48,22.4 34.6,22.4 "
|
||||
id="polygon6" />
|
||||
<polygon
|
||||
opacity="0.3"
|
||||
fill="#FF8C00"
|
||||
enable-background="new "
|
||||
points="34.6,22.4 47.6,2.5 37.4,2.5 26.6,27.1 39.4,27.2 20.4,61.5 48,22.4 "
|
||||
id="polygon8" />
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 7.7 KiB |
|
@ -0,0 +1,199 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="taskHubConnected.svg"
|
||||
xml:space="preserve"
|
||||
enable-background="new 0 0 64 64"
|
||||
viewBox="0 0 64 64"
|
||||
y="0px"
|
||||
x="0px"
|
||||
id="Layer_1"
|
||||
version="1.1"><metadata
|
||||
id="metadata17"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs15" /><sodipodi:namedview
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-x="85"
|
||||
inkscape:cy="44.509449"
|
||||
inkscape:cx="32"
|
||||
inkscape:zoom="8.1206794"
|
||||
showgrid="false"
|
||||
id="namedview13"
|
||||
inkscape:window-height="1470"
|
||||
inkscape:window-width="2160"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
inkscape:document-rotation="0" />
|
||||
<g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
|
||||
id="Layer_1-5"><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
id="Azure" /><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
id="BizTalk_Services"
|
||||
transform="translate(37.3575,-2500.2)" /><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
id="Key_Vault" /><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
id="Mobile_Engagement" /><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
id="Office_subscription"><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
id="Office_subscription_1_" /></g></g><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
transform="matrix(1.9931036,0,0,1.9931036,-119.22001,-85.548973)"
|
||||
id="paths"><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Access_control" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Azure_active_directory" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="API_Management" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Azure_automation" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Azure_SQL_database" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Azure_subscription" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Backup_service" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Bitbucket_code_source" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Azure_cache" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Content_delivery_network__x28_CDN_x29_" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Cloud_service" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="CodePlex" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Dropbox_code_source" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Express_route" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Git_repository" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="GitHub_code" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="HD_Insight" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Health_monitoring" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Healthy" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="BizTalk_hybrid_connection" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Hybrid_connection_manager_for_BizTalk_hybrid_connection" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Hyper-V_recovery_manager" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Machine_learning" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Media_services" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Microsoft_account" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Mobile_services" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Multi-factor_authentication" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="MySQL_database" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Notification_hub" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Notification_topic" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Cloud_Office_365" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Office_365" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="OS_image" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Remote_app" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Task_scheduler" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Azure_SDK" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Service_bus" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Service_bus_queue" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Service_bus_relay" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Service_bus_topic" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Service_endpoint" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Custom_create" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="SQL_data_sync" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="SQL_reporting" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Startup_task" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Windows_Azure_storage" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Storage_blob" /><g
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
id="Storage_table"><path
|
||||
style="fill:#c8c8c8;fill-opacity:0.60000002"
|
||||
fill="#0078d7"
|
||||
d="m 85.5,42.3 h -19 L 57,58.7 66.5,75.1 h 18.9 l 9.5,-16.4 z m -10.4,8.6 h 4.4 v 4.4 h -4.4 z m 0,5.5 h 4.4 v 4.4 h -4.4 z m 0,5.5 h 4.4 v 4.4 h -4.4 z m -5.5,-11 H 74 v 4.4 h -4.4 z m 0,5.5 H 74 v 4.4 h -4.4 z m 0,5.5 H 74 v 4.4 h -4.4 z m 15.3,6.8 H 67 V 50.8 h 1.2 v 16.7 0 0 H 84.9 Z M 85,66.3 H 80.6 V 61.9 H 85 Z m 0,-5.5 H 80.6 V 56.4 H 85 Z m 0,-5.5 H 80.6 V 50.9 H 85 Z"
|
||||
id="path56" /></g></g><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
|
||||
id="Layer_57" /><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
|
||||
id="Ibiza_Symbols"><g
|
||||
style="fill:#b4b4b4;fill-opacity:1"
|
||||
id="SQL_Database_Premium" /></g><g
|
||||
id="g10">
|
||||
<path
|
||||
id="path2"
|
||||
d="M63.6,32.4c0.6-0.6,0.5-1.7,0-2.3L60.5,27L46.7,13.6c-0.6-0.6-1.5-0.6-2.2,0l0,0c-0.6,0.6-0.8,1.7,0,2.3 L59,30.1c0.6,0.6,0.6,1.7,0,2.3L44.2,47.1c-0.6,0.6-0.6,1.7,0,2.3l0,0c0.6,0.6,1.7,0.5,2.2,0l13.7-13.6c0,0,0,0,0.1-0.1L63.6,32.4z "
|
||||
fill="#3999C6" />
|
||||
<path
|
||||
id="path4"
|
||||
d="M0.4,32.4c-0.6-0.6-0.5-1.7,0-2.3L3.5,27l13.8-13.4c0.6-0.6,1.5-0.6,2.2,0l0,0c0.6,0.6,0.8,1.7,0,2.3 L5.3,30.1c-0.6,0.6-0.6,1.7,0,2.3l14.5,14.7c0.6,0.6,0.6,1.7,0,2.3l0,0c-0.6,0.6-1.7,0.5-2.2,0L3.6,36c0,0,0,0-0.1-0.1L0.4,32.4z"
|
||||
fill="#3999C6" />
|
||||
<polygon
|
||||
id="polygon6"
|
||||
points="47.6,2.5 28.1,2.5 17.6,32.1 30.4,32.2 20.4,61.5 48,22.4 34.6,22.4 "
|
||||
fill="#FCD116" />
|
||||
<polygon
|
||||
id="polygon8"
|
||||
points="34.6,22.4 47.6,2.5 37.4,2.5 26.6,27.1 39.4,27.2 20.4,61.5 48,22.4 "
|
||||
enable-background="new "
|
||||
fill="#FF8C00"
|
||||
opacity="0.3" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#ff0000;fill-opacity:1"
|
||||
transform="matrix(0.3136085,0,0,0.28473247,12.242084,-28.645509)"
|
||||
id="shape20-11-3"><title
|
||||
id="title32-4">Sheet.20</title><path
|
||||
style="fill:#ff0000;fill-opacity:1"
|
||||
id="path34-3"
|
||||
class="st4"
|
||||
d="m 0,200.91 h 18.19 l 16.25,-0.87 12.34,-44.32 c 0.65,-2.61 1.95,-7.83 7.15,-7.83 5.2,0 5.85,5.22 5.85,7.83 l 0.65,73.88 9.09,-25.21 c 0.65,-2.61 2.6,-3.48 3.9,-3.48 h 52.63 c 2.6,0 4.55,3.48 4.55,6.09 0,2.61 -1.95,7.82 -4.55,7.82 H 78.62 l -16.89,45.2 c -1.3,4.34 -2.6,6.08 -6.5,5.21 -1.95,-0.87 -3.9,-4.34 -3.9,-6.95 l -0.65,-67.8 -5.2,18.26 c -0.65,2.6 -1.95,6.08 -3.9,6.08 H 25.99 5.85 L 2.6,207.87 Z" /></g></svg>
|
После Ширина: | Высота: | Размер: 8.3 KiB |
|
@ -0,0 +1,298 @@
|
|||
const portscanner = require('portscanner');
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as crypto from 'crypto';
|
||||
import * as killProcessTree from 'tree-kill';
|
||||
import axios from 'axios';
|
||||
import { spawn, spawnSync, ChildProcess } from 'child_process';
|
||||
import * as CryptoJS from 'crypto-js';
|
||||
|
||||
import { ConnStringUtils } from "./ConnStringUtils";
|
||||
|
||||
import * as SharedConstants from './SharedConstants';
|
||||
import { Settings } from './Settings';
|
||||
|
||||
// Responsible for running the backend process
|
||||
export class BackendProcess {
|
||||
|
||||
constructor(private _binariesFolder: string,
|
||||
private _storageConnectionSettings: StorageConnectionSettings,
|
||||
private _removeMyselfFromList: () => void,
|
||||
private _log: (l: string) => void)
|
||||
{ }
|
||||
|
||||
// Underlying Storage Connection Strings
|
||||
get storageConnectionStrings(): string[] {
|
||||
return this._storageConnectionSettings.storageConnStrings;
|
||||
}
|
||||
|
||||
// Information about the started backend (if it was successfully started)
|
||||
get backendUrl(): string {
|
||||
return this._backendUrl;
|
||||
}
|
||||
|
||||
// Folder where backend is run from (might be different, if the backend needs to be published first)
|
||||
get binariesFolder(): string {
|
||||
return this._eventualBinariesFolder;
|
||||
}
|
||||
|
||||
// Kills the pending backend process
|
||||
cleanup(): Promise<any> {
|
||||
|
||||
this._backendPromise = null;
|
||||
this._backendUrl = '';
|
||||
|
||||
if (!this._funcProcess) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
console.log('Killing func process...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
||||
// The process is a shell. So to stop func.exe, we need to kill the entire process tree.
|
||||
killProcessTree(this._funcProcess!.pid, resolve);
|
||||
this._funcProcess = null;
|
||||
});
|
||||
}
|
||||
|
||||
get backendCommunicationNonce(): string { return this._backendCommunicationNonce; }
|
||||
|
||||
// Ensures that the backend is running (starts it, if needed) and returns its properties
|
||||
getBackend(): Promise<void> {
|
||||
|
||||
if (!!this._backendPromise) {
|
||||
return this._backendPromise;
|
||||
}
|
||||
|
||||
this._backendPromise = new Promise<void>((resolve, reject) => {
|
||||
|
||||
vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: `Starting the backend `,
|
||||
cancellable: true
|
||||
}, (progress, token) => new Promise(stopProgress => {
|
||||
|
||||
// Starting the backend on a first available port
|
||||
portscanner.findAPortNotInUse(37072, 38000).then((portNr: number) => {
|
||||
|
||||
const backendUrl = Settings().backendBaseUrl.replace('{portNr}', portNr.toString());
|
||||
progress.report({ message: backendUrl });
|
||||
|
||||
// Now running func.exe in backend folder
|
||||
this.startBackendOnPort(portNr, backendUrl, token)
|
||||
.then(resolve, reject)
|
||||
.finally(() => stopProgress(undefined));
|
||||
|
||||
}, (err: any) => { stopProgress(undefined); reject(`Failed to choose port for backend: ${err.message}`); });
|
||||
}));
|
||||
});
|
||||
|
||||
// Allowing the user to try again
|
||||
this._backendPromise.catch(() => {
|
||||
|
||||
// This call is important, without it a typo in connString would persist until vsCode restart
|
||||
this._removeMyselfFromList();
|
||||
});
|
||||
|
||||
return this._backendPromise;
|
||||
}
|
||||
|
||||
// Reference to the shell instance running func.exe
|
||||
private _funcProcess: ChildProcess | null = null;
|
||||
|
||||
// Promise that resolves when the backend is started successfully
|
||||
private _backendPromise: Promise<void> | null = null;
|
||||
|
||||
// Information about the started backend (if it was successfully started)
|
||||
private _backendUrl: string = '';
|
||||
|
||||
// Folder where backend is run from (might be different, if the backend needs to be published first)
|
||||
private _eventualBinariesFolder: string = this._binariesFolder;
|
||||
|
||||
// A nonce for communicating with the backend
|
||||
private _backendCommunicationNonce = crypto.randomBytes(64).toString('base64');
|
||||
|
||||
// Runs the backend Function instance on some port
|
||||
private startBackendOnPort(portNr: number, backendUrl: string, cancelToken: vscode.CancellationToken): Promise<void> {
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
this._log(`Attempting to start the backend from ${this._binariesFolder} on ${backendUrl}...`);
|
||||
|
||||
if (!fs.existsSync(this._binariesFolder)) {
|
||||
reject(`Couldn't find backend binaries in ${this._binariesFolder}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is a source code project
|
||||
if (fs.readdirSync(this._binariesFolder).some(fn => fn.toLowerCase().endsWith('.csproj'))) {
|
||||
|
||||
const publishFolder = path.join(this._binariesFolder, 'publish');
|
||||
|
||||
// if it wasn't published yet
|
||||
if (!fs.existsSync(publishFolder)) {
|
||||
|
||||
// publishing it
|
||||
const publishProcess = spawnSync('dotnet', ['publish', '-o', publishFolder],
|
||||
{ cwd: this._binariesFolder, encoding: 'utf8' }
|
||||
);
|
||||
|
||||
if (!!publishProcess.stdout) {
|
||||
this._log(publishProcess.stdout.toString());
|
||||
}
|
||||
|
||||
if (publishProcess.status !== 0) {
|
||||
|
||||
const err = 'dotnet publish failed. ' +
|
||||
(!!publishProcess.stderr ? publishProcess.stderr.toString() : `status: ${publishProcess.status}`);
|
||||
|
||||
this._log(`ERROR: ${err}`);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._eventualBinariesFolder = publishFolder;
|
||||
}
|
||||
|
||||
// Important to inherit the context from VsCode, so that globally installed tools can be found
|
||||
const env = process.env;
|
||||
|
||||
env[SharedConstants.NonceEnvironmentVariableName] = this._backendCommunicationNonce;
|
||||
|
||||
// Also setting AzureWebJobsSecretStorageType to 'files', so that the backend doesn't need Azure Storage
|
||||
env['AzureWebJobsSecretStorageType'] = 'files';
|
||||
|
||||
if (this._storageConnectionSettings.isMsSql) {
|
||||
|
||||
env[SharedConstants.MsSqlConnStringEnvironmentVariableName] = this._storageConnectionSettings.storageConnStrings[0];
|
||||
|
||||
// For MSSQL just need to set DFM_HUB_NAME to something, doesn't matter what it is so far
|
||||
env[SharedConstants.HubNameEnvironmentVariableName] = this._storageConnectionSettings.hubName;
|
||||
|
||||
} else {
|
||||
|
||||
// Need to unset this, in case it was set previously
|
||||
delete env[SharedConstants.HubNameEnvironmentVariableName];
|
||||
|
||||
env['AzureWebJobsStorage'] = this._storageConnectionSettings.storageConnStrings[0];
|
||||
}
|
||||
|
||||
this._funcProcess = spawn('func', ['start', '--port', portNr.toString(), '--csharp'], {
|
||||
cwd: this._eventualBinariesFolder,
|
||||
shell: true,
|
||||
env
|
||||
});
|
||||
|
||||
this._funcProcess.stdout.on('data', (data) => {
|
||||
const msg = data.toString();
|
||||
this._log(msg);
|
||||
|
||||
if (msg.toLowerCase().includes('no valid combination of account information found')) {
|
||||
reject('The provided Storage Connection String and/or Hub Name seem to be invalid.');
|
||||
}
|
||||
});
|
||||
|
||||
this._funcProcess!.stderr.on('data', (data) => {
|
||||
const msg = data.toString();
|
||||
this._log(`ERROR: ${msg}`);
|
||||
reject(`Func: ${msg}`);
|
||||
});
|
||||
|
||||
console.log(`Waiting for ${backendUrl} to respond...`);
|
||||
|
||||
// Waiting for the backend to be ready
|
||||
const timeoutInSeconds = Settings().backendTimeoutInSeconds;
|
||||
const intervalInMs = 500, numOfTries = timeoutInSeconds * 1000 / intervalInMs;
|
||||
var i = numOfTries;
|
||||
const intervalToken = setInterval(() => {
|
||||
|
||||
const headers: any = {};
|
||||
headers[SharedConstants.NonceHeaderName] = this._backendCommunicationNonce;
|
||||
|
||||
// Pinging the backend and returning its URL when ready
|
||||
axios.get(`${backendUrl}/--${this._storageConnectionSettings.hubName}/about`, { headers }).then(response => {
|
||||
console.log(`The backend is now running on ${backendUrl}`);
|
||||
clearInterval(intervalToken);
|
||||
|
||||
this._backendUrl = backendUrl;
|
||||
|
||||
resolve();
|
||||
}, err => {
|
||||
|
||||
if (!!err.response && err.response.status === 401) {
|
||||
// This typically happens when mistyping Task Hub name
|
||||
|
||||
clearInterval(intervalToken);
|
||||
reject(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
if (cancelToken.isCancellationRequested) {
|
||||
|
||||
clearInterval(intervalToken);
|
||||
reject(`Cancelled by the user`);
|
||||
|
||||
} else if (--i <= 0) {
|
||||
console.log(`Timed out waiting for the backend!`);
|
||||
clearInterval(intervalToken);
|
||||
reject(`No response within ${timeoutInSeconds} seconds. Ensure you have the latest Azure Functions Core Tools installed globally.`);
|
||||
}
|
||||
|
||||
}, intervalInMs);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageConnectionSettings {
|
||||
|
||||
get storageConnStrings(): string[] { return this._connStrings; };
|
||||
get hubName(): string { return this._hubName; };
|
||||
get connStringHashKey(): string { return this._connStringHashKey; }
|
||||
get hashKey(): string { return this._hashKey; }
|
||||
get isFromLocalSettingsJson(): boolean { return this._fromLocalSettingsJson; }
|
||||
get isMsSql(): boolean { return !!ConnStringUtils.GetSqlServerName(this._connStrings[0]); }
|
||||
|
||||
constructor(private _connStrings: string[],
|
||||
private _hubName: string,
|
||||
private _fromLocalSettingsJson: boolean = false) {
|
||||
|
||||
this._connStringHashKey = StorageConnectionSettings.GetConnStringHashKey(this._connStrings);
|
||||
this._hashKey = this._connStringHashKey + this._hubName.toLowerCase();
|
||||
}
|
||||
|
||||
static GetConnStringHashKey(connStrings: string[]): string {
|
||||
|
||||
const sqlServerName = ConnStringUtils.GetSqlServerName(connStrings[0]);
|
||||
|
||||
if (!!sqlServerName) {
|
||||
return sqlServerName + ConnStringUtils.GetSqlDatabaseName(connStrings[0]);
|
||||
}
|
||||
|
||||
return ConnStringUtils.GetTableEndpoint(connStrings[0]).toLowerCase();
|
||||
}
|
||||
|
||||
static MaskStorageConnString(connString: string): string {
|
||||
return connString.replace(/AccountKey=[^;]+/gi, 'AccountKey=*****');
|
||||
}
|
||||
|
||||
private readonly _connStringHashKey: string;
|
||||
private readonly _hashKey: string;
|
||||
}
|
||||
|
||||
// Creates the SharedKeyLite signature to query Table Storage REST API, also adds other needed headers
|
||||
export function CreateAuthHeadersForTableStorage(accountName: string, accountKey: string, queryUrl: string): {} {
|
||||
|
||||
const dateInUtc = new Date().toUTCString();
|
||||
const signature = CryptoJS.HmacSHA256(`${dateInUtc}\n/${accountName}/${queryUrl}`, CryptoJS.enc.Base64.parse(accountKey));
|
||||
|
||||
return {
|
||||
'Authorization': `SharedKeyLite ${accountName}:${signature.toString(CryptoJS.enc.Base64)}`,
|
||||
'x-ms-date': dateInUtc,
|
||||
'x-ms-version': '2015-12-11',
|
||||
'Accept': 'application/json;odata=nometadata'
|
||||
};
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
|
||||
import { Settings } from './Settings';
|
||||
|
||||
export class ConnStringUtils {
|
||||
|
||||
// Extracts AccountName from Storage Connection String
|
||||
static GetAccountName(connString: string): string {
|
||||
const match = /AccountName=([^;]+)/i.exec(connString);
|
||||
return (!!match && match.length > 0) ? match[1] : '';
|
||||
}
|
||||
|
||||
// Extracts AccountKey from Storage Connection String
|
||||
static GetAccountKey(connString: string): string {
|
||||
const match = /AccountKey=([^;]+)/i.exec(connString);
|
||||
return (!!match && match.length > 0) ? match[1] : '';
|
||||
}
|
||||
|
||||
// Extracts DefaultEndpointsProtocol from Storage Connection String
|
||||
static GetDefaultEndpointsProtocol(connString: string): string {
|
||||
const match = /DefaultEndpointsProtocol=([^;]+)/i.exec(connString);
|
||||
return (!!match && match.length > 0) ? match[1] : 'https';
|
||||
}
|
||||
|
||||
// Extracts TableEndpoint from Storage Connection String
|
||||
static GetTableEndpoint(connString: string): string {
|
||||
|
||||
const accountName = ConnStringUtils.GetAccountName(connString);
|
||||
if (!accountName) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const endpointsProtocol = ConnStringUtils.GetDefaultEndpointsProtocol(connString);
|
||||
|
||||
const suffixMatch = /EndpointSuffix=([^;]+)/i.exec(connString);
|
||||
if (!!suffixMatch && suffixMatch.length > 0) {
|
||||
|
||||
return `${endpointsProtocol}://${accountName}.table.${suffixMatch[1]}/`;
|
||||
}
|
||||
|
||||
const endpointMatch = /TableEndpoint=([^;]+)/i.exec(connString);
|
||||
return (!!endpointMatch && endpointMatch.length > 0) ? endpointMatch[1] : `${endpointsProtocol}://${accountName}.table.core.windows.net/`;
|
||||
}
|
||||
|
||||
// Replaces 'UseDevelopmentStorage=true' with full Storage Emulator connection string
|
||||
static ExpandEmulatorShortcutIfNeeded(connString: string): string {
|
||||
|
||||
if (connString.includes('UseDevelopmentStorage=true')) {
|
||||
return Settings().storageEmulatorConnectionString;
|
||||
}
|
||||
|
||||
return connString;
|
||||
}
|
||||
|
||||
// Extracts server name from MSSQL Connection String
|
||||
static GetSqlServerName(connString: string): string {
|
||||
const match = /(Data Source|Server)=([^;]+)/i.exec(connString);
|
||||
return (!!match && match.length > 1) ? match[2] : '';
|
||||
}
|
||||
|
||||
// Extracts database name from MSSQL Connection String
|
||||
static GetSqlDatabaseName(connString: string): string {
|
||||
const match = /Initial Catalog=([^;]+)/i.exec(connString);
|
||||
return (!!match && match.length > 0) ? match[1] : '';
|
||||
}
|
||||
|
||||
// Extracts human-readable storage name from a bunch of connection strings
|
||||
static GetStorageName(connStrings: string[]): string {
|
||||
|
||||
const serverName = this.GetSqlServerName(connStrings[0]);
|
||||
|
||||
if (!serverName) {
|
||||
return this.GetAccountName(connStrings[0]);
|
||||
}
|
||||
|
||||
const dbName = this.GetSqlDatabaseName(connStrings[0]);
|
||||
|
||||
return serverName + (!dbName ? '' : '/' + dbName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as rimraf from 'rimraf';
|
||||
|
||||
import { FunctionGraphView } from "./FunctionGraphView";
|
||||
import { traverseFunctionProject } from './az-func-as-a-graph/traverseFunctionProject';
|
||||
import { FunctionsMap, ProxiesMap } from './az-func-as-a-graph/FunctionsMap';
|
||||
|
||||
export type TraversalResult = {
|
||||
functions: FunctionsMap;
|
||||
proxies: ProxiesMap;
|
||||
};
|
||||
|
||||
// Aggregates Function Graph views
|
||||
export class FunctionGraphList {
|
||||
|
||||
constructor(private _context: vscode.ExtensionContext, logChannel?: vscode.OutputChannel) {
|
||||
this._log = !logChannel ? (s: any) => { } : (s: any) => logChannel!.append(s);
|
||||
}
|
||||
|
||||
traverseFunctions(projectPath: string): Promise<TraversalResult> {
|
||||
|
||||
const isCurrentProject = projectPath === vscode.workspace.rootPath;
|
||||
|
||||
if (isCurrentProject && !!this._traversalResult) {
|
||||
return Promise.resolve(this._traversalResult);
|
||||
}
|
||||
|
||||
return traverseFunctionProject(projectPath, this._log).then(result => {
|
||||
|
||||
this._tempFolders.push(...result.tempFolders);
|
||||
|
||||
// Caching current project's functions
|
||||
if (isCurrentProject) {
|
||||
|
||||
this._traversalResult = { functions: result.functions, proxies: result.proxies };
|
||||
|
||||
// And cleanup the cache on any change to the file system
|
||||
if (!!this._watcher) {
|
||||
this._watcher.dispose();
|
||||
}
|
||||
this._watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(projectPath, '**/*'));
|
||||
|
||||
const cacheCleanupRoutine = () => {
|
||||
|
||||
this._traversalResult = undefined;
|
||||
|
||||
if (!!this._watcher) {
|
||||
this._watcher.dispose();
|
||||
this._watcher = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this._watcher.onDidCreate(cacheCleanupRoutine);
|
||||
this._watcher.onDidDelete(cacheCleanupRoutine);
|
||||
this._watcher.onDidChange(cacheCleanupRoutine);
|
||||
}
|
||||
|
||||
return { functions: result.functions, proxies: result.proxies };
|
||||
});
|
||||
}
|
||||
|
||||
visualize(item?: vscode.Uri): void {
|
||||
|
||||
// If host.json was clicked
|
||||
if (!!item && item.scheme === 'file' && item.fsPath.toLowerCase().endsWith('host.json')) {
|
||||
|
||||
this.visualizeProjectPath(path.dirname(item.fsPath));
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultProjectPath = '';
|
||||
const ws = vscode.workspace;
|
||||
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'host.json'))) {
|
||||
defaultProjectPath = ws.rootPath;
|
||||
}
|
||||
|
||||
vscode.window.showInputBox({ value: defaultProjectPath, prompt: 'Local path or link to GitHub repo' }).then(projectPath => {
|
||||
|
||||
if (!!projectPath) {
|
||||
this.visualizeProjectPath(projectPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
visualizeProjectPath(projectPath: string): void {
|
||||
|
||||
this._views.push(new FunctionGraphView(this._context, projectPath, this));
|
||||
}
|
||||
|
||||
// Closes all views
|
||||
cleanup(): void {
|
||||
|
||||
if (!!this._watcher) {
|
||||
this._watcher.dispose();
|
||||
this._watcher = undefined;
|
||||
}
|
||||
|
||||
for (const view of this._views) {
|
||||
view.cleanup();
|
||||
}
|
||||
|
||||
for (var tempFolder of this._tempFolders) {
|
||||
|
||||
this._log(`Removing ${tempFolder}`);
|
||||
try {
|
||||
rimraf.sync(tempFolder)
|
||||
} catch (err) {
|
||||
this._log(`Failed to remove ${tempFolder}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _views: FunctionGraphView[] = [];
|
||||
private _traversalResult?: TraversalResult;
|
||||
private _watcher?: vscode.FileSystemWatcher;
|
||||
private _tempFolders: string[] = [];
|
||||
private _log: (line: string) => void;
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { MonitorView } from './MonitorView';
|
||||
import { FunctionGraphList, TraversalResult } from './FunctionGraphList';
|
||||
|
||||
// Represents the function graph view
|
||||
export class FunctionGraphView
|
||||
{
|
||||
constructor(private _context: vscode.ExtensionContext,
|
||||
private _functionProjectPath: string,
|
||||
private _functionGraphList: FunctionGraphList) {
|
||||
|
||||
this._staticsFolder = path.join(this._context.extensionPath, 'backend', 'DfmStatics');
|
||||
|
||||
this._webViewPanel = this.showWebView();
|
||||
}
|
||||
|
||||
// Closes this web view
|
||||
cleanup(): void {
|
||||
|
||||
if (!!this._webViewPanel) {
|
||||
this._webViewPanel.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Path to html statics
|
||||
private _staticsFolder: string;
|
||||
|
||||
// Reference to the already opened WebView with the main page
|
||||
private _webViewPanel: vscode.WebviewPanel | null = null;
|
||||
|
||||
// Functions and proxies currently shown
|
||||
private _traversalResult?: TraversalResult;
|
||||
|
||||
private static readonly ViewType = 'durableFunctionsMonitorFunctionGraph';
|
||||
|
||||
// Opens a WebView with function graph page in it
|
||||
private showWebView(): vscode.WebviewPanel {
|
||||
|
||||
const title = `Functions Graph (${this._functionProjectPath})`;
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
FunctionGraphView.ViewType,
|
||||
title,
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
retainContextWhenHidden: true,
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.file(this._staticsFolder)]
|
||||
}
|
||||
);
|
||||
|
||||
var html = fs.readFileSync(path.join(this._staticsFolder, 'index.html'), 'utf8');
|
||||
html = MonitorView.fixLinksToStatics(html, this._staticsFolder, panel.webview);
|
||||
|
||||
html = this.embedTheme(html);
|
||||
html = this.embedParams(html, !!this._functionProjectPath);
|
||||
|
||||
panel.webview.html = html;
|
||||
|
||||
// handle events from WebView
|
||||
panel.webview.onDidReceiveMessage(request => {
|
||||
|
||||
switch (request.method) {
|
||||
case 'SaveAs':
|
||||
|
||||
// Just to be extra sure...
|
||||
if (!MonitorView.looksLikeSvg(request.data)) {
|
||||
vscode.window.showErrorMessage(`Invalid data format. Save failed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Saving some file to local hard drive
|
||||
vscode.window.showSaveDialog({ filters: { 'SVG Images': ['svg'] } }).then(filePath => {
|
||||
|
||||
if (!filePath || !filePath.fsPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFile(filePath!.fsPath, request.data, err => {
|
||||
if (!err) {
|
||||
vscode.window.showInformationMessage(`Saved to ${filePath!.fsPath}`);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(`Failed to save. ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
|
||||
case 'SaveFunctionGraphAsJson':
|
||||
|
||||
if (!this._traversalResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Saving some file to local hard drive
|
||||
vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file('dfm-func-map.json'), filters: { 'JSON': ['json'] } }).then(filePath => {
|
||||
|
||||
if (!filePath || !filePath.fsPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFile(filePath!.fsPath, JSON.stringify(this._traversalResult, null, 3), err => {
|
||||
if (!err) {
|
||||
vscode.window.showInformationMessage(`Saved to ${filePath!.fsPath}`);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(`Failed to save. ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
|
||||
case 'GotoFunctionCode':
|
||||
|
||||
if (!this._traversalResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
const functionName = request.url;
|
||||
var functionOrProxy: any = null;
|
||||
|
||||
if (functionName.startsWith('proxy.')) {
|
||||
|
||||
functionOrProxy = this._traversalResult.proxies[functionName.substr(6)];
|
||||
|
||||
} else {
|
||||
|
||||
functionOrProxy = this._traversalResult.functions[functionName];
|
||||
}
|
||||
|
||||
vscode.window.showTextDocument(vscode.Uri.file(functionOrProxy.filePath)).then(ed => {
|
||||
|
||||
const pos = ed.document.positionAt(!!functionOrProxy.pos ? functionOrProxy.pos : 0);
|
||||
|
||||
ed.selection = new vscode.Selection(pos, pos);
|
||||
ed.revealRange(new vscode.Range(pos, pos));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercepting request for Function Map
|
||||
if (request.method === "GET" && request.url === '/function-map') {
|
||||
|
||||
if (!this._functionProjectPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = request.id;
|
||||
this._functionGraphList.traverseFunctions(this._functionProjectPath).then(result => {
|
||||
|
||||
this._traversalResult = result;
|
||||
|
||||
panel.webview.postMessage({
|
||||
id: requestId, data: {
|
||||
functions: result.functions,
|
||||
proxies: result.proxies
|
||||
}
|
||||
});
|
||||
|
||||
}, err => {
|
||||
// err might fail to serialize here, so passing err.message only
|
||||
panel.webview.postMessage({ id: requestId, err: { message: err.message } });
|
||||
});
|
||||
}
|
||||
|
||||
}, undefined, this._context.subscriptions);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
// Embeds the current color theme
|
||||
private embedTheme(html: string): string {
|
||||
|
||||
if ([2, 3].includes((vscode.window as any).activeColorTheme.kind)) {
|
||||
return html.replace('<script>var DfmClientConfig={}</script>', '<script>var DfmClientConfig={\'theme\':\'dark\'}</script>');
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// Embeds some other parameters in the HTML served
|
||||
private embedParams(html: string, isFunctionGraphAvailable: boolean): string {
|
||||
return html
|
||||
.replace(
|
||||
`<script>var IsFunctionGraphAvailable=0</script>`,
|
||||
`<script>var IsFunctionGraphAvailable=${!!isFunctionGraphAvailable ? 1 : 0}</script>`
|
||||
)
|
||||
.replace(
|
||||
`<script>var DfmViewMode=0</script>`,
|
||||
`<script>var DfmViewMode=1</script>`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,393 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
import { MonitorView } from "./MonitorView";
|
||||
import { MonitorViewList } from "./MonitorViewList";
|
||||
import { StorageAccountTreeItem } from './StorageAccountTreeItem';
|
||||
import { StorageAccountTreeItems } from './StorageAccountTreeItems';
|
||||
import { TaskHubTreeItem } from './TaskHubTreeItem';
|
||||
import { SubscriptionTreeItems } from './SubscriptionTreeItems';
|
||||
import { SubscriptionTreeItem } from './SubscriptionTreeItem';
|
||||
import { FunctionGraphList } from './FunctionGraphList';
|
||||
import { Settings, UpdateSetting } from './Settings';
|
||||
import { StorageConnectionSettings } from './BackendProcess';
|
||||
|
||||
// Root object in the hierarchy. Also serves data for the TreeView.
|
||||
export class MonitorTreeDataProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
|
||||
|
||||
constructor(private _context: vscode.ExtensionContext, functionGraphList: FunctionGraphList, logChannel?: vscode.OutputChannel) {
|
||||
|
||||
this._monitorViews = new MonitorViewList(this._context,
|
||||
functionGraphList,
|
||||
() => this._onDidChangeTreeData.fire(),
|
||||
!logChannel ? () => { } : (l) => logChannel.append(l));
|
||||
|
||||
const resourcesFolderPath = this._context.asAbsolutePath('resources');
|
||||
this._storageAccounts = new StorageAccountTreeItems(resourcesFolderPath, this._monitorViews);
|
||||
|
||||
// Using Azure Account extension to connect to Azure, get subscriptions etc.
|
||||
const azureAccountExtension = vscode.extensions.getExtension('ms-vscode.azure-account');
|
||||
|
||||
// Typings for azureAccount are here: https://github.com/microsoft/vscode-azure-account/blob/master/src/azure-account.api.d.ts
|
||||
const azureAccount = !!azureAccountExtension ? azureAccountExtension.exports : undefined;
|
||||
|
||||
if (!!azureAccount && !!azureAccount.onFiltersChanged) {
|
||||
|
||||
// When user changes their list of filtered subscriptions (or just relogins to Azure)...
|
||||
this._context.subscriptions.push(azureAccount.onFiltersChanged(() => this.refresh()));
|
||||
}
|
||||
|
||||
this._subscriptions = new SubscriptionTreeItems(
|
||||
this._context,
|
||||
azureAccount,
|
||||
this._storageAccounts,
|
||||
() => this._onDidChangeTreeData.fire(),
|
||||
resourcesFolderPath,
|
||||
!logChannel ? () => { } : (l) => logChannel.appendLine(l)
|
||||
);
|
||||
|
||||
// Also trying to parse current project's files and create a Task Hub node for them
|
||||
const connSettingsFromCurrentProject = this._monitorViews.getStorageConnectionSettingsFromCurrentProject();
|
||||
if (!!connSettingsFromCurrentProject) {
|
||||
this._storageAccounts.addNodeForConnectionSettings(connSettingsFromCurrentProject);
|
||||
}
|
||||
}
|
||||
|
||||
// Does nothing, actually
|
||||
getTreeItem(element: vscode.TreeItem): vscode.TreeItem { return element; }
|
||||
|
||||
// Returns the children of `element` or root if no element is passed.
|
||||
getChildren(element?: vscode.TreeItem): Promise<vscode.TreeItem[]> {
|
||||
|
||||
if (!element) {
|
||||
return this._subscriptions.getNonEmptyNodes();
|
||||
}
|
||||
|
||||
const subscriptionNode = element as SubscriptionTreeItem;
|
||||
if (subscriptionNode.isSubscriptionTreeItem) {
|
||||
|
||||
const storageAccountNodes = subscriptionNode.storageAccountNodes;
|
||||
|
||||
// Initially collapsing those storage nodes, that don't have attached TaskHubs at the moment
|
||||
for (const n of storageAccountNodes) {
|
||||
if (!n.isAttached) {
|
||||
n.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(storageAccountNodes);
|
||||
}
|
||||
|
||||
// If this is a storage account tree item
|
||||
const item = element as StorageAccountTreeItem;
|
||||
if (this._storageAccounts.nodes.includes(item)) {
|
||||
return Promise.resolve(item.childItems);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
// Handles 'Attach' context menu item or a click on a tree node
|
||||
attachToTaskHub(taskHubItem: TaskHubTreeItem | null, messageToWebView: any = undefined): void {
|
||||
|
||||
if (!!this._inProgress) {
|
||||
console.log(`Another operation already in progress...`);
|
||||
return;
|
||||
}
|
||||
|
||||
// This could happen, if the command is executed via Command Palette (and not via menu)
|
||||
if (!taskHubItem) {
|
||||
this.createOrActivateMonitorView(false, messageToWebView);
|
||||
return;
|
||||
}
|
||||
|
||||
this._inProgress = true;
|
||||
const monitorView = this._monitorViews.getOrCreateFromStorageConnectionSettings(taskHubItem.storageConnectionSettings);
|
||||
|
||||
monitorView.show(messageToWebView).then(() => {
|
||||
|
||||
this._onDidChangeTreeData.fire();
|
||||
this._inProgress = false;
|
||||
|
||||
}, (err: any) => {
|
||||
// .finally() doesn't work here - vscode.window.showErrorMessage() blocks it until user
|
||||
// closes the error message. As a result, _inProgress remains true until then, which blocks all commands
|
||||
|
||||
this._inProgress = false;
|
||||
vscode.window.showErrorMessage(!err.message ? err : err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Triggers when F5 is being hit
|
||||
handleOnDebugSessionStarted() {
|
||||
|
||||
if (!!this._monitorViews.isAnyMonitorViewVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const DfmDoNotAskUponDebugSession = 'DfmDoNotAskUponDebugSession';
|
||||
const doNotAsk = this._context.globalState.get(DfmDoNotAskUponDebugSession, false);
|
||||
|
||||
if (!Settings().showWhenDebugSessionStarts && !!doNotAsk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultTaskHubName = 'TestHubName';
|
||||
const curConnSettings = this._monitorViews.getStorageConnectionSettingsFromCurrentProject(defaultTaskHubName);
|
||||
if (!curConnSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Settings().showWhenDebugSessionStarts) {
|
||||
|
||||
const prompt = `Do you want Durable Functions Monitor to be automatically shown when you start debugging a Durable Functions project? You can always change this preference via Settings.`;
|
||||
vscode.window.showWarningMessage(prompt, `Yes`, `No, and don't ask again`).then(answer => {
|
||||
|
||||
if (answer === `No, and don't ask again`) {
|
||||
|
||||
UpdateSetting('showWhenDebugSessionStarts', false);
|
||||
this._context.globalState.update(DfmDoNotAskUponDebugSession, true);
|
||||
|
||||
} else if (answer === `Yes`) {
|
||||
|
||||
UpdateSetting('showWhenDebugSessionStarts', true);
|
||||
|
||||
this.showUponDebugSession(
|
||||
curConnSettings.hubName !== defaultTaskHubName ? curConnSettings : undefined
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
this.showUponDebugSession(
|
||||
curConnSettings.hubName !== defaultTaskHubName ? curConnSettings : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handles 'Detach' context menu item
|
||||
detachFromTaskHub(storageAccountItem: StorageAccountTreeItem) {
|
||||
|
||||
if (!storageAccountItem) {
|
||||
vscode.window.showInformationMessage('This command is only available via context menu');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!this._inProgress) {
|
||||
console.log(`Another operation already in progress...`);
|
||||
return;
|
||||
}
|
||||
this._inProgress = true;
|
||||
|
||||
this._monitorViews.detachBackend(storageAccountItem.storageConnStrings).then(() => {
|
||||
|
||||
this._onDidChangeTreeData.fire();
|
||||
this._inProgress = false;
|
||||
|
||||
}, err => {
|
||||
this._inProgress = false;
|
||||
vscode.window.showErrorMessage(`Failed to detach from Task Hub. ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Handles 'Delete Task Hub' context menu item
|
||||
deleteTaskHub(taskHubItem: TaskHubTreeItem) {
|
||||
|
||||
if (!taskHubItem) {
|
||||
vscode.window.showInformationMessage('This command is only available via context menu');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!this._inProgress) {
|
||||
console.log(`Another operation already in progress...`);
|
||||
return;
|
||||
}
|
||||
|
||||
const monitorView = this._monitorViews.getOrCreateFromStorageConnectionSettings(taskHubItem.storageConnectionSettings);
|
||||
if (!monitorView) {
|
||||
console.log(`Tried to delete a detached Task Hub`);
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = `This will permanently delete all Azure Storage resources used by '${taskHubItem.label}' orchestration service. There should be no running Function instances for this Task Hub present. Are you sure you want to proceed?`;
|
||||
vscode.window.showWarningMessage(prompt, 'Yes', 'No').then(answer => {
|
||||
|
||||
if (answer === 'Yes') {
|
||||
|
||||
this._inProgress = true;
|
||||
monitorView.deleteTaskHub().then(() => {
|
||||
|
||||
taskHubItem.removeFromTree();
|
||||
|
||||
this._onDidChangeTreeData.fire();
|
||||
this._inProgress = false;
|
||||
|
||||
}, (err) => {
|
||||
this._inProgress = false;
|
||||
vscode.window.showErrorMessage(`Failed to delete Task Hub. ${err}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handles 'Open in Storage Explorer' context menu item
|
||||
async openTableInStorageExplorer(taskHubItem: TaskHubTreeItem, table: 'Instances' | 'History') {
|
||||
|
||||
// Using Azure Storage extension for this
|
||||
var storageExt = vscode.extensions.getExtension('ms-azuretools.vscode-azurestorage');
|
||||
if (!storageExt) {
|
||||
vscode.window.showErrorMessage(`For this to work, please, install [Azure Storage](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurestorage) extension.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if (!storageExt.isActive) {
|
||||
await storageExt.activate();
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand('azureStorage.openTable', {
|
||||
|
||||
root: {
|
||||
storageAccountId: taskHubItem.storageAccountId,
|
||||
subscriptionId: taskHubItem.subscriptionId
|
||||
},
|
||||
|
||||
tableName: taskHubItem.hubName + table
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(`Failed to execute command. ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handles 'Attach' button
|
||||
attachToAnotherTaskHub() {
|
||||
|
||||
this.createOrActivateMonitorView(true);
|
||||
}
|
||||
|
||||
// Handles 'Refresh' button
|
||||
refresh() {
|
||||
this._subscriptions.cleanup();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
// Handles 'Detach from all Task Hubs' button
|
||||
detachFromAllTaskHubs() {
|
||||
|
||||
if (!!this._inProgress) {
|
||||
console.log(`Another operation already in progress...`);
|
||||
return;
|
||||
}
|
||||
this._inProgress = true;
|
||||
|
||||
this.cleanup().catch(err => {
|
||||
vscode.window.showErrorMessage(`Failed to detach from Task Hub. ${err}`);
|
||||
}).finally(() => {
|
||||
this._onDidChangeTreeData.fire();
|
||||
this._inProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Handles 'Go to instanceId...' context menu item
|
||||
gotoInstanceId(taskHubItem: TaskHubTreeItem | null) {
|
||||
|
||||
// Trying to get a running backend instance.
|
||||
// If the relevant MonitorView is currently not visible, don't want to show it - that's why all the custom logic here.
|
||||
var monitorView = !taskHubItem ?
|
||||
this._monitorViews.firstOrDefault() :
|
||||
this._monitorViews.getOrCreateFromStorageConnectionSettings(taskHubItem.storageConnectionSettings);
|
||||
|
||||
if (!!monitorView) {
|
||||
|
||||
monitorView.gotoInstanceId();
|
||||
|
||||
} else {
|
||||
|
||||
this.createOrActivateMonitorView(false).then(view => {
|
||||
if (!!view) {
|
||||
|
||||
// Not sure why this timeout here is needed, but without it the quickPick isn't shown
|
||||
setTimeout(() => {
|
||||
view.gotoInstanceId();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Stops all backend processes and closes all views
|
||||
cleanup(): Promise<any> {
|
||||
return this._monitorViews.cleanup();
|
||||
}
|
||||
|
||||
private _inProgress: boolean = false;
|
||||
|
||||
private _monitorViews: MonitorViewList;
|
||||
private _storageAccounts: StorageAccountTreeItems;
|
||||
private _subscriptions: SubscriptionTreeItems;
|
||||
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem | undefined> = new vscode.EventEmitter<StorageAccountTreeItem | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem | undefined> = this._onDidChangeTreeData.event;
|
||||
|
||||
// Shows or makes active the main view
|
||||
private createOrActivateMonitorView(alwaysCreateNew: boolean, messageToWebView: any = undefined): Promise<MonitorView | null> {
|
||||
|
||||
if (!!this._inProgress) {
|
||||
console.log(`Another operation already in progress...`);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return new Promise<MonitorView>((resolve, reject) => {
|
||||
|
||||
this._monitorViews.getOrAdd(alwaysCreateNew).then(monitorView => {
|
||||
|
||||
this._inProgress = true;
|
||||
|
||||
monitorView.show(messageToWebView).then(() => {
|
||||
|
||||
this._storageAccounts.addNodeForMonitorView(monitorView);
|
||||
this._onDidChangeTreeData.fire();
|
||||
this._inProgress = false;
|
||||
|
||||
resolve(monitorView);
|
||||
|
||||
}, (err) => {
|
||||
// .finally() doesn't work here - vscode.window.showErrorMessage() blocks it until user
|
||||
// closes the error message. As a result, _inProgress remains true until then, which blocks all commands
|
||||
this._inProgress = false;
|
||||
vscode.window.showErrorMessage(!err.message ? err : err.message);
|
||||
});
|
||||
|
||||
}, vscode.window.showErrorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Shows the main view upon a debug session
|
||||
private showUponDebugSession(connSettingsFromCurrentProject?: StorageConnectionSettings) {
|
||||
|
||||
if (!!this._inProgress) {
|
||||
console.log(`Another operation already in progress...`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._monitorViews.showUponDebugSession(connSettingsFromCurrentProject).then(monitorView => {
|
||||
|
||||
this._inProgress = true;
|
||||
|
||||
monitorView.show().then(() => {
|
||||
|
||||
this._storageAccounts.addNodeForMonitorView(monitorView);
|
||||
this._onDidChangeTreeData.fire();
|
||||
this._inProgress = false;
|
||||
|
||||
}, (err) => {
|
||||
// .finally() doesn't work here - vscode.window.showErrorMessage() blocks it until user
|
||||
// closes the error message. As a result, _inProgress remains true until then, which blocks all commands
|
||||
this._inProgress = false;
|
||||
vscode.window.showErrorMessage(!err.message ? err : err.message);
|
||||
});
|
||||
}, vscode.window.showErrorMessage);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,424 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import axios from 'axios';
|
||||
|
||||
import * as SharedConstants from './SharedConstants';
|
||||
|
||||
import { BackendProcess, StorageConnectionSettings } from './BackendProcess';
|
||||
import { ConnStringUtils } from './ConnStringUtils';
|
||||
import { Settings } from './Settings';
|
||||
import { FunctionGraphList } from './FunctionGraphList';
|
||||
|
||||
// Represents the main view, along with all detailed views
|
||||
export class MonitorView
|
||||
{
|
||||
// Storage Connection settings (connString and hubName) of this Monitor View
|
||||
get storageConnectionSettings(): StorageConnectionSettings {
|
||||
return new StorageConnectionSettings(this._backend.storageConnectionStrings, this._hubName);
|
||||
}
|
||||
|
||||
get isVisible(): boolean {
|
||||
return !!this._webViewPanel;
|
||||
}
|
||||
|
||||
// Path to html statics
|
||||
get staticsFolder(): string {
|
||||
return path.join(this._backend.binariesFolder, 'DfmStatics');
|
||||
}
|
||||
|
||||
constructor(private _context: vscode.ExtensionContext,
|
||||
private _backend: BackendProcess,
|
||||
private _hubName: string,
|
||||
private _functionGraphList: FunctionGraphList,
|
||||
private _onViewStatusChanged: () => void) {
|
||||
|
||||
const ws = vscode.workspace;
|
||||
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'host.json'))) {
|
||||
this._functionProjectPath = ws.rootPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Closes all WebViews
|
||||
cleanup(): void {
|
||||
|
||||
for (var childPanel of this._childWebViewPanels) {
|
||||
childPanel.dispose();
|
||||
}
|
||||
this._childWebViewPanels = [];
|
||||
|
||||
if (!!this._webViewPanel) {
|
||||
this._webViewPanel.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Shows or makes active the main view
|
||||
show(messageToWebView: any = undefined): Promise<void> {
|
||||
|
||||
if (!!this._webViewPanel) {
|
||||
// Didn't find a way to check whether the panel still exists.
|
||||
// So just have to catch a "panel disposed" exception here.
|
||||
try {
|
||||
|
||||
this._webViewPanel.reveal();
|
||||
if (!!messageToWebView) {
|
||||
// BUG: WebView might actually appear in 3 states: disposed, visible and inactive.
|
||||
// Didn't find the way to distinguish the last two.
|
||||
// But when it is inactive, it will be activated with above reveal() method,
|
||||
// and then miss this message we're sending here. No good solution for this problem so far...
|
||||
this._webViewPanel.webview.postMessage(messageToWebView);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
this._webViewPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
this._backend.getBackend().then(() => {
|
||||
|
||||
try {
|
||||
this._webViewPanel = this.showWebView('', messageToWebView);
|
||||
|
||||
this._webViewPanel.onDidDispose(() => {
|
||||
this._webViewPanel = null;
|
||||
this._onViewStatusChanged();
|
||||
});
|
||||
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(`WebView failed: ${err}`);
|
||||
}
|
||||
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Permanently deletes all underlying Storage resources for this Task Hub
|
||||
deleteTaskHub(): Promise<void> {
|
||||
|
||||
if (!this._backend.backendUrl) {
|
||||
return Promise.reject('Backend is not started');
|
||||
}
|
||||
|
||||
const headers: any = {};
|
||||
headers[SharedConstants.NonceHeaderName] = this._backend.backendCommunicationNonce;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
const url = `${this._backend.backendUrl}/--${this._hubName}/delete-task-hub`;
|
||||
axios.post(url, {}, { headers }).then(() => {
|
||||
this.cleanup();
|
||||
resolve();
|
||||
}, err => reject(err.message));
|
||||
});
|
||||
}
|
||||
|
||||
// Handles 'Goto instanceId...' context menu item
|
||||
gotoInstanceId() {
|
||||
|
||||
this.askForInstanceId().then(instanceId => {
|
||||
|
||||
// Opening another WebView
|
||||
this._childWebViewPanels.push(this.showWebView(instanceId));
|
||||
});
|
||||
}
|
||||
|
||||
// Converts script and CSS links
|
||||
static fixLinksToStatics(originalHtml: string, pathToBackend: string, webView: vscode.Webview): string {
|
||||
|
||||
var resultHtml: string = originalHtml;
|
||||
|
||||
const regex = / (href|src)="\/([0-9a-z.\/]+)"/ig;
|
||||
var match: RegExpExecArray | null;
|
||||
while (match = regex.exec(originalHtml)) {
|
||||
|
||||
const relativePath = match[2];
|
||||
const localPath = path.join(pathToBackend, relativePath);
|
||||
const newPath = webView.asWebviewUri(vscode.Uri.file(localPath)).toString();
|
||||
|
||||
resultHtml = resultHtml.replace(`/${relativePath}`, newPath);
|
||||
}
|
||||
|
||||
return resultHtml;
|
||||
}
|
||||
|
||||
// Validates incoming SVG, just to be extra sure...
|
||||
static looksLikeSvg(data: string): boolean {
|
||||
return data.startsWith('<svg') && data.endsWith('</svg>') && !data.includes('<script');
|
||||
}
|
||||
|
||||
// Reference to the already opened WebView with the main page
|
||||
private _webViewPanel: vscode.WebviewPanel | null = null;
|
||||
|
||||
// Reference to all child WebViews
|
||||
private _childWebViewPanels: vscode.WebviewPanel[] = [];
|
||||
|
||||
// Functions and proxies currently shown
|
||||
private _functionsAndProxies: { [name: string]: { filePath?: string, pos?: number } } = {};
|
||||
|
||||
private _functionProjectPath: string = '';
|
||||
|
||||
private static readonly ViewType = 'durableFunctionsMonitor';
|
||||
private static readonly GlobalStateName = MonitorView.ViewType + 'WebViewState';
|
||||
|
||||
// Opens a WebView with main page or orchestration page in it
|
||||
private showWebView(orchestrationId: string = '', messageToWebView: any = undefined): vscode.WebviewPanel {
|
||||
|
||||
const title = (!!orchestrationId) ?
|
||||
`Instance '${orchestrationId}'`
|
||||
:
|
||||
`Durable Functions Monitor (${this.taskHubFullTitle})`;
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
MonitorView.ViewType,
|
||||
title,
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
retainContextWhenHidden: true,
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.file(this.staticsFolder)]
|
||||
}
|
||||
);
|
||||
|
||||
var html = fs.readFileSync(path.join(this.staticsFolder, 'index.html'), 'utf8');
|
||||
html = MonitorView.fixLinksToStatics(html, this.staticsFolder, panel.webview);
|
||||
|
||||
// Also passing persisted settings via HTML
|
||||
const webViewState = this._context.globalState.get(MonitorView.GlobalStateName, {});
|
||||
|
||||
html = this.embedOrchestrationIdAndState(html, orchestrationId, webViewState);
|
||||
html = this.embedIsFunctionGraphAvailable(html, !!this._functionProjectPath);
|
||||
html = this.embedThemeAndSettings(html);
|
||||
|
||||
panel.webview.html = html;
|
||||
|
||||
// handle events from WebView
|
||||
panel.webview.onDidReceiveMessage(request => {
|
||||
|
||||
switch (request.method) {
|
||||
case 'IAmReady':
|
||||
// Sending an initial message (if any), when the webView is ready
|
||||
if (!!messageToWebView) {
|
||||
panel.webview.postMessage(messageToWebView);
|
||||
messageToWebView = undefined;
|
||||
}
|
||||
return;
|
||||
case 'PersistState':
|
||||
// Persisting state values
|
||||
const webViewState = this._context.globalState.get(MonitorView.GlobalStateName, {}) as any;
|
||||
webViewState[request.key] = request.data;
|
||||
this._context.globalState.update(MonitorView.GlobalStateName, webViewState);
|
||||
return;
|
||||
case 'OpenInNewWindow':
|
||||
// Opening another WebView
|
||||
this._childWebViewPanels.push(this.showWebView(request.url));
|
||||
return;
|
||||
case 'SaveAs':
|
||||
|
||||
// Just to be extra sure...
|
||||
if (!MonitorView.looksLikeSvg(request.data)) {
|
||||
vscode.window.showErrorMessage(`Invalid data format. Save failed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Saving some file to local hard drive
|
||||
vscode.window.showSaveDialog({ filters: { 'SVG Images': ['svg'] } }).then(filePath => {
|
||||
|
||||
if (!filePath || !filePath.fsPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFile(filePath!.fsPath, request.data, err => {
|
||||
if (!err) {
|
||||
vscode.window.showInformationMessage(`Saved to ${filePath!.fsPath}`);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(`Failed to save. ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
case 'GotoFunctionCode':
|
||||
|
||||
const func = this._functionsAndProxies[request.url];
|
||||
if (!!func && !!func.filePath) {
|
||||
|
||||
vscode.window.showTextDocument(vscode.Uri.file(func.filePath)).then(ed => {
|
||||
|
||||
const pos = ed.document.positionAt(!!func.pos ? func.pos : 0);
|
||||
|
||||
ed.selection = new vscode.Selection(pos, pos);
|
||||
ed.revealRange(new vscode.Range(pos, pos));
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
case 'VisualizeFunctionsAsAGraph':
|
||||
|
||||
const ws = vscode.workspace;
|
||||
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'host.json'))) {
|
||||
this._functionGraphList.visualizeProjectPath(ws.rootPath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercepting request for Function Map
|
||||
if (request.method === "GET" && request.url === '/function-map') {
|
||||
|
||||
if (!this._functionProjectPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = request.id;
|
||||
this._functionGraphList.traverseFunctions(this._functionProjectPath).then(result => {
|
||||
|
||||
this._functionsAndProxies = {};
|
||||
for (const name in result.functions) {
|
||||
this._functionsAndProxies[name] = result.functions[name];
|
||||
}
|
||||
for (const name in result.proxies) {
|
||||
this._functionsAndProxies['proxy.' + name] = result.proxies[name];
|
||||
}
|
||||
|
||||
panel.webview.postMessage({
|
||||
id: requestId, data: {
|
||||
functions: result.functions,
|
||||
proxies: result.proxies
|
||||
}
|
||||
});
|
||||
|
||||
}, err => {
|
||||
// err might fail to serialize here, so passing err.message only
|
||||
panel.webview.postMessage({ id: requestId, err: { message: err.message } });
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Then it's just a propagated HTTP request
|
||||
const requestId = request.id;
|
||||
|
||||
const headers: any = {};
|
||||
headers[SharedConstants.NonceHeaderName] = this._backend.backendCommunicationNonce;
|
||||
|
||||
// Workaround for https://github.com/Azure/azure-functions-durable-extension/issues/1926
|
||||
var hubName = this._hubName;
|
||||
if (hubName === 'TestHubName' && request.method === 'POST' && request.url.match(/\/(orchestrations|restart)$/i)) {
|
||||
// Turning task hub name into lower case, this allows to bypass function name validation
|
||||
hubName = 'testhubname';
|
||||
}
|
||||
|
||||
axios.request({
|
||||
url: `${this._backend.backendUrl}/--${hubName}${request.url}`,
|
||||
method: request.method,
|
||||
data: request.data,
|
||||
headers
|
||||
}).then(response => {
|
||||
|
||||
panel.webview.postMessage({ id: requestId, data: response.data });
|
||||
}, err => {
|
||||
|
||||
panel.webview.postMessage({ id: requestId, err: { message: err.message, response: { data: !err.response ? undefined : err.response.data } } });
|
||||
});
|
||||
|
||||
}, undefined, this._context.subscriptions);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
// Embeds the current color theme
|
||||
private embedThemeAndSettings(html: string): string {
|
||||
|
||||
const theme = [2, 3].includes((vscode.window as any).activeColorTheme.kind) ? 'dark' : 'light';
|
||||
|
||||
return html.replace('<script>var DfmClientConfig={}</script>',
|
||||
`<script>var DfmClientConfig={'theme':'${theme}','showTimeAs':'${Settings().showTimeAs}'}</script>`);
|
||||
}
|
||||
|
||||
// Embeds the orchestrationId in the HTML served
|
||||
private embedOrchestrationIdAndState(html: string, orchestrationId: string, state: any): string {
|
||||
return html.replace(
|
||||
`<script>var OrchestrationIdFromVsCode="",StateFromVsCode={}</script>`,
|
||||
`<script>var OrchestrationIdFromVsCode="${orchestrationId}",StateFromVsCode=${JSON.stringify(state)}</script>`
|
||||
);
|
||||
}
|
||||
|
||||
// Embeds the isFunctionGraphAvailable flag in the HTML served
|
||||
private embedIsFunctionGraphAvailable(html: string, isFunctionGraphAvailable: boolean): string {
|
||||
|
||||
if (!isFunctionGraphAvailable) {
|
||||
return html;
|
||||
}
|
||||
|
||||
return html.replace(
|
||||
`<script>var IsFunctionGraphAvailable=0</script>`,
|
||||
`<script>var IsFunctionGraphAvailable=1</script>`
|
||||
);
|
||||
}
|
||||
|
||||
private askForInstanceId(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
|
||||
var instanceId = '';
|
||||
const instanceIdPick = vscode.window.createQuickPick();
|
||||
|
||||
instanceIdPick.onDidHide(() => instanceIdPick.dispose());
|
||||
|
||||
instanceIdPick.onDidChangeSelection(items => {
|
||||
if (!!items && !!items.length) {
|
||||
instanceId = items[0].label;
|
||||
}
|
||||
});
|
||||
|
||||
// Still allowing to type free text
|
||||
instanceIdPick.onDidChangeValue(value => {
|
||||
instanceId = value;
|
||||
|
||||
// Loading suggestions from backend
|
||||
if (instanceId.length > 1) {
|
||||
this.getInstanceIdSuggestions(instanceId).then(suggestions => {
|
||||
|
||||
instanceIdPick.items = suggestions.map(id => {
|
||||
return { label: id };
|
||||
});
|
||||
});
|
||||
} else {
|
||||
instanceIdPick.items = [];
|
||||
}
|
||||
});
|
||||
|
||||
instanceIdPick.onDidAccept(() => {
|
||||
if (!!instanceId) {
|
||||
resolve(instanceId);
|
||||
}
|
||||
instanceIdPick.hide();
|
||||
});
|
||||
|
||||
instanceIdPick.title = `(${this.taskHubFullTitle}) instanceId to go to:`;
|
||||
|
||||
instanceIdPick.show();
|
||||
// If nothing is selected, leaving the promise unresolved, so nothing more happens
|
||||
});
|
||||
}
|
||||
|
||||
// Human-readable TaskHub title in form '[storage-account]/[task-hub]'
|
||||
private get taskHubFullTitle(): string {
|
||||
|
||||
return `${ConnStringUtils.GetStorageName(this._backend.storageConnectionStrings)}/${this._hubName}`;
|
||||
}
|
||||
|
||||
// Returns orchestration/entity instanceIds that start with prefix
|
||||
private getInstanceIdSuggestions(prefix: string): Promise<string[]> {
|
||||
|
||||
const headers: any = {};
|
||||
headers[SharedConstants.NonceHeaderName] = this._backend.backendCommunicationNonce;
|
||||
|
||||
return axios.get(`${this._backend.backendUrl}/--${this._hubName}/id-suggestions(prefix='${prefix}')`, { headers })
|
||||
.then(response => {
|
||||
return response.data as string[];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,418 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import axios from 'axios';
|
||||
|
||||
import { ConnStringUtils } from "./ConnStringUtils";
|
||||
|
||||
import { MonitorView } from "./MonitorView";
|
||||
import { BackendProcess, StorageConnectionSettings, CreateAuthHeadersForTableStorage } from './BackendProcess';
|
||||
import { Settings } from './Settings';
|
||||
import { FunctionGraphList } from './FunctionGraphList';
|
||||
|
||||
// Represents all MonitorViews created so far
|
||||
export class MonitorViewList {
|
||||
|
||||
constructor(private _context: vscode.ExtensionContext,
|
||||
private _functionGraphList: FunctionGraphList,
|
||||
private _onViewStatusChanged: () => void,
|
||||
private _log: (line: string) => void) {
|
||||
}
|
||||
|
||||
isAnyMonitorViewVisible(): boolean {
|
||||
return Object.keys(this._monitorViews).some(k => !!this._monitorViews[k] && this._monitorViews[k].isVisible);
|
||||
}
|
||||
|
||||
isMonitorViewVisible(connSettings: StorageConnectionSettings): boolean {
|
||||
const monitorView = this._monitorViews[connSettings.hashKey];
|
||||
return !!monitorView && monitorView.isVisible;
|
||||
}
|
||||
|
||||
// Creates a new MonitorView with provided connection settings
|
||||
getOrCreateFromStorageConnectionSettings(connSettings: StorageConnectionSettings): MonitorView {
|
||||
|
||||
var monitorView = this._monitorViews[connSettings.hashKey];
|
||||
if (!!monitorView) {
|
||||
return monitorView;
|
||||
}
|
||||
|
||||
monitorView = new MonitorView(this._context,
|
||||
this.getOrAddBackend(connSettings),
|
||||
connSettings.hubName,
|
||||
this._functionGraphList,
|
||||
this._onViewStatusChanged);
|
||||
|
||||
this._monitorViews[connSettings.hashKey] = monitorView;
|
||||
return monitorView;
|
||||
}
|
||||
|
||||
// Gets an existing (first in the list) MonitorView,
|
||||
// or initializes a new one by asking user for connection settings
|
||||
getOrAdd(alwaysCreateNew: boolean): Promise<MonitorView> {
|
||||
|
||||
const keys = Object.keys(this._monitorViews);
|
||||
if (!alwaysCreateNew && keys.length > 0) {
|
||||
return Promise.resolve(this._monitorViews[keys[0]]);
|
||||
}
|
||||
|
||||
return new Promise<MonitorView>((resolve, reject) => {
|
||||
this.askForStorageConnectionSettings().then(connSettings => {
|
||||
|
||||
const monitorView = this.getOrCreateFromStorageConnectionSettings(connSettings);
|
||||
resolve(monitorView);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
firstOrDefault(): MonitorView | null {
|
||||
|
||||
const keys = Object.keys(this._monitorViews);
|
||||
if (keys.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._monitorViews[keys[0]];
|
||||
}
|
||||
|
||||
// Parses local project files and tries to infer connction settings from them
|
||||
getStorageConnectionSettingsFromCurrentProject(defaultTaskHubName?: string): StorageConnectionSettings | null {
|
||||
|
||||
const hostJson = this.readHostJson();
|
||||
|
||||
if (hostJson.storageProviderType === 'mssql') {
|
||||
|
||||
const sqlConnectionString = this.getValueFromLocalSettings(hostJson.connectionStringName);
|
||||
if (!sqlConnectionString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StorageConnectionSettings(
|
||||
[sqlConnectionString],
|
||||
'DurableFunctionsHub',
|
||||
true);
|
||||
}
|
||||
|
||||
var hubName: string | undefined = hostJson.hubName;
|
||||
if (!hubName) {
|
||||
|
||||
hubName = defaultTaskHubName;
|
||||
if (!hubName) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const storageConnString = this.getValueFromLocalSettings('AzureWebJobsStorage');
|
||||
if (!storageConnString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StorageConnectionSettings([ConnStringUtils.ExpandEmulatorShortcutIfNeeded(storageConnString)], hubName, true);
|
||||
}
|
||||
|
||||
// Stops all backend processes and closes all views
|
||||
cleanup(): Promise<any> {
|
||||
|
||||
Object.keys(this._monitorViews).map(k => this._monitorViews[k].cleanup());
|
||||
this._monitorViews = {};
|
||||
|
||||
const backends = this._backends;
|
||||
this._backends = {};
|
||||
return Promise.all(Object.keys(backends).map(k => backends[k].cleanup()));
|
||||
}
|
||||
|
||||
detachBackend(storageConnStrings: string[]): Promise<any> {
|
||||
|
||||
const connStringHashKey = StorageConnectionSettings.GetConnStringHashKey(storageConnStrings);
|
||||
|
||||
// Closing all views related to this connection
|
||||
for (const key of Object.keys(this._monitorViews)) {
|
||||
const monitorView = this._monitorViews[key];
|
||||
|
||||
if (monitorView.storageConnectionSettings.connStringHashKey === connStringHashKey) {
|
||||
|
||||
monitorView.cleanup();
|
||||
delete this._monitorViews[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Stopping background process
|
||||
const backendProcess = this._backends[connStringHashKey];
|
||||
if (!backendProcess) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return backendProcess.cleanup().then(() => {
|
||||
delete this._backends[connStringHashKey];
|
||||
});
|
||||
}
|
||||
|
||||
getBackendUrl(storageConnStrings: string[]): string {
|
||||
|
||||
const backendProcess = this._backends[StorageConnectionSettings.GetConnStringHashKey(storageConnStrings)];
|
||||
return !backendProcess ? '' : backendProcess.backendUrl;
|
||||
}
|
||||
|
||||
showUponDebugSession(connSettingsFromCurrentProject?: StorageConnectionSettings): Promise<MonitorView> {
|
||||
|
||||
if (!connSettingsFromCurrentProject) {
|
||||
return this.getOrAdd(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(this.getOrCreateFromStorageConnectionSettings(connSettingsFromCurrentProject));
|
||||
}
|
||||
|
||||
private _monitorViews: { [key: string]: MonitorView } = {};
|
||||
private _backends: { [key: string]: BackendProcess } = {};
|
||||
|
||||
private getOrAddBackend(connSettings: StorageConnectionSettings): BackendProcess {
|
||||
|
||||
// If a backend for this connection already exists, then just returning the existing one.
|
||||
var backendProcess = this._backends[connSettings.connStringHashKey];
|
||||
|
||||
if (!backendProcess) {
|
||||
|
||||
var binariesFolder = Settings().customPathToBackendBinaries;
|
||||
if (!binariesFolder) {
|
||||
|
||||
if (connSettings.isMsSql) {
|
||||
binariesFolder = path.join(this._context.extensionPath, 'custom-backends', 'mssql');
|
||||
} else if (Settings().backendVersionToUse === '.Net Core 2.1') {
|
||||
binariesFolder = path.join(this._context.extensionPath, 'custom-backends', 'netcore21');
|
||||
} else if (Settings().backendVersionToUse === '.Net Core 3.1') {
|
||||
binariesFolder = path.join(this._context.extensionPath, 'custom-backends', 'netcore31');
|
||||
} else {
|
||||
binariesFolder = path.join(this._context.extensionPath, 'backend');
|
||||
}
|
||||
}
|
||||
|
||||
backendProcess = new BackendProcess(
|
||||
binariesFolder,
|
||||
connSettings,
|
||||
() => this.detachBackend(connSettings.storageConnStrings),
|
||||
this._log
|
||||
);
|
||||
|
||||
this._backends[connSettings.connStringHashKey] = backendProcess;
|
||||
}
|
||||
|
||||
return backendProcess;
|
||||
}
|
||||
|
||||
// Obtains Storage Connection String and Hub Name from user
|
||||
private askForStorageConnectionSettings(): Promise<StorageConnectionSettings> {
|
||||
|
||||
return new Promise<StorageConnectionSettings>((resolve, reject) => {
|
||||
|
||||
// Asking the user for Connection String
|
||||
var connStringToShow = '';
|
||||
const connStringFromLocalSettings = this.getValueFromLocalSettings('AzureWebJobsStorage');
|
||||
|
||||
if (!!connStringFromLocalSettings) {
|
||||
connStringToShow = StorageConnectionSettings.MaskStorageConnString(connStringFromLocalSettings);
|
||||
}
|
||||
|
||||
vscode.window.showInputBox({ value: connStringToShow, prompt: 'Storage or MSSQL Connection String' }).then(connString => {
|
||||
|
||||
if (!connString) {
|
||||
// Leaving the promise unresolved, so nothing more happens
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user didn't change it
|
||||
if (connString === connStringToShow) {
|
||||
// Then setting it back to non-masked one
|
||||
connString = connStringFromLocalSettings;
|
||||
}
|
||||
|
||||
// If it is MSSQL storage provider
|
||||
if (!!ConnStringUtils.GetSqlServerName(connString)) {
|
||||
|
||||
resolve(new StorageConnectionSettings([connString!], 'DurableFunctionsHub'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Dealing with 'UseDevelopmentStorage=true' early
|
||||
connString = ConnStringUtils.ExpandEmulatorShortcutIfNeeded(connString);
|
||||
|
||||
// Asking the user for Hub Name
|
||||
var hubName = '';
|
||||
const hubPick = vscode.window.createQuickPick();
|
||||
|
||||
hubPick.onDidHide(() => hubPick.dispose());
|
||||
|
||||
hubPick.onDidChangeSelection(items => {
|
||||
if (!!items && !!items.length) {
|
||||
hubName = items[0].label;
|
||||
}
|
||||
});
|
||||
|
||||
// Still allowing to type free text
|
||||
hubPick.onDidChangeValue(value => {
|
||||
hubName = value;
|
||||
});
|
||||
|
||||
hubPick.onDidAccept(() => {
|
||||
if (!!hubName) {
|
||||
resolve(new StorageConnectionSettings([connString!], hubName));
|
||||
}
|
||||
hubPick.hide();
|
||||
});
|
||||
|
||||
hubPick.title = 'Hub Name';
|
||||
|
||||
var hubNameFromHostJson = this.readHostJson().hubName;
|
||||
if (!!hubNameFromHostJson) {
|
||||
|
||||
hubPick.items = [{
|
||||
label: hubNameFromHostJson,
|
||||
description: '(from host.json)'
|
||||
}];
|
||||
hubPick.placeholder = hubNameFromHostJson;
|
||||
|
||||
} else {
|
||||
|
||||
hubPick.items = [{
|
||||
label: 'DurableFunctionsHub',
|
||||
description: '(default hub name)'
|
||||
}];
|
||||
|
||||
hubPick.placeholder = 'DurableFunctionsHub';
|
||||
}
|
||||
|
||||
// Loading other hub names directly from Table Storage
|
||||
this.loadHubNamesFromTableStorage(connString).then(hubNames => {
|
||||
|
||||
if (hubNames.length >= 0) {
|
||||
|
||||
// Adding loaded names to the list
|
||||
hubPick.items = hubNames.map(label => {
|
||||
return { label: label, description: '(from Table Storage)' };
|
||||
});
|
||||
|
||||
hubPick.placeholder = hubNames[0];
|
||||
}
|
||||
});
|
||||
|
||||
hubPick.show();
|
||||
// If nothing is selected, leaving the promise unresolved, so nothing more happens
|
||||
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
private loadHubNamesFromTableStorage(storageConnString: string): Promise<string[]> {
|
||||
return new Promise<string[]>((resolve) => {
|
||||
|
||||
const accountName = ConnStringUtils.GetAccountName(storageConnString);
|
||||
const accountKey = ConnStringUtils.GetAccountKey(storageConnString);
|
||||
const tableEndpoint = ConnStringUtils.GetTableEndpoint(storageConnString);
|
||||
|
||||
if (!accountName || !accountKey) {
|
||||
// Leaving the promise unresolved
|
||||
return;
|
||||
}
|
||||
|
||||
getTaskHubNamesFromTableStorage(accountName, accountKey, tableEndpoint).then(hubNames => {
|
||||
|
||||
if (!hubNames || hubNames.length <= 0) {
|
||||
// Leaving the promise unresolved
|
||||
return;
|
||||
}
|
||||
resolve(hubNames);
|
||||
|
||||
}, err => {
|
||||
console.log(`Failed to load the list of tables. ${err.message}`);
|
||||
// Leaving the promise unresolved
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getValueFromLocalSettings(valueName: string): string {
|
||||
|
||||
const ws = vscode.workspace;
|
||||
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'local.settings.json'))) {
|
||||
|
||||
const localSettings = JSON.parse(fs.readFileSync(path.join(ws.rootPath, 'local.settings.json'), 'utf8'));
|
||||
|
||||
if (!!localSettings.Values && !!localSettings.Values[valueName]) {
|
||||
return localSettings.Values[valueName];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private readHostJson(): { hubName: string, storageProviderType: 'default' | 'mssql', connectionStringName: string } {
|
||||
|
||||
const result = { hubName: '', storageProviderType: 'default' as any, connectionStringName: '' };
|
||||
|
||||
const ws = vscode.workspace;
|
||||
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'host.json'))) {
|
||||
|
||||
var hostJson;
|
||||
try {
|
||||
hostJson = JSON.parse(fs.readFileSync(path.join(ws.rootPath, 'host.json'), 'utf8'));
|
||||
} catch (err) {
|
||||
|
||||
console.log(`Failed to parse host.json. ${(err as any).message}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!!hostJson && !!hostJson.extensions && hostJson.extensions.durableTask) {
|
||||
|
||||
const durableTask = hostJson.extensions.durableTask;
|
||||
if (!!durableTask.HubName || !!durableTask.hubName) {
|
||||
result.hubName = !!durableTask.HubName ? durableTask.HubName : durableTask.hubName
|
||||
}
|
||||
|
||||
if (!!durableTask.storageProvider && durableTask.storageProvider.type === 'mssql') {
|
||||
result.storageProviderType = 'mssql';
|
||||
result.connectionStringName = durableTask.storageProvider.connectionStringName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Tries to load the list of TaskHub names from a storage account.
|
||||
// Had to handcraft this code, since @azure/data-tables package is still in beta :(
|
||||
export async function getTaskHubNamesFromTableStorage(accountName: string, accountKey: string, tableEndpointUrl: string): Promise<string[] | null> {
|
||||
|
||||
if (!tableEndpointUrl) {
|
||||
tableEndpointUrl = `https://${accountName}.table.core.windows.net/`;
|
||||
} else if (!tableEndpointUrl.endsWith('/')) {
|
||||
tableEndpointUrl += '/';
|
||||
}
|
||||
|
||||
// Local emulator URLs contain account name _after_ host (like http://127.0.0.1:10002/devstoreaccount1/ ),
|
||||
// and this part should be included when obtaining SAS
|
||||
const tableEndpointUrlParts = tableEndpointUrl.split('/');
|
||||
const tableQueryUrl = (tableEndpointUrlParts.length > 3 && !!tableEndpointUrlParts[3]) ?
|
||||
`${tableEndpointUrlParts[3]}/Tables` :
|
||||
'Tables';
|
||||
|
||||
// Creating the SharedKeyLite signature to query Table Storage REST API for the list of tables
|
||||
const authHeaders = CreateAuthHeadersForTableStorage(accountName, accountKey, tableQueryUrl);
|
||||
|
||||
var response: any;
|
||||
try {
|
||||
response = await axios.get(`${tableEndpointUrl}Tables`, { headers: authHeaders });
|
||||
} catch (err) {
|
||||
console.log(`Failed to load hub names from table storage. ${(err as any).message}`);
|
||||
}
|
||||
|
||||
if (!response || !response.data || !response.data.value || response.data.value.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const instancesTables: string[] = response.data.value.map((table: any) => table.TableName)
|
||||
.filter((tableName: string) => tableName.endsWith('Instances'))
|
||||
.map((tableName: string) => tableName.substr(0, tableName.length - 'Instances'.length));
|
||||
|
||||
const historyTables: string[] = response.data.value.map((table: any) => table.TableName)
|
||||
.filter((tableName: string) => tableName.endsWith('History'))
|
||||
.map((tableName: string) => tableName.substr(0, tableName.length - 'History'.length));
|
||||
|
||||
// Considering it to be a hub, if it has both *Instances and *History tables
|
||||
return instancesTables.filter(name => historyTables.indexOf(name) >= 0);
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
// Returns config values stored in VsCode's settings.json
|
||||
export function Settings(): ISettings {
|
||||
|
||||
const config = vscode.workspace.getConfiguration('durableFunctionsMonitor');
|
||||
|
||||
// Better to have default values hardcoded here (not only in package.json) as well
|
||||
return {
|
||||
backendBaseUrl: config.get<string>('backendBaseUrl', 'http://localhost:{portNr}/a/p/i'),
|
||||
backendVersionToUse: config.get<'Default' | '.Net Core 3.1'>('backendVersionToUse', 'Default'),
|
||||
customPathToBackendBinaries: config.get<string>('customPathToBackendBinaries', ''),
|
||||
backendTimeoutInSeconds: config.get<number>('backendTimeoutInSeconds', 60),
|
||||
storageEmulatorConnectionString: config.get<string>('storageEmulatorConnectionString', 'AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;'),
|
||||
enableLogging: config.get<boolean>('enableLogging', false),
|
||||
showTimeAs: config.get<'UTC' | 'Local'>('showTimeAs', 'UTC'),
|
||||
showWhenDebugSessionStarts: config.get<boolean>('showWhenDebugSessionStarts', false),
|
||||
};
|
||||
}
|
||||
|
||||
// Updates a config value stored in VsCode's settings.json
|
||||
export function UpdateSetting(name: string, val: any) {
|
||||
|
||||
const config = vscode.workspace.getConfiguration('durableFunctionsMonitor');
|
||||
config.update(name, val, true);
|
||||
}
|
||||
|
||||
interface ISettings
|
||||
{
|
||||
backendBaseUrl: string;
|
||||
backendVersionToUse: 'Default' | '.Net Core 3.1' | '.Net Core 2.1';
|
||||
customPathToBackendBinaries: string;
|
||||
backendTimeoutInSeconds: number;
|
||||
storageEmulatorConnectionString: string;
|
||||
enableLogging: boolean;
|
||||
showTimeAs: 'UTC' | 'Local';
|
||||
showWhenDebugSessionStarts: boolean;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export const NonceEnvironmentVariableName = 'DFM_NONCE';
|
||||
export const NonceHeaderName = 'x-dfm-nonce';
|
||||
export const MsSqlConnStringEnvironmentVariableName = 'DFM_SQL_CONNECTION_STRING';
|
||||
export const HubNameEnvironmentVariableName = 'DFM_HUB_NAME';
|
|
@ -0,0 +1,105 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { StorageConnectionSettings } from './BackendProcess';
|
||||
import { ConnStringUtils } from "./ConnStringUtils";
|
||||
import { TaskHubTreeItem } from "./TaskHubTreeItem";
|
||||
import { MonitorViewList } from "./MonitorViewList";
|
||||
|
||||
// Represents the Storage Account item in the TreeView
|
||||
export class StorageAccountTreeItem extends vscode.TreeItem {
|
||||
|
||||
constructor(private _connStrings: string[],
|
||||
private _resourcesFolderPath: string,
|
||||
private _monitorViewList: MonitorViewList,
|
||||
private _fromLocalSettingsJson: boolean = false) {
|
||||
|
||||
super(ConnStringUtils.GetStorageName(_connStrings), vscode.TreeItemCollapsibleState.Expanded);
|
||||
|
||||
this.isMsSqlStorage = !!ConnStringUtils.GetSqlServerName(this._connStrings[0]);
|
||||
}
|
||||
|
||||
readonly isMsSqlStorage: boolean;
|
||||
isV2StorageAccount: boolean = false;
|
||||
storageAccountId: string = '';
|
||||
|
||||
get isAttached(): boolean {
|
||||
return !!this.backendUrl;
|
||||
}
|
||||
|
||||
get backendUrl(): string {
|
||||
return this._monitorViewList.getBackendUrl(this._connStrings);
|
||||
}
|
||||
|
||||
get storageName(): string {
|
||||
return this.label!;
|
||||
}
|
||||
|
||||
get storageConnStrings(): string[] {
|
||||
return this._connStrings;
|
||||
}
|
||||
|
||||
get childItems(): TaskHubTreeItem[] {
|
||||
return this._taskHubItems;
|
||||
}
|
||||
|
||||
get tooltip(): string {
|
||||
|
||||
if (this._fromLocalSettingsJson) {
|
||||
return `from local.settings.json`;
|
||||
}
|
||||
|
||||
if (!!this.isMsSqlStorage) {
|
||||
return 'MSSQL Storage Provider';
|
||||
}
|
||||
|
||||
return StorageConnectionSettings.MaskStorageConnString(this._connStrings[0]);
|
||||
}
|
||||
|
||||
// Something to show to the right of this item
|
||||
get description(): string {
|
||||
return `${this._taskHubItems.length} Task Hubs`;
|
||||
}
|
||||
|
||||
// Item's icon
|
||||
get iconPath(): string {
|
||||
if (!!this.isMsSqlStorage) {
|
||||
return path.join(this._resourcesFolderPath, this.isAttached ? 'mssqlAttached.svg' : 'mssql.svg');
|
||||
}
|
||||
if (this.isV2StorageAccount) {
|
||||
return path.join(this._resourcesFolderPath, this.isAttached ? 'storageAccountV2Attached.svg' : 'storageAccountV2.svg');
|
||||
}
|
||||
return path.join(this._resourcesFolderPath, this.isAttached ? 'storageAccountAttached.svg' : 'storageAccount.svg');
|
||||
}
|
||||
|
||||
// For binding context menu to this tree node
|
||||
get contextValue(): string {
|
||||
return this.isAttached ? 'storageAccount-attached' : 'storageAccount-detached';
|
||||
}
|
||||
|
||||
// For sorting
|
||||
static compare(first: StorageAccountTreeItem, second: StorageAccountTreeItem): number {
|
||||
const a = first.storageName.toLowerCase();
|
||||
const b = second.storageName.toLowerCase();
|
||||
return a === b ? 0 : (a < b ? -1 : 1);
|
||||
}
|
||||
|
||||
// Creates or returns existing TaskHubTreeItem by hub name
|
||||
getOrAdd(hubName: string): TaskHubTreeItem {
|
||||
|
||||
var hubItem = this._taskHubItems.find(taskHub => taskHub.hubName.toLowerCase() === hubName.toLowerCase());
|
||||
if (!hubItem) {
|
||||
hubItem = new TaskHubTreeItem(this, hubName, this._resourcesFolderPath);
|
||||
this._taskHubItems.push(hubItem);
|
||||
this._taskHubItems.sort(TaskHubTreeItem.compare);
|
||||
}
|
||||
|
||||
return hubItem;
|
||||
}
|
||||
|
||||
isTaskHubVisible(hubName: string): boolean {
|
||||
return this._monitorViewList.isMonitorViewVisible(new StorageConnectionSettings(this._connStrings, hubName));
|
||||
}
|
||||
|
||||
private _taskHubItems: TaskHubTreeItem[] = [];
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { MonitorView } from "./MonitorView";
|
||||
import { MonitorViewList } from "./MonitorViewList";
|
||||
import { StorageAccountTreeItem } from "./StorageAccountTreeItem";
|
||||
import { StorageConnectionSettings } from "./BackendProcess";
|
||||
import { ConnStringUtils } from "./ConnStringUtils";
|
||||
import { TaskHubTreeItem } from "./TaskHubTreeItem";
|
||||
|
||||
// Represents the list of Storage Account items in the TreeView
|
||||
export class StorageAccountTreeItems {
|
||||
|
||||
constructor(private _resourcesFolderPath: string, private _monitorViewList: MonitorViewList) {}
|
||||
|
||||
get nodes(): StorageAccountTreeItem[] {
|
||||
return this._storageAccountItems;
|
||||
}
|
||||
|
||||
get taskHubNodes(): TaskHubTreeItem[] {
|
||||
return ([] as TaskHubTreeItem[]).concat(...this._storageAccountItems.map(n => n.childItems));
|
||||
}
|
||||
|
||||
// Adds a node to the tree for MonitorView, that's already running
|
||||
addNodeForMonitorView(monitorView: MonitorView): void {
|
||||
|
||||
const storageConnStrings = monitorView.storageConnectionSettings.storageConnStrings;
|
||||
const storageName = ConnStringUtils.GetStorageName(storageConnStrings);
|
||||
const hubName = monitorView.storageConnectionSettings.hubName;
|
||||
|
||||
// Only creating a new tree node, if no node for this account exists so far
|
||||
var node = this._storageAccountItems.find(item => item.storageName.toLowerCase() === storageName.toLowerCase());
|
||||
if (!node) {
|
||||
|
||||
node = new StorageAccountTreeItem(storageConnStrings, this._resourcesFolderPath, this._monitorViewList);
|
||||
|
||||
this._storageAccountItems.push(node);
|
||||
this._storageAccountItems.sort(StorageAccountTreeItem.compare);
|
||||
}
|
||||
|
||||
node.getOrAdd(hubName);
|
||||
}
|
||||
|
||||
// Adds a detached node to the tree for the specified storage connection settings
|
||||
addNodeForConnectionSettings(connSettings: StorageConnectionSettings, isV2StorageAccount: boolean = false, storageAccountId: string = ''): void {
|
||||
|
||||
const storageConnStrings = connSettings.storageConnStrings;
|
||||
const hubName = connSettings.hubName;
|
||||
|
||||
// Trying to infer account name from connection string
|
||||
const storageName = ConnStringUtils.GetStorageName(storageConnStrings);
|
||||
if (!storageName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only creating a new tree node, if no node for this account exists so far
|
||||
var node = this._storageAccountItems.find(item => item.storageName === storageName);
|
||||
if (!node) {
|
||||
|
||||
node = new StorageAccountTreeItem(storageConnStrings,
|
||||
this._resourcesFolderPath,
|
||||
this._monitorViewList,
|
||||
connSettings.isFromLocalSettingsJson
|
||||
);
|
||||
|
||||
this._storageAccountItems.push(node);
|
||||
this._storageAccountItems.sort(StorageAccountTreeItem.compare);
|
||||
}
|
||||
|
||||
node.isV2StorageAccount = isV2StorageAccount;
|
||||
node.storageAccountId = storageAccountId;
|
||||
|
||||
node.getOrAdd(hubName);
|
||||
}
|
||||
|
||||
private _storageAccountItems: StorageAccountTreeItem[] = [];
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { StorageAccountTreeItem } from "./StorageAccountTreeItem";
|
||||
import { StorageAccountTreeItems } from "./StorageAccountTreeItems";
|
||||
|
||||
// Represents an Azure Subscription in the TreeView
|
||||
export class SubscriptionTreeItem extends vscode.TreeItem {
|
||||
|
||||
get isSubscriptionTreeItem(): boolean { return true; }
|
||||
|
||||
// Returns storage account nodes, that belong to this subscription
|
||||
get storageAccountNodes(): StorageAccountTreeItem[] {
|
||||
return this._storageAccounts.nodes.filter(a => this.isMyStorageAccount(a));
|
||||
}
|
||||
|
||||
constructor(subscriptionName: string,
|
||||
private _storageAccounts: StorageAccountTreeItems,
|
||||
private _storageAccountNames: string[],
|
||||
protected _resourcesFolderPath: string
|
||||
) {
|
||||
super(subscriptionName, vscode.TreeItemCollapsibleState.Expanded);
|
||||
this.iconPath = path.join(this._resourcesFolderPath, 'azureSubscription.svg');
|
||||
}
|
||||
|
||||
// Checks whether this storage account belongs to this subscription.
|
||||
isMyStorageAccount(accNode: StorageAccountTreeItem): boolean {
|
||||
|
||||
// The only way to do this is by matching the account name against all account names in this subscription.
|
||||
// We need to fetch these acccount names for other purposes anyway, so why not using them here as well
|
||||
// (as opposite to making separate roundtrips to get subscriptionId for a given account).
|
||||
return this._storageAccountNames.includes(accNode.storageName);
|
||||
}
|
||||
}
|
||||
|
||||
// Represents a special node in the TreeView where all 'orphaned' (those with unidentified subscription) storage accounts go
|
||||
export class DefaultSubscriptionTreeItem extends SubscriptionTreeItem {
|
||||
|
||||
constructor(storageAccounts: StorageAccountTreeItems,
|
||||
private _otherSubscriptionNodes: SubscriptionTreeItem[],
|
||||
resourcesFolderPath: string
|
||||
) {
|
||||
super('Storages', storageAccounts, [], resourcesFolderPath);
|
||||
this.iconPath = path.join(this._resourcesFolderPath, 'storageAccounts.svg');
|
||||
}
|
||||
|
||||
// Checks whether this storage account belongs to this tree item.
|
||||
isMyStorageAccount(accNode: StorageAccountTreeItem): boolean {
|
||||
|
||||
// Let's see if this account belongs to any other subscription node. If not - it's ours.
|
||||
return this._otherSubscriptionNodes.every(n => !n.isMyStorageAccount(accNode));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
import { StorageManagementClient } from "@azure/arm-storage";
|
||||
import { StorageAccount } from "@azure/arm-storage/src/models";
|
||||
|
||||
import { SubscriptionTreeItem, DefaultSubscriptionTreeItem } from "./SubscriptionTreeItem";
|
||||
import { StorageAccountTreeItems } from "./StorageAccountTreeItems";
|
||||
import { getTaskHubNamesFromTableStorage } from './MonitorViewList';
|
||||
import { ConnStringUtils } from "./ConnStringUtils";
|
||||
import { Settings } from './Settings';
|
||||
import { StorageConnectionSettings } from "./BackendProcess";
|
||||
|
||||
// Full typings for this can be found here: https://github.com/microsoft/vscode-azure-account/blob/master/src/azure-account.api.d.ts
|
||||
type AzureSubscription = { session: { credentials2: any }, subscription: { subscriptionId: string, displayName: string } };
|
||||
|
||||
// Represents the list of Azure Subscriptions in the TreeView
|
||||
export class SubscriptionTreeItems {
|
||||
|
||||
constructor(private _context: vscode.ExtensionContext,
|
||||
private _azureAccount: any,
|
||||
private _storageAccounts: StorageAccountTreeItems,
|
||||
private _onStorageAccountsChanged: () => void,
|
||||
private _resourcesFolderPath: string,
|
||||
private _log: (l: string) => void)
|
||||
{ }
|
||||
|
||||
// Returns subscription nodes, but only those that have some TaskHubs in them
|
||||
async getNonEmptyNodes(): Promise<SubscriptionTreeItem[]> {
|
||||
|
||||
if (!this._nodes) {
|
||||
|
||||
// Need to wait until Azure Account ext loads the filtered list of subscriptions
|
||||
if (!this._azureAccount || !await this._azureAccount.waitForFilters()) {
|
||||
|
||||
this._nodes = [];
|
||||
|
||||
} else {
|
||||
|
||||
// Showing only filtered subscriptions and ignoring those, which are hidden
|
||||
const subscriptions = this._azureAccount.filters;
|
||||
|
||||
this._nodes = await this.loadSubscriptionNodes(subscriptions);
|
||||
}
|
||||
|
||||
// Adding the 'default subscription' node, where all orphaned (unrecognized) storage accounts will go to.
|
||||
this._nodes.push(new DefaultSubscriptionTreeItem(this._storageAccounts, this._nodes.slice(), this._resourcesFolderPath));
|
||||
|
||||
// Also pinging local Storage Emulator and deliberately not awaiting
|
||||
this.tryLoadingTaskHubsForLocalStorageEmulator();
|
||||
}
|
||||
|
||||
// Only showing non-empty subscriptions
|
||||
return this._nodes.filter(n => n.storageAccountNodes.length > 0);
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
this._nodes = undefined;
|
||||
}
|
||||
|
||||
private _nodes?: SubscriptionTreeItem[];
|
||||
|
||||
private async tryLoadingStorageAccountsForSubscription(storageManagementClient: StorageManagementClient): Promise<StorageAccount[]> {
|
||||
|
||||
const result: StorageAccount[] = [];
|
||||
|
||||
var storageAccountsPartialResponse = await storageManagementClient.storageAccounts.list();
|
||||
result.push(...storageAccountsPartialResponse);
|
||||
|
||||
while (!!storageAccountsPartialResponse.nextLink) {
|
||||
|
||||
storageAccountsPartialResponse = await storageManagementClient.storageAccounts.listNext(storageAccountsPartialResponse.nextLink);
|
||||
result.push(...storageAccountsPartialResponse);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static HasAlreadyShownStorageV2Warning = false;
|
||||
|
||||
private showWarning4V2StorageAccounts(v2AccountNames: string[]): void {
|
||||
|
||||
const DfmDoNotShowStorageV2Warning = 'DfmDoNotShowStorageV2Warning';
|
||||
|
||||
if (!!SubscriptionTreeItems.HasAlreadyShownStorageV2Warning || !!this._context.globalState.get(DfmDoNotShowStorageV2Warning, false) || !v2AccountNames.length) {
|
||||
return;
|
||||
}
|
||||
SubscriptionTreeItems.HasAlreadyShownStorageV2Warning = true;
|
||||
|
||||
const prompt = `Looks like your Durable Functions are using the following General-purpose V2 Storage accounts: ${v2AccountNames.join(', ')}. Combined with Durable Functions, V2 Storage accounts can be more expensive under high loads. Consider using General-purpose V1 Storage instead.`;
|
||||
vscode.window.showWarningMessage(prompt, 'OK', `Don't Show Again`).then(answer => {
|
||||
|
||||
if (answer === `Don't Show Again`) {
|
||||
this._context.globalState.update(DfmDoNotShowStorageV2Warning, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async tryLoadingTaskHubsForSubscription(storageManagementClient: StorageManagementClient, storageAccounts: StorageAccount[]): Promise<boolean> {
|
||||
|
||||
const v2AccountNames: string[] = [];
|
||||
var taskHubsAdded = false;
|
||||
await Promise.all(storageAccounts.map(async storageAccount => {
|
||||
|
||||
// Extracting resource group name
|
||||
const match = /\/resourceGroups\/([^\/]+)\/providers/gi.exec(storageAccount.id!);
|
||||
if (!match || match.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const resourceGroupName = match[1];
|
||||
|
||||
const storageKeys = await storageManagementClient.storageAccounts.listKeys(resourceGroupName, storageAccount.name!);
|
||||
if (!storageKeys.keys || storageKeys.keys.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Choosing the key that looks best
|
||||
var storageKey = storageKeys.keys.find(k => !k.permissions || k.permissions.toLowerCase() === "full");
|
||||
if (!storageKey) {
|
||||
storageKey = storageKeys.keys.find(k => !k.permissions || k.permissions.toLowerCase() === "read");
|
||||
}
|
||||
if (!storageKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tableEndpoint = '';
|
||||
if (!!storageAccount.primaryEndpoints) {
|
||||
tableEndpoint = storageAccount.primaryEndpoints.table!;
|
||||
}
|
||||
|
||||
const hubNames = await getTaskHubNamesFromTableStorage(storageAccount.name!, storageKey.value!, tableEndpoint);
|
||||
if (!hubNames || !hubNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isV2StorageAccount = storageAccount.kind === 'StorageV2';
|
||||
|
||||
if (isV2StorageAccount) {
|
||||
v2AccountNames.push(storageAccount.name!);
|
||||
}
|
||||
|
||||
for (const hubName of hubNames) {
|
||||
|
||||
this._storageAccounts.addNodeForConnectionSettings(
|
||||
new StorageConnectionSettings([this.getConnectionStringForStorageAccount(storageAccount, storageKey.value!)], hubName, false),
|
||||
isV2StorageAccount,
|
||||
storageAccount.id
|
||||
);
|
||||
|
||||
taskHubsAdded = true;
|
||||
}
|
||||
}));
|
||||
|
||||
// Notifying about potentially higher costs of V2 accounts
|
||||
this.showWarning4V2StorageAccounts(v2AccountNames);
|
||||
|
||||
return taskHubsAdded;
|
||||
}
|
||||
|
||||
private async tryLoadingTaskHubsForLocalStorageEmulator(): Promise<void> {
|
||||
|
||||
const emulatorConnString = Settings().storageEmulatorConnectionString;
|
||||
|
||||
const accountName = ConnStringUtils.GetAccountName(emulatorConnString);
|
||||
const accountKey = ConnStringUtils.GetAccountKey(emulatorConnString);
|
||||
const tableEndpoint = ConnStringUtils.GetTableEndpoint(emulatorConnString);
|
||||
|
||||
const hubNames = await getTaskHubNamesFromTableStorage(accountName, accountKey, tableEndpoint);
|
||||
if (!hubNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const hubName of hubNames) {
|
||||
this._storageAccounts.addNodeForConnectionSettings(new StorageConnectionSettings([emulatorConnString], hubName));
|
||||
}
|
||||
|
||||
if (hubNames.length > 0) {
|
||||
this._onStorageAccountsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private getConnectionStringForStorageAccount(account: StorageAccount, storageKey: string): string {
|
||||
|
||||
var endpoints = '';
|
||||
if (!!account.primaryEndpoints) {
|
||||
endpoints = `BlobEndpoint=${account.primaryEndpoints!.blob};QueueEndpoint=${account.primaryEndpoints!.queue};TableEndpoint=${account.primaryEndpoints!.table};FileEndpoint=${account.primaryEndpoints!.file};`;
|
||||
} else {
|
||||
endpoints = `BlobEndpoint=https://${account.name}.blob.core.windows.net/;QueueEndpoint=https://${account.name}.queue.core.windows.net/;TableEndpoint=https://${account.name}.table.core.windows.net/;FileEndpoint=https://${account.name}.file.core.windows.net/;`;
|
||||
}
|
||||
|
||||
return `DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${storageKey};${endpoints}`;
|
||||
}
|
||||
|
||||
private async loadSubscriptionNodes(subscriptions: AzureSubscription[]): Promise<SubscriptionTreeItem[]> {
|
||||
|
||||
const results = await Promise.all(subscriptions.map(async s => {
|
||||
|
||||
try {
|
||||
const storageManagementClient = new StorageManagementClient(s.session.credentials2, s.subscription.subscriptionId);
|
||||
|
||||
// Trying to load all storage account names in this subscription.
|
||||
// We need that list of names to subsequently match storage account nodes with their subscription nodes.
|
||||
const storageAccounts = await this.tryLoadingStorageAccountsForSubscription(storageManagementClient);
|
||||
|
||||
// Now let's try to detect and load TaskHubs in this subscription.
|
||||
// Many things can go wrong there, that is why we're doing it so asynchronously
|
||||
this.tryLoadingTaskHubsForSubscription(storageManagementClient, storageAccounts)
|
||||
.then(anyMoreTaskHubsAdded => {
|
||||
if (anyMoreTaskHubsAdded) {
|
||||
this._onStorageAccountsChanged();
|
||||
}
|
||||
}, err => {
|
||||
this._onStorageAccountsChanged();
|
||||
this._log(`Failed to load TaskHubs from subscription ${s.subscription.displayName}. ${err.message}`);
|
||||
});
|
||||
|
||||
return {
|
||||
subscriptionId: s.subscription.subscriptionId,
|
||||
subscriptionName: s.subscription.displayName,
|
||||
storageAccountNames: storageAccounts.map(a => a.name!)
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
|
||||
this._log(`Failed to load storage accounts from subscription ${s.subscription.displayName}. ${err.message}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
return results
|
||||
.filter(r => r !== null)
|
||||
.map(r => new SubscriptionTreeItem(
|
||||
r!.subscriptionName,
|
||||
this._storageAccounts,
|
||||
r!.storageAccountNames,
|
||||
this._resourcesFolderPath
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { StorageConnectionSettings } from './BackendProcess';
|
||||
import { StorageAccountTreeItem } from "./StorageAccountTreeItem";
|
||||
|
||||
// Represents the Task Hub item in the TreeView
|
||||
export class TaskHubTreeItem extends vscode.TreeItem {
|
||||
|
||||
constructor(private _parentItem: StorageAccountTreeItem, private _hubName: string, private _resourcesFolderPath: string) {
|
||||
super(_hubName);
|
||||
}
|
||||
|
||||
get storageAccountId(): string {
|
||||
return this._parentItem.storageAccountId;
|
||||
}
|
||||
|
||||
get subscriptionId(): string {
|
||||
|
||||
const match = /\/subscriptions\/([^\/]+)\/resourceGroups/gi.exec(this._parentItem.storageAccountId);
|
||||
if (!match || match.length <= 0) {
|
||||
return '';
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
get hubName(): string {
|
||||
return this._hubName;
|
||||
}
|
||||
|
||||
// Gets associated storage connection settings
|
||||
get storageConnectionSettings(): StorageConnectionSettings {
|
||||
return new StorageConnectionSettings(this._parentItem.storageConnStrings, this._hubName);
|
||||
}
|
||||
|
||||
// Item's icon
|
||||
get iconPath(): string {
|
||||
return path.join(this._resourcesFolderPath, this._parentItem.isTaskHubVisible(this._hubName) ? 'taskHubAttached.svg' : 'taskHub.svg');
|
||||
}
|
||||
|
||||
// As a tooltip, showing the backend's URL
|
||||
get tooltip(): string {
|
||||
|
||||
const backendUrl = this._parentItem.backendUrl;
|
||||
return !backendUrl ? '' : `${backendUrl}/${this._hubName}`;
|
||||
}
|
||||
|
||||
// This is what happens when the item is being clicked
|
||||
get command(): vscode.Command {
|
||||
return {
|
||||
title: 'Attach',
|
||||
command: 'durableFunctionsMonitorTreeView.attachToTaskHub',
|
||||
arguments: [this]
|
||||
};
|
||||
}
|
||||
|
||||
// For binding context menu to this tree node
|
||||
get contextValue(): string {
|
||||
return this._parentItem.isAttached ? 'taskHub-attached' : 'taskHub-detached';
|
||||
}
|
||||
|
||||
// For sorting
|
||||
static compare(first: TaskHubTreeItem, second: TaskHubTreeItem): number {
|
||||
const a = first.label!.toLowerCase();
|
||||
const b = second.label!.toLowerCase();
|
||||
return a === b ? 0 : (a < b ? -1 : 1);
|
||||
}
|
||||
|
||||
// Drops itself from parent's list
|
||||
removeFromTree(): void {
|
||||
|
||||
this._parentItem.childItems.splice(this._parentItem.childItems.indexOf(this), 1);
|
||||
}
|
||||
}
|
35
durablefunctionsmonitor-vscodeext/src/az-func-as-a-graph/FunctionsMap.d.ts
поставляемый
Normal file
|
@ -0,0 +1,35 @@
|
|||
|
||||
export type FunctionsMap = {
|
||||
[name: string]: {
|
||||
bindings: any[],
|
||||
isCalledBy: string[],
|
||||
isSignalledBy: { name: string, signalName: string }[],
|
||||
isCalledByItself?: boolean,
|
||||
filePath?: string,
|
||||
pos?: number,
|
||||
lineNr?: number
|
||||
}
|
||||
};
|
||||
|
||||
export type ProxiesMap = {
|
||||
[name: string]: {
|
||||
matchCondition?: {
|
||||
methods?: string[];
|
||||
route?: string;
|
||||
};
|
||||
backendUri?: string;
|
||||
requestOverrides?: {};
|
||||
responseOverrides?: {};
|
||||
filePath?: string,
|
||||
pos?: number,
|
||||
lineNr?: number,
|
||||
warningNotAddedToCsProjFile?: boolean
|
||||
}
|
||||
};
|
||||
|
||||
export type TraverseFunctionResult = {
|
||||
functions: FunctionsMap;
|
||||
proxies: ProxiesMap;
|
||||
tempFolders: string[];
|
||||
projectFolder: string;
|
||||
};
|
|
@ -0,0 +1,347 @@
|
|||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import { FunctionsMap, ProxiesMap, TraverseFunctionResult } from './FunctionsMap';
|
||||
import {
|
||||
getCodeInBrackets, TraversalRegexes, DotNetBindingsParser,
|
||||
isDotNetProjectAsync, posToLineNr, cloneFromGitHub
|
||||
} from './traverseFunctionProjectUtils';
|
||||
|
||||
const ExcludedFolders = ['node_modules', 'obj', '.vs', '.vscode', '.env', '.python_packages', '.git', '.github'];
|
||||
|
||||
// Collects all function.json files in a Functions project. Also tries to supplement them with bindings
|
||||
// extracted from .Net code (if the project is .Net). Also parses and organizes orchestrators/activities
|
||||
// (if the project uses Durable Functions)
|
||||
export async function traverseFunctionProject(projectFolder: string, log: (s: any) => void)
|
||||
: Promise<TraverseFunctionResult> {
|
||||
|
||||
var functions: FunctionsMap = {}, tempFolders = [];
|
||||
|
||||
// If it is a git repo, cloning it
|
||||
if (projectFolder.toLowerCase().startsWith('http')) {
|
||||
|
||||
log(`Cloning ${projectFolder}`);
|
||||
|
||||
const gitInfo = await cloneFromGitHub(projectFolder);
|
||||
|
||||
log(`Successfully cloned to ${gitInfo.gitTempFolder}`);
|
||||
|
||||
tempFolders.push(gitInfo.gitTempFolder);
|
||||
projectFolder = gitInfo.projectFolder;
|
||||
}
|
||||
|
||||
const hostJsonMatch = await findFileRecursivelyAsync(projectFolder, 'host.json', false);
|
||||
if (!hostJsonMatch) {
|
||||
throw new Error('host.json file not found under the provided project path');
|
||||
}
|
||||
|
||||
log(`>>> Found host.json at ${hostJsonMatch.filePath}`);
|
||||
|
||||
var hostJsonFolder = path.dirname(hostJsonMatch.filePath);
|
||||
|
||||
// If it is a C# function, we'll need to dotnet publish first
|
||||
if (await isDotNetProjectAsync(hostJsonFolder)) {
|
||||
|
||||
const publishTempFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'dotnet-publish-'));
|
||||
tempFolders.push(publishTempFolder);
|
||||
|
||||
log(`>>> Publishing ${hostJsonFolder} to ${publishTempFolder}...`);
|
||||
execSync(`dotnet publish -o ${publishTempFolder}`, { cwd: hostJsonFolder });
|
||||
hostJsonFolder = publishTempFolder;
|
||||
}
|
||||
|
||||
// Reading function.json files, in parallel
|
||||
const promises = (await fs.promises.readdir(hostJsonFolder)).map(async functionName => {
|
||||
|
||||
const fullPath = path.join(hostJsonFolder, functionName);
|
||||
const functionJsonFilePath = path.join(fullPath, 'function.json');
|
||||
|
||||
const isDirectory = (await fs.promises.lstat(fullPath)).isDirectory();
|
||||
const functionJsonExists = fs.existsSync(functionJsonFilePath);
|
||||
|
||||
if (isDirectory && functionJsonExists) {
|
||||
|
||||
try {
|
||||
const functionJsonString = await fs.promises.readFile(functionJsonFilePath, { encoding: 'utf8' });
|
||||
const functionJson = JSON.parse(functionJsonString);
|
||||
|
||||
functions[functionName] = { bindings: functionJson.bindings, isCalledBy: [], isSignalledBy: [] };
|
||||
|
||||
} catch (err) {
|
||||
log(`>>> Failed to parse ${functionJsonFilePath}: ${err}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
// Now enriching data from function.json with more info extracted from code
|
||||
functions = await mapOrchestratorsAndActivitiesAsync(functions, projectFolder, hostJsonFolder);
|
||||
|
||||
// Also reading proxies
|
||||
const proxies = await readProxiesJson(projectFolder, log);
|
||||
|
||||
return { functions, proxies, tempFolders, projectFolder };
|
||||
}
|
||||
|
||||
// Tries to read proxies.json file from project folder
|
||||
async function readProxiesJson(projectFolder: string, log: (s: any) => void): Promise<ProxiesMap> {
|
||||
|
||||
const proxiesJsonPath = path.join(projectFolder, 'proxies.json');
|
||||
if (!fs.existsSync(proxiesJsonPath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const proxiesJsonString = await fs.promises.readFile(proxiesJsonPath, { encoding: 'utf8' });
|
||||
try {
|
||||
|
||||
const proxies = JSON.parse(proxiesJsonString).proxies as ProxiesMap;
|
||||
if (!proxies) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var notAddedToCsProjFile = false;
|
||||
if (await isDotNetProjectAsync(projectFolder)) {
|
||||
|
||||
// Also checking that proxies.json is added to .csproj file
|
||||
|
||||
const csProjFile = await findFileRecursivelyAsync(projectFolder, '.+\\.csproj$', true);
|
||||
const proxiesJsonEntryRegex = new RegExp(`\\s*=\\s*"proxies.json"\\s*>`);
|
||||
|
||||
if (!!csProjFile && csProjFile.code && (!proxiesJsonEntryRegex.exec(csProjFile.code))) {
|
||||
|
||||
notAddedToCsProjFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also adding filePath and lineNr
|
||||
for (var proxyName in proxies) {
|
||||
|
||||
const proxy = proxies[proxyName];
|
||||
proxy.filePath = proxiesJsonPath;
|
||||
if (notAddedToCsProjFile) {
|
||||
proxy.warningNotAddedToCsProjFile = true;
|
||||
}
|
||||
|
||||
const proxyNameRegex = new RegExp(`"${proxyName}"\\s*:`);
|
||||
const match = proxyNameRegex.exec(proxiesJsonString);
|
||||
if (!!match) {
|
||||
|
||||
proxy.pos = match.index;
|
||||
proxy.lineNr = posToLineNr(proxiesJsonString, proxy.pos);
|
||||
}
|
||||
}
|
||||
|
||||
return proxies;
|
||||
|
||||
} catch(err) {
|
||||
|
||||
log(`>>> Failed to parse ${proxiesJsonPath}: ${err}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// fileName can be a regex, pattern should be a regex (which will be searched for in the matching files).
|
||||
// If returnFileContents == true, returns file content. Otherwise returns full path to the file.
|
||||
async function findFileRecursivelyAsync(folder: string, fileName: string, returnFileContents: boolean, pattern?: RegExp)
|
||||
: Promise<{ filePath: string, code?: string, pos?: number, length?: number } | undefined> {
|
||||
|
||||
const fileNameRegex = new RegExp(fileName, 'i');
|
||||
|
||||
for (const name of await fs.promises.readdir(folder)) {
|
||||
var fullPath = path.join(folder, name);
|
||||
|
||||
if ((await fs.promises.lstat(fullPath)).isDirectory()) {
|
||||
|
||||
if (ExcludedFolders.includes(name.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await findFileRecursivelyAsync(fullPath, fileName, returnFileContents, pattern);
|
||||
if (!!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
} else if (!!fileNameRegex.exec(name)) {
|
||||
|
||||
if (!pattern) {
|
||||
return {
|
||||
filePath: fullPath,
|
||||
code: returnFileContents ? (await fs.promises.readFile(fullPath, { encoding: 'utf8' })) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
const code = await fs.promises.readFile(fullPath, { encoding: 'utf8' });
|
||||
const match = pattern.exec(code);
|
||||
|
||||
if (!!match) {
|
||||
return {
|
||||
filePath: fullPath,
|
||||
code: returnFileContents ? code : undefined,
|
||||
pos: match.index,
|
||||
length: match[0].length
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Tries to match orchestrations and their activities by parsing source code
|
||||
async function mapOrchestratorsAndActivitiesAsync(functions: FunctionsMap, projectFolder: string, hostJsonFolder: string): Promise<{}> {
|
||||
|
||||
const isDotNet = await isDotNetProjectAsync(projectFolder);
|
||||
const functionNames = Object.keys(functions);
|
||||
|
||||
const orchestratorNames = functionNames.filter(name => functions[name].bindings.some((b: any) => b.type === 'orchestrationTrigger'));
|
||||
const orchestrators = await getFunctionsAndTheirCodesAsync(orchestratorNames, isDotNet, projectFolder, hostJsonFolder);
|
||||
|
||||
const activityNames = Object.keys(functions).filter(name => functions[name].bindings.some((b: any) => b.type === 'activityTrigger'));
|
||||
const activities = await getFunctionsAndTheirCodesAsync(activityNames, isDotNet, projectFolder, hostJsonFolder);
|
||||
|
||||
const entityNames = functionNames.filter(name => functions[name].bindings.some((b: any) => b.type === 'entityTrigger'));
|
||||
const entities = await getFunctionsAndTheirCodesAsync(entityNames, isDotNet, projectFolder, hostJsonFolder);
|
||||
|
||||
const otherFunctionNames = functionNames.filter(name => !functions[name].bindings.some((b: any) => ['orchestrationTrigger', 'activityTrigger', 'entityTrigger'].includes(b.type)));
|
||||
const otherFunctions = await getFunctionsAndTheirCodesAsync(otherFunctionNames, isDotNet, projectFolder, hostJsonFolder);
|
||||
|
||||
for (const orch of orchestrators) {
|
||||
|
||||
// Trying to match this orchestrator with its calling function
|
||||
const regex = TraversalRegexes.getStartNewOrchestrationRegex(orch.name);
|
||||
for (const func of otherFunctions) {
|
||||
|
||||
// If this function seems to be calling that orchestrator
|
||||
if (!!regex.exec(func.code)) {
|
||||
functions[orch.name].isCalledBy.push(func.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Matching suborchestrators
|
||||
for (const subOrch of orchestrators) {
|
||||
if (orch.name === subOrch.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this orchestrator seems to be calling that suborchestrator
|
||||
const regex = TraversalRegexes.getCallSubOrchestratorRegex(subOrch.name);
|
||||
if (!!regex.exec(orch.code)) {
|
||||
|
||||
// Mapping that suborchestrator to this orchestrator
|
||||
functions[subOrch.name].isCalledBy.push(orch.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Mapping activities to orchestrators
|
||||
mapActivitiesToOrchestrator(functions, orch, activityNames);
|
||||
|
||||
// Checking whether orchestrator calls itself
|
||||
if (!!TraversalRegexes.continueAsNewRegex.exec(orch.code)) {
|
||||
functions[orch.name].isCalledByItself = true;
|
||||
}
|
||||
|
||||
// Trying to map event producers with their consumers
|
||||
const eventNames = getEventNames(orch.code);
|
||||
for (const eventName of eventNames) {
|
||||
|
||||
const regex = TraversalRegexes.getRaiseEventRegex(eventName);
|
||||
for (const func of otherFunctions) {
|
||||
|
||||
// If this function seems to be sending that event
|
||||
if (!!regex.exec(func.code)) {
|
||||
functions[orch.name].isSignalledBy.push({ name: func.name, signalName: eventName });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
|
||||
// Trying to match this entity with its calling function
|
||||
for (const func of otherFunctions) {
|
||||
|
||||
// If this function seems to be calling that entity
|
||||
const regex = TraversalRegexes.getSignalEntityRegex(entity.name);
|
||||
if (!!regex.exec(func.code)) {
|
||||
functions[entity.name].isCalledBy.push(func.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDotNet) {
|
||||
|
||||
// Trying to extract extra binding info from C# code
|
||||
for (const func of otherFunctions) {
|
||||
|
||||
const moreBindings = DotNetBindingsParser.tryExtractBindings(func.code);
|
||||
functions[func.name].bindings.push(...moreBindings);
|
||||
}
|
||||
}
|
||||
|
||||
// Also adding file paths and code positions
|
||||
for (const func of otherFunctions.concat(orchestrators).concat(activities).concat(entities)) {
|
||||
functions[func.name].filePath = func.filePath;
|
||||
functions[func.name].pos = func.pos;
|
||||
functions[func.name].lineNr = func.lineNr;
|
||||
}
|
||||
|
||||
return functions;
|
||||
}
|
||||
|
||||
// Tries to extract event names that this orchestrator is awaiting
|
||||
function getEventNames(orchestratorCode: string): string[] {
|
||||
|
||||
const result = [];
|
||||
|
||||
const regex = TraversalRegexes.waitForExternalEventRegex;
|
||||
var match: RegExpExecArray | null;
|
||||
while (!!(match = regex.exec(orchestratorCode))) {
|
||||
result.push(match[4]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Tries to load code for functions of certain type
|
||||
async function getFunctionsAndTheirCodesAsync(functionNames: string[], isDotNet: boolean, projectFolder: string, hostJsonFolder: string)
|
||||
: Promise<{ name: string, code: string, filePath: string, pos: number, lineNr: number }[]> {
|
||||
|
||||
const promises = functionNames.map(async name => {
|
||||
|
||||
const match = await (isDotNet ?
|
||||
findFileRecursivelyAsync(projectFolder, '.+\\.(f|c)s$', true, TraversalRegexes.getDotNetFunctionNameRegex(name)) :
|
||||
findFileRecursivelyAsync(path.join(hostJsonFolder, name), '(index\\.ts|index\\.js|__init__\\.py)$', true));
|
||||
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const code = !isDotNet ? match.code : getCodeInBrackets(match.code!, match.pos! + match.length!, '{', '}', ' \n');
|
||||
const pos = !match.pos ? 0 : match.pos;
|
||||
const lineNr = posToLineNr(match.code, pos);
|
||||
|
||||
return { name, code, filePath: match.filePath, pos, lineNr };
|
||||
});
|
||||
|
||||
return (await Promise.all(promises)).filter(f => !!f) as any;
|
||||
}
|
||||
|
||||
// Tries to match orchestrator with its activities
|
||||
function mapActivitiesToOrchestrator(functions: FunctionsMap, orch: {name: string, code: string}, activityNames: string[]): void {
|
||||
|
||||
for (const activityName of activityNames) {
|
||||
|
||||
// If this orchestrator seems to be calling this activity
|
||||
const regex = TraversalRegexes.getCallActivityRegex(activityName);
|
||||
if (!!regex.exec(orch.code)) {
|
||||
|
||||
// Then mapping this activity to this orchestrator
|
||||
if (!functions[activityName].isCalledBy) {
|
||||
functions[activityName].isCalledBy = [];
|
||||
}
|
||||
functions[activityName].isCalledBy.push(orch.name);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// Does a git clone into a temp folder and returns info about that cloned code
|
||||
export async function cloneFromGitHub(url: string): Promise<{gitTempFolder: string, projectFolder: string}> {
|
||||
|
||||
var repoName = '', branchName = '', relativePath = '', gitTempFolder = '';
|
||||
|
||||
var restOfUrl: string[] = [];
|
||||
const match = /(https:\/\/github.com\/.*?)\/([^\/]+)(\/tree\/)?(.*)/i.exec(url);
|
||||
|
||||
if (!match || match.length < 5) {
|
||||
|
||||
// expecting repo name to be the last segment of remote origin URL
|
||||
repoName = url.substr(url.lastIndexOf('/') + 1);
|
||||
|
||||
} else {
|
||||
|
||||
const orgUrl = match[1];
|
||||
|
||||
repoName = match[2];
|
||||
if (repoName.toLowerCase().endsWith('.git')) {
|
||||
repoName = repoName.substr(0, repoName.length - 4);
|
||||
}
|
||||
|
||||
url = `${orgUrl}/${repoName}.git`;
|
||||
|
||||
if (!!match[4]) {
|
||||
restOfUrl = match[4].split('/').filter(s => !!s);
|
||||
}
|
||||
}
|
||||
|
||||
gitTempFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'git-clone-'));
|
||||
|
||||
// The provided URL might contain both branch name and relative path. The only way to separate one from another
|
||||
// is to repeatedly try cloning assumed branch names, until we finally succeed.
|
||||
for (var i = restOfUrl.length; i > 0; i--) {
|
||||
|
||||
try {
|
||||
|
||||
const assumedBranchName = restOfUrl.slice(0, i).join('/');
|
||||
execSync(`git clone ${url} --branch ${assumedBranchName}`, { cwd: gitTempFolder });
|
||||
|
||||
branchName = assumedBranchName;
|
||||
relativePath = path.join(...restOfUrl.slice(i, restOfUrl.length));
|
||||
|
||||
break;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
|
||||
// Just doing a normal git clone
|
||||
execSync(`git clone ${url}`, { cwd: gitTempFolder });
|
||||
}
|
||||
|
||||
return { gitTempFolder, projectFolder: path.join(gitTempFolder, repoName, relativePath) };
|
||||
}
|
||||
|
||||
// Primitive way of getting a line number out of symbol position
|
||||
export function posToLineNr(code: string | undefined, pos: number): number {
|
||||
if (!code) {
|
||||
return 0;
|
||||
}
|
||||
const lineBreaks = code.substr(0, pos).match(/(\r\n|\r|\n)/g);
|
||||
return !lineBreaks ? 1 : lineBreaks.length + 1;
|
||||
}
|
||||
|
||||
// Checks if the given folder looks like a .Net project
|
||||
export async function isDotNetProjectAsync(projectFolder: string): Promise<boolean> {
|
||||
return (await fs.promises.readdir(projectFolder)).some(fn => {
|
||||
fn = fn.toLowerCase();
|
||||
return fn.endsWith('.sln') ||
|
||||
fn.endsWith('.fsproj') ||
|
||||
(fn.endsWith('.csproj') && fn !== 'extensions.csproj');
|
||||
});
|
||||
}
|
||||
|
||||
// Complements regex's inability to keep up with nested brackets
|
||||
export function getCodeInBrackets(str: string, startFrom: number, openingBracket: string, closingBracket: string, mustHaveSymbols: string = ''): string {
|
||||
|
||||
var bracketCount = 0, openBracketPos = 0, mustHaveSymbolFound = !mustHaveSymbols;
|
||||
for (var i = startFrom; i < str.length; i++) {
|
||||
switch (str[i]) {
|
||||
case openingBracket:
|
||||
if (bracketCount <= 0) {
|
||||
openBracketPos = i + 1;
|
||||
}
|
||||
bracketCount++;
|
||||
break;
|
||||
case closingBracket:
|
||||
bracketCount--;
|
||||
if (bracketCount <= 0 && mustHaveSymbolFound) {
|
||||
return str.substring(startFrom, i + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (bracketCount > 0 && mustHaveSymbols.includes(str[i])) {
|
||||
mustHaveSymbolFound = true;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// General-purpose regexes
|
||||
export class TraversalRegexes {
|
||||
|
||||
static getStartNewOrchestrationRegex(orchName: string): RegExp {
|
||||
return new RegExp(`(StartNew|StartNewAsync|start_new)(\\s*<[\\w\\.-\\[\\]\\<\\>,\\s]+>)?\\s*\\(\\s*(["'\`]|nameof\\s*\\(\\s*[\\w\\.-]*|[\\w\\s\\.]+\\.\\s*)${orchName}\\s*["'\\),]{1}`, 'i');
|
||||
}
|
||||
|
||||
static getCallSubOrchestratorRegex(subOrchName: string): RegExp {
|
||||
return new RegExp(`(CallSubOrchestrator|CallSubOrchestratorWithRetry|call_sub_orchestrator)(Async)?(\\s*<[\\w\\.-\\[\\]\\<\\>,\\s]+>)?\\s*\\(\\s*(["'\`]|nameof\\s*\\(\\s*[\\w\\.-]*|[\\w\\s\\.]+\\.\\s*)${subOrchName}\\s*["'\\),]{1}`, 'i');
|
||||
}
|
||||
|
||||
static readonly continueAsNewRegex = new RegExp(`ContinueAsNew\\s*\\(`, 'i');
|
||||
|
||||
static getRaiseEventRegex(eventName: string): RegExp {
|
||||
return new RegExp(`(RaiseEvent|raise_event)(Async)?(.|\r|\n)*${eventName}`, 'i');
|
||||
}
|
||||
|
||||
static getSignalEntityRegex(entityName: string): RegExp {
|
||||
return new RegExp(`${entityName}\\s*["'>]{1}`);
|
||||
}
|
||||
|
||||
static readonly waitForExternalEventRegex = new RegExp(`(WaitForExternalEvent|wait_for_external_event)(<[\\s\\w,\\.-\\[\\]\\(\\)\\<\\>]+>)?\\s*\\(\\s*(nameof\\s*\\(\\s*|["'\`]|[\\w\\s\\.]+\\.\\s*)?([\\s\\w\\.-]+)\\s*["'\`\\),]{1}`, 'gi');
|
||||
|
||||
static getDotNetFunctionNameRegex(funcName: string): RegExp {
|
||||
return new RegExp(`FunctionName(Attribute)?\\s*\\(\\s*(nameof\\s*\\(\\s*|["'\`]|[\\w\\s\\.]+\\.\\s*)${funcName}\\s*["'\`\\)]{1}`)
|
||||
}
|
||||
|
||||
static getCallActivityRegex(activityName: string): RegExp {
|
||||
return new RegExp(`(CallActivity|call_activity)[\\s\\w,\\.-<>\\[\\]\\(\\)\\?]*\\([\\s\\w\\.-]*["'\`]?${activityName}\\s*["'\`\\),]{1}`, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// In .Net not all bindings are mentioned in function.json, so we need to analyze source code to extract them
|
||||
export class DotNetBindingsParser {
|
||||
|
||||
// Extracts additional bindings info from C#/F# source code
|
||||
static tryExtractBindings(funcCode: string): {type: string, direction: string}[] {
|
||||
|
||||
const result: {type: string, direction: string}[] = [];
|
||||
|
||||
if (!funcCode) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const regex = this.bindingAttributeRegex;
|
||||
var match: RegExpExecArray | null;
|
||||
while (!!(match = regex.exec(funcCode))) {
|
||||
|
||||
const isReturn = !!match[2];
|
||||
|
||||
const attributeName = match[3];
|
||||
const attributeCodeStartIndex = match.index + match[0].length - 1;
|
||||
const attributeCode = getCodeInBrackets(funcCode, attributeCodeStartIndex, '(', ')', '');
|
||||
|
||||
this.isOutRegex.lastIndex = attributeCodeStartIndex + attributeCode.length;
|
||||
const isOut = !!this.isOutRegex.exec(funcCode);
|
||||
|
||||
switch (attributeName) {
|
||||
case 'Blob': {
|
||||
const binding: any = { type: 'blob', direction: isReturn || isOut ? 'out' : 'in' };
|
||||
|
||||
const paramsMatch = this.blobParamsRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['path'] = paramsMatch[1];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Table': {
|
||||
const binding: any = { type: 'table', direction: isReturn || isOut ? 'out' : 'in' };
|
||||
|
||||
const paramsMatch = this.singleParamRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['tableName'] = paramsMatch[2];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'CosmosDB': {
|
||||
const binding: any = { type: 'cosmosDB', direction: isReturn || isOut ? 'out' : 'in' };
|
||||
|
||||
const paramsMatch = this.cosmosDbParamsRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['databaseName'] = paramsMatch[1];
|
||||
binding['collectionName'] = paramsMatch[3];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'SignalRConnectionInfo': {
|
||||
const binding: any = { type: 'signalRConnectionInfo', direction: 'in' };
|
||||
|
||||
const paramsMatch = this.signalRConnInfoParamsRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['hubName'] = paramsMatch[1];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'EventGrid': {
|
||||
const binding: any = { type: 'eventGrid', direction: 'out' };
|
||||
|
||||
const paramsMatch = this.eventGridParamsRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['topicEndpointUri'] = paramsMatch[1];
|
||||
binding['topicKeySetting'] = paramsMatch[3];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'EventHub': {
|
||||
const binding: any = { type: 'eventHub', direction: 'out' };
|
||||
|
||||
const paramsMatch = this.eventHubParamsRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['eventHubName'] = paramsMatch[1];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Queue': {
|
||||
const binding: any = { type: 'queue', direction: 'out' };
|
||||
|
||||
const paramsMatch = this.singleParamRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['queueName'] = paramsMatch[2];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ServiceBus': {
|
||||
const binding: any = { type: 'serviceBus', direction: 'out' };
|
||||
|
||||
const paramsMatch = this.singleParamRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['queueName'] = paramsMatch[2];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'SignalR': {
|
||||
const binding: any = { type: 'signalR', direction: 'out' };
|
||||
|
||||
const paramsMatch = this.signalRParamsRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['hubName'] = paramsMatch[1];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'RabbitMQ': {
|
||||
const binding: any = { type: 'rabbitMQ', direction: 'out' };
|
||||
|
||||
const paramsMatch = this.rabbitMqParamsRegex.exec(attributeCode);
|
||||
if (!!paramsMatch) {
|
||||
binding['queueName'] = paramsMatch[1];
|
||||
}
|
||||
result.push(binding);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'SendGrid': {
|
||||
result.push({ type: 'sendGrid', direction: 'out' });
|
||||
break;
|
||||
}
|
||||
case 'TwilioSms': {
|
||||
result.push({ type: 'twilioSms', direction: 'out' });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static readonly bindingAttributeRegex = new RegExp(`\\[(<)?\\s*(return:)?\\s*(\\w+)(Attribute)?\\s*\\(`, 'g');
|
||||
static readonly singleParamRegex = new RegExp(`("|nameof\\s*\\()?([\\w\\.-]+)`);
|
||||
static readonly eventHubParamsRegex = new RegExp(`"([^"]+)"`);
|
||||
static readonly signalRParamsRegex = new RegExp(`"([^"]+)"`);
|
||||
static readonly rabbitMqParamsRegex = new RegExp(`"([^"]+)"`);
|
||||
static readonly blobParamsRegex = new RegExp(`"([^"]+)"`);
|
||||
static readonly cosmosDbParamsRegex = new RegExp(`"([^"]+)"(.|\r|\n)+?"([^"]+)"`);
|
||||
static readonly signalRConnInfoParamsRegex = new RegExp(`"([^"]+)"`);
|
||||
static readonly eventGridParamsRegex = new RegExp(`"([^"]+)"(.|\r|\n)+?"([^"]+)"`);
|
||||
|
||||
static readonly isOutRegex = new RegExp(`\\]\\s*(out |ICollector|IAsyncCollector).*?(,|\\()`, 'g');
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
import { MonitorTreeDataProvider } from './MonitorTreeDataProvider';
|
||||
import { FunctionGraphList } from './FunctionGraphList';
|
||||
import { Settings } from './Settings';
|
||||
|
||||
var monitorTreeDataProvider: MonitorTreeDataProvider;
|
||||
var functionGraphList: FunctionGraphList;
|
||||
|
||||
// Name for our logging OutputChannel
|
||||
const OutputChannelName = 'Durable Functions Monitor';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// For logging
|
||||
const logChannel = Settings().enableLogging ? vscode.window.createOutputChannel(OutputChannelName) : undefined;
|
||||
if (!!logChannel) {
|
||||
context.subscriptions.push(logChannel);
|
||||
}
|
||||
|
||||
functionGraphList = new FunctionGraphList(context, logChannel);
|
||||
monitorTreeDataProvider = new MonitorTreeDataProvider(context, functionGraphList, logChannel);
|
||||
|
||||
context.subscriptions.push(
|
||||
|
||||
vscode.debug.onDidStartDebugSession(() => monitorTreeDataProvider.handleOnDebugSessionStarted()),
|
||||
|
||||
vscode.commands.registerCommand('extension.durableFunctionsMonitor',
|
||||
() => monitorTreeDataProvider.attachToTaskHub(null)),
|
||||
|
||||
vscode.commands.registerCommand('extension.durableFunctionsMonitorPurgeHistory',
|
||||
() => monitorTreeDataProvider.attachToTaskHub(null, { id: 'purgeHistory' })),
|
||||
|
||||
vscode.commands.registerCommand('extension.durableFunctionsMonitorCleanEntityStorage',
|
||||
() => monitorTreeDataProvider.attachToTaskHub(null, { id: 'cleanEntityStorage' })),
|
||||
|
||||
vscode.commands.registerCommand('extension.durableFunctionsMonitorGotoInstanceId',
|
||||
() => monitorTreeDataProvider.gotoInstanceId(null)),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.purgeHistory',
|
||||
(item) => monitorTreeDataProvider.attachToTaskHub(item, { id: 'purgeHistory' })),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.cleanEntityStorage',
|
||||
(item) => monitorTreeDataProvider.attachToTaskHub(item, { id: 'cleanEntityStorage' })),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.startNewInstance',
|
||||
(item) => monitorTreeDataProvider.attachToTaskHub(item, { id: 'startNewInstance' })),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.attachToTaskHub',
|
||||
(item) => monitorTreeDataProvider.attachToTaskHub(item)),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.detachFromTaskHub',
|
||||
(item) => monitorTreeDataProvider.detachFromTaskHub(item)),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.deleteTaskHub',
|
||||
(item) => monitorTreeDataProvider.deleteTaskHub(item)),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.gotoInstanceId',
|
||||
(item) => monitorTreeDataProvider.gotoInstanceId(item)),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.refresh',
|
||||
() => monitorTreeDataProvider.refresh()),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.attachToAnotherTaskHub',
|
||||
() => monitorTreeDataProvider.attachToAnotherTaskHub()),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.detachFromAllTaskHubs',
|
||||
() => monitorTreeDataProvider.detachFromAllTaskHubs()),
|
||||
|
||||
vscode.window.registerTreeDataProvider('durableFunctionsMonitorTreeView', monitorTreeDataProvider),
|
||||
|
||||
vscode.commands.registerCommand('extension.durableFunctionsMonitorVisualizeAsGraph',
|
||||
(item) => functionGraphList.visualize(item)),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.openInstancesInStorageExplorer',
|
||||
(item) => monitorTreeDataProvider.openTableInStorageExplorer(item, 'Instances')),
|
||||
|
||||
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.openHistoryInStorageExplorer',
|
||||
(item) => monitorTreeDataProvider.openTableInStorageExplorer(item, 'History')),
|
||||
);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
functionGraphList.cleanup();
|
||||
return monitorTreeDataProvider.cleanup();
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import * as path from 'path';
|
||||
|
||||
import { runTests } from 'vscode-test';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// The folder containing the Extension Manifest package.json
|
||||
// Passed to `--extensionDevelopmentPath`
|
||||
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
|
||||
|
||||
// The path to test runner
|
||||
// Passed to --extensionTestsPath
|
||||
const extensionTestsPath = path.resolve(__dirname, './suite/index');
|
||||
|
||||
// Download VS Code, unzip it and run the integration test
|
||||
await runTests({ extensionDevelopmentPath, extensionTestsPath });
|
||||
} catch (err) {
|
||||
console.error('Failed to run tests');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,15 @@
|
|||
import * as assert from 'assert';
|
||||
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
// import * as myExtension from '../extension';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
|
||||
test('Sample test', () => {
|
||||
assert.equal(-1, [1, 2, 3].indexOf(5));
|
||||
assert.equal(-1, [1, 2, 3].indexOf(0));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import * as path from 'path';
|
||||
import * as Mocha from 'mocha';
|
||||
import * as glob from 'glob';
|
||||
|
||||
export function run(): Promise<void> {
|
||||
// Create the mocha test
|
||||
const mocha = new Mocha({
|
||||
ui: 'tdd',
|
||||
});
|
||||
mocha.useColors(true);
|
||||
|
||||
const testsRoot = path.resolve(__dirname, '..');
|
||||
|
||||
return new Promise((c, e) => {
|
||||
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
|
||||
if (err) {
|
||||
return e(err);
|
||||
}
|
||||
|
||||
// Add files to the test suite
|
||||
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
|
||||
|
||||
try {
|
||||
// Run the mocha test
|
||||
mocha.run(failures => {
|
||||
if (failures > 0) {
|
||||
e(new Error(`${failures} tests failed.`));
|
||||
} else {
|
||||
c();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
e(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true, /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test",
|
||||
"backend"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"rules": {
|
||||
"no-string-throw": true,
|
||||
"no-unused-expression": true,
|
||||
"no-duplicate-variable": true,
|
||||
"curly": true,
|
||||
"class-name": true,
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"triple-equals": true
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# nuget.exe
|
||||
nuget.exe
|
||||
|
||||
# Azure Functions localsettings file
|
||||
local.settings.json
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"ms-azuretools.vscode-azurefunctions",
|
||||
"ms-vscode.csharp"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to .NET Functions",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"azureFunctions.deploySubpath": "bin/Release/netcoreapp3.1/publish",
|
||||
"azureFunctions.projectLanguage": "C#",
|
||||
"azureFunctions.projectRuntime": "~2",
|
||||
"debug.internalConsoleOptions": "neverOpen",
|
||||
"azureFunctions.preDeployTask": "publish"
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "clean",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"clean",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"type": "process",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"build",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"type": "process",
|
||||
"dependsOn": "clean",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "clean release",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"clean",
|
||||
"--configuration",
|
||||
"Release",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"type": "process",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"publish",
|
||||
"--configuration",
|
||||
"Release",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"type": "process",
|
||||
"dependsOn": "clean release",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"type": "func",
|
||||
"dependsOn": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/bin/Debug/netcoreapp3.1"
|
||||
},
|
||||
"command": "host start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": "$func-watch"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.IdentityModel.Protocols;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
internal static class Auth
|
||||
{
|
||||
// Magic constant for turning auth off
|
||||
public const string ISureKnowWhatIAmDoingNonce = "i_sure_know_what_i_am_doing";
|
||||
|
||||
// Value of WEBSITE_AUTH_UNAUTHENTICATED_ACTION config setting, when server-directed login flow is configured
|
||||
public const string UnauthenticatedActionRedirectToLoginPage = "RedirectToLoginPage";
|
||||
|
||||
// Default user name claim name
|
||||
public const string PreferredUserNameClaim = "preferred_username";
|
||||
|
||||
// Roles claim name
|
||||
private const string RolesClaim = "roles";
|
||||
|
||||
// If DFM_NONCE was passed as env variable, validates that the incoming request contains it. Throws UnauthorizedAccessException, if it doesn't.
|
||||
public static bool IsNonceSetAndValid(IHeaderDictionary headers)
|
||||
{
|
||||
// From now on it is the only way to skip auth
|
||||
if (DfmEndpoint.Settings.DisableAuthentication)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string nonce = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_NONCE);
|
||||
|
||||
if (!string.IsNullOrEmpty(nonce))
|
||||
{
|
||||
// Checking the nonce header
|
||||
if (nonce == headers["x-dfm-nonce"])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new UnauthorizedAccessException("Invalid nonce. Call is rejected.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validates that the incoming request is properly authenticated
|
||||
public static async Task ValidateIdentityAsync(ClaimsPrincipal principal, IHeaderDictionary headers, IRequestCookieCollection cookies, string taskHubName)
|
||||
{
|
||||
// First validating Task Hub name, if it was specified
|
||||
if (!string.IsNullOrEmpty(taskHubName))
|
||||
{
|
||||
await ThrowIfTaskHubNameIsInvalid(taskHubName);
|
||||
}
|
||||
|
||||
// Starting with nonce (used when running as a VsCode extension)
|
||||
if (IsNonceSetAndValid(headers))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Then validating anti-forgery token
|
||||
ThrowIfXsrfTokenIsInvalid(headers, cookies);
|
||||
|
||||
// Trying with EasyAuth
|
||||
var userNameClaim = principal?.FindFirst(DfmEndpoint.Settings.UserNameClaimName);
|
||||
if (userNameClaim == null)
|
||||
{
|
||||
// Validating and parsing the token ourselves
|
||||
principal = await ValidateToken(headers["Authorization"]);
|
||||
userNameClaim = principal.FindFirst(DfmEndpoint.Settings.UserNameClaimName);
|
||||
}
|
||||
|
||||
if (userNameClaim == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException($"'{DfmEndpoint.Settings.UserNameClaimName}' claim is missing in the incoming identity. Call is rejected.");
|
||||
}
|
||||
|
||||
if (DfmEndpoint.Settings.AllowedUserNames != null)
|
||||
{
|
||||
if (!DfmEndpoint.Settings.AllowedUserNames.Contains(userNameClaim.Value))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"User {userNameClaim.Value} is not mentioned in {EnvVariableNames.DFM_ALLOWED_USER_NAMES} config setting. Call is rejected");
|
||||
}
|
||||
}
|
||||
|
||||
// Also validating App Roles, if set
|
||||
if (DfmEndpoint.Settings.AllowedAppRoles != null)
|
||||
{
|
||||
var roleClaims = principal.FindAll(RolesClaim);
|
||||
if (!roleClaims.Any(claim => DfmEndpoint.Settings.AllowedAppRoles.Contains(claim.Value)))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"User {userNameClaim.Value} doesn't have any of roles mentioned in {EnvVariableNames.DFM_ALLOWED_APP_ROLES} config setting. Call is rejected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<HashSet<string>> GetTaskHubNamesFromStorage(string connStringName)
|
||||
{
|
||||
var tableNames = await TableClient.GetTableClient(connStringName).ListTableNamesAsync();
|
||||
|
||||
var hubNames = new HashSet<string>(tableNames
|
||||
.Where(n => n.EndsWith("Instances"))
|
||||
.Select(n => n.Remove(n.Length - "Instances".Length)),
|
||||
StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
hubNames.IntersectWith(tableNames
|
||||
.Where(n => n.EndsWith("History"))
|
||||
.Select(n => n.Remove(n.Length - "History".Length)));
|
||||
|
||||
return hubNames;
|
||||
}
|
||||
|
||||
// Lists all allowed Task Hubs. The returned HashSet is configured to ignore case.
|
||||
public static async Task<HashSet<string>> GetAllowedTaskHubNamesAsync()
|
||||
{
|
||||
// Respecting DFM_HUB_NAME, if it is set
|
||||
string dfmHubName = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_HUB_NAME);
|
||||
if (!string.IsNullOrEmpty(dfmHubName))
|
||||
{
|
||||
return new HashSet<string>(dfmHubName.Split(','), StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
// Also respecting host.json setting, when set
|
||||
dfmHubName = TryGetHubNameFromHostJson();
|
||||
if (!string.IsNullOrEmpty(dfmHubName))
|
||||
{
|
||||
return new HashSet<string>(new string[] { dfmHubName }, StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
// Otherwise trying to load table names from the Storage
|
||||
try
|
||||
{
|
||||
var hubNames = await GetTaskHubNamesFromStorage(EnvVariableNames.AzureWebJobsStorage);
|
||||
|
||||
// Also checking alternative connection strings
|
||||
foreach(var connName in AlternativeConnectionStringNames)
|
||||
{
|
||||
var connAndHubNames = (await GetTaskHubNamesFromStorage(Globals.GetFullConnectionStringEnvVariableName(connName)))
|
||||
.Select(hubName => Globals.CombineConnNameAndHubName(connName, hubName));
|
||||
|
||||
hubNames.UnionWith(connAndHubNames);
|
||||
}
|
||||
|
||||
return hubNames;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Intentionally returning null. Need to skip validation, if for some reason list of tables
|
||||
// cannot be loaded from Storage. But only in that case.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Regex ValidTaskHubNameRegex = new Regex(@"^[\w-]{3,128}$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static Task<HashSet<string>> HubNamesTask = GetAllowedTaskHubNamesAsync();
|
||||
|
||||
// Checks that a Task Hub name looks like a Task Hub name
|
||||
public static void ThrowIfTaskHubNameHasInvalidSymbols(string hubName)
|
||||
{
|
||||
if (!ValidTaskHubNameRegex.Match(hubName).Success)
|
||||
{
|
||||
throw new ArgumentException($"Task Hub name is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
// Checks that a Task Hub name is valid for this instace
|
||||
public static async Task ThrowIfTaskHubNameIsInvalid(string hubName)
|
||||
{
|
||||
// Two bugs away. Validating that the incoming Task Hub name looks like a Task Hub name
|
||||
ThrowIfTaskHubNameHasInvalidSymbols(hubName);
|
||||
|
||||
var hubNames = await HubNamesTask;
|
||||
|
||||
if (hubNames == null || !hubNames.Contains(hubName))
|
||||
{
|
||||
// doing double-check, by reloading hub names
|
||||
HubNamesTask = GetAllowedTaskHubNamesAsync();
|
||||
hubNames = await HubNamesTask;
|
||||
}
|
||||
|
||||
// If existing Task Hub names cannot be read from Storage, we can only skip validation and return true.
|
||||
// Note, that it will never be null, if DFM_HUB_NAME is set. So authZ is always in place.
|
||||
if (hubNames == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hubNames.Contains(hubName))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Task Hub '{hubName}' is not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
// Compares our XSRF tokens, that come from cookies and headers
|
||||
public static void ThrowIfXsrfTokenIsInvalid(IHeaderDictionary headers, IRequestCookieCollection cookies)
|
||||
{
|
||||
string tokenFromHeaders = headers[Globals.XsrfTokenCookieAndHeaderName];
|
||||
|
||||
if (string.IsNullOrEmpty(tokenFromHeaders))
|
||||
{
|
||||
throw new UnauthorizedAccessException("XSRF token is missing.");
|
||||
}
|
||||
|
||||
string tokenFromCookies = cookies[Globals.XsrfTokenCookieAndHeaderName];
|
||||
|
||||
if (tokenFromCookies != tokenFromHeaders)
|
||||
{
|
||||
throw new UnauthorizedAccessException("XSRF tokens do not match.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static string[] AlternativeConnectionStringNames = GetAlternativeConnectionStringNames().ToArray();
|
||||
|
||||
internal static IEnumerable<string> GetAlternativeConnectionStringNames()
|
||||
{
|
||||
var envVars = Environment.GetEnvironmentVariables();
|
||||
foreach(DictionaryEntry kv in envVars)
|
||||
{
|
||||
string variableName = kv.Key.ToString();
|
||||
if (variableName.StartsWith(EnvVariableNames.DFM_ALTERNATIVE_CONNECTION_STRING_PREFIX))
|
||||
{
|
||||
yield return variableName.Substring(EnvVariableNames.DFM_ALTERNATIVE_CONNECTION_STRING_PREFIX.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string TryGetHubNameFromHostJson()
|
||||
{
|
||||
try
|
||||
{
|
||||
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
|
||||
string functionAppFolder = Path.GetDirectoryName(Path.GetDirectoryName(assemblyLocation));
|
||||
|
||||
string hostJsonFileName = Path.Combine(functionAppFolder, "host.json");
|
||||
dynamic hostJson = JObject.Parse(File.ReadAllText(hostJsonFileName));
|
||||
|
||||
string hubName = hostJson.extensions.durableTask.hubName;
|
||||
if (hubName.StartsWith('%') && hubName.EndsWith('%'))
|
||||
{
|
||||
hubName = Environment.GetEnvironmentVariable(hubName.Trim('%'));
|
||||
}
|
||||
|
||||
return hubName;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal static JwtSecurityTokenHandler MockedJwtSecurityTokenHandler = null;
|
||||
|
||||
private static async Task<ClaimsPrincipal> ValidateToken(string authorizationHeader)
|
||||
{
|
||||
if (string.IsNullOrEmpty(authorizationHeader))
|
||||
{
|
||||
throw new UnauthorizedAccessException("No access token provided. Call is rejected.");
|
||||
}
|
||||
|
||||
string clientId = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID);
|
||||
if (string.IsNullOrEmpty(clientId))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Specify the Valid Audience value via '{EnvVariableNames.WEBSITE_AUTH_CLIENT_ID}' config setting. Typically it is the ClientId of your AAD application.");
|
||||
}
|
||||
|
||||
string openIdIssuer = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER);
|
||||
if (string.IsNullOrEmpty(openIdIssuer))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Specify the Valid Issuer value via '{EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER}' config setting. Typically it looks like 'https://login.microsoftonline.com/<your-aad-tenant-id>/v2.0'.");
|
||||
}
|
||||
|
||||
string token = authorizationHeader.Substring("Bearer ".Length);
|
||||
|
||||
var validationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidAudiences = new[] { clientId },
|
||||
ValidIssuers = new[] { openIdIssuer },
|
||||
// Yes, it is OK to await a Task multiple times like this
|
||||
IssuerSigningKeys = await GetSigningKeysTask,
|
||||
// According to internet, this should not be needed (despite the fact that the default value is false)
|
||||
// But better to be two-bugs away
|
||||
ValidateIssuerSigningKey = true
|
||||
};
|
||||
|
||||
var handler = MockedJwtSecurityTokenHandler ?? new JwtSecurityTokenHandler();
|
||||
|
||||
return handler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
|
||||
}
|
||||
|
||||
// Caching the keys for 24 hours
|
||||
internal static Task<ICollection<SecurityKey>> GetSigningKeysTask = InitGetSigningKeysTask(86400, 0);
|
||||
|
||||
internal static Task<ICollection<SecurityKey>> InitGetSigningKeysTask(int cacheTtlInSeconds, int retryCount = 0)
|
||||
{
|
||||
// If you ever use this code in Asp.Net, don't forget to wrap this line with Task.Run(), to decouple from SynchronizationContext
|
||||
var task = GetSigningKeysAsync();
|
||||
|
||||
// Adding cache-flushing continuation
|
||||
task.ContinueWith(async t =>
|
||||
{
|
||||
// If the data retrieval failed, then retrying immediately, but no more than 3 times.
|
||||
// Otherwise re-populating the cache in cacheTtlInSeconds.
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
if (retryCount > 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(cacheTtlInSeconds));
|
||||
}
|
||||
|
||||
GetSigningKeysTask = InitGetSigningKeysTask(cacheTtlInSeconds, t.IsFaulted ? retryCount + 1 : 0);
|
||||
});
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private static async Task<ICollection<SecurityKey>> GetSigningKeysAsync()
|
||||
{
|
||||
string openIdIssuer = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER);
|
||||
if (string.IsNullOrEmpty(openIdIssuer))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Specify the Valid Issuer value via '{EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER}' config setting. Typically it looks like 'https://login.microsoftonline.com/<your-aad-tenant-id>/v2.0'.");
|
||||
}
|
||||
|
||||
if (openIdIssuer.EndsWith("/v2.0"))
|
||||
{
|
||||
openIdIssuer = openIdIssuer.Substring(0, openIdIssuer.Length - "/v2.0".Length);
|
||||
}
|
||||
|
||||
string stsDiscoveryEndpoint = $"{openIdIssuer}/.well-known/openid-configuration";
|
||||
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
|
||||
var config = await configManager.GetConfigurationAsync();
|
||||
|
||||
return config.SigningKeys;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.WindowsAzure.Storage;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
// Contains all logic of loading custom tab/html templates
|
||||
// TODO: respect alternative connection strings
|
||||
class CustomTemplates
|
||||
{
|
||||
internal static Task<LiquidTemplatesMap> GetTabTemplatesAsync()
|
||||
{
|
||||
if (TabTemplatesTask == null)
|
||||
{
|
||||
TabTemplatesTask = string.IsNullOrEmpty(DfmEndpoint.Settings.CustomTemplatesFolderName) ?
|
||||
GetTabTemplatesFromStorageAsync() : GetTabTemplatesFromFolderAsync(DfmEndpoint.Settings.CustomTemplatesFolderName);
|
||||
}
|
||||
|
||||
return TabTemplatesTask;
|
||||
}
|
||||
|
||||
internal static Task<string> GetCustomMetaTagCodeAsync()
|
||||
{
|
||||
if (CustomMetaTagCodeTask == null)
|
||||
{
|
||||
CustomMetaTagCodeTask = string.IsNullOrEmpty(DfmEndpoint.Settings.CustomTemplatesFolderName) ?
|
||||
GetCustomMetaTagCodeFromStorageAsync() : GetCustomMetaTagCodeFromFolderAsync(DfmEndpoint.Settings.CustomTemplatesFolderName);
|
||||
}
|
||||
|
||||
return CustomMetaTagCodeTask;
|
||||
}
|
||||
|
||||
internal static Task<FunctionMapsMap> GetFunctionMapsAsync()
|
||||
{
|
||||
if (FunctionMapsTask == null)
|
||||
{
|
||||
FunctionMapsTask = string.IsNullOrEmpty(DfmEndpoint.Settings.CustomTemplatesFolderName) ?
|
||||
GetFunctionMapsFromStorageAsync() : GetFunctionMapsFromFolderAsync(DfmEndpoint.Settings.CustomTemplatesFolderName);
|
||||
}
|
||||
|
||||
return FunctionMapsTask;
|
||||
}
|
||||
|
||||
// Yes, it is OK to use Task in this way.
|
||||
// The Task code will only be executed once. All subsequent/parallel awaits will get the same returned value.
|
||||
// Tasks do have the same behavior as Lazy<T>.
|
||||
private static Task<LiquidTemplatesMap> TabTemplatesTask;
|
||||
|
||||
private static Task<string> CustomMetaTagCodeTask;
|
||||
|
||||
private static Task<FunctionMapsMap> FunctionMapsTask;
|
||||
|
||||
// Tries to load liquid templates from underlying Azure Storage
|
||||
private static async Task<LiquidTemplatesMap> GetTabTemplatesFromStorageAsync()
|
||||
{
|
||||
var result = new LiquidTemplatesMap();
|
||||
try
|
||||
{
|
||||
string connectionString = Environment.GetEnvironmentVariable(EnvVariableNames.AzureWebJobsStorage);
|
||||
var blobClient = CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient();
|
||||
|
||||
// Listing all blobs in durable-functions-monitor/tab-templates folder
|
||||
var container = blobClient.GetContainerReference(Globals.TemplateContainerName);
|
||||
|
||||
string templateFolderName = Globals.TabTemplateFolderName + "/";
|
||||
var templateNames = await container.ListBlobsAsync(templateFolderName);
|
||||
|
||||
// Loading blobs in parallel
|
||||
await Task.WhenAll(templateNames.Select(async templateName =>
|
||||
{
|
||||
var blob = await blobClient.GetBlobReferenceFromServerAsync(templateName.Uri);
|
||||
|
||||
// Expecting the blob name to be like "[Tab Name].[EntityTypeName].liquid" or just "[Tab Name].liquid"
|
||||
var nameParts = blob.Name.Substring(templateFolderName.Length).Split('.');
|
||||
if (nameParts.Length < 2 || nameParts.Last() != "liquid")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string tabName = nameParts[0];
|
||||
string entityTypeName = nameParts.Length > 2 ? nameParts[1] : string.Empty;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
await blob.DownloadToStreamAsync(stream);
|
||||
string templateText = Encoding.UTF8.GetString(stream.ToArray());
|
||||
|
||||
result.GetOrAdd(entityTypeName, new ConcurrentDictionary<string, string>())[tabName] = templateText;
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Intentionally swallowing all exceptions here
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Tries to load liquid templates from local folder
|
||||
private static async Task<LiquidTemplatesMap> GetTabTemplatesFromFolderAsync(string folderName)
|
||||
{
|
||||
var result = new LiquidTemplatesMap();
|
||||
|
||||
string binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
string templatesFolder = Path.Combine(binFolder, "..", folderName, Globals.TabTemplateFolderName);
|
||||
|
||||
if (!Directory.Exists(templatesFolder))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach(var templateFilePath in Directory.EnumerateFiles(templatesFolder, "*.liquid"))
|
||||
{
|
||||
var nameParts = Path.GetFileName(templateFilePath).Split('.');
|
||||
if(nameParts.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string tabName = nameParts[0];
|
||||
string entityTypeName = nameParts.Length > 2 ? nameParts[1] : string.Empty;
|
||||
string templateText = await File.ReadAllTextAsync(templateFilePath);
|
||||
|
||||
result.GetOrAdd(entityTypeName, new ConcurrentDictionary<string, string>())[tabName] = templateText;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Tries to load code for our meta tag from Storage
|
||||
private static async Task<string> GetCustomMetaTagCodeFromStorageAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var blobClient = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable(EnvVariableNames.AzureWebJobsStorage)).CreateCloudBlobClient();
|
||||
var container = blobClient.GetContainerReference(Globals.TemplateContainerName);
|
||||
var blob = container.GetBlobReference(Globals.CustomMetaTagBlobName);
|
||||
|
||||
if (!(await blob.ExistsAsync()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
await blob.DownloadToStreamAsync(stream);
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Intentionally swallowing all exceptions here
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Tries to load code for our meta tag from local folder
|
||||
private static async Task<string> GetCustomMetaTagCodeFromFolderAsync(string folderName)
|
||||
{
|
||||
string binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
string filePath = Path.Combine(binFolder, "..", folderName, Globals.CustomMetaTagBlobName);
|
||||
|
||||
if(!File.Exists(filePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await File.ReadAllTextAsync(filePath);
|
||||
}
|
||||
|
||||
// Tries to load Function Maps from underlying Azure Storage
|
||||
private static async Task<FunctionMapsMap> GetFunctionMapsFromStorageAsync()
|
||||
{
|
||||
var result = new FunctionMapsMap();
|
||||
try
|
||||
{
|
||||
string connectionString = Environment.GetEnvironmentVariable(EnvVariableNames.AzureWebJobsStorage);
|
||||
var blobClient = CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient();
|
||||
|
||||
// Listing all blobs in durable-functions-monitor/function-maps folder
|
||||
var container = blobClient.GetContainerReference(Globals.TemplateContainerName);
|
||||
|
||||
string functionMapFolderName = Globals.FunctionMapFolderName + "/";
|
||||
var fileNames = await container.ListBlobsAsync(functionMapFolderName);
|
||||
|
||||
// Loading blobs in parallel
|
||||
await Task.WhenAll(fileNames.Select(async templateName =>
|
||||
{
|
||||
var blob = await blobClient.GetBlobReferenceFromServerAsync(templateName.Uri);
|
||||
|
||||
// Expecting the blob name to be like "dfm-function-map.[TaskHubName].json" or just "dfm-function-map.json"
|
||||
var nameParts = blob.Name.Substring(functionMapFolderName.Length).Split('.');
|
||||
if (nameParts.Length < 2 || nameParts.First() != Globals.FunctionMapFilePrefix || nameParts.Last() != "json")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string taskHubName = nameParts.Length > 2 ? nameParts[1] : string.Empty;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
await blob.DownloadToStreamAsync(stream);
|
||||
string templateText = Encoding.UTF8.GetString(stream.ToArray());
|
||||
|
||||
result.TryAdd(taskHubName, templateText);
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Intentionally swallowing all exceptions here
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Tries to load Function Maps from local folder
|
||||
private static async Task<FunctionMapsMap> GetFunctionMapsFromFolderAsync(string folderName)
|
||||
{
|
||||
var result = new FunctionMapsMap();
|
||||
|
||||
string binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
string functionMapsFolder = Path.Combine(binFolder, "..", folderName, Globals.FunctionMapFolderName);
|
||||
|
||||
if (!Directory.Exists(functionMapsFolder))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach(var filePath in Directory.EnumerateFiles(functionMapsFolder, $"{Globals.FunctionMapFilePrefix}*.json"))
|
||||
{
|
||||
var nameParts = Path.GetFileName(filePath).Split('.');
|
||||
if (nameParts.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string taskHubName = nameParts.Length > 2 ? nameParts[1] : string.Empty;
|
||||
string json = await File.ReadAllTextAsync(filePath);
|
||||
|
||||
result.TryAdd(taskHubName, json);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Represents the liquid template map
|
||||
class LiquidTemplatesMap: ConcurrentDictionary<string, IDictionary<string, string>>
|
||||
{
|
||||
public List<string> GetTemplateNames(string entityTypeName)
|
||||
{
|
||||
var result = new List<string>();
|
||||
IDictionary<string, string> templates;
|
||||
|
||||
// Getting template names for all entity types
|
||||
if (this.TryGetValue(string.Empty, out templates))
|
||||
{
|
||||
result.AddRange(templates.Keys);
|
||||
}
|
||||
|
||||
// Getting template names for this particular entity type
|
||||
if (this.TryGetValue(entityTypeName, out templates))
|
||||
{
|
||||
result.AddRange(templates.Keys);
|
||||
}
|
||||
|
||||
result.Sort();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetTemplate(string entityTypeName, string templateName)
|
||||
{
|
||||
string result = null;
|
||||
IDictionary<string, string> templates;
|
||||
|
||||
// Getting template names for all entity types
|
||||
if (this.TryGetValue(string.Empty, out templates))
|
||||
{
|
||||
if(templates.TryGetValue(templateName, out result)){
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Getting template names for this particular entity type
|
||||
if (this.TryGetValue(entityTypeName, out templates))
|
||||
{
|
||||
if (templates.TryGetValue(templateName, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Represents the map of Function Maps
|
||||
class FunctionMapsMap : ConcurrentDictionary<string, string>
|
||||
{
|
||||
public string GetFunctionMap(string taskHubName)
|
||||
{
|
||||
string result = null;
|
||||
|
||||
// Getting Function Map for this particular Task Hub
|
||||
if (!this.TryGetValue(taskHubName, out result))
|
||||
{
|
||||
// Getting Function Map for all Task Hubs
|
||||
this.TryGetValue(string.Empty, out result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using Microsoft.WindowsAzure.Storage;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
// Adds extra fields to DurableOrchestrationStatus returned by IDurableClient.GetStatusAsync()
|
||||
class DetailedOrchestrationStatus : DurableOrchestrationStatus
|
||||
{
|
||||
public EntityTypeEnum EntityType { get; private set; }
|
||||
public EntityId? EntityId { get; private set; }
|
||||
|
||||
public List<string> TabTemplateNames
|
||||
{
|
||||
get
|
||||
{
|
||||
// The underlying Task never throws, so it's OK.
|
||||
var templatesMap = CustomTemplates.GetTabTemplatesAsync().Result;
|
||||
return templatesMap.GetTemplateNames(this.GetEntityTypeName());
|
||||
}
|
||||
}
|
||||
|
||||
public DetailedOrchestrationStatus(DurableOrchestrationStatus that, string connName)
|
||||
{
|
||||
this.Name = that.Name;
|
||||
this.InstanceId = that.InstanceId;
|
||||
this.CreatedTime = that.CreatedTime;
|
||||
this.LastUpdatedTime = that.LastUpdatedTime;
|
||||
this.RuntimeStatus = that.RuntimeStatus;
|
||||
this.Output = that.Output;
|
||||
this.CustomStatus = that.CustomStatus;
|
||||
this.History = that.History;
|
||||
|
||||
// Detecting whether it is an Orchestration or a Durable Entity
|
||||
var match = ExpandedOrchestrationStatus.EntityIdRegex.Match(this.InstanceId);
|
||||
if (match.Success)
|
||||
{
|
||||
this.EntityType = EntityTypeEnum.DurableEntity;
|
||||
this.EntityId = new EntityId(match.Groups[1].Value, match.Groups[2].Value);
|
||||
}
|
||||
|
||||
this.Input = this.ConvertInput(that.Input, connName);
|
||||
}
|
||||
|
||||
internal string GetEntityTypeName()
|
||||
{
|
||||
return this.EntityType == EntityTypeEnum.DurableEntity ? this.EntityId.Value.EntityName : this.Name;
|
||||
}
|
||||
|
||||
private JToken ConvertInput(JToken input, string connName)
|
||||
{
|
||||
if (this.EntityType != EntityTypeEnum.DurableEntity)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
// Temp fix for https://github.com/Azure/azure-functions-durable-extension/issues/1786
|
||||
if (input.Type == JTokenType.String && input.ToString().ToLowerInvariant().StartsWith("https://"))
|
||||
{
|
||||
string connectionString = Environment.GetEnvironmentVariable(Globals.GetFullConnectionStringEnvVariableName(connName));
|
||||
var blobClient = CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient();
|
||||
var blob = blobClient.GetBlobReferenceFromServerAsync(new Uri(input.ToString())).Result;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
blob.DownloadToStreamAsync(stream).Wait();
|
||||
stream.Position = 0;
|
||||
using (var gzipStream = new GZipStream(stream, CompressionMode.Decompress))
|
||||
using (var streamReader = new StreamReader(gzipStream))
|
||||
using (var jsonTextReader = new JsonTextReader(streamReader))
|
||||
{
|
||||
input = JToken.ReadFrom(jsonTextReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stateToken = input["state"];
|
||||
if (stateToken == null || stateToken.Type != JTokenType.String)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var stateString = stateToken.Value<string>();
|
||||
if (!(stateString.StartsWith('{') && stateString.EndsWith('}')))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
// Converting JSON string into JSON object
|
||||
input["state"] = JObject.Parse(stateString);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
enum EntityTypeEnum
|
||||
{
|
||||
Orchestration = 0,
|
||||
DurableEntity
|
||||
}
|
||||
|
||||
// Adds extra fields to DurableOrchestrationStatus returned by IDurableClient.ListInstancesAsync()
|
||||
class ExpandedOrchestrationStatus : DurableOrchestrationStatus
|
||||
{
|
||||
public static readonly Regex EntityIdRegex = new Regex(@"@(\w+)@(.+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
public EntityTypeEnum EntityType { get; private set; }
|
||||
public EntityId? EntityId { get; private set; }
|
||||
public double Duration { get; private set; }
|
||||
public string LastEvent
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this._detailsTask == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (this._lastEvent != null)
|
||||
{
|
||||
return this._lastEvent;
|
||||
}
|
||||
|
||||
this._lastEvent = string.Empty;
|
||||
DurableOrchestrationStatus details;
|
||||
try
|
||||
{
|
||||
// For some orchestrations getting an extended status might fail due to bugs in DurableOrchestrationClient.
|
||||
// So just returning an empty string in that case.
|
||||
details = this._detailsTask.Result;
|
||||
}
|
||||
catch(Exception)
|
||||
{
|
||||
return this._lastEvent;
|
||||
}
|
||||
|
||||
if (details.History == null)
|
||||
{
|
||||
return this._lastEvent;
|
||||
}
|
||||
|
||||
var lastEvent = details.History
|
||||
.Select(e => e["Name"] ?? e["FunctionName"] )
|
||||
.LastOrDefault(e => e != null);
|
||||
|
||||
if (lastEvent == null)
|
||||
{
|
||||
return this._lastEvent;
|
||||
}
|
||||
|
||||
this._lastEvent = lastEvent.ToString();
|
||||
return this._lastEvent;
|
||||
}
|
||||
}
|
||||
|
||||
public ExpandedOrchestrationStatus(DurableOrchestrationStatus that,
|
||||
Task<DurableOrchestrationStatus> detailsTask,
|
||||
HashSet<string> hiddenColumns)
|
||||
{
|
||||
this.Name = that.Name;
|
||||
this.InstanceId = that.InstanceId;
|
||||
this.CreatedTime = that.CreatedTime;
|
||||
this.LastUpdatedTime = that.LastUpdatedTime;
|
||||
this.Duration = Math.Round((that.LastUpdatedTime - that.CreatedTime).TotalMilliseconds);
|
||||
this.RuntimeStatus = that.RuntimeStatus;
|
||||
|
||||
this.Input = hiddenColumns.Contains("input") ? null : that.Input;
|
||||
this.Output = hiddenColumns.Contains("output") ? null : that.Output;
|
||||
this.CustomStatus = hiddenColumns.Contains("customStatus") ? null : that.CustomStatus;
|
||||
|
||||
// Detecting whether it is an Orchestration or a Durable Entity
|
||||
var match = EntityIdRegex.Match(this.InstanceId);
|
||||
if(match.Success)
|
||||
{
|
||||
this.EntityType = EntityTypeEnum.DurableEntity;
|
||||
this.EntityId = new EntityId(match.Groups[1].Value, match.Groups[2].Value);
|
||||
}
|
||||
|
||||
this._detailsTask = detailsTask;
|
||||
}
|
||||
|
||||
internal string GetEntityTypeName()
|
||||
{
|
||||
return this.EntityType == EntityTypeEnum.DurableEntity ? this.EntityId.Value.EntityName : this.Name;
|
||||
}
|
||||
|
||||
private Task<DurableOrchestrationStatus> _detailsTask;
|
||||
private string _lastEvent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
// A parsed $filter clause
|
||||
class FilterClause
|
||||
{
|
||||
public FilterClause(string filterString)
|
||||
{
|
||||
if (filterString == null)
|
||||
{
|
||||
filterString = string.Empty;
|
||||
}
|
||||
|
||||
filterString = this.ExtractTimeRange(filterString);
|
||||
filterString = this.ExtractRuntimeStatuses(filterString);
|
||||
this.ExtractPredicate(filterString);
|
||||
}
|
||||
|
||||
public Func<string, bool> Predicate { get; private set; }
|
||||
public string FieldName { get; private set; }
|
||||
|
||||
public DateTime? TimeFrom { get; private set; }
|
||||
public DateTime? TimeTill { get; private set; }
|
||||
public string[] RuntimeStatuses { get; private set; }
|
||||
|
||||
private string ExtractTimeRange(string filterClause)
|
||||
{
|
||||
this.TimeFrom = null;
|
||||
this.TimeTill = null;
|
||||
|
||||
if (string.IsNullOrEmpty(filterClause))
|
||||
{
|
||||
return filterClause;
|
||||
}
|
||||
|
||||
var match = TimeFromRegex.Match(filterClause);
|
||||
if (match.Success)
|
||||
{
|
||||
this.TimeFrom = DateTime.Parse(match.Groups[3].Value);
|
||||
filterClause = filterClause.Substring(0, match.Index) + filterClause.Substring(match.Index + match.Length);
|
||||
}
|
||||
|
||||
match = TimeTillRegex.Match(filterClause);
|
||||
if (match.Success)
|
||||
{
|
||||
this.TimeTill = DateTime.Parse(match.Groups[2].Value);
|
||||
filterClause = filterClause.Substring(0, match.Index) + filterClause.Substring(match.Index + match.Length);
|
||||
}
|
||||
|
||||
return filterClause;
|
||||
}
|
||||
|
||||
private string ExtractRuntimeStatuses(string filterClause)
|
||||
{
|
||||
this.RuntimeStatuses = null;
|
||||
|
||||
if (string.IsNullOrEmpty(filterClause))
|
||||
{
|
||||
return filterClause;
|
||||
}
|
||||
|
||||
var match = RuntimeStatusRegex.Match(filterClause);
|
||||
if (match.Success)
|
||||
{
|
||||
this.RuntimeStatuses = match.Groups[2].Value
|
||||
.Split(',').Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(s => s.Trim(' ', '\'')).ToArray();
|
||||
|
||||
filterClause = filterClause.Substring(0, match.Index) + filterClause.Substring(match.Index + match.Length);
|
||||
}
|
||||
|
||||
return filterClause;
|
||||
}
|
||||
|
||||
private void ExtractPredicate(string filterString)
|
||||
{
|
||||
// startswith(field-name, 'value') eq true|false
|
||||
var match = StartsWithRegex.Match(filterString);
|
||||
if (match.Success)
|
||||
{
|
||||
|
||||
bool result = true;
|
||||
if (match.Groups.Count > 4)
|
||||
{
|
||||
result = match.Groups[4].Value != "false";
|
||||
}
|
||||
string arg = match.Groups[2].Value;
|
||||
|
||||
this.Predicate = (v) => v.StartsWith(arg) == result;
|
||||
}
|
||||
// contains(field-name, 'value') eq true|false
|
||||
else if ((match = ContainsRegex.Match(filterString)).Success)
|
||||
{
|
||||
bool result = true;
|
||||
if (match.Groups.Count > 4)
|
||||
{
|
||||
result = match.Groups[4].Value != "false";
|
||||
}
|
||||
string arg = match.Groups[2].Value;
|
||||
|
||||
this.Predicate = (v) => v.Contains(arg) == result;
|
||||
}
|
||||
// field-name eq|ne 'value'
|
||||
else if ((match = EqRegex.Match(filterString)).Success)
|
||||
{
|
||||
string value = match.Groups[3].Value;
|
||||
string op = match.Groups[2].Value;
|
||||
|
||||
this.Predicate = (v) =>
|
||||
{
|
||||
bool res = value == "null" ? string.IsNullOrEmpty(v) : v == value;
|
||||
return op == "ne" ? !res : res;
|
||||
};
|
||||
}
|
||||
// field-name in ('value1','value2','value3')
|
||||
else if ((match = InRegex.Match(filterString)).Success)
|
||||
{
|
||||
string value = match.Groups[2].Value.Trim();
|
||||
|
||||
string[] values;
|
||||
if (value.StartsWith("'"))
|
||||
{
|
||||
values = LazyQuotesRegex.Matches(value)
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
values = match.Groups[2].Value
|
||||
.Split(',').Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(s => s.Trim(' ', '\''))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if ((match.Groups.Count) > 4 && (match.Groups[4].Value == "false"))
|
||||
{
|
||||
this.Predicate = (v) => !values.Contains(v);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Predicate = (v) => values.Contains(v);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.Predicate != null)
|
||||
{
|
||||
this.FieldName = match.Groups[1].Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Regex StartsWithRegex = new Regex(@"startswith\s*\(\s*(\w+)\s*,\s*'([^']+)'\s*\)\s*(eq)?\s*(true|false)?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex ContainsRegex = new Regex(@"contains\s*\(\s*(\w+)\s*,\s*'([^']+)'\s*\)\s*(eq)?\s*(true|false)?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex EqRegex = new Regex(@"(\w+)\s+(eq|ne)\s*'([^']+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex InRegex = new Regex(@"(\w+)\s+in\s*\((.*)\)\s*(eq)?\s*(true|false)?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex LazyQuotesRegex = new Regex(@"'(.*?)'", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex RuntimeStatusRegex = new Regex(@"\s*(and\s+)?runtimeStatus\s+in\s*\(([^\)]*)\)(\s*and)?\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex TimeFromRegex = new Regex(@"\s*(and\s+)?(createdTime|timestamp)\s+ge\s+'([\d-:.T]{19,}Z)'(\s*and)?\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex TimeTillRegex = new Regex(@"\s*(and\s+)?createdTime\s+le\s+'([\d-:.T]{19,}Z)'(\s*and)?\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
static class FilterClauseExtensions
|
||||
{
|
||||
// Applies a filter to a collection of items
|
||||
internal static IEnumerable<T> ApplyFilter<T>(this IEnumerable<T> items, FilterClause filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter.FieldName))
|
||||
{
|
||||
// if field to be filtered is not specified, returning everything
|
||||
foreach (var orchestration in items)
|
||||
{
|
||||
yield return orchestration;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (filter.Predicate == null)
|
||||
{
|
||||
// if filter expression is invalid, returning nothing
|
||||
yield break;
|
||||
}
|
||||
|
||||
var propInfo = typeof(T).GetProperties()
|
||||
.FirstOrDefault(p => p.Name.Equals(filter.FieldName, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (propInfo == null)
|
||||
{
|
||||
// if field name is invalid, returning nothing
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (filter.Predicate(item.GetPropertyValueAsString(propInfo)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for formatting orchestration field values
|
||||
internal static string GetPropertyValueAsString<T>(this T orchestration, PropertyInfo propInfo)
|
||||
{
|
||||
object propValue = propInfo.GetValue(orchestration);
|
||||
|
||||
if (propValue == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Explicitly handling DateTime as 'yyyy-MM-ddTHH:mm:ssZ'
|
||||
if (propInfo.PropertyType == typeof(DateTime))
|
||||
{
|
||||
return ((DateTime)propValue).ToString(Globals.SerializerSettings.DateFormatString);
|
||||
}
|
||||
|
||||
return propValue.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.WindowsAzure.Storage.Blob;
|
||||
using Microsoft.WindowsAzure.Storage.Table;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Linq;
|
||||
|
||||
[assembly: InternalsVisibleToAttribute("durablefunctionsmonitor.dotnetbackend.tests")]
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
static class EnvVariableNames
|
||||
{
|
||||
public const string AzureWebJobsStorage = "AzureWebJobsStorage";
|
||||
public const string WEBSITE_SITE_NAME = "WEBSITE_SITE_NAME";
|
||||
public const string WEBSITE_AUTH_CLIENT_ID = "WEBSITE_AUTH_CLIENT_ID";
|
||||
public const string WEBSITE_AUTH_OPENID_ISSUER = "WEBSITE_AUTH_OPENID_ISSUER";
|
||||
public const string WEBSITE_AUTH_UNAUTHENTICATED_ACTION = "WEBSITE_AUTH_UNAUTHENTICATED_ACTION";
|
||||
public const string DFM_ALLOWED_USER_NAMES = "DFM_ALLOWED_USER_NAMES";
|
||||
public const string DFM_ALLOWED_APP_ROLES = "DFM_ALLOWED_APP_ROLES";
|
||||
public const string DFM_HUB_NAME = "DFM_HUB_NAME";
|
||||
public const string DFM_NONCE = "DFM_NONCE";
|
||||
public const string DFM_CLIENT_CONFIG = "DFM_CLIENT_CONFIG";
|
||||
public const string DFM_MODE = "DFM_MODE";
|
||||
public const string DFM_USERNAME_CLAIM_NAME = "DFM_USERNAME_CLAIM_NAME";
|
||||
public const string DFM_ALTERNATIVE_CONNECTION_STRING_PREFIX = "DFM_ALTERNATIVE_CONNECTION_STRING_";
|
||||
}
|
||||
|
||||
static class Globals
|
||||
{
|
||||
public const string XsrfTokenCookieAndHeaderName = "x-dfm-xsrf-token";
|
||||
public const string TemplateContainerName = "durable-functions-monitor";
|
||||
public const string TabTemplateFolderName = "tab-templates";
|
||||
public const string FunctionMapFolderName = "function-maps";
|
||||
public const string FunctionMapFilePrefix = "dfm-func-map";
|
||||
public const string CustomMetaTagBlobName = "custom-meta-tag.htm";
|
||||
|
||||
public const string ConnAndTaskHubNameSeparator = "-";
|
||||
|
||||
public const string HubNameRouteParamName = "{hubName}";
|
||||
|
||||
// Constant, that defines the /a/p/i/{connName}-{hubName} route prefix, to let Functions Host distinguish api methods from statics
|
||||
public const string ApiRoutePrefix = "a/p/i/{connName}-{hubName}";
|
||||
|
||||
public static void SplitConnNameAndHubName(string connAndHubName, out string connName, out string hubName)
|
||||
{
|
||||
int pos = connAndHubName.LastIndexOf("-");
|
||||
if (pos < 0)
|
||||
{
|
||||
connName = null;
|
||||
hubName = connAndHubName;
|
||||
}
|
||||
else
|
||||
{
|
||||
connName = connAndHubName.Substring(0, pos);
|
||||
hubName = connAndHubName.Substring(pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CombineConnNameAndHubName(string connName, string hubName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(connName) || connName == "-")
|
||||
{
|
||||
return hubName;
|
||||
}
|
||||
|
||||
return $"{connName}{ConnAndTaskHubNameSeparator}{hubName}";
|
||||
}
|
||||
|
||||
public static bool IsDefaultConnectionStringName(string connName)
|
||||
{
|
||||
return string.IsNullOrEmpty(connName) || connName == "-";
|
||||
}
|
||||
|
||||
public static string GetFullConnectionStringEnvVariableName(string connName)
|
||||
{
|
||||
if (IsDefaultConnectionStringName(connName))
|
||||
{
|
||||
return EnvVariableNames.AzureWebJobsStorage;
|
||||
}
|
||||
else
|
||||
{
|
||||
return EnvVariableNames.DFM_ALTERNATIVE_CONNECTION_STRING_PREFIX + connName;
|
||||
}
|
||||
}
|
||||
|
||||
// Applies authN/authZ rules and handles incoming HTTP request. Also does error handling.
|
||||
public static async Task<IActionResult> HandleAuthAndErrors(this HttpRequest req, string connName, string hubName, ILogger log, Func<Task<IActionResult>> todo)
|
||||
{
|
||||
return await HandleErrors(req, log, async () => {
|
||||
|
||||
await Auth.ValidateIdentityAsync(req.HttpContext.User, req.Headers, req.Cookies, CombineConnNameAndHubName(connName, hubName));
|
||||
|
||||
return await todo();
|
||||
});
|
||||
}
|
||||
|
||||
// Handles incoming HTTP request with error handling.
|
||||
public static async Task<IActionResult> HandleErrors(this HttpRequest req, ILogger log, Func<Task<IActionResult>> todo)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await todo();
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
log.LogError(ex, $"DFM failed to authenticate request");
|
||||
return new UnauthorizedResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogError(ex, "DFM failed");
|
||||
return new BadRequestObjectResult(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// Lists all blobs from Azure Blob Container
|
||||
public static async Task<IEnumerable<IListBlobItem>> ListBlobsAsync(this CloudBlobContainer container, string prefix)
|
||||
{
|
||||
var result = new List<IListBlobItem>();
|
||||
BlobContinuationToken token = null;
|
||||
do
|
||||
{
|
||||
var nextBatch = await container.ListBlobsSegmentedAsync(prefix, token);
|
||||
result.AddRange(nextBatch.Results);
|
||||
token = nextBatch.ContinuationToken;
|
||||
}
|
||||
while (token != null);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fighting with https://github.com/Azure/azure-functions-durable-js/issues/94
|
||||
// Could use a custom JsonConverter, but it won't be invoked for nested items :(
|
||||
public static string FixUndefinedsInJson(this string json)
|
||||
{
|
||||
return json.Replace("\": undefined", "\": null");
|
||||
}
|
||||
|
||||
// Shared JSON serialization settings
|
||||
public static JsonSerializerSettings SerializerSettings = GetSerializerSettings();
|
||||
|
||||
// A customized way of returning JsonResult, to cope with Functions v2/v3 incompatibility
|
||||
public static ContentResult ToJsonContentResult(this object result, Func<string, string> applyThisToJson = null)
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(result, Globals.SerializerSettings);
|
||||
if(applyThisToJson != null)
|
||||
{
|
||||
json = applyThisToJson(json);
|
||||
}
|
||||
return new ContentResult() { Content = json, ContentType = "application/json" };
|
||||
}
|
||||
|
||||
public static IEnumerable<T> ApplyTop<T>(this IEnumerable<T> collection, IQueryCollection query)
|
||||
{
|
||||
var clause = query["$top"];
|
||||
return clause.Any() ? collection.Take(int.Parse(clause)) : collection;
|
||||
}
|
||||
public static IEnumerable<T> ApplySkip<T>(this IEnumerable<T> collection, IQueryCollection query)
|
||||
{
|
||||
var clause = query["$skip"];
|
||||
return clause.Any() ? collection.Skip(int.Parse(clause)) : collection;
|
||||
}
|
||||
|
||||
private static JsonSerializerSettings GetSerializerSettings()
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
DateFormatString = "yyyy-MM-ddTHH:mm:ssZ",
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
settings.Converters.Add(new StringEnumConverter());
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
|
||||
using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations;
|
||||
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
// Base class for all HTTP request handlers.
|
||||
// Provides tooling for authZ and error handling.
|
||||
public abstract class HttpHandlerBase
|
||||
{
|
||||
// Default instance of IDurableClientFactory, injected via ctor
|
||||
private readonly IDurableClientFactory _durableClientFactory;
|
||||
|
||||
public HttpHandlerBase(IDurableClientFactory durableClientFactory)
|
||||
{
|
||||
this._durableClientFactory = durableClientFactory;
|
||||
}
|
||||
|
||||
// Applies authN/authZ rules and handles incoming HTTP request. Also creates IDurableClient (when needed) and does error handling.
|
||||
protected async Task<IActionResult> HandleAuthAndErrors(IDurableClient defaultDurableClient, HttpRequest req, string connName, string hubName, ILogger log, Func<IDurableClient, Task<IActionResult>> todo)
|
||||
{
|
||||
return await Globals.HandleErrors(req, log, async () => {
|
||||
|
||||
await Auth.ValidateIdentityAsync(req.HttpContext.User, req.Headers, req.Cookies, Globals.CombineConnNameAndHubName(connName, hubName));
|
||||
|
||||
// For default storage connections using default durableClient, injected normally, as a parameter.
|
||||
// Only using IDurableClientFactory for custom connections, just in case.
|
||||
var durableClient = Globals.IsDefaultConnectionStringName(connName) ?
|
||||
defaultDurableClient :
|
||||
this._durableClientFactory.CreateClient(new DurableClientOptions
|
||||
{
|
||||
TaskHub = hubName,
|
||||
ConnectionName = Globals.GetFullConnectionStringEnvVariableName(connName)
|
||||
});
|
||||
|
||||
return await todo(durableClient);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
|
||||
using Microsoft.WindowsAzure.Storage.Table;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
static class OrchestrationHistory
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches orchestration instance history directly from XXXHistory table
|
||||
/// Tries to mimic this algorithm: https://github.com/Azure/azure-functions-durable-extension/blob/main/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs#L718
|
||||
/// Intentionally returns IEnumerable<>, because the consuming code not always iterates through all of it.
|
||||
/// </summary>
|
||||
public static IEnumerable<HistoryEvent> GetHistoryDirectlyFromTable(IDurableClient durableClient, string connName, string hubName, string instanceId)
|
||||
{
|
||||
var tableClient = TableClient.GetTableClient(connName);
|
||||
|
||||
// Need to fetch executionId first
|
||||
|
||||
var instanceEntity = tableClient.ExecuteAsync($"{hubName}Instances", TableOperation.Retrieve(instanceId, string.Empty))
|
||||
.Result.Result as DynamicTableEntity;
|
||||
|
||||
string executionId = instanceEntity.Properties.ContainsKey("ExecutionId") ?
|
||||
instanceEntity.Properties["ExecutionId"].StringValue :
|
||||
null;
|
||||
|
||||
var instanceIdFilter = TableQuery.CombineFilters
|
||||
(
|
||||
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, instanceId),
|
||||
TableOperators.And,
|
||||
TableQuery.GenerateFilterCondition("ExecutionId", QueryComparisons.Equal, executionId)
|
||||
);
|
||||
|
||||
// Fetching _all_ correlated events with a separate parallel query. This seems to be the only option.
|
||||
var correlatedEventsQuery = new TableQuery<HistoryEntity>().Where
|
||||
(
|
||||
TableQuery.CombineFilters
|
||||
(
|
||||
instanceIdFilter,
|
||||
TableOperators.And,
|
||||
TableQuery.GenerateFilterConditionForInt("TaskScheduledId", QueryComparisons.GreaterThanOrEqual, 0)
|
||||
)
|
||||
);
|
||||
|
||||
var correlatedEventsTask = tableClient.GetAllAsync($"{hubName}History", correlatedEventsQuery)
|
||||
.ContinueWith(t => t.Result.ToDictionary(e => e.TaskScheduledId));
|
||||
|
||||
// Memorizing 'ExecutionStarted' event, to further correlate with 'ExecutionCompleted'
|
||||
HistoryEntity executionStartedEvent = null;
|
||||
|
||||
// Fetching the history
|
||||
var query = new TableQuery<HistoryEntity>().Where(instanceIdFilter);
|
||||
|
||||
foreach (var evt in tableClient.GetAll($"{hubName}History", query))
|
||||
{
|
||||
switch (evt.EventType)
|
||||
{
|
||||
case "TaskScheduled":
|
||||
case "SubOrchestrationInstanceCreated":
|
||||
|
||||
// Trying to match the completion event
|
||||
correlatedEventsTask.Result.TryGetValue(evt.EventId, out var correlatedEvt);
|
||||
if (correlatedEvt != null)
|
||||
{
|
||||
yield return correlatedEvt.ToHistoryEvent
|
||||
(
|
||||
evt._Timestamp,
|
||||
evt.Name,
|
||||
correlatedEvt.EventType == "GenericEvent" ? evt.EventType : null,
|
||||
evt.InstanceId
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return evt.ToHistoryEvent();
|
||||
}
|
||||
|
||||
break;
|
||||
case "ExecutionStarted":
|
||||
|
||||
executionStartedEvent = evt;
|
||||
|
||||
yield return evt.ToHistoryEvent(null, evt.Name);
|
||||
|
||||
break;
|
||||
case "ExecutionCompleted":
|
||||
case "ExecutionFailed":
|
||||
case "ExecutionTerminated":
|
||||
|
||||
yield return evt.ToHistoryEvent(executionStartedEvent?._Timestamp);
|
||||
|
||||
break;
|
||||
case "ContinueAsNew":
|
||||
case "TimerCreated":
|
||||
case "TimerFired":
|
||||
case "EventRaised":
|
||||
case "EventSent":
|
||||
|
||||
yield return evt.ToHistoryEvent();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HistoryEvent ToHistoryEvent(this HistoryEntity evt,
|
||||
DateTimeOffset? scheduledTime = null,
|
||||
string functionName = null,
|
||||
string eventType = null,
|
||||
string subOrchestrationId = null)
|
||||
{
|
||||
return new HistoryEvent
|
||||
{
|
||||
Timestamp = evt._Timestamp.ToUniversalTime(),
|
||||
EventType = eventType ?? evt.EventType,
|
||||
EventId = evt.TaskScheduledId,
|
||||
Name = string.IsNullOrEmpty(evt.Name) ? functionName : evt.Name,
|
||||
Result = evt.Result,
|
||||
Details = evt.Details,
|
||||
SubOrchestrationId = subOrchestrationId,
|
||||
ScheduledTime = scheduledTime,
|
||||
DurationInMs = scheduledTime.HasValue ? (evt._Timestamp - scheduledTime.Value).TotalMilliseconds : 0
|
||||
};
|
||||
}
|
||||
|
||||
internal static HistoryEvent ToHistoryEvent(JToken token)
|
||||
{
|
||||
dynamic dynamicToken = token;
|
||||
|
||||
return new HistoryEvent
|
||||
{
|
||||
Timestamp = dynamicToken.Timestamp,
|
||||
EventType = dynamicToken.EventType,
|
||||
EventId = dynamicToken.EventId,
|
||||
Name = string.IsNullOrEmpty(dynamicToken.Name) ? dynamicToken.FunctionName : dynamicToken.Name,
|
||||
ScheduledTime = dynamicToken.ScheduledTime,
|
||||
Result = dynamicToken.Result?.ToString(),
|
||||
Details = dynamicToken.Details?.ToString(),
|
||||
DurationInMs = dynamicToken.DurationInMs,
|
||||
SubOrchestrationId = dynamicToken.SubOrchestrationId
|
||||
};
|
||||
}
|
||||
|
||||
internal static IEnumerable<HistoryEvent> ApplyTimeFrom(this IEnumerable<HistoryEvent> events, DateTime? timeFrom)
|
||||
{
|
||||
if (timeFrom == null)
|
||||
{
|
||||
return events;
|
||||
}
|
||||
|
||||
return events.Where(evt => evt.Timestamp >= timeFrom);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a record in orchestration's history
|
||||
/// </summary>
|
||||
public class HistoryEvent
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public string EventType { get; set; }
|
||||
public int? EventId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DateTimeOffset? ScheduledTime { get; set; }
|
||||
public string Result { get; set; }
|
||||
public string Details { get; set; }
|
||||
public double? DurationInMs { get; set; }
|
||||
public string SubOrchestrationId { get; set; }
|
||||
}
|
||||
|
||||
// Represents an record in XXXHistory table
|
||||
class HistoryEntity : TableEntity
|
||||
{
|
||||
public string InstanceId { get; set; }
|
||||
public string EventType { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DateTimeOffset _Timestamp { get; set; }
|
||||
public string Result { get; set; }
|
||||
public string Details { get; set; }
|
||||
public int EventId { get; set; }
|
||||
public int? TaskScheduledId { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines functional mode for DurableFunctionsMonitor endpoint.
|
||||
/// </summary>
|
||||
public enum DfmMode
|
||||
{
|
||||
Normal = 0,
|
||||
ReadOnly
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DurableFunctionsMonitor configuration settings
|
||||
/// </summary>
|
||||
public class DfmSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Turns authentication off for DurableFunctionsMonitor endpoint.
|
||||
/// WARNING: this might not only expose DurableFunctionsMonitor to the public, but also
|
||||
/// expose all other HTTP-triggered endpoints in your project. Make sure you know what you're doing.
|
||||
/// </summary>
|
||||
public bool DisableAuthentication { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Functional mode for DurableFunctionsMonitor endpoint.
|
||||
/// Currently only Normal (default) and ReadOnly modes are supported.
|
||||
/// </summary>
|
||||
public DfmMode Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of App Roles, that are allowed to access DurableFunctionsMonitor endpoint. Users/Groups then need
|
||||
/// to be assigned one of these roles via AAD Enterprise Applications->[your AAD app]->Users and Groups tab.
|
||||
/// Once set, the incoming access token is expected to contain one of these in its 'roles' claim.
|
||||
/// </summary>
|
||||
public IEnumerable<string> AllowedAppRoles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of users, that are allowed to access DurableFunctionsMonitor endpoint. You typically put emails into here.
|
||||
/// Once set, the incoming access token is expected to contain one of these names in its 'preferred_username' claim.
|
||||
/// </summary>
|
||||
public IEnumerable<string> AllowedUserNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Folder where to search for custom tab/html templates.
|
||||
/// Must be a part of your Functions project and be adjacent to your host.json file.
|
||||
/// </summary>
|
||||
public string CustomTemplatesFolderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the claim (from ClaimsCredential) to be used as a user name.
|
||||
/// Defaults to "preferred_username"
|
||||
/// </summary>
|
||||
public string UserNameClaimName { get; set; }
|
||||
|
||||
public DfmSettings()
|
||||
{
|
||||
this.UserNameClaimName = Auth.PreferredUserNameClaim;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A set of extension points that can be customized by the client code, when DFM is used in 'injected' mode.
|
||||
/// </summary>
|
||||
public class DfmExtensionPoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Routine for fetching orchestration history.
|
||||
/// Takes IDurableClient, connString env variable name, taskHubName and instanceId and returns IEnumerable[HistoryEvent].
|
||||
/// Provide your own implementation for a custom storage provider.
|
||||
/// Default implementation fetches history directly from XXXHistory table.
|
||||
/// </summary>
|
||||
public Func<IDurableClient, string, string, string, IEnumerable<HistoryEvent>> GetInstanceHistoryRoutine { get; set; }
|
||||
|
||||
public DfmExtensionPoints()
|
||||
{
|
||||
this.GetInstanceHistoryRoutine = OrchestrationHistory.GetHistoryDirectlyFromTable;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DurableFunctionsMonitor configuration
|
||||
/// </summary>
|
||||
public static class DfmEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes DurableFunctionsMonitor endpoint with some settings
|
||||
/// </summary>
|
||||
/// <param name="settings">When null, default settings are used</param>
|
||||
/// <param name="extensionPoints">Routines, that can be customized by client code. When null, default instance of DfmExtensionPoints is used</param>
|
||||
public static void Setup(DfmSettings settings = null, DfmExtensionPoints extensionPoints = null)
|
||||
{
|
||||
string dfmNonce = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_NONCE);
|
||||
|
||||
_settings = settings;
|
||||
|
||||
if (_settings == null)
|
||||
{
|
||||
string dfmAllowedUserNames = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_ALLOWED_USER_NAMES);
|
||||
string dfmAllowedAppRoles = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_ALLOWED_APP_ROLES);
|
||||
string dfmMode = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_MODE);
|
||||
string dfmUserNameClaimName = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_USERNAME_CLAIM_NAME);
|
||||
|
||||
_settings = new DfmSettings()
|
||||
{
|
||||
// Don't want to move the below initializatin to DfmSettings's ctor. The idea is: either _everything_ comes
|
||||
// from env variables or _everything_ is configured programmatically. To avoid unclarity we shouldn't mix these two approaches.
|
||||
DisableAuthentication = dfmNonce == Auth.ISureKnowWhatIAmDoingNonce,
|
||||
Mode = dfmMode == DfmMode.ReadOnly.ToString() ? DfmMode.ReadOnly : DfmMode.Normal,
|
||||
AllowedUserNames = dfmAllowedUserNames == null ? null : dfmAllowedUserNames.Split(','),
|
||||
AllowedAppRoles = dfmAllowedAppRoles == null ? null : dfmAllowedAppRoles.Split(','),
|
||||
UserNameClaimName = string.IsNullOrEmpty(dfmUserNameClaimName) ? Auth.PreferredUserNameClaim : dfmUserNameClaimName
|
||||
};
|
||||
}
|
||||
|
||||
if (extensionPoints != null)
|
||||
{
|
||||
_extensionPoints = extensionPoints;
|
||||
}
|
||||
|
||||
// Also initializing CustomUserAgent value based on input parameters
|
||||
if (!string.IsNullOrEmpty(dfmNonce) && (dfmNonce != Auth.ISureKnowWhatIAmDoingNonce))
|
||||
{
|
||||
_customUserAgent = $"DurableFunctionsMonitor-VsCodeExt/{GetVersion()}";
|
||||
}
|
||||
else
|
||||
{
|
||||
_customUserAgent = $"DurableFunctionsMonitor-Injected/{GetVersion()}";
|
||||
}
|
||||
}
|
||||
|
||||
internal static DfmSettings Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_settings != null)
|
||||
{
|
||||
return _settings;
|
||||
}
|
||||
|
||||
if (!AreWeInStandaloneMode())
|
||||
{
|
||||
throw new InvalidOperationException("Make sure you called DfmEndpoint.Setup() in your code");
|
||||
}
|
||||
|
||||
DfmEndpoint.Setup();
|
||||
|
||||
// Need to reinitialize CustomUserAgent
|
||||
_customUserAgent = $"DurableFunctionsMonitor-Standalone/{GetVersion()}";
|
||||
|
||||
return _settings;
|
||||
}
|
||||
}
|
||||
|
||||
internal static DfmExtensionPoints ExtensionPoints
|
||||
{
|
||||
get { return _extensionPoints; }
|
||||
}
|
||||
|
||||
internal static string CustomUserAgent
|
||||
{
|
||||
get { return _customUserAgent; }
|
||||
}
|
||||
|
||||
private static DfmSettings _settings = null;
|
||||
private static DfmExtensionPoints _extensionPoints = new DfmExtensionPoints();
|
||||
private static string _customUserAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether we should do our internal initialization (Standalone mode)
|
||||
/// or throw an exception when not initialized (Injected mode)
|
||||
/// </summary>
|
||||
private static bool AreWeInStandaloneMode()
|
||||
{
|
||||
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
|
||||
if (string.IsNullOrEmpty(assemblyLocation))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string currentFolder = Path.GetDirectoryName(assemblyLocation);
|
||||
string targetsFileName = "durablefunctionsmonitor.dotnetbackend.targets";
|
||||
|
||||
// Using our .targets file as a marker. It should only appear in our own output folder
|
||||
return File.Exists(Path.Combine(currentFolder, targetsFileName)) ||
|
||||
File.Exists(Path.Combine(Path.GetDirectoryName(currentFolder), targetsFileName));
|
||||
}
|
||||
|
||||
private static string GetVersion()
|
||||
{
|
||||
var version = typeof(DfmEndpoint).Assembly.GetName().Version;
|
||||
return $"{version.Major}.{version.Minor}.{version.Build}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.WindowsAzure.Storage.Table;
|
||||
using Microsoft.WindowsAzure.Storage;
|
||||
|
||||
namespace DurableFunctionsMonitor.DotNetBackend
|
||||
{
|
||||
// CloudTableClient wrapper interface. Seems to be the only way to unit-test.
|
||||
public interface ITableClient
|
||||
{
|
||||
// Gets the list of table names
|
||||
Task<IEnumerable<string>> ListTableNamesAsync();
|
||||
|
||||
// Synchronously retrieves all results from Azure Table
|
||||
IEnumerable<TEntity> GetAll<TEntity>(string tableName, TableQuery<TEntity> query) where TEntity : TableEntity, new();
|
||||
|
||||
// Asynchronously retrieves all results from Azure Table
|
||||
Task<IEnumerable<TEntity>> GetAllAsync<TEntity>(string tableName, TableQuery<TEntity> query) where TEntity : TableEntity, new();
|
||||
|
||||
// Executes a TableOperation
|
||||
Task<TableResult> ExecuteAsync(string tableName, TableOperation operation);
|
||||
}
|
||||
|
||||
// CloudTableClient wrapper. Seems to be the only way to unit-test.
|
||||
class TableClient: ITableClient
|
||||
{
|
||||
// Cannot use DI functionality (our startup method will not be called when installed as a NuGet package),
|
||||
// so just leaving this as an internal static variable.
|
||||
internal static ITableClient MockedTableClient = null;
|
||||
|
||||
public static ITableClient GetTableClient(string connStringName)
|
||||
{
|
||||
if (MockedTableClient != null)
|
||||
{
|
||||
return MockedTableClient;
|
||||
}
|
||||
|
||||
return new TableClient(connStringName);
|
||||
}
|
||||
|
||||
private TableClient(string connStringName)
|
||||
{
|
||||
string connectionString = Environment.GetEnvironmentVariable(connStringName);
|
||||
this._client = CloudStorageAccount.Parse(connectionString).CreateCloudTableClient();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<string>> ListTableNamesAsync()
|
||||
{
|
||||
// Overriding User-Agent header
|
||||
var operationContext = new OperationContext
|
||||
{
|
||||
CustomUserAgent = DfmEndpoint.CustomUserAgent
|
||||
};
|
||||
|
||||
var result = new List<string>();
|
||||
TableContinuationToken token = null;
|
||||
do
|
||||
{
|
||||
var nextBatch = await this._client.ListTablesSegmentedAsync(null, null, token, null, operationContext);
|
||||
result.AddRange(nextBatch.Results.Select(r => r.Name));
|
||||
token = nextBatch.ContinuationToken;
|
||||
}
|
||||
while (token != null);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TEntity> GetAll<TEntity>(string tableName, TableQuery<TEntity> query)
|
||||
where TEntity : TableEntity, new()
|
||||
{
|
||||
var table = this._client.GetTableReference(tableName);
|
||||
|
||||
// Overriding User-Agent header
|
||||
var operationContext = new OperationContext
|
||||
{
|
||||
CustomUserAgent = DfmEndpoint.CustomUserAgent
|
||||
};
|
||||
|
||||
TableContinuationToken token = null;
|
||||
do
|
||||
{
|
||||
var nextBatch = table.ExecuteQuerySegmentedAsync(query, token, null, operationContext).Result;
|
||||
|
||||
foreach (var evt in nextBatch.Results)
|
||||
{
|
||||
yield return evt;
|
||||
}
|
||||
|
||||
token = nextBatch.ContinuationToken;
|
||||
}
|
||||
while (token != null);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<TEntity>> GetAllAsync<TEntity>(string tableName, TableQuery<TEntity> query)
|
||||
where TEntity : TableEntity, new()
|
||||
{
|
||||
var table = this._client.GetTableReference(tableName);
|
||||
|
||||
// Overriding User-Agent header
|
||||
var operationContext = new OperationContext
|
||||
{
|
||||
CustomUserAgent = DfmEndpoint.CustomUserAgent
|
||||
};
|
||||
|
||||
var result = new List<TEntity>();
|
||||
TableContinuationToken token = null;
|
||||
do
|
||||
{
|
||||
var nextBatch = await table.ExecuteQuerySegmentedAsync(query, token, null, operationContext);
|
||||
|
||||
result.AddRange(nextBatch.Results);
|
||||
token = nextBatch.ContinuationToken;
|
||||
}
|
||||
while (token != null);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<TableResult> ExecuteAsync(string tableName, TableOperation operation)
|
||||
{
|
||||
// Overriding User-Agent header
|
||||
var operationContext = new OperationContext
|
||||
{
|
||||
CustomUserAgent = DfmEndpoint.CustomUserAgent
|
||||
};
|
||||
|
||||
return this._client.GetTableReference(tableName).ExecuteAsync(operation, null, operationContext);
|
||||
}
|
||||
|
||||
private readonly CloudTableClient _client;
|
||||
}
|
||||
}
|
После Ширина: | Высота: | Размер: 1.9 KiB |
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by Microsoft Visio, SVG Export logo.svg Page-1 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"
|
||||
width="3.94551in" height="3.69586in" viewBox="0 0 284.076 266.102" xml:space="preserve" color-interpolation-filters="sRGB"
|
||||
class="st5">
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.st1 {fill:#3999c6;stroke:none;stroke-linecap:butt;stroke-width:9.5625}
|
||||
.st2 {fill:#fcd116;stroke:none;stroke-linecap:butt;stroke-width:9.5625}
|
||||
.st3 {fill:#ff8c00;fill-opacity:0.3;stroke:none;stroke-linecap:butt;stroke-width:9.5625}
|
||||
.st4 {fill:#ff0000;stroke:none;stroke-linecap:butt;stroke-width:0.75}
|
||||
.st5 {fill:none;fill-rule:evenodd;font-size:12px;overflow:visible;stroke-linecap:square;stroke-miterlimit:3}
|
||||
]]>
|
||||
</style>
|
||||
|
||||
<g>
|
||||
<title>Page-1</title>
|
||||
<g id="group1-1" transform="translate(26.8951,-27.5625)">
|
||||
<title>Sheet.1</title>
|
||||
<g id="group2-2">
|
||||
<title>Sheet.2</title>
|
||||
<g id="shape3-3" transform="translate(156.623,-41.1226)">
|
||||
<title>Sheet.3</title>
|
||||
<path d="M71.52 203.17 C73.66 201.02 73.31 197.09 71.52 194.94 L60.43 183.86 L11.09 135.94 C8.94 133.79 5.72
|
||||
133.79 3.22 135.94 C1.07 138.09 0.36 142.02 3.22 144.16 L55.07 194.94 C57.21 197.09 57.21 201.02
|
||||
55.07 203.17 L2.15 255.73 C0 257.88 0 261.81 2.15 263.96 C4.29 266.1 8.22 265.74 10.01 263.96 L59
|
||||
215.32 C59 215.32 59 215.32 59.36 214.97 L71.52 203.17 Z" class="st1"/>
|
||||
</g>
|
||||
<g id="shape4-5" transform="translate(0,-41.1226)">
|
||||
<title>Sheet.4</title>
|
||||
<path d="M2.15 203.17 C0 201.02 0.36 197.09 2.15 194.94 L13.23 183.86 L62.58 135.94 C64.72 133.79 67.94 133.79
|
||||
70.44 135.94 C72.59 138.09 73.31 142.02 70.44 144.16 L19.67 194.94 C17.52 197.09 17.52 201.02 19.67
|
||||
203.17 L71.52 255.73 C73.66 257.88 73.66 261.81 71.52 263.96 C69.37 266.1 65.44 265.74 63.65 263.96
|
||||
L13.59 216.04 C13.59 216.04 13.59 216.04 13.23 215.68 L2.15 203.17 Z" class="st1"/>
|
||||
</g>
|
||||
<g id="shape5-7" transform="translate(63.6506,0)">
|
||||
<title>Sheet.5</title>
|
||||
<path d="M107.28 55.12 L37.55 55.12 L0 160.97 L45.77 161.33 L10.01 266.1 L108.71 126.28 L60.79 126.28 L107.28
|
||||
55.12 Z" class="st2"/>
|
||||
</g>
|
||||
<g id="shape6-9" transform="translate(73.663,0)">
|
||||
<title>Sheet.6</title>
|
||||
<path d="M50.78 126.28 L97.26 55.12 L60.79 55.12 L22.17 143.09 L67.94 143.45 L0 266.1 L98.69 126.28 L50.78 126.28
|
||||
Z" class="st3"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape20-11" transform="translate(77.9528,-80.2178)">
|
||||
<title>Sheet.20</title>
|
||||
<path d="M0 200.91 L18.19 200.91 L34.44 200.04 L46.78 155.72 C47.43 153.11 48.73 147.89 53.93 147.89 C59.13 147.89 59.78
|
||||
153.11 59.78 155.72 L60.43 229.6 L69.52 204.39 C70.17 201.78 72.12 200.91 73.42 200.91 L126.05 200.91 C128.65
|
||||
200.91 130.6 204.39 130.6 207 C130.6 209.61 128.65 214.82 126.05 214.82 L78.62 214.82 L61.73 260.02 C60.43
|
||||
264.36 59.13 266.1 55.23 265.23 C53.28 264.36 51.33 260.89 51.33 258.28 L50.68 190.48 L45.48 208.74 C44.83
|
||||
211.34 43.53 214.82 41.58 214.82 L25.99 214.82 L5.85 214.82 L2.6 207.87 L0 200.91 Z" class="st4"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 3.2 KiB |
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "Durable Functions Monitor",
|
||||
"name": "Durable Functions Monitor",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "64x64",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
.react-vis-magic-css-import-rule{display:inherit}.rv-treemap{font-size:12px;position:relative}.rv-treemap__leaf{overflow:hidden;position:absolute}.rv-treemap__leaf--circle{align-items:center;border-radius:100%;display:flex;justify-content:center}.rv-treemap__leaf__content{overflow:hidden;padding:10px;text-overflow:ellipsis}.rv-xy-plot{color:#c3c3c3;position:relative}.rv-xy-plot canvas{pointer-events:none}.rv-xy-plot .rv-xy-canvas{pointer-events:none;position:absolute}.rv-xy-plot__inner{display:block}.rv-xy-plot__axis__line{fill:none;stroke-width:2px;stroke:#e6e6e9}.rv-xy-plot__axis__tick__line{stroke:#e6e6e9}.rv-xy-plot__axis__tick__text,.rv-xy-plot__axis__title text{fill:#6b6b76;font-size:11px}.rv-xy-plot__grid-lines__line{stroke:#e6e6e9}.rv-xy-plot__circular-grid-lines__line{fill-opacity:0;stroke:#e6e6e9}.rv-xy-plot__series,.rv-xy-plot__series path{pointer-events:all}.rv-xy-plot__series--line{fill:none;stroke:#000;stroke-width:2px}.rv-crosshair{position:absolute;font-size:11px;pointer-events:none}.rv-crosshair__line{background:#47d3d9;width:1px}.rv-crosshair__inner{position:absolute;text-align:left;top:0}.rv-crosshair__inner__content{border-radius:4px;background:#3a3a48;color:#fff;font-size:12px;padding:7px 10px;box-shadow:0 2px 4px rgba(0,0,0,.5)}.rv-crosshair__inner--left{right:4px}.rv-crosshair__inner--right{left:4px}.rv-crosshair__title{font-weight:700;white-space:nowrap}.rv-crosshair__item{white-space:nowrap}.rv-hint{position:absolute;pointer-events:none}.rv-hint__content{border-radius:4px;padding:7px 10px;font-size:12px;background:#3a3a48;box-shadow:0 2px 4px rgba(0,0,0,.5);color:#fff;text-align:left;white-space:nowrap}.rv-discrete-color-legend{box-sizing:border-box;overflow-y:auto;font-size:12px}.rv-discrete-color-legend.horizontal{white-space:nowrap}.rv-discrete-color-legend-item{color:#3a3a48;border-radius:1px;padding:9px 10px}.rv-discrete-color-legend-item.horizontal{display:inline-block}.rv-discrete-color-legend-item.horizontal .rv-discrete-color-legend-item__title{margin-left:0;display:block}.rv-discrete-color-legend-item__color{display:inline-block;vertical-align:middle;overflow:visible}.rv-discrete-color-legend-item__color__path{stroke:#dcdcdc;stroke-width:2px}.rv-discrete-color-legend-item__title{margin-left:10px}.rv-discrete-color-legend-item.disabled{color:#b8b8b8}.rv-discrete-color-legend-item.clickable{cursor:pointer}.rv-discrete-color-legend-item.clickable:hover{background:#f9f9f9}.rv-search-wrapper{display:flex;flex-direction:column}.rv-search-wrapper__form{flex:0 1}.rv-search-wrapper__form__input{width:100%;color:#a6a6a5;border:1px solid #e5e5e4;padding:7px 10px;font-size:12px;box-sizing:border-box;border-radius:2px;margin:0 0 9px;outline:0}.rv-search-wrapper__contents{flex:1 1;overflow:auto}.rv-continuous-color-legend{font-size:12px}.rv-continuous-color-legend .rv-gradient{height:4px;border-radius:2px;margin-bottom:5px}.rv-continuous-size-legend{font-size:12px}.rv-continuous-size-legend .rv-bubbles{text-align:justify;overflow:hidden;margin-bottom:5px;width:100%}.rv-continuous-size-legend .rv-bubble{background:#d8d9dc;display:inline-block;vertical-align:bottom}.rv-continuous-size-legend .rv-spacer{display:inline-block;font-size:0;line-height:0;width:100%}.rv-legend-titles{height:16px;position:relative}.rv-legend-titles__center,.rv-legend-titles__left,.rv-legend-titles__right{position:absolute;white-space:nowrap;overflow:hidden}.rv-legend-titles__center{display:block;text-align:center;width:100%}.rv-legend-titles__right{right:0}.rv-radial-chart .rv-xy-plot__series--label{pointer-events:none}
|
||||
/*# sourceMappingURL=2.62e7949a.chunk.css.map */
|
|
@ -0,0 +1,2 @@
|
|||
html{overflow-y:scroll!important}body{margin:0;padding:0;font-family:sans-serif;display:table;width:100%}.top-appbar{box-shadow:none!important}.top-toolbar{padding-top:5px;padding-bottom:18px;max-width:100vw;min-width:900px}.toolbar-select{min-width:150px}.long-text-cell{min-width:160px;max-width:250px}.long-text-cell-input{font-size:x-small!important}.long-text-cell-input:hover{text-decoration:underline;cursor:pointer;opacity:.8}.empty-table-placeholder{padding:30px;text-align:center}.name-cell{min-width:165px;word-break:break-all}.link-with-pointer-cursor{cursor:pointer}.time-zone-name-span{padding-left:2px;font-size:x-small}.title-typography{padding-right:10px}.app-bar{margin-bottom:10px}.instance-id-input{width:320px}.instance-id-valid .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline{border-color:green!important}.raw-html-div{padding:10px}.login-progress{text-align:center;margin-top:20px;margin-bottom:20px}.task-hub-list{padding-bottom:25px!important}.show-time-as-typography{padding-top:10px;padding-left:16px;padding-right:10px}.datetime-cell{min-width:145px}.till-checkbox{padding:20px 14px 4px 4px!important}.till-label{padding-left:15px!important}.from-input{width:215px;margin-left:10px!important}.till-input{width:215px}.time-period-menu-drop-btn{width:32px!important;min-width:32px!important;margin-top:16px!important}.filter-value-input{flex:1 1;margin-left:20px!important}.filtered-column-input{width:180px}.items-count-label{margin-top:8px!important;padding-left:15px;height:20px}.instance-id-cell{min-width:145px;max-width:270px;word-break:break-all}.output-cell{min-width:75px;max-width:200px}.entity-type-radio{margin-bottom:0}.toolbar-grid1{width:260px!important}.toolbar-grid1-item2{padding-top:20px;min-height:68px!important;min-width:257px!important}.toolbar-grid2{margin-left:40px;margin-right:40px}.toolbar-grid2-item-1{display:flex}.toolbar-grid2-item1-select{margin-left:20px!important}.toolbar-grid2-item2{padding-top:20px}.toolbar-grid3{width:auto!important}.toolbar-grid3-item2{padding-top:26px}.toolbar-runtime-status-group{margin-left:30px;margin-right:30px}.toolbar-runtime-status-group-label{padding-left:10px!important}.form-control-float-right{float:right}.column-hide-button{width:8px!important;padding:0!important;margin-right:-8px!important;float:right;opacity:.7}.unhide-button{vertical-align:text-bottom!important;font-size:12px}.tab-buttons{min-height:37px!important}.histogram-legend{padding-left:70px;white-space:normal!important}.histogram-legend-dark-mode>div>span{color:#d3d3d3}.status-checkbox{padding-left:10px!important;padding-top:5px!important;padding-bottom:5px!important}.autorefresh-select{min-width:130px}.refresh-button{width:130px}.selected-statuses-box{display:flex;flex-wrap:wrap}.message-snackbar{top:80px!important}.error-icon{margin-right:10px;margin-bottom:-7px}.error-snackbar-content{background-color:red!important}.settings-group{padding-top:5px;padding-left:20px;padding-bottom:5px}.link-to-az-func-as-a-graph{padding-top:10px}.metrics-chip{margin-right:3px;font-size:xx-small!important}.metrics-span{position:absolute;visibility:hidden;white-space:nowrap}.total-metrics-span{display:inherit;padding-top:9px;padding-left:20px}.diagram-div{padding-top:30px;padding-bottom:30px}.diagram-div>svg{display:block;margin:auto;width:100%!important;height:100%!important}.link-to-az-func-as-a-graph{float:right;padding-right:10px}.details-top-toolbar{padding-top:5px;padding-bottom:20px;max-width:100vw;min-width:900px}.grid-container{padding-top:20px;padding-right:15px}.grid-item{padding-left:15px}.details-datetime-cell{min-width:250px}.history-events-count-label{padding-top:10px;padding-left:16px;padding-bottom:10px}.details-refresh-button{width:90px}.functions-graph-tab-span{display:inline-flex;flex-direction:row}.functions-graph-link-icon{width:15px!important;margin-top:-2px;padding-left:5px}.history-filtered-column-input{width:150px}.history-filter-value-input{width:250px}.history-appbar{margin-top:15px;margin-bottom:15px;padding-top:5px;padding-bottom:5px}.history-toolbar{margin-left:-8px!important;padding-left:0!important;padding-top:17px!important}.history-from-input{width:210px}.history-from-checkbox{padding-top:18px!important}.history-from-label{margin-left:-40px}.purge-history-from-input,.purge-history-till-input{padding-bottom:20px!important}.purge-history-till-input{margin-left:20px!important}.purge-history-apply-to{padding-top:10px!important;padding-bottom:10px!important}.success-message{color:green!important}.dialog-text-field{padding-bottom:10px}
|
||||
/*# sourceMappingURL=main.12374d2f.chunk.css.map */
|
|
@ -0,0 +1,855 @@
|
|||
/*
|
||||
* __ ___
|
||||
* _____/ /___ __/ (_)____
|
||||
* / ___/ __/ / / / / / ___/
|
||||
* (__ ) /_/ /_/ / / (__ )
|
||||
* /____/\__/\__, /_/_/____/
|
||||
* /____/
|
||||
*
|
||||
* light - weight css preprocessor @licence MIT
|
||||
*/
|
||||
|
||||
/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@license MIT
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Wait for document loaded before starting the execution
|
||||
*/
|
||||
|
||||
/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
this file except in compliance with the License. You may obtain a copy of the
|
||||
License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
||||
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
||||
MERCHANTABLITY OR NON-INFRINGEMENT.
|
||||
|
||||
See the Apache Version 2.0 License for specific language governing permissions
|
||||
and limitations under the License.
|
||||
***************************************************************************** */
|
||||
|
||||
/*! ../../config */
|
||||
|
||||
/*! ../../dagre-wrapper/index.js */
|
||||
|
||||
/*! ../../logger */
|
||||
|
||||
/*! ../../mermaidAPI */
|
||||
|
||||
/*! ../../utils */
|
||||
|
||||
/*! ../common/common */
|
||||
|
||||
/*! ../config */
|
||||
|
||||
/*! ../createLabel */
|
||||
|
||||
/*! ../diagrams/class/svgDraw */
|
||||
|
||||
/*! ../intersect/index.js */
|
||||
|
||||
/*! ../logger */
|
||||
|
||||
/*! ../package.json */
|
||||
|
||||
/*! ../utils */
|
||||
|
||||
/*! ./../../../../node_modules/process/browser.js */
|
||||
|
||||
/*! ./../../../../node_modules/webpack/buildin/module.js */
|
||||
|
||||
/*! ./../process/browser.js */
|
||||
|
||||
/*! ./classDb */
|
||||
|
||||
/*! ./clusters */
|
||||
|
||||
/*! ./config */
|
||||
|
||||
/*! ./createLabel */
|
||||
|
||||
/*! ./defaultConfig */
|
||||
|
||||
/*! ./diagrams/class/classDb */
|
||||
|
||||
/*! ./diagrams/class/classRenderer */
|
||||
|
||||
/*! ./diagrams/class/classRenderer-v2 */
|
||||
|
||||
/*! ./diagrams/class/parser/classDiagram */
|
||||
|
||||
/*! ./diagrams/class/styles */
|
||||
|
||||
/*! ./diagrams/common/common */
|
||||
|
||||
/*! ./diagrams/er/erDb */
|
||||
|
||||
/*! ./diagrams/er/erRenderer */
|
||||
|
||||
/*! ./diagrams/er/parser/erDiagram */
|
||||
|
||||
/*! ./diagrams/er/styles */
|
||||
|
||||
/*! ./diagrams/flowchart/flowDb */
|
||||
|
||||
/*! ./diagrams/flowchart/flowRenderer */
|
||||
|
||||
/*! ./diagrams/flowchart/flowRenderer-v2 */
|
||||
|
||||
/*! ./diagrams/flowchart/parser/flow */
|
||||
|
||||
/*! ./diagrams/flowchart/styles */
|
||||
|
||||
/*! ./diagrams/gantt/ganttDb */
|
||||
|
||||
/*! ./diagrams/gantt/ganttRenderer */
|
||||
|
||||
/*! ./diagrams/gantt/parser/gantt */
|
||||
|
||||
/*! ./diagrams/gantt/styles */
|
||||
|
||||
/*! ./diagrams/git/gitGraphAst */
|
||||
|
||||
/*! ./diagrams/git/gitGraphRenderer */
|
||||
|
||||
/*! ./diagrams/git/parser/gitGraph */
|
||||
|
||||
/*! ./diagrams/git/styles */
|
||||
|
||||
/*! ./diagrams/info/infoDb */
|
||||
|
||||
/*! ./diagrams/info/infoRenderer */
|
||||
|
||||
/*! ./diagrams/info/parser/info */
|
||||
|
||||
/*! ./diagrams/info/styles */
|
||||
|
||||
/*! ./diagrams/pie/parser/pie */
|
||||
|
||||
/*! ./diagrams/pie/pieDb */
|
||||
|
||||
/*! ./diagrams/pie/pieRenderer */
|
||||
|
||||
/*! ./diagrams/pie/styles */
|
||||
|
||||
/*! ./diagrams/sequence/parser/sequenceDiagram */
|
||||
|
||||
/*! ./diagrams/sequence/sequenceDb */
|
||||
|
||||
/*! ./diagrams/sequence/sequenceRenderer */
|
||||
|
||||
/*! ./diagrams/sequence/styles */
|
||||
|
||||
/*! ./diagrams/state/parser/stateDiagram */
|
||||
|
||||
/*! ./diagrams/state/stateDb */
|
||||
|
||||
/*! ./diagrams/state/stateRenderer */
|
||||
|
||||
/*! ./diagrams/state/stateRenderer-v2 */
|
||||
|
||||
/*! ./diagrams/state/styles */
|
||||
|
||||
/*! ./diagrams/user-journey/journeyDb */
|
||||
|
||||
/*! ./diagrams/user-journey/journeyRenderer */
|
||||
|
||||
/*! ./diagrams/user-journey/parser/journey */
|
||||
|
||||
/*! ./diagrams/user-journey/styles */
|
||||
|
||||
/*! ./edges */
|
||||
|
||||
/*! ./erDb */
|
||||
|
||||
/*! ./erMarkers */
|
||||
|
||||
/*! ./errorRenderer */
|
||||
|
||||
/*! ./flowChartShapes */
|
||||
|
||||
/*! ./flowDb */
|
||||
|
||||
/*! ./ganttDb */
|
||||
|
||||
/*! ./gitGraphAst */
|
||||
|
||||
/*! ./id-cache.js */
|
||||
|
||||
/*! ./infoDb */
|
||||
|
||||
/*! ./intersect-circle.js */
|
||||
|
||||
/*! ./intersect-ellipse */
|
||||
|
||||
/*! ./intersect-ellipse.js */
|
||||
|
||||
/*! ./intersect-line */
|
||||
|
||||
/*! ./intersect-node.js */
|
||||
|
||||
/*! ./intersect-polygon.js */
|
||||
|
||||
/*! ./intersect-rect.js */
|
||||
|
||||
/*! ./intersect/index.js */
|
||||
|
||||
/*! ./intersect/intersect-rect */
|
||||
|
||||
/*! ./journeyDb */
|
||||
|
||||
/*! ./logger */
|
||||
|
||||
/*! ./markers */
|
||||
|
||||
/*! ./mermaid-graphlib */
|
||||
|
||||
/*! ./mermaidAPI */
|
||||
|
||||
/*! ./nodes */
|
||||
|
||||
/*! ./parser/classDiagram */
|
||||
|
||||
/*! ./parser/erDiagram */
|
||||
|
||||
/*! ./parser/flow */
|
||||
|
||||
/*! ./parser/gantt */
|
||||
|
||||
/*! ./parser/gitGraph */
|
||||
|
||||
/*! ./parser/info */
|
||||
|
||||
/*! ./parser/journey */
|
||||
|
||||
/*! ./parser/pie */
|
||||
|
||||
/*! ./parser/sequenceDiagram */
|
||||
|
||||
/*! ./parser/stateDiagram */
|
||||
|
||||
/*! ./pieDb */
|
||||
|
||||
/*! ./sequenceDb */
|
||||
|
||||
/*! ./shapes */
|
||||
|
||||
/*! ./shapes/note */
|
||||
|
||||
/*! ./shapes/util */
|
||||
|
||||
/*! ./stateDb */
|
||||
|
||||
/*! ./styles */
|
||||
|
||||
/*! ./svgDraw */
|
||||
|
||||
/*! ./theme-base */
|
||||
|
||||
/*! ./theme-dark */
|
||||
|
||||
/*! ./theme-default */
|
||||
|
||||
/*! ./theme-forest */
|
||||
|
||||
/*! ./theme-helpers */
|
||||
|
||||
/*! ./theme-neutral */
|
||||
|
||||
/*! ./themes */
|
||||
|
||||
/*! ./util */
|
||||
|
||||
/*! ./utils */
|
||||
|
||||
/*! @braintree/sanitize-url */
|
||||
|
||||
/*! Check if previously processed */
|
||||
|
||||
/*! d3 */
|
||||
|
||||
/*! dagre */
|
||||
|
||||
/*! dagre-d3 */
|
||||
|
||||
/*! dagre-d3/lib/label/add-html-label.js */
|
||||
|
||||
/*! entity-decode/browser */
|
||||
|
||||
/*! exports provided: LEVELS, logger, setLogLevel */
|
||||
|
||||
/*! exports provided: addClasses, addRelations, setConf, drawOld, draw, default */
|
||||
|
||||
/*! exports provided: addToRender, addToRenderV2, default */
|
||||
|
||||
/*! exports provided: bounds, drawActors, setConf, draw, default */
|
||||
|
||||
/*! exports provided: calcThemeVariables, default */
|
||||
|
||||
/*! exports provided: clear, insertEdgeLabel, positionEdgeLabel, intersection, insertEdge */
|
||||
|
||||
/*! exports provided: clusterDb, clear, extractDecendants, validate, findNonClusterChild, adjustClustersAndEdges, extractor, sortNodesByHierarchy */
|
||||
|
||||
/*! exports provided: default */
|
||||
|
||||
/*! exports provided: defaultConfig, updateCurrentConfig, setSiteConfig, setSiteConfigDelta, updateSiteConfig, getSiteConfig, setConfig, getConfig, sanitize, addDirective, reset */
|
||||
|
||||
/*! exports provided: detectInit, detectDirective, detectType, isSubstringInArray, interpolateToCurve, formatUrl, runFunc, getStylesFromArray, generateId, random, assignWithDepth, getTextObj, drawSimpleText, wrapLabel, calculateTextHeight, calculateTextWidth, calculateTextDimensions, calculateSvgSizeAttrs, configureSvgSize, default */
|
||||
|
||||
/*! exports provided: drawEdge, drawClass, parseMember, default */
|
||||
|
||||
/*! exports provided: drawRect, drawFace, drawCircle, drawText, drawLabel, drawSection, drawTask, drawBackgroundRect, getTextObj, getNoteRect, default */
|
||||
|
||||
/*! exports provided: drawRect, drawText, drawLabel, drawActor, anchorElement, drawActivation, drawLoop, drawBackgroundRect, insertArrowHead, insertSequenceNumber, insertArrowCrossHead, getTextObj, getNoteRect, default */
|
||||
|
||||
/*! exports provided: drawStartState, drawDivider, drawSimpleState, drawDescrState, addTitleAndBox, drawText, drawNote, drawState, drawEdge */
|
||||
|
||||
/*! exports provided: encodeEntities, decodeEntities, default */
|
||||
|
||||
/*! exports provided: getRows, removeScript, sanitizeText, lineBreakRegex, hasBreaks, splitBreaks, default */
|
||||
|
||||
/*! exports provided: getThemeVariables */
|
||||
|
||||
/*! exports provided: insertCluster, getClusterTitleWidth, clear, positionCluster */
|
||||
|
||||
/*! exports provided: insertNode, setNodeElem, clear, positionNode */
|
||||
|
||||
/*! exports provided: labelHelper, updateNodeBounds, insertPolygonShape */
|
||||
|
||||
/*! exports provided: mkBorder */
|
||||
|
||||
/*! exports provided: name, version, description, main, keywords, scripts, repository, author, license, standard, dependencies, devDependencies, files, yarn-upgrade-all, sideEffects, husky, default */
|
||||
|
||||
/*! exports provided: parseDirective, addActor, addMessage, addSignal, getMessages, getActors, getActor, getActorKeys, getTitle, getTitleWrapped, enableSequenceNumbers, showSequenceNumbers, setWrap, autoWrap, clear, parseMessage, LINETYPE, ARROWTYPE, PLACEMENT, addNote, setTitle, apply, default */
|
||||
|
||||
/*! exports provided: parseDirective, addClass, lookUpDomId, clear, getClass, getClasses, getRelations, addRelation, addAnnotation, addMember, addMembers, cleanupLabel, setCssClass, setLink, setClickEvent, bindFunctions, lineType, relationType, default */
|
||||
|
||||
/*! exports provided: parseDirective, addState, clear, getState, getStates, logDocuments, getRelations, addRelation, cleanupLabel, lineType, relationType, default */
|
||||
|
||||
/*! exports provided: parseDirective, clear, setAxisFormat, getAxisFormat, setTodayMarker, getTodayMarker, setDateFormat, enableInclusiveEndDates, endDatesAreInclusive, getDateFormat, setExcludes, getExcludes, setTitle, getTitle, addSection, getSections, getTasks, addTask, findTaskById, addTaskOrg, setLink, setClass, setClickEvent, bindFunctions, default */
|
||||
|
||||
/*! exports provided: parseDirective, clear, setTitle, getTitle, addSection, getSections, getTasks, addTask, addTaskOrg, default */
|
||||
|
||||
/*! exports provided: parseDirective, default */
|
||||
|
||||
/*! exports provided: parseDirective, lookUpDomId, addVertex, addSingleLink, addLink, updateLinkInterpolate, updateLink, addClass, setDirection, setClass, setLink, getTooltip, setClickEvent, bindFunctions, getDirection, getVertices, getEdges, getClasses, clear, setGen, defaultStyle, addSubGraph, getDepthFirstPos, indexNodes, getSubGraphs, firstGraph, default */
|
||||
|
||||
/*! exports provided: render */
|
||||
|
||||
/*! exports provided: set, get, keys, size, default */
|
||||
|
||||
/*! exports provided: setConf, addVertices, addEdges, getClasses, draw, default */
|
||||
|
||||
/*! exports provided: setConf, draw, bounds, drawTasks, default */
|
||||
|
||||
/*! exports provided: setConf, draw, default */
|
||||
|
||||
/*! exports provided: setConf, getClasses, draw, default */
|
||||
|
||||
/*! exports provided: setDirection, setOptions, getOptions, commit, branch, merge, checkout, reset, prettyPrint, clear, getBranchesAsObjArray, getBranches, getCommits, getCommitsArray, getCurrentBranch, getDirection, getHead, default */
|
||||
|
||||
/*! exports provided: setMessage, getMessage, setInfo, getInfo, default */
|
||||
|
||||
/*! fs */
|
||||
|
||||
/*! graphlib */
|
||||
|
||||
/*! khroma */
|
||||
|
||||
/*! moment-mini */
|
||||
|
||||
/*! no static exports found */
|
||||
|
||||
/*! path */
|
||||
|
||||
/*! sequence config was passed as #1 */
|
||||
|
||||
/*! stylis */
|
||||
|
||||
/*!*********************!*\
|
||||
!*** external "d3" ***!
|
||||
\*********************/
|
||||
|
||||
/*!**********************!*\
|
||||
!*** ./package.json ***!
|
||||
\**********************/
|
||||
|
||||
/*!**********************!*\
|
||||
!*** ./src/utils.js ***!
|
||||
\**********************/
|
||||
|
||||
/*!***********************!*\
|
||||
!*** ./src/config.js ***!
|
||||
\***********************/
|
||||
|
||||
/*!***********************!*\
|
||||
!*** ./src/logger.js ***!
|
||||
\***********************/
|
||||
|
||||
/*!***********************!*\
|
||||
!*** ./src/styles.js ***!
|
||||
\***********************/
|
||||
|
||||
/*!************************!*\
|
||||
!*** ./src/mermaid.js ***!
|
||||
\************************/
|
||||
|
||||
/*!************************!*\
|
||||
!*** external "dagre" ***!
|
||||
\************************/
|
||||
|
||||
/*!*************************!*\
|
||||
!*** external "khroma" ***!
|
||||
\*************************/
|
||||
|
||||
/*!*************************!*\
|
||||
!*** external "stylis" ***!
|
||||
\*************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** ./src/mermaidAPI.js ***!
|
||||
\***************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** external "dagre-d3" ***!
|
||||
\***************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** external "graphlib" ***!
|
||||
\***************************/
|
||||
|
||||
/*!*****************************!*\
|
||||
!*** ./src/themes/index.js ***!
|
||||
\*****************************/
|
||||
|
||||
/*!******************************!*\
|
||||
!*** ./src/defaultConfig.js ***!
|
||||
\******************************/
|
||||
|
||||
/*!******************************!*\
|
||||
!*** ./src/errorRenderer.js ***!
|
||||
\******************************/
|
||||
|
||||
/*!******************************!*\
|
||||
!*** external "moment-mini" ***!
|
||||
\******************************/
|
||||
|
||||
/*!*********************************!*\
|
||||
!*** ./src/diagrams/er/erDb.js ***!
|
||||
\*********************************/
|
||||
|
||||
/*!**********************************!*\
|
||||
!*** ./src/themes/theme-base.js ***!
|
||||
\**********************************/
|
||||
|
||||
/*!**********************************!*\
|
||||
!*** ./src/themes/theme-dark.js ***!
|
||||
\**********************************/
|
||||
|
||||
/*!***********************************!*\
|
||||
!*** (webpack)/buildin/module.js ***!
|
||||
\***********************************/
|
||||
|
||||
/*!***********************************!*\
|
||||
!*** ./src/diagrams/er/styles.js ***!
|
||||
\***********************************/
|
||||
|
||||
/*!***********************************!*\
|
||||
!*** ./src/diagrams/pie/pieDb.js ***!
|
||||
\***********************************/
|
||||
|
||||
/*!************************************!*\
|
||||
!*** ./src/dagre-wrapper/edges.js ***!
|
||||
\************************************/
|
||||
|
||||
/*!************************************!*\
|
||||
!*** ./src/dagre-wrapper/index.js ***!
|
||||
\************************************/
|
||||
|
||||
/*!************************************!*\
|
||||
!*** ./src/dagre-wrapper/nodes.js ***!
|
||||
\************************************/
|
||||
|
||||
/*!************************************!*\
|
||||
!*** ./src/diagrams/git/styles.js ***!
|
||||
\************************************/
|
||||
|
||||
/*!************************************!*\
|
||||
!*** ./src/diagrams/pie/styles.js ***!
|
||||
\************************************/
|
||||
|
||||
/*!************************************!*\
|
||||
!*** ./src/themes/theme-forest.js ***!
|
||||
\************************************/
|
||||
|
||||
/*!*************************************!*\
|
||||
!*** ./src/diagrams/info/infoDb.js ***!
|
||||
\*************************************/
|
||||
|
||||
/*!*************************************!*\
|
||||
!*** ./src/diagrams/info/styles.js ***!
|
||||
\*************************************/
|
||||
|
||||
/*!*************************************!*\
|
||||
!*** ./src/themes/theme-default.js ***!
|
||||
\*************************************/
|
||||
|
||||
/*!*************************************!*\
|
||||
!*** ./src/themes/theme-helpers.js ***!
|
||||
\*************************************/
|
||||
|
||||
/*!*************************************!*\
|
||||
!*** ./src/themes/theme-neutral.js ***!
|
||||
\*************************************/
|
||||
|
||||
/*!**************************************!*\
|
||||
!*** ./src/dagre-wrapper/markers.js ***!
|
||||
\**************************************/
|
||||
|
||||
/*!**************************************!*\
|
||||
!*** ./src/diagrams/class/styles.js ***!
|
||||
\**************************************/
|
||||
|
||||
/*!**************************************!*\
|
||||
!*** ./src/diagrams/er/erMarkers.js ***!
|
||||
\**************************************/
|
||||
|
||||
/*!**************************************!*\
|
||||
!*** ./src/diagrams/gantt/styles.js ***!
|
||||
\**************************************/
|
||||
|
||||
/*!**************************************!*\
|
||||
!*** ./src/diagrams/state/shapes.js ***!
|
||||
\**************************************/
|
||||
|
||||
/*!**************************************!*\
|
||||
!*** ./src/diagrams/state/styles.js ***!
|
||||
\**************************************/
|
||||
|
||||
/*!***************************************!*\
|
||||
!*** ./src/dagre-wrapper/clusters.js ***!
|
||||
\***************************************/
|
||||
|
||||
/*!***************************************!*\
|
||||
!*** ./src/diagrams/class/classDb.js ***!
|
||||
\***************************************/
|
||||
|
||||
/*!***************************************!*\
|
||||
!*** ./src/diagrams/class/svgDraw.js ***!
|
||||
\***************************************/
|
||||
|
||||
/*!***************************************!*\
|
||||
!*** ./src/diagrams/common/common.js ***!
|
||||
\***************************************/
|
||||
|
||||
/*!***************************************!*\
|
||||
!*** ./src/diagrams/er/erRenderer.js ***!
|
||||
\***************************************/
|
||||
|
||||
/*!***************************************!*\
|
||||
!*** ./src/diagrams/gantt/ganttDb.js ***!
|
||||
\***************************************/
|
||||
|
||||
/*!***************************************!*\
|
||||
!*** ./src/diagrams/state/stateDb.js ***!
|
||||
\***************************************/
|
||||
|
||||
/*!****************************************!*\
|
||||
!*** ./src/diagrams/state/id-cache.js ***!
|
||||
\****************************************/
|
||||
|
||||
/*!****************************************!*\
|
||||
!*** external "entity-decode/browser" ***!
|
||||
\****************************************/
|
||||
|
||||
/*!*****************************************!*\
|
||||
!*** ./node_modules/process/browser.js ***!
|
||||
\*****************************************/
|
||||
|
||||
/*!*****************************************!*\
|
||||
!*** ./src/diagrams/git/gitGraphAst.js ***!
|
||||
\*****************************************/
|
||||
|
||||
/*!*****************************************!*\
|
||||
!*** ./src/diagrams/pie/pieRenderer.js ***!
|
||||
\*****************************************/
|
||||
|
||||
/*!*****************************************!*\
|
||||
!*** ./src/diagrams/sequence/styles.js ***!
|
||||
\*****************************************/
|
||||
|
||||
/*!******************************************!*\
|
||||
!*** ./src/dagre-wrapper/createLabel.js ***!
|
||||
\******************************************/
|
||||
|
||||
/*!******************************************!*\
|
||||
!*** ./src/dagre-wrapper/shapes/note.js ***!
|
||||
\******************************************/
|
||||
|
||||
/*!******************************************!*\
|
||||
!*** ./src/dagre-wrapper/shapes/util.js ***!
|
||||
\******************************************/
|
||||
|
||||
/*!******************************************!*\
|
||||
!*** ./src/diagrams/flowchart/flowDb.js ***!
|
||||
\******************************************/
|
||||
|
||||
/*!******************************************!*\
|
||||
!*** ./src/diagrams/flowchart/styles.js ***!
|
||||
\******************************************/
|
||||
|
||||
/*!******************************************!*\
|
||||
!*** ./src/diagrams/sequence/svgDraw.js ***!
|
||||
\******************************************/
|
||||
|
||||
/*!******************************************!*\
|
||||
!*** external "@braintree/sanitize-url" ***!
|
||||
\******************************************/
|
||||
|
||||
/*!*******************************************!*\
|
||||
!*** ./src/diagrams/info/infoRenderer.js ***!
|
||||
\*******************************************/
|
||||
|
||||
/*!*******************************************!*\
|
||||
!*** ./src/diagrams/pie/parser/pie.jison ***!
|
||||
\*******************************************/
|
||||
|
||||
/*!*********************************************!*\
|
||||
!*** ./src/diagrams/class/classRenderer.js ***!
|
||||
\*********************************************/
|
||||
|
||||
/*!*********************************************!*\
|
||||
!*** ./src/diagrams/gantt/ganttRenderer.js ***!
|
||||
\*********************************************/
|
||||
|
||||
/*!*********************************************!*\
|
||||
!*** ./src/diagrams/info/parser/info.jison ***!
|
||||
\*********************************************/
|
||||
|
||||
/*!*********************************************!*\
|
||||
!*** ./src/diagrams/sequence/sequenceDb.js ***!
|
||||
\*********************************************/
|
||||
|
||||
/*!*********************************************!*\
|
||||
!*** ./src/diagrams/state/stateRenderer.js ***!
|
||||
\*********************************************/
|
||||
|
||||
/*!*********************************************!*\
|
||||
!*** ./src/diagrams/user-journey/styles.js ***!
|
||||
\*********************************************/
|
||||
|
||||
/*!**********************************************!*\
|
||||
!*** ./src/dagre-wrapper/intersect/index.js ***!
|
||||
\**********************************************/
|
||||
|
||||
/*!**********************************************!*\
|
||||
!*** ./src/diagrams/git/gitGraphRenderer.js ***!
|
||||
\**********************************************/
|
||||
|
||||
/*!**********************************************!*\
|
||||
!*** ./src/diagrams/user-journey/svgDraw.js ***!
|
||||
\**********************************************/
|
||||
|
||||
/*!***********************************************!*\
|
||||
!*** ./node_modules/path-browserify/index.js ***!
|
||||
\***********************************************/
|
||||
|
||||
/*!***********************************************!*\
|
||||
!*** ./src/dagre-wrapper/mermaid-graphlib.js ***!
|
||||
\***********************************************/
|
||||
|
||||
/*!***********************************************!*\
|
||||
!*** ./src/diagrams/gantt/parser/gantt.jison ***!
|
||||
\***********************************************/
|
||||
|
||||
/*!************************************************!*\
|
||||
!*** ./src/diagrams/class/classRenderer-v2.js ***!
|
||||
\************************************************/
|
||||
|
||||
/*!************************************************!*\
|
||||
!*** ./src/diagrams/er/parser/erDiagram.jison ***!
|
||||
\************************************************/
|
||||
|
||||
/*!************************************************!*\
|
||||
!*** ./src/diagrams/flowchart/flowRenderer.js ***!
|
||||
\************************************************/
|
||||
|
||||
/*!************************************************!*\
|
||||
!*** ./src/diagrams/git/parser/gitGraph.jison ***!
|
||||
\************************************************/
|
||||
|
||||
/*!************************************************!*\
|
||||
!*** ./src/diagrams/state/stateRenderer-v2.js ***!
|
||||
\************************************************/
|
||||
|
||||
/*!************************************************!*\
|
||||
!*** ./src/diagrams/user-journey/journeyDb.js ***!
|
||||
\************************************************/
|
||||
|
||||
/*!**************************************************!*\
|
||||
!*** ./src/diagrams/flowchart/parser/flow.jison ***!
|
||||
\**************************************************/
|
||||
|
||||
/*!***************************************************!*\
|
||||
!*** ./src/diagrams/flowchart/flowChartShapes.js ***!
|
||||
\***************************************************/
|
||||
|
||||
/*!***************************************************!*\
|
||||
!*** ./src/diagrams/flowchart/flowRenderer-v2.js ***!
|
||||
\***************************************************/
|
||||
|
||||
/*!***************************************************!*\
|
||||
!*** ./src/diagrams/sequence/sequenceRenderer.js ***!
|
||||
\***************************************************/
|
||||
|
||||
/*!******************************************************!*\
|
||||
!*** ./node_modules/node-libs-browser/mock/empty.js ***!
|
||||
\******************************************************/
|
||||
|
||||
/*!******************************************************!*\
|
||||
!*** ./src/diagrams/class/parser/classDiagram.jison ***!
|
||||
\******************************************************/
|
||||
|
||||
/*!******************************************************!*\
|
||||
!*** ./src/diagrams/state/parser/stateDiagram.jison ***!
|
||||
\******************************************************/
|
||||
|
||||
/*!******************************************************!*\
|
||||
!*** ./src/diagrams/user-journey/journeyRenderer.js ***!
|
||||
\******************************************************/
|
||||
|
||||
/*!*******************************************************!*\
|
||||
!*** ./src/dagre-wrapper/intersect/intersect-line.js ***!
|
||||
\*******************************************************/
|
||||
|
||||
/*!*******************************************************!*\
|
||||
!*** ./src/dagre-wrapper/intersect/intersect-node.js ***!
|
||||
\*******************************************************/
|
||||
|
||||
/*!*******************************************************!*\
|
||||
!*** ./src/dagre-wrapper/intersect/intersect-rect.js ***!
|
||||
\*******************************************************/
|
||||
|
||||
/*!*******************************************************!*\
|
||||
!*** external "dagre-d3/lib/label/add-html-label.js" ***!
|
||||
\*******************************************************/
|
||||
|
||||
/*!********************************************************!*\
|
||||
!*** ./src/diagrams/user-journey/parser/journey.jison ***!
|
||||
\********************************************************/
|
||||
|
||||
/*!*********************************************************!*\
|
||||
!*** ./src/dagre-wrapper/intersect/intersect-circle.js ***!
|
||||
\*********************************************************/
|
||||
|
||||
/*!**********************************************************!*\
|
||||
!*** ./src/dagre-wrapper/intersect/intersect-ellipse.js ***!
|
||||
\**********************************************************/
|
||||
|
||||
/*!**********************************************************!*\
|
||||
!*** ./src/dagre-wrapper/intersect/intersect-polygon.js ***!
|
||||
\**********************************************************/
|
||||
|
||||
/*!************************************************************!*\
|
||||
!*** ./src/diagrams/sequence/parser/sequenceDiagram.jison ***!
|
||||
\************************************************************/
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright (c) 2012-2013 Chris Pettitt
|
||||
*
|
||||
* 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:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A better abstraction over CSS.
|
||||
*
|
||||
* @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present
|
||||
* @website https://github.com/cssinjs/jss
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/** @license React v0.18.0
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.12.0
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.12.0
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.12.0
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**!
|
||||
* @fileOverview Kickass library to create and place poppers near their reference elements.
|
||||
* @version 1.16.1-lts
|
||||
* @license
|
||||
* Copyright (c) 2016 Federico Zivolo and contributors
|
||||
*
|
||||
* 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:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
//! moment.js
|