Merge pull request #2 from microsoft/amgupt/hotfixupdate
code update from GD team to MS Github
This commit is contained in:
Коммит
6dc7d49af0
|
@ -3,14 +3,16 @@
|
|||
2. Create a mail account for alerts. The preferable provider is the [https://outlook.live.com/](https://outlook.live.com/)
|
||||
|
||||
3. Install tools on deployment host:
|
||||
* Powershell version 7.0.3 (on Mac and Linux), 5.1.0 (on Windows)
|
||||
* Powershell module 'Az' version 4.4.0
|
||||
* Powershell module 'Az.ManagedServiceIdentity' version 0.7.3
|
||||
* Poswershell module 'AzureADPreview' version 2.0.2.105 (only on Windows platform)
|
||||
* Az Cli version 2.10.0
|
||||
* Oracle Java 11.0.8
|
||||
* yarn package manager version 1.22.0
|
||||
* nodeJS version 12.16.1
|
||||
* Powershell version 7.0.3 [Instruction for Mac and Linux](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-macos?view=powershell-7), 5.1.0 (on Windows, should be pre-installed)
|
||||
* Powershell module 'Az' version 4.5.0 [Install instructions](https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-4.5.0)
|
||||
* Powershell module 'Az.ManagedServiceIdentity'
|
||||
```Install-Module -Name Az.ManagedServiceIdentity -Scope AllUsers```
|
||||
* Poswershell module 'AzureADPreview' version 2.0-preview (only on Windows platform) [Install instructions](https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2?view=azureadps-2.0-preview)
|
||||
* Az Cli version 2.10.0 [Install instructions](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest)
|
||||
* Oracle Java 11.0.8 [Download page](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html)
|
||||
* yarn package manager version 1.22.0 [Download page](https://classic.yarnpkg.com/en/docs/install/)
|
||||
* nodeJS version 12.16.1 [Download page](https://nodejs.org/dist/v12.16.1/)
|
||||
|
||||
|
||||
## Windows 2016 install tips
|
||||
AzureAD module doesn't work correctly in Powershell 7.0, when trying to use both modules it generates exceptions
|
||||
|
@ -39,15 +41,50 @@ uniq by nature.
|
|||
SubscriptionId and TenantId are two important parameters and should be verified they both have
|
||||
correct ids.
|
||||
|
||||
Environment type parameter envType ('Prod' or 'Dev') should be configured correctly. Based on this
|
||||
parameter the corresponding set of DFP permissions will be applied on application.
|
||||
|
||||
## Environment type parameter
|
||||
Environment type parameter envType (allowed values `Prod`,`Dev`) should be configured properly.
|
||||
This parameter modifies following configuration settings
|
||||
1. DFP roles, assigned to Azure AD application
|
||||
For Dev environment roles with prefix 'Sandbox' will be applied
|
||||
2. List of reply-urls in Azure AD application
|
||||
For Dev environment 'localhost' urls will be added to provide a way to debug locally
|
||||
3. The appropriate spring profile will be configured for backend Java Web App
|
||||
|
||||
## Alerts configuration
|
||||
In order to configure alerts for the solution, add the following parameter with
|
||||
list of email addresses to properties file:
|
||||
```
|
||||
"alertReceivers": {
|
||||
"value": [
|
||||
"<email@address1>",
|
||||
"<email@address2>"
|
||||
]
|
||||
}
|
||||
```
|
||||
Alerts will be sent to this list of recepients.
|
||||
|
||||
When this parameter is absent in the configuration file, alerts will be disabled.
|
||||
|
||||
**Verify application options:**
|
||||
* Check all durations, URLs, and roles in [queue service property file](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/queues/src/main/resources/application.yml)
|
||||
* Check all durations, URLs, and roles in [analytics service property file](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/analytics/src/main/resources/application.yml)
|
||||
|
||||
Depending on what you defined in deployment properties as the envType, the different property files will be used:
|
||||
* For `Dev` check all durations, URLs, and roles in:
|
||||
* [queue service property file](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/queues/src/main/resources/application-int.yml)
|
||||
* [queue service property file for secondary region](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/queues/src/main/resources/application-int-secondary.yml)
|
||||
* [analytics service property file](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/analytics/src/main/resources/application-int.yml)
|
||||
* [analytics service property file for secondary region](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/analytics/src/main/resources/application-int-secondary.yml)
|
||||
* For `Dev` check all durations, URLs, and roles in:
|
||||
* [queue service property file](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/queues/src/main/resources/application-prod.yml)
|
||||
* [queue service property file for secondary region](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/queues/src/main/resources/application-prod-secondary.yml)
|
||||
* [analytics service property file](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/analytics/src/main/resources/application-prod.yml)
|
||||
* [analytics service property file for secondary region](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/analytics/src/main/resources/application-prod-secondary.yml)
|
||||
* For both environment types all other parameters are specified in common files (common files have lower priority and
|
||||
any value in env-specific property files will override values defined here):
|
||||
* [common queue service property file](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/queues/src/main/resources/application.yml)
|
||||
* [commion analytics service property file](https://github.com/griddynamics/msd365fp-manual-review/blob/master/backend/analytics/src/main/resources/application.yml)
|
||||
|
||||
Configuration files are available in the future in AppService.
|
||||
|
||||
WARNING! Redeployment will override any manual changes in configuration files.
|
||||
|
||||
## Custom domain configuration
|
||||
|
@ -131,6 +168,13 @@ create two new subscriptions using obtained connection strings.
|
|||
3. On the DFP rule configuration page `https://dfp.microsoft-int.com/<TenantId>/env/ga/purchase/rules` for
|
||||
Purchase Protection create a new rule with `Trace()` clause which will send purchases to Manual Review.
|
||||
|
||||
### New users with deployment permissions
|
||||
Following permissions should be configured for the new user, who should perform deployments when initial
|
||||
deployment is already done:
|
||||
1. Add `Owner` permission on Azure Maps Account, created for deployment
|
||||
2. Add policy for new user on Azure Key Vault, created for deployment, with management permissions on secrets
|
||||
|
||||
|
||||
## Notes about run deployment on Mac OS
|
||||
**Grant DFP Api permissions**
|
||||
This step uses 'AzureAD' powershell module and can't be performed automatically when deployment run on
|
||||
|
|
|
@ -54,22 +54,34 @@ foreach ($la in @($logWorkspace, $logWorkspaceSecondary)) {
|
|||
--force true -y
|
||||
}
|
||||
|
||||
Write-Host "Removing service principal $prefix"
|
||||
Remove-AzADServicePrincipal `
|
||||
-DisplayName $prefix `
|
||||
-Force
|
||||
# remove AD service principal
|
||||
if (Get-AzADServicePrincipal -DisplayName $prefix -ErrorAction Ignore) {
|
||||
Write-Host "Removing Azure AD service principal $prefix"
|
||||
Remove-AzADServicePrincipal `
|
||||
-DisplayName $prefix `
|
||||
-Force
|
||||
}
|
||||
|
||||
Write-Host "Removing Azure AD application $prefix"
|
||||
Remove-AzADApplication `
|
||||
-DisplayName $prefix `
|
||||
-Force
|
||||
# remove AD application
|
||||
if (Get-AzADApplication -DisplayName $prefix -ErrorAction Ignore) {
|
||||
Write-Host "Removing Azure AD application $prefix"
|
||||
Remove-AzADApplication `
|
||||
-DisplayName $prefix `
|
||||
-Force
|
||||
}
|
||||
|
||||
Write-Host "Removing Azure AD group $mapsGroupName"
|
||||
Remove-AzADGroup `
|
||||
-DisplayName $mapsGroupName `
|
||||
-Force
|
||||
# remove AD maps group
|
||||
if (Get-AzADGroup -DisplayName $mapsGroupName -ErrorAction Ignore) {
|
||||
Write-Host "Removing Azure AD group $mapsGroupName"
|
||||
Remove-AzADGroup `
|
||||
-DisplayName $mapsGroupName `
|
||||
-Force
|
||||
}
|
||||
|
||||
Write-Host "Removing resource group"$deploymentResourceGroup
|
||||
Remove-AzResourceGroup -Name $deploymentResourceGroup -Force
|
||||
# remove deployment resource group
|
||||
if (Get-AzResourceGroup -Name $deploymentResourceGroup -ErrorAction Ignore) {
|
||||
Write-Host "Removing resource group"$deploymentResourceGroup
|
||||
Remove-AzResourceGroup -Name $deploymentResourceGroup -Force
|
||||
}
|
||||
|
||||
[System.Console]::Beep()
|
||||
|
|
|
@ -71,8 +71,12 @@ if (!($APP_SP_ID))
|
|||
|
||||
# Set admin consent
|
||||
Write-Host "= Set admin consent"
|
||||
az ad app permission admin-consent --id "${CLIENT_ID}"
|
||||
|
||||
# This operation sometimes raise an exception, do some retries
|
||||
for ($i = 0; $i -lt 3; $i++) {
|
||||
az ad app permission admin-consent --id "${CLIENT_ID}"
|
||||
if ($LastExitCode -ne 0) { Start-Sleep -s 2 } else { break }
|
||||
}
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed, check logs"
|
||||
}
|
||||
|
|
191
arm/main.json
191
arm/main.json
|
@ -122,6 +122,43 @@
|
|||
},
|
||||
"tenantShortName": {
|
||||
"type": "string"
|
||||
},
|
||||
"alertReceivers": {
|
||||
"type": "array",
|
||||
"defaultValue": []
|
||||
},
|
||||
"NoIncomingEventsDfphubEvaluationFrequency": {
|
||||
"type": "string",
|
||||
"defaultValue": "PT1H",
|
||||
"allowedValues": [
|
||||
"PT1M",
|
||||
"PT5M",
|
||||
"PT15M",
|
||||
"PT30M",
|
||||
"PT1H"
|
||||
]
|
||||
},
|
||||
"NoIncomingEventsDfphubWindowSize": {
|
||||
"type": "string",
|
||||
"defaultValue": "P1D",
|
||||
"allowedValues": [
|
||||
"PT1M",
|
||||
"PT5M",
|
||||
"PT15M",
|
||||
"PT30M",
|
||||
"PT1H",
|
||||
"PT6H",
|
||||
"PT12H",
|
||||
"P1D"
|
||||
]
|
||||
},
|
||||
"NoIncomingEventsDfphubThreshold": {
|
||||
"type": "int",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"exceptionsThresholdAlert": {
|
||||
"type": "int",
|
||||
"defaultValue": 200
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
|
@ -143,6 +180,7 @@
|
|||
"powershellPurgeCDNTemplateUri": "[replace(variables('templateBaseUri'),'main.json','templates/scriptPsPurgeCDN.json')]",
|
||||
"cdnTemplateUri": "[replace(variables('templateBaseUri'),'main.json','templates/cdn.json')]",
|
||||
"fdTemplateUri": "[replace(variables('templateBaseUri'),'main.json','templates/frontDoor.json')]",
|
||||
"alertsTemplateUri": "[replace(variables('templateBaseUri'),'main.json','templates/alerts.json')]",
|
||||
"storageAccountNameFE": "[replace(toLower(concat(variables('commonPrefix'), 'static')),'-','')]",
|
||||
"mapAccountName": "[concat(variables('commonPrefix'), '-map')]",
|
||||
"cosmosDbAccountName": "[concat(variables('commonPrefix'), '-storage')]",
|
||||
|
@ -166,7 +204,9 @@
|
|||
"LogWorkspaceSecondaryName": "[concat(variables('commonPrefix'), '-secondary-log-analytics-ws')]",
|
||||
"cdnName": "[concat(variables('commonPrefix'), '-cdn')]",
|
||||
"fdName": "[variables('commonPrefix')]",
|
||||
"keyVaultEndpoint": "[concat('https://',parameters('keyVaultName'),'.vault.azure.net/')]"
|
||||
"keyVaultEndpoint": "[concat('https://',parameters('keyVaultName'),'.vault.azure.net/')]",
|
||||
"actionGroupName": "[concat(variables('commonPrefix'), '-email-action-group')]"
|
||||
|
||||
},
|
||||
"resources": [
|
||||
{ // FE Storage Account
|
||||
|
@ -875,9 +915,6 @@
|
|||
"appInsightsName": {
|
||||
"value": "[variables('appInsightName')]"
|
||||
},
|
||||
"appInsightsResourceId": {
|
||||
"value": "[reference('applicationInsightTemplate').outputs.appInsightsResourceId.value]"
|
||||
},
|
||||
"subscriptionId": {
|
||||
"value": "[parameters('subscriptionId')]"
|
||||
},
|
||||
|
@ -910,6 +947,76 @@
|
|||
},
|
||||
"prefix": {
|
||||
"value": "[variables('commonPrefix')]"
|
||||
},
|
||||
"fdName": {
|
||||
"value": "[variables('fdName')]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ // Application insights SECONDARY REGION Dashboard 1
|
||||
"type": "Microsoft.Resources/deployments",
|
||||
"apiVersion": "2019-10-01",
|
||||
"name": "dashboard1SecondaryTemplate",
|
||||
"resourceGroup": "[resourceGroup().name]",
|
||||
"dependsOn": [
|
||||
"Microsoft.Resources/deployments/applicationInsightSecondaryTemplate",
|
||||
"Microsoft.Resources/deployments/cosmosDbTemplate",
|
||||
"Microsoft.Resources/deployments/ehubSecondaryTemplate",
|
||||
"Microsoft.Resources/deployments/applicationQueuesSecondaryTemplate",
|
||||
"Microsoft.Resources/deployments/applicationAnalyticsSecondaryTemplate"
|
||||
],
|
||||
"properties": {
|
||||
"mode": "Incremental",
|
||||
"templateLink": {
|
||||
"uri": "[variables('dashboard1TemplateUri')]",
|
||||
"contentVersion":"1.0.0.0"
|
||||
},
|
||||
"parameters": {
|
||||
"dashboardName": {
|
||||
"value": "[concat(reference('applicationInsightSecondaryTemplate').outputs.appInsightsAppId.value,'-dashboard')]"
|
||||
},
|
||||
"location": {
|
||||
"value": "[parameters('secondaryRegion')]"
|
||||
},
|
||||
"appInsightsName": {
|
||||
"value": "[variables('appInsightSecondaryName')]"
|
||||
},
|
||||
"subscriptionId": {
|
||||
"value": "[parameters('subscriptionId')]"
|
||||
},
|
||||
"rgName": {
|
||||
"value": "[resourceGroup().name]"
|
||||
},
|
||||
"cosmosDbAccountName": {
|
||||
"value": "[variables('cosmosDbAccountName')]"
|
||||
},
|
||||
"cosmosDbId": {
|
||||
"value": "[reference('cosmosDbTemplate').outputs.CosmosDbAccountResourceId.value]"
|
||||
},
|
||||
"ehubName": {
|
||||
"value": "[variables('ehubSecondaryName')]"
|
||||
},
|
||||
"ehubId": {
|
||||
"value": "[reference('ehubSecondaryTemplate').outputs.ehubResourceId.value]"
|
||||
},
|
||||
"appSiteQueuesName": {
|
||||
"value": "[variables('appQueuesSecondaryName')]"
|
||||
},
|
||||
"appSiteQueuesId": {
|
||||
"value": "[reference('applicationQueuesSecondaryTemplate').outputs.appSiteResourceId.value]"
|
||||
},
|
||||
"appSiteAnalyticsName": {
|
||||
"value": "[variables('appAnalyticsSecondaryName')]"
|
||||
},
|
||||
"appSiteAnalyticsId": {
|
||||
"value": "[reference('applicationAnalyticsSecondaryTemplate').outputs.appSiteResourceId.value]"
|
||||
},
|
||||
"prefix": {
|
||||
"value": "[variables('commonPrefix')]"
|
||||
},
|
||||
"fdName": {
|
||||
"value": "[variables('fdName')]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1138,6 +1245,82 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ // Alerts
|
||||
"condition": "[not(equals(length(parameters('alertReceivers')),0))]",
|
||||
"type": "Microsoft.Resources/deployments",
|
||||
"apiVersion": "2019-10-01",
|
||||
"name": "alertsTemplate",
|
||||
"dependsOn": [
|
||||
"Microsoft.Resources/deployments/fdTemplate",
|
||||
"Microsoft.Resources/deployments/powershellTemplateFDEnableHTTPS"
|
||||
],
|
||||
"properties": {
|
||||
"mode": "Incremental",
|
||||
"templateLink": {
|
||||
"uri": "[variables('alertsTemplateUri')]",
|
||||
"contentVersion":"1.0.0.0"
|
||||
},
|
||||
"parameters": {
|
||||
"prefix": {
|
||||
"value": "[parameters('prefix')]"
|
||||
},
|
||||
"primaryRegion": {
|
||||
"value": "[parameters('primaryRegion')]"
|
||||
},
|
||||
"secondaryRegion": {
|
||||
"value": "[parameters('secondaryRegion')]"
|
||||
},
|
||||
"fdName": {
|
||||
"value": "[variables('fdName')]"
|
||||
},
|
||||
"actionGroupName": {
|
||||
"value": "[variables('actionGroupName')]"
|
||||
},
|
||||
"alertReceivers": {
|
||||
"value": "[parameters('alertReceivers')]"
|
||||
},
|
||||
"appInsightName": {
|
||||
"value": "[variables('appInsightName')]"
|
||||
},
|
||||
"appInsightSecondaryName": {
|
||||
"value": "[variables('appInsightSecondaryName')]"
|
||||
},
|
||||
"appAnalyticsAddress": {
|
||||
"value": "[createArray(reference('applicationAnalyticsTemplate').outputs.defaultHostName.value,reference('applicationAnalyticsSecondaryTemplate').outputs.defaultHostName.value)]"
|
||||
},
|
||||
"appQueuesAddress": {
|
||||
"value": "[createArray(reference('applicationQueuesTemplate').outputs.defaultHostName.value,reference('applicationQueuesSecondaryTemplate').outputs.defaultHostName.value)]"
|
||||
},
|
||||
"cdnAddress": {
|
||||
"value": "[reference('cdnTemplate').outputs.cdnHostName.value]"
|
||||
},
|
||||
"backendAppList": {
|
||||
"value": "[createArray(variables('appAnalyticsName'),variables('appQueuesName'))]"
|
||||
},
|
||||
"backendAppSecondaryList": {
|
||||
"value": "[createArray(variables('appAnalyticsSecondaryName'),variables('appQueuesSecondaryName'))]"
|
||||
},
|
||||
"ehubName": {
|
||||
"value": "[variables('ehubName')]"
|
||||
},
|
||||
"ehubSecondaryName": {
|
||||
"value": "[variables('ehubSecondaryName')]"
|
||||
},
|
||||
"NoIncomingEventsDfphubEvaluationFrequency": {
|
||||
"value": "[parameters('NoIncomingEventsDfphubEvaluationFrequency')]"
|
||||
},
|
||||
"NoIncomingEventsDfphubWindowSize": {
|
||||
"value": "[parameters('NoIncomingEventsDfphubWindowSize')]"
|
||||
},
|
||||
"NoIncomingEventsDfphubThreshold": {
|
||||
"value": "[parameters('NoIncomingEventsDfphubThreshold')]"
|
||||
},
|
||||
"exceptionsThresholdAlert": {
|
||||
"value": "[parameters('exceptionsThresholdAlert')]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"value": "eastus"
|
||||
},
|
||||
"throuhgputAnalyticsDb": {
|
||||
"value": 1100
|
||||
"value": 1000
|
||||
},
|
||||
"throuhgputQueuesDb": {
|
||||
"value": 500
|
||||
|
@ -58,6 +58,23 @@
|
|||
},
|
||||
"mailSmtpHost": {
|
||||
"value": "smtp.office365.com"
|
||||
},
|
||||
"alertReceivers": {
|
||||
"value": [
|
||||
"ms_gd_engineering@griddynamics.com"
|
||||
]
|
||||
},
|
||||
"NoIncomingEventsDfphubEvaluationFrequency": {
|
||||
"value": "PT1H"
|
||||
},
|
||||
"NoIncomingEventsDfphubThreshold": {
|
||||
"value": 0
|
||||
},
|
||||
"NoIncomingEventsDfphubWindowSize": {
|
||||
"value": "P1D"
|
||||
},
|
||||
"exceptionsThresholdAlert": {
|
||||
"value": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"value": "eastus"
|
||||
},
|
||||
"throuhgputAnalyticsDb": {
|
||||
"value": 1100
|
||||
"value": 1000
|
||||
},
|
||||
"throuhgputQueuesDb": {
|
||||
"value": 500
|
||||
|
@ -58,6 +58,23 @@
|
|||
},
|
||||
"mailSmtpHost": {
|
||||
"value": "smtp.office365.com"
|
||||
},
|
||||
"alertReceivers": {
|
||||
"value": [
|
||||
"ms_gd_engineering@griddynamics.com"
|
||||
]
|
||||
},
|
||||
"NoIncomingEventsDfphubEvaluationFrequency": {
|
||||
"value": "PT1H"
|
||||
},
|
||||
"NoIncomingEventsDfphubThreshold": {
|
||||
"value": 0
|
||||
},
|
||||
"NoIncomingEventsDfphubWindowSize": {
|
||||
"value": "P1D"
|
||||
},
|
||||
"exceptionsThresholdAlert": {
|
||||
"value": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"value": "eastus"
|
||||
},
|
||||
"throuhgputAnalyticsDb": {
|
||||
"value": 1100
|
||||
"value": 1000
|
||||
},
|
||||
"throuhgputQueuesDb": {
|
||||
"value": 500
|
||||
|
@ -58,6 +58,21 @@
|
|||
},
|
||||
"mailSmtpHost": {
|
||||
"value": "smtp.office365.com"
|
||||
},
|
||||
"alertReceivers": {
|
||||
"value": []
|
||||
},
|
||||
"NoIncomingEventsDfphubEvaluationFrequency": {
|
||||
"value": "PT1H"
|
||||
},
|
||||
"NoIncomingEventsDfphubThreshold": {
|
||||
"value": 0
|
||||
},
|
||||
"NoIncomingEventsDfphubWindowSize": {
|
||||
"value": "P1D"
|
||||
},
|
||||
"exceptionsThresholdAlert": {
|
||||
"value": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,584 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"actionGroupName": {
|
||||
"type": "string"
|
||||
},
|
||||
"alertReceivers": {
|
||||
"type": "array"
|
||||
},
|
||||
"appInsightName": {
|
||||
"type": "string"
|
||||
},
|
||||
"appInsightSecondaryName": {
|
||||
"type": "string"
|
||||
},
|
||||
"fdName": {
|
||||
"type": "string"
|
||||
},
|
||||
"primaryRegion": {
|
||||
"type": "string"
|
||||
},
|
||||
"secondaryRegion": {
|
||||
"type": "string"
|
||||
},
|
||||
"appAnalyticsAddress": {
|
||||
"type": "array"
|
||||
},
|
||||
"appQueuesAddress": {
|
||||
"type": "array"
|
||||
},
|
||||
"cdnAddress": {
|
||||
"type": "string"
|
||||
},
|
||||
"failedLocationCountAlert": {
|
||||
"type": "int",
|
||||
"defaultValue": 1
|
||||
},
|
||||
"exceptionsThresholdAlert": {
|
||||
"type": "int",
|
||||
"defaultValue": 200
|
||||
},
|
||||
"backendAppList": {
|
||||
"type": "array"
|
||||
},
|
||||
"backendAppSecondaryList": {
|
||||
"type": "array"
|
||||
},
|
||||
"ehubName": {
|
||||
"type": "String"
|
||||
},
|
||||
"ehubSecondaryName": {
|
||||
"type": "String"
|
||||
},
|
||||
"NoIncomingEventsDfphubEvaluationFrequency": {
|
||||
"type": "string",
|
||||
"defaultValue": "PT1H",
|
||||
"allowedValues": [
|
||||
"PT1M",
|
||||
"PT5M",
|
||||
"PT15M",
|
||||
"PT30M",
|
||||
"PT1H"
|
||||
]
|
||||
},
|
||||
"NoIncomingEventsDfphubWindowSize": {
|
||||
"type": "string",
|
||||
"defaultValue": "P1D",
|
||||
"allowedValues": [
|
||||
"PT1M",
|
||||
"PT5M",
|
||||
"PT15M",
|
||||
"PT30M",
|
||||
"PT1H",
|
||||
"PT6H",
|
||||
"PT12H",
|
||||
"P1D"
|
||||
]
|
||||
},
|
||||
"NoIncomingEventsDfphubThreshold": {
|
||||
"type": "int",
|
||||
"defaultValue": 0
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"actionGroupShortName": "email-alert",
|
||||
"frontdoorsEntrypointWebtestShortName": "[concat(parameters('prefix'),'-entrypoint-test')]",
|
||||
"frontdoorsEntrypointWebtestName": "[concat(variables('frontdoorsEntrypointWebtestShortName'),'-',parameters('appInsightName'))]",
|
||||
"frontdoorsEntrypointWebtestSecondaryName": "[concat(variables('frontdoorsEntrypointWebtestShortName'),'-',parameters('appInsightSecondaryName'))]",
|
||||
"frontdoorsEntrypointWebtestCheckLocations": [
|
||||
{
|
||||
"Id": "us-ca-sjc-azr"
|
||||
},
|
||||
{
|
||||
"Id": "us-tx-sn1-azr"
|
||||
},
|
||||
{
|
||||
"Id": "us-il-ch1-azr"
|
||||
},
|
||||
{
|
||||
"Id": "us-va-ash-azr"
|
||||
},
|
||||
{
|
||||
"Id": "us-fl-mia-edge"
|
||||
}
|
||||
],
|
||||
"frontdoorsEntrypointWebTestUrl": "[concat('https://',parameters('fdName'),'.azurefd.net')]",
|
||||
"frontdoorsEntrypointWebTestConfig": "[concat('<WebTest Name=\"',variables('frontdoorsEntrypointWebtestShortName'),'\" Id=\"',guid(variables('frontdoorsEntrypointWebtestName')),'\" Enabled=\"True\" CssProjectStructure=\"\" CssIteration=\"\" Timeout=\"120\" WorkItemIds=\"\" xmlns=\"http://microsoft.com/schemas/VisualStudio/TeamTest/2010\" Description=\"\" CredentialUserName=\"\" CredentialPassword=\"\" PreAuthenticate=\"True\" Proxy=\"default\" StopOnError=\"False\" RecordedResultFile=\"\" ResultsLocale=\"\"> <Items> <Request Method=\"GET\" Guid=\"a3bcafbc-adea-4f0e-a883-699d21d615a2\" Version=\"1.1\" Url=\"',variables('frontdoorsEntrypointWebTestUrl'),'/index.html','\" ThinkTime=\"0\" Timeout=\"120\" ParseDependentRequests=\"False\" FollowRedirects=\"True\" RecordResult=\"True\" Cache=\"False\" ResponseTimeGoal=\"0\" Encoding=\"utf-8\" ExpectedHttpStatusCode=\"200\" ExpectedResponseUrl=\"\" ReportingName=\"\" IgnoreHttpStatusCode=\"False\" /> </Items> </WebTest>')]",
|
||||
"fdBackendAlertName": "[concat(parameters('fdName'),'-backend-state-alert')]",
|
||||
"fdBackendsList": [
|
||||
"[concat(parameters('appAnalyticsAddress')[0],':443')]",
|
||||
"[concat(parameters('appAnalyticsAddress')[1],':443')]",
|
||||
"[concat(parameters('appQueuesAddress')[0],':443')]",
|
||||
"[concat(parameters('appQueuesAddress')[1],':443')]",
|
||||
"[concat(parameters('cdnAddress'),':443')]"
|
||||
],
|
||||
"metricalertExceptionsName": "[concat(parameters('prefix'),'-server-exceptions-alert')]",
|
||||
"metricalertExceptionsSecondaryName": "[concat(parameters('prefix'),'-secondary-server-exceptions-alert')]",
|
||||
"scheduledqueryrulesTaskIdleTooLongName": "[concat(parameters('prefix'),'-task-idle-too-long-alert')]",
|
||||
"scheduledqueryrulesTaskIdleTooLongSecondaryName": "[concat(parameters('prefix'),'-secondary-task-idle-too-long-alert')]",
|
||||
"metricalertNoIncomingEventsDfphubName": "[concat(parameters('prefix'),'-no-incomeing-events-dfp-hub-alert')]",
|
||||
"metricalertNoIncomingEventsDfphubSecondaryName": "[concat(parameters('prefix'),'-secondary-no-incomeing-events-dfp-hub-alert')]"
|
||||
|
||||
},
|
||||
"resources": [
|
||||
{ // Create Action Group
|
||||
"type": "microsoft.insights/actionGroups",
|
||||
"apiVersion": "2019-03-01",
|
||||
"name": "[parameters('actionGroupName')]",
|
||||
"location": "Global",
|
||||
"properties": {
|
||||
"groupShortName": "[variables('actionGroupShortName')]",
|
||||
"enabled": true,
|
||||
"copy": [
|
||||
{
|
||||
"name": "emailReceivers",
|
||||
"count": "[length(parameters('alertReceivers'))]",
|
||||
"input": {
|
||||
"name": "[concat(parameters('prefix'),'-alert-email-',copyIndex('emailReceivers'))]",
|
||||
"emailAddress": "[parameters('alertReceivers')[copyIndex('emailReceivers')]]",
|
||||
"useCommonAlertSchema": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ // Create web test entrypoint
|
||||
"type": "microsoft.insights/webtests",
|
||||
"apiVersion": "2015-05-01",
|
||||
"name": "[variables('frontdoorsEntrypointWebtestName')]",
|
||||
"location": "[parameters('primaryRegion')]",
|
||||
"tags": {
|
||||
"[concat('hidden-link:',resourceId('microsoft.insights/components/', parameters('appInsightName')))]": "Resource"
|
||||
},
|
||||
"properties": {
|
||||
"SyntheticMonitorId": "[variables('frontdoorsEntrypointWebtestName')]",
|
||||
"Name": "[variables('frontdoorsEntrypointWebtestShortName')]",
|
||||
"Enabled": true,
|
||||
"Frequency": 300,
|
||||
"Timeout": 120,
|
||||
"Kind": "ping",
|
||||
"RetryEnabled": false,
|
||||
"Locations": "[variables('frontdoorsEntrypointWebtestCheckLocations')]",
|
||||
"Configuration": {
|
||||
"WebTest": "[variables('frontdoorsEntrypointWebTestConfig')]"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ // Alert on entrypoint test
|
||||
"type": "microsoft.insights/metricalerts",
|
||||
"apiVersion": "2018-03-01",
|
||||
"name": "[variables('frontdoorsEntrypointWebtestName')]",
|
||||
"location": "global",
|
||||
"dependsOn": [
|
||||
"[resourceId('microsoft.insights/webtests', variables('frontdoorsEntrypointWebtestName'))]"
|
||||
],
|
||||
"tags": {
|
||||
"[concat('hidden-link:',resourceId('microsoft.insights/components/', parameters('appInsightName')))]": "Resource",
|
||||
"[concat('hidden-link:',resourceId('microsoft.insights/webtests', variables('frontdoorsEntrypointWebtestName')))]": "Resource"
|
||||
},
|
||||
"properties": {
|
||||
"description": "[concat('Alert rule for availability test \"', variables('frontdoorsEntrypointWebTestUrl'), '\"')]",
|
||||
"severity": 1,
|
||||
"enabled": true,
|
||||
"scopes": [
|
||||
"[resourceId('microsoft.insights/webtests', variables('frontdoorsEntrypointWebtestName'))]",
|
||||
"[resourceId('microsoft.insights/components', parameters('appInsightName'))]"
|
||||
],
|
||||
"evaluationFrequency": "PT1M",
|
||||
"windowSize": "PT5M",
|
||||
"criteria": {
|
||||
"odata.type": "Microsoft.Azure.Monitor.WebtestLocationAvailabilityCriteria",
|
||||
"webTestId": "[resourceId('Microsoft.Insights/webtests', variables('frontdoorsEntrypointWebtestName'))]",
|
||||
"componentId": "[resourceId('Microsoft.Insights/components', parameters('appInsightName'))]",
|
||||
"failedLocationCount": "[parameters('failedLocationCountAlert')]"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"actionGroupId": "[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ // Create web test entrypoint SECONDARY REGION
|
||||
"type": "microsoft.insights/webtests",
|
||||
"apiVersion": "2015-05-01",
|
||||
"name": "[variables('frontdoorsEntrypointWebtestSecondaryName')]",
|
||||
"location": "[parameters('secondaryRegion')]",
|
||||
"tags": {
|
||||
"[concat('hidden-link:',resourceId('microsoft.insights/components/', parameters('appInsightSecondaryName')))]": "Resource"
|
||||
},
|
||||
"properties": {
|
||||
"SyntheticMonitorId": "[variables('frontdoorsEntrypointWebtestSecondaryName')]",
|
||||
"Name": "[variables('frontdoorsEntrypointWebtestShortName')]",
|
||||
"Enabled": true,
|
||||
"Frequency": 300,
|
||||
"Timeout": 120,
|
||||
"Kind": "ping",
|
||||
"RetryEnabled": false,
|
||||
"Locations": "[variables('frontdoorsEntrypointWebtestCheckLocations')]",
|
||||
"Configuration": {
|
||||
"WebTest": "[variables('frontdoorsEntrypointWebTestConfig')]"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ // Alert on entrypoint test SECONDARY REGION
|
||||
"type": "microsoft.insights/metricalerts",
|
||||
"apiVersion": "2018-03-01",
|
||||
"name": "[variables('frontdoorsEntrypointWebtestSecondaryName')]",
|
||||
"location": "global",
|
||||
"dependsOn": [
|
||||
"[resourceId('microsoft.insights/webtests', variables('frontdoorsEntrypointWebtestSecondaryName'))]"
|
||||
],
|
||||
"tags": {
|
||||
"[concat('hidden-link:',resourceId('microsoft.insights/components/', parameters('appInsightSecondaryName')))]": "Resource",
|
||||
"[concat('hidden-link:',resourceId('microsoft.insights/webtests', variables('frontdoorsEntrypointWebtestSecondaryName')))]": "Resource"
|
||||
},
|
||||
"properties": {
|
||||
"description": "[concat('Alert rule for availability test \"', variables('frontdoorsEntrypointWebTestUrl'), '\"')]",
|
||||
"severity": 1,
|
||||
"enabled": true,
|
||||
"scopes": [
|
||||
"[resourceId('microsoft.insights/webtests', variables('frontdoorsEntrypointWebtestSecondaryName'))]",
|
||||
"[resourceId('microsoft.insights/components', parameters('appInsightSecondaryName'))]"
|
||||
],
|
||||
"evaluationFrequency": "PT1M",
|
||||
"windowSize": "PT5M",
|
||||
"criteria": {
|
||||
"odata.type": "Microsoft.Azure.Monitor.WebtestLocationAvailabilityCriteria",
|
||||
"webTestId": "[resourceId('Microsoft.Insights/webtests', variables('frontdoorsEntrypointWebtestSecondaryName'))]",
|
||||
"componentId": "[resourceId('Microsoft.Insights/components', parameters('appInsightSecondaryName'))]",
|
||||
"failedLocationCount": "[parameters('failedLocationCountAlert')]"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"actionGroupId": "[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ // Alert on Front Doors backand failure
|
||||
"type": "microsoft.insights/metricAlerts",
|
||||
"apiVersion": "2018-03-01",
|
||||
"name": "[variables('fdBackendAlertName')]",
|
||||
"location": "global",
|
||||
"dependsOn": [],
|
||||
"properties": {
|
||||
"description": "Alert rule for Front Doors backends state",
|
||||
"severity": 1,
|
||||
"enabled": true,
|
||||
"scopes": [
|
||||
"[resourceId('Microsoft.Network/frontdoors', parameters('fdName'))]"
|
||||
],
|
||||
"evaluationFrequency": "PT1M",
|
||||
"windowSize": "PT5M",
|
||||
"criteria": {
|
||||
"allOf": [
|
||||
{
|
||||
"threshold": 50,
|
||||
"name": "Metric1",
|
||||
"metricNamespace": "microsoft.network/frontdoors",
|
||||
"metricName": "BackendHealthPercentage",
|
||||
"dimensions": [
|
||||
{
|
||||
"name": "Backend",
|
||||
"operator": "Include",
|
||||
"values": "[variables('fdBackendsList')]"
|
||||
}
|
||||
],
|
||||
"operator": "LessThan",
|
||||
"timeAggregation": "Average",
|
||||
"criterionType": "StaticThresholdCriterion"
|
||||
}
|
||||
],
|
||||
"odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria"
|
||||
},
|
||||
"autoMitigate": true,
|
||||
"targetResourceType": "Microsoft.Network/frontdoors",
|
||||
"targetResourceRegion": "global",
|
||||
"actions": [
|
||||
{
|
||||
"actionGroupId": "[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]",
|
||||
"webHookProperties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ // Alert on backend server exceptions
|
||||
"type": "microsoft.insights/metricalerts",
|
||||
"apiVersion": "2018-03-01",
|
||||
"name": "[variables('metricalertExceptionsName')]",
|
||||
"location": "global",
|
||||
"dependsOn": [],
|
||||
"properties": {
|
||||
"description": "Alert rule for server exceptions in backend applications",
|
||||
"severity": 1,
|
||||
"enabled": true,
|
||||
"scopes": [
|
||||
"[resourceId('microsoft.insights/components', parameters('appInsightName'))]"
|
||||
],
|
||||
"evaluationFrequency": "PT1M",
|
||||
"windowSize": "PT5M",
|
||||
"criteria": {
|
||||
"allOf": [
|
||||
{
|
||||
"threshold": "[parameters('exceptionsThresholdAlert')]",
|
||||
"name": "Metric1",
|
||||
"metricNamespace": "microsoft.insights/components",
|
||||
"metricName": "exceptions/server",
|
||||
"dimensions": [
|
||||
{
|
||||
"name": "cloud/roleName",
|
||||
"operator": "Include",
|
||||
"values": "[parameters('backendAppList')]"
|
||||
}
|
||||
],
|
||||
"operator": "GreaterThan",
|
||||
"timeAggregation": "Count",
|
||||
"criterionType": "StaticThresholdCriterion"
|
||||
}
|
||||
],
|
||||
"odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria"
|
||||
},
|
||||
"autoMitigate": true,
|
||||
"targetResourceType": "Microsoft.Insights/components",
|
||||
"targetResourceRegion": "[parameters('primaryRegion')]",
|
||||
"actions": [
|
||||
{
|
||||
"actionGroupId": "[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]",
|
||||
"webHookProperties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ // Alert on backend server exceptions SECONDARY REGION
|
||||
"type": "microsoft.insights/metricalerts",
|
||||
"apiVersion": "2018-03-01",
|
||||
"name": "[variables('metricalertExceptionsSecondaryName')]",
|
||||
"location": "global",
|
||||
"dependsOn": [],
|
||||
"properties": {
|
||||
"description": "Alert rule for server exceptions in backend applications",
|
||||
"severity": 1,
|
||||
"enabled": true,
|
||||
"scopes": [
|
||||
"[resourceId('microsoft.insights/components', parameters('appInsightSecondaryName'))]"
|
||||
],
|
||||
"evaluationFrequency": "PT1M",
|
||||
"windowSize": "PT5M",
|
||||
"criteria": {
|
||||
"allOf": [
|
||||
{
|
||||
"threshold": "[parameters('exceptionsThresholdAlert')]",
|
||||
"name": "Metric1",
|
||||
"metricNamespace": "microsoft.insights/components",
|
||||
"metricName": "exceptions/server",
|
||||
"dimensions": [
|
||||
{
|
||||
"name": "cloud/roleName",
|
||||
"operator": "Include",
|
||||
"values": "[parameters('backendAppSecondaryList')]"
|
||||
}
|
||||
],
|
||||
"operator": "GreaterThan",
|
||||
"timeAggregation": "Count",
|
||||
"criterionType": "StaticThresholdCriterion"
|
||||
}
|
||||
],
|
||||
"odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria"
|
||||
},
|
||||
"autoMitigate": true,
|
||||
"targetResourceType": "Microsoft.Insights/components",
|
||||
"targetResourceRegion": "[parameters('secondaryRegion')]",
|
||||
"actions": [
|
||||
{
|
||||
"actionGroupId": "[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]",
|
||||
"webHookProperties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ // Alert for idle tasks
|
||||
"type": "microsoft.insights/scheduledqueryrules",
|
||||
"apiVersion": "2018-04-16",
|
||||
"name": "[variables('scheduledqueryrulesTaskIdleTooLongName')]",
|
||||
"location": "[parameters('primaryRegion')]",
|
||||
"dependsOn": [],
|
||||
"properties": {
|
||||
"description": "Backend Java application task idle for too long time",
|
||||
"enabled": "true",
|
||||
"source": {
|
||||
"query": "traces\n| where message matches regex \"Task \\\\[.*\\\\] is idle for too long. Last execution was \\\\[.*\\\\] minutes ago with status message: \\\\[.*\\\\]\"\n| project taskname=extract(\"Task \\\\[(.*)\\\\] is idle for too long. Last execution was \\\\[.*\\\\] minutes ago with status message: \\\\[.*\\\\]\", 1, message),timestamp\n| summarize AggregatedValue = count() by bin(timestamp, 5m),taskname\n",
|
||||
"authorizedResources": [],
|
||||
"dataSourceId": "[resourceId('microsoft.insights/components', parameters('appInsightName'))]",
|
||||
"queryType": "ResultCount"
|
||||
},
|
||||
"schedule": {
|
||||
"frequencyInMinutes": 15,
|
||||
"timeWindowInMinutes": 30
|
||||
},
|
||||
"action": {
|
||||
"severity": "3",
|
||||
"aznsAction": {
|
||||
"actionGroup": [
|
||||
"[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]"
|
||||
]
|
||||
},
|
||||
"trigger": {
|
||||
"thresholdOperator": "GreaterThanOrEqual",
|
||||
"threshold": 1,
|
||||
"metricTrigger": {
|
||||
"thresholdOperator": "GreaterThan",
|
||||
"threshold": 1,
|
||||
"metricTriggerType": "Consecutive",
|
||||
"metricColumn": "taskname"
|
||||
}
|
||||
},
|
||||
"odata.type": "Microsoft.WindowsAzure.Management.Monitoring.Alerts.Models.Microsoft.AppInsights.Nexus.DataContracts.Resources.ScheduledQueryRules.AlertingAction"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ // Alert for idle tasks SECONDARY REGION
|
||||
"type": "microsoft.insights/scheduledqueryrules",
|
||||
"apiVersion": "2018-04-16",
|
||||
"name": "[variables('scheduledqueryrulesTaskIdleTooLongSecondaryName')]",
|
||||
"location": "[parameters('secondaryRegion')]",
|
||||
"dependsOn": [],
|
||||
"properties": {
|
||||
"description": "Backend Java application task idle for too long time",
|
||||
"enabled": "true",
|
||||
"source": {
|
||||
"query": "traces\n| where message matches regex \"Task \\\\[.*\\\\] is idle for too long. Last execution was \\\\[.*\\\\] minutes ago with status message: \\\\[.*\\\\]\"\n| project taskname=extract(\"Task \\\\[(.*)\\\\] is idle for too long. Last execution was \\\\[.*\\\\] minutes ago with status message: \\\\[.*\\\\]\", 1, message),timestamp\n| summarize AggregatedValue = count() by bin(timestamp, 5m),taskname\n",
|
||||
"authorizedResources": [],
|
||||
"dataSourceId": "[resourceId('microsoft.insights/components', parameters('appInsightSecondaryName'))]",
|
||||
"queryType": "ResultCount"
|
||||
},
|
||||
"schedule": {
|
||||
"frequencyInMinutes": 15,
|
||||
"timeWindowInMinutes": 30
|
||||
},
|
||||
"action": {
|
||||
"severity": "3",
|
||||
"aznsAction": {
|
||||
"actionGroup": [
|
||||
"[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]"
|
||||
]
|
||||
},
|
||||
"trigger": {
|
||||
"thresholdOperator": "GreaterThanOrEqual",
|
||||
"threshold": 1,
|
||||
"metricTrigger": {
|
||||
"thresholdOperator": "GreaterThan",
|
||||
"threshold": 1,
|
||||
"metricTriggerType": "Consecutive",
|
||||
"metricColumn": "taskname"
|
||||
}
|
||||
},
|
||||
"odata.type": "Microsoft.WindowsAzure.Management.Monitoring.Alerts.Models.Microsoft.AppInsights.Nexus.DataContracts.Resources.ScheduledQueryRules.AlertingAction"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ // Alert for absent incoming events on dfp-hub
|
||||
"type": "microsoft.insights/metricAlerts",
|
||||
"apiVersion": "2018-03-01",
|
||||
"name": "[variables('metricalertNoIncomingEventsDfphubName')]",
|
||||
"location": "global",
|
||||
"dependsOn": [],
|
||||
"properties": {
|
||||
"description": "No incoming messages in dfp-hub for long period of time",
|
||||
"severity": 3,
|
||||
"enabled": true,
|
||||
"scopes": [
|
||||
"[resourceId('Microsoft.EventHub/namespaces', parameters('ehubName'))]"
|
||||
],
|
||||
"evaluationFrequency": "[parameters('NoIncomingEventsDfphubEvaluationFrequency')]",
|
||||
"windowSize": "[parameters('NoIncomingEventsDfphubWindowSize')]",
|
||||
"criteria": {
|
||||
"allOf": [
|
||||
{
|
||||
"threshold": "[parameters('NoIncomingEventsDfphubThreshold')]",
|
||||
"name": "Metric1",
|
||||
"metricNamespace": "Microsoft.EventHub/namespaces",
|
||||
"metricName": "IncomingMessages",
|
||||
"dimensions": [
|
||||
{
|
||||
"name": "EntityName",
|
||||
"operator": "Include",
|
||||
"values": [
|
||||
"dfp-hub"
|
||||
]
|
||||
}
|
||||
],
|
||||
"operator": "LessThanOrEqual",
|
||||
"timeAggregation": "Total",
|
||||
"criterionType": "StaticThresholdCriterion"
|
||||
}
|
||||
],
|
||||
"odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria"
|
||||
},
|
||||
"autoMitigate": true,
|
||||
"targetResourceType": "Microsoft.EventHub/namespaces",
|
||||
"targetResourceRegion": "[parameters('primaryRegion')]",
|
||||
"actions": [
|
||||
{
|
||||
"actionGroupId": "[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]",
|
||||
"webHookProperties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ // Alert for absent incoming events on dfp-hub SECONDARY REGION
|
||||
"type": "microsoft.insights/metricAlerts",
|
||||
"apiVersion": "2018-03-01",
|
||||
"name": "[variables('metricalertNoIncomingEventsDfphubSecondaryName')]",
|
||||
"location": "global",
|
||||
"dependsOn": [],
|
||||
"properties": {
|
||||
"description": "No incoming messages in dfp-hub for long period of time",
|
||||
"severity": 3,
|
||||
"enabled": true,
|
||||
"scopes": [
|
||||
"[resourceId('Microsoft.EventHub/namespaces', parameters('ehubSecondaryName'))]"
|
||||
],
|
||||
"evaluationFrequency": "[parameters('NoIncomingEventsDfphubEvaluationFrequency')]",
|
||||
"windowSize": "[parameters('NoIncomingEventsDfphubWindowSize')]",
|
||||
"criteria": {
|
||||
"allOf": [
|
||||
{
|
||||
"threshold": "[parameters('NoIncomingEventsDfphubThreshold')]",
|
||||
"name": "Metric1",
|
||||
"metricNamespace": "Microsoft.EventHub/namespaces",
|
||||
"metricName": "IncomingMessages",
|
||||
"dimensions": [
|
||||
{
|
||||
"name": "EntityName",
|
||||
"operator": "Include",
|
||||
"values": [
|
||||
"dfp-hub"
|
||||
]
|
||||
}
|
||||
],
|
||||
"operator": "LessThanOrEqual",
|
||||
"timeAggregation": "Total",
|
||||
"criterionType": "StaticThresholdCriterion"
|
||||
}
|
||||
],
|
||||
"odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria"
|
||||
},
|
||||
"autoMitigate": true,
|
||||
"targetResourceType": "Microsoft.EventHub/namespaces",
|
||||
"targetResourceRegion": "[parameters('secondaryRegion')]",
|
||||
"actions": [
|
||||
{
|
||||
"actionGroupId": "[resourceId('microsoft.insights/actionGroups',parameters('actionGroupName'))]",
|
||||
"webHookProperties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -163,14 +163,14 @@
|
|||
{
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2020-04-01",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/Alert')]",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/Alerts')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosDbAccountName'), 'AnalyticsDB')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "Alert",
|
||||
"id": "Alerts",
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
|
@ -229,14 +229,14 @@
|
|||
{
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2020-04-01",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/ConfigurableAppSetting')]",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/ConfigurableAppSettings')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosDbAccountName'), 'AnalyticsDB')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "ConfigurableAppSetting",
|
||||
"id": "ConfigurableAppSettings",
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
|
@ -405,14 +405,14 @@
|
|||
{
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2020-04-01",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/Resolution')]",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/Resolutions')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosDbAccountName'), 'AnalyticsDB')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "Resolution",
|
||||
"id": "Resolutions",
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
|
@ -427,14 +427,14 @@
|
|||
{
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2020-04-01",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/QueuesDB/Settings')]",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/QueuesDB/ConfigurableAppSettings')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosDbAccountName'), 'QueuesDB')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "Settings",
|
||||
"id": "ConfigurableAppSettings",
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
|
@ -449,14 +449,14 @@
|
|||
{
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2020-04-01",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/Task')]",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/Tasks')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosDbAccountName'), 'AnalyticsDB')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "Task",
|
||||
"id": "Tasks",
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
|
@ -471,14 +471,14 @@
|
|||
{
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2020-04-01",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/QueuesDB/Task')]",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/QueuesDB/Tasks')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosDbAccountName'), 'QueuesDB')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "Task",
|
||||
"id": "Tasks",
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
|
@ -493,9 +493,9 @@
|
|||
{
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/userDefinedFunctions",
|
||||
"apiVersion": "2020-04-01",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/Alert/getTimestampBucket')]",
|
||||
"name": "[concat(parameters('cosmosDbAccountName'), '/AnalyticsDB/Alerts/getTimestampBucket')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('cosmosDbAccountName'), 'AnalyticsDB', 'Alert')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('cosmosDbAccountName'), 'AnalyticsDB', 'Alerts')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosDbAccountName'), 'AnalyticsDB')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]"
|
||||
],
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"dashboardName": {
|
||||
"defaultValue": "61414f76-0b17-4bac-b03b-a2ec21c0a3ee-dashboard ",
|
||||
"type": "String"
|
||||
},
|
||||
"location": {
|
||||
|
@ -13,25 +12,17 @@
|
|||
"description": "Specifies the location in which the Azure Storage resources should be deployed."
|
||||
}
|
||||
},
|
||||
"appInsightsResourceId": {
|
||||
"defaultValue": " /subscriptions/0cf0b616-8add-4f2e-b426-ff243e283635/resourceGroups/msd365fp-mr-stage-be/providers/Microsoft.Insights/components/msd365fp-mr-stage-app-insights",
|
||||
"type": "String"
|
||||
},
|
||||
"appInsightsName": {
|
||||
"type": "string",
|
||||
"defaultValue": "[parameters('appInsightsName')]"
|
||||
"type": "string"
|
||||
},
|
||||
"subscriptionId": {
|
||||
"type": "string",
|
||||
"defaultValue": "0cf0b616-8add-4f2e-b426-ff243e283635 "
|
||||
"type": "string"
|
||||
},
|
||||
"rgName": {
|
||||
"type": "string",
|
||||
"defaultValue": "msd365fp-mr-stage-be "
|
||||
"type": "string"
|
||||
},
|
||||
"cosmosDbAccountName": {
|
||||
"type": "string",
|
||||
"defaultValue": "msd365fp-mr-stage-storage "
|
||||
"type": "string"
|
||||
},
|
||||
"cosmosDbId": {
|
||||
"type": "string"
|
||||
|
@ -56,11 +47,15 @@
|
|||
},
|
||||
"prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"fdName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"appInsightsResourceId": "[resourceId('microsoft.insights/components/', parameters('appInsightsName'))]",
|
||||
"singleQuote": "'",
|
||||
"queuesQuery": "[concat('requests\n| where (cloud_RoleName has ',variables('singleQuote'), parameters('prefix'),variables('singleQuote'), ' and (name has ',variables('singleQuote'),'/api/queues/',variables('singleQuote'),') and success == true\n| summarize avgDuration=avg(duration) by bin(timestamp, 5m), name\n| project avgDuration, name, timestamp\n| render timechart\n')]",
|
||||
"queuesQuery": "[concat('requests\n| where (cloud_RoleName has ',variables('singleQuote'), parameters('prefix'),variables('singleQuote'), ') and (name has ',variables('singleQuote'),'/api/queues/',variables('singleQuote'),') and success == true\n| summarize avgDuration=avg(duration) by bin(timestamp, 5m), name\n| project avgDuration, name, timestamp\n| render timechart\n')]",
|
||||
"dashboardsQuery": "[concat('requests\n| where (cloud_RoleName has ',variables('singleQuote'),parameters('prefix'),variables('singleQuote'),') and (name has ',variables('singleQuote'),'/api/dashboards',variables('singleQuote'),') and success == true\n| summarize avgDuration=avg(duration) by bin(timestamp, 5m), name\n| project avgDuration, name, timestamp\n| render timechart\n')]",
|
||||
"itemsQuery": "[concat('requests\n| where (cloud_RoleName has ',variables('singleQuote'),parameters('prefix'),variables('singleQuote'),') and (name has ',variables('singleQuote'),'/api/items/',variables('singleQuote'),') and success == true\n| summarize avgDuration=avg(duration) by bin(timestamp, 5m), name\n| project avgDuration, name, timestamp\n| render timechart\n')]"
|
||||
},
|
||||
|
@ -89,7 +84,7 @@
|
|||
"inputs": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "[parameters('appInsightsResourceId')]"
|
||||
"value": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
{
|
||||
"name": "Version",
|
||||
|
@ -153,7 +148,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ResourceId",
|
||||
"value": "[parameters('appInsightsResourceId')]"
|
||||
"value": "[variables('appInsightsResourceId')]"
|
||||
}
|
||||
],
|
||||
"type": "Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart",
|
||||
|
@ -287,7 +282,7 @@
|
|||
"inputs": [
|
||||
{
|
||||
"name": "ComponentId",
|
||||
"value": "[parameters('appInsightsResourceId')]"
|
||||
"value": "[variables('appInsightsResourceId')]"
|
||||
}
|
||||
],
|
||||
"type": "Extension/AppInsightsExtension/PartType/SearchNavButtonOverviewAdaptedPart",
|
||||
|
@ -392,7 +387,7 @@
|
|||
"inputs": [
|
||||
{
|
||||
"name": "ResourceId",
|
||||
"value": "[parameters('appInsightsResourceId')]"
|
||||
"value": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
{
|
||||
"name": "DataModel",
|
||||
|
@ -455,7 +450,7 @@
|
|||
"inputs": [
|
||||
{
|
||||
"name": "ResourceId",
|
||||
"value": "[parameters('appInsightsResourceId')]"
|
||||
"value": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
{
|
||||
"name": "DataModel",
|
||||
|
@ -502,7 +497,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "performanceCounters/processIOBytesPerSecond",
|
||||
"aggregationType": 4,
|
||||
|
@ -548,7 +543,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "performanceCounters/processIOBytesPerSecond",
|
||||
"aggregationType": 4,
|
||||
|
@ -601,7 +596,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "requests/failed",
|
||||
"aggregationType": 7,
|
||||
|
@ -637,7 +632,7 @@
|
|||
"extensionName": "HubsExtension",
|
||||
"bladeName": "ResourceMenuBlade",
|
||||
"parameters": {
|
||||
"id": "[parameters('appInsightsResourceId')]",
|
||||
"id": "[variables('appInsightsResourceId')]",
|
||||
"menuid": "failures"
|
||||
}
|
||||
}
|
||||
|
@ -658,7 +653,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "requests/failed",
|
||||
"aggregationType": 7,
|
||||
|
@ -695,7 +690,7 @@
|
|||
"extensionName": "HubsExtension",
|
||||
"bladeName": "ResourceMenuBlade",
|
||||
"parameters": {
|
||||
"id": "[parameters('appInsightsResourceId')]",
|
||||
"id": "[variables('appInsightsResourceId')]",
|
||||
"menuid": "failures"
|
||||
}
|
||||
}
|
||||
|
@ -722,7 +717,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "requests/duration",
|
||||
"aggregationType": 4,
|
||||
|
@ -758,7 +753,7 @@
|
|||
"extensionName": "HubsExtension",
|
||||
"bladeName": "ResourceMenuBlade",
|
||||
"parameters": {
|
||||
"id": "[parameters('appInsightsResourceId')]",
|
||||
"id": "[variables('appInsightsResourceId')]",
|
||||
"menuid": "performance"
|
||||
}
|
||||
}
|
||||
|
@ -779,7 +774,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "requests/duration",
|
||||
"aggregationType": 4,
|
||||
|
@ -816,7 +811,7 @@
|
|||
"extensionName": "HubsExtension",
|
||||
"bladeName": "ResourceMenuBlade",
|
||||
"parameters": {
|
||||
"id": "[parameters('appInsightsResourceId')]",
|
||||
"id": "[variables('appInsightsResourceId')]",
|
||||
"menuid": "performance"
|
||||
}
|
||||
}
|
||||
|
@ -843,7 +838,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "performanceCounters/memoryAvailableBytes",
|
||||
"aggregationType": 4,
|
||||
|
@ -889,7 +884,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "performanceCounters/memoryAvailableBytes",
|
||||
"aggregationType": 4,
|
||||
|
@ -942,7 +937,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "exceptions/server",
|
||||
"aggregationType": 7,
|
||||
|
@ -954,7 +949,7 @@
|
|||
},
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "dependencies/failed",
|
||||
"aggregationType": 7,
|
||||
|
@ -1000,7 +995,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "exceptions/server",
|
||||
"aggregationType": 7,
|
||||
|
@ -1012,7 +1007,7 @@
|
|||
},
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "dependencies/failed",
|
||||
"aggregationType": 7,
|
||||
|
@ -1065,7 +1060,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "performanceCounters/processorCpuPercentage",
|
||||
"aggregationType": 4,
|
||||
|
@ -1077,7 +1072,7 @@
|
|||
},
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "performanceCounters/processCpuPercentage",
|
||||
"aggregationType": 4,
|
||||
|
@ -1123,7 +1118,7 @@
|
|||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "performanceCounters/processorCpuPercentage",
|
||||
"aggregationType": 4,
|
||||
|
@ -1135,7 +1130,7 @@
|
|||
},
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[parameters('appInsightsResourceId')]"
|
||||
"id": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"name": "performanceCounters/processCpuPercentage",
|
||||
"aggregationType": 4,
|
||||
|
@ -1176,7 +1171,7 @@
|
|||
"position": {
|
||||
"x": 0,
|
||||
"y": 8,
|
||||
"colSpan": 6,
|
||||
"colSpan": 4,
|
||||
"rowSpan": 4
|
||||
},
|
||||
"metadata": {
|
||||
|
@ -1238,9 +1233,9 @@
|
|||
},
|
||||
"19": {
|
||||
"position": {
|
||||
"x": 6,
|
||||
"x": 4,
|
||||
"y": 8,
|
||||
"colSpan": 6,
|
||||
"colSpan": 4,
|
||||
"rowSpan": 4
|
||||
},
|
||||
"metadata": {
|
||||
|
@ -1313,6 +1308,75 @@
|
|||
}
|
||||
},
|
||||
"20": {
|
||||
"position": {
|
||||
"x": 8,
|
||||
"y": 8,
|
||||
"colSpan": 4,
|
||||
"rowSpan": 4
|
||||
},
|
||||
"metadata": {
|
||||
"inputs": [
|
||||
{
|
||||
"name": "options",
|
||||
"isOptional": true
|
||||
},
|
||||
{
|
||||
"name": "sharedTimeRange",
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"type": "Extension/HubsExtension/PartType/MonitorChartPart",
|
||||
"settings": {
|
||||
"content": {
|
||||
"options": {
|
||||
"chart": {
|
||||
"metrics": [
|
||||
{
|
||||
"resourceMetadata": {
|
||||
"id": "[resourceId('Microsoft.Network/frontdoors', parameters('fdName'))]"
|
||||
},
|
||||
"name": "BackendHealthPercentage",
|
||||
"aggregationType": 4,
|
||||
"namespace": "microsoft.network/frontdoors",
|
||||
"metricVisualization": {
|
||||
"displayName": "Backend Health Percentage",
|
||||
"resourceDisplayName": "[parameters('prefix')]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "[concat('Avg Backend Health Percentage for ',parameters('prefix'),' by Backend')]",
|
||||
"titleKind": 1,
|
||||
"visualization": {
|
||||
"chartType": 2,
|
||||
"legendVisualization": {
|
||||
"isVisible": true,
|
||||
"position": 2,
|
||||
"hideSubtitle": false
|
||||
},
|
||||
"axisVisualization": {
|
||||
"x": {
|
||||
"isVisible": true,
|
||||
"axisType": 2
|
||||
},
|
||||
"y": {
|
||||
"isVisible": true,
|
||||
"axisType": 1
|
||||
}
|
||||
},
|
||||
"disablePinning": true
|
||||
},
|
||||
"grouping": {
|
||||
"dimension": "Backend",
|
||||
"sort": 2,
|
||||
"top": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"21": {
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
|
@ -1334,7 +1398,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"21": {
|
||||
"22": {
|
||||
"position": {
|
||||
"x": 6,
|
||||
"y": 12,
|
||||
|
@ -1356,7 +1420,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"22": {
|
||||
"23": {
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 13,
|
||||
|
@ -1456,7 +1520,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"23": {
|
||||
"24": {
|
||||
"position": {
|
||||
"x": 6,
|
||||
"y": 13,
|
||||
|
@ -1556,7 +1620,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"24": {
|
||||
"25": {
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 17,
|
||||
|
@ -1632,7 +1696,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"25": {
|
||||
"26": {
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
|
@ -1647,7 +1711,7 @@
|
|||
"SubscriptionId": "[parameters('subscriptionId')]",
|
||||
"ResourceGroup": "[parameters('rgName')]",
|
||||
"Name": "[parameters('appInsightsName')]",
|
||||
"ResourceId": "[parameters('appInsightsResourceId')]"
|
||||
"ResourceId": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"isOptional": true
|
||||
},
|
||||
|
@ -1733,7 +1797,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"26": {
|
||||
"27": {
|
||||
"position": {
|
||||
"x": 6,
|
||||
"y": 20,
|
||||
|
@ -1748,7 +1812,7 @@
|
|||
"SubscriptionId": "[parameters('subscriptionId')]",
|
||||
"ResourceGroup": "[parameters('rgName')]",
|
||||
"Name": "[parameters('appInsightsName')]",
|
||||
"ResourceId": "[parameters('appInsightsResourceId')]"
|
||||
"ResourceId": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"isOptional": true
|
||||
},
|
||||
|
@ -1834,7 +1898,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"27": {
|
||||
"28": {
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 24,
|
||||
|
@ -1849,7 +1913,7 @@
|
|||
"SubscriptionId": "[parameters('subscriptionId')]",
|
||||
"ResourceGroup": "[parameters('rgName')]",
|
||||
"Name": "[parameters('appInsightsName')]",
|
||||
"ResourceId": "[parameters('appInsightsResourceId')]"
|
||||
"ResourceId": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"isOptional": true
|
||||
},
|
||||
|
@ -1935,7 +1999,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"28": {
|
||||
"29": {
|
||||
"position": {
|
||||
"x": 6,
|
||||
"y": 24,
|
||||
|
@ -1950,7 +2014,7 @@
|
|||
"SubscriptionId": "[parameters('subscriptionId')]",
|
||||
"ResourceGroup": "[parameters('rgName')]",
|
||||
"Name": "[parameters('appInsightsName')]",
|
||||
"ResourceId": "[parameters('appInsightsResourceId')]"
|
||||
"ResourceId": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"isOptional": true
|
||||
},
|
||||
|
@ -1979,7 +2043,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Query",
|
||||
"value": "requests\n| where (cloud_RoleName has 'msd365fp-mr') and (name has 'TaskService') and success == true\n| project duration, replace('TaskService.', '', name), timestamp\n| render timechart\n",
|
||||
"value": "[concat('requests\n| where (cloud_RoleName has ','\"',parameters('prefix'),'\") and (name has \"TaskService\") and success == true\n| project duration, replace(\"TaskService.\", \"\", name), timestamp\n| render timechart\n')]",
|
||||
"isOptional": true
|
||||
},
|
||||
{
|
||||
|
@ -2036,7 +2100,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"29": {
|
||||
"30": {
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 28,
|
||||
|
@ -2051,7 +2115,7 @@
|
|||
"SubscriptionId": "[parameters('subscriptionId')]",
|
||||
"ResourceGroup": "[parameters('rgName')]",
|
||||
"Name": "[parameters('appInsightsName')]",
|
||||
"ResourceId": "[parameters('appInsightsResourceId')]"
|
||||
"ResourceId": "[variables('appInsightsResourceId')]"
|
||||
},
|
||||
"isOptional": true
|
||||
},
|
||||
|
|
|
@ -38,8 +38,8 @@ and also has access to read Azure Key Vault secrets in required environment.
|
|||
The project is running under Gradle configuration and has multi-module architecture. There are two executable modules:
|
||||
`queues` and `analytics` along with several libraries. Libraries are intended to separate
|
||||
common logic across reusable modules. It's not possible to run libraries apart from
|
||||
executable modules. You should build and run executable modules with `./gradlew` or `.\gradlew.bat`
|
||||
scripts inside their folders.
|
||||
executable modules. You should build and run executable modules with `./gradlew`
|
||||
(or `.\gradlew.bat` for Windows environment) scripts inside their folders.
|
||||
|
||||
In case if you only need to build executable packages you can use
|
||||
```shell script
|
||||
|
@ -85,7 +85,7 @@ To install the project locally please follow these steps:
|
|||
a terminal session if you run from console or in IDE run configuration if you work with it
|
||||
(e.g. [run configuration in IDEA](https://www.jetbrains.com/help/objc/add-environment-variables-and-program-arguments.html#add-environment-variables)).
|
||||
2. Execute command `./gradlew clean build` in project root directory or run `clean` and then `build` tasks in IDE
|
||||
3. Define advanced variables for `bootRun` task in queues/analytics modules:
|
||||
3. (Optional) Define advanced variables for `bootRun` task in queues/analytics modules:
|
||||
* `SPRING_PROFILES_ACTIVE=local` activates `application-local.yml` configuration which overrides default `application.yml` file.
|
||||
* `SPRING_OUTPUT_ANSI_ENABLED=ALWAYS` prints colorful logs in console output.
|
||||
* `SERVER_PORT=8081` change the port of one spring application to 8081 to be able to run both of them simultaneously.
|
||||
|
@ -237,6 +237,9 @@ please setup outliners like on this picture:
|
|||
```
|
||||
* Order of annotations on classes should be (the first one is the closest to class declaration): `org.springframework` >
|
||||
`org.projectlombok` > `io.swagger.core` > other
|
||||
* API naming should follow [common best practices](https://restfulapi.net/resource-naming)
|
||||
* Name of containers / tables should reflect it's content (e.g. if container stores especially RedHotChillyPepper
|
||||
entities then the name should be `RedHotChillyPeppers` with notion of all attributes and in a plural form)
|
||||
|
||||
### Logging
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,24 +1,28 @@
|
|||
package com.griddynamics.msd365fp.manualreview.analytics.config;
|
||||
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
|
||||
@UtilityClass
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("java:S2386")
|
||||
public class Constants {
|
||||
|
||||
public static final String DEFAULT_PAGE_REQUEST_SIZE_STR = "1000";
|
||||
public static final int DEFAULT_PAGE_REQUEST_SIZE = 1000;
|
||||
public static final String RESOLUTION_CONTAINER_NAME = "Resolution";
|
||||
|
||||
public static final String RESOLUTION_CONTAINER_NAME = "Resolutions";
|
||||
public static final String ITEM_LABEL_ACTIVITY_CONTAINER_NAME = "ItemLabelActivities";
|
||||
public static final String ITEM_LOCK_ACTIVITY_CONTAINER_NAME = "ItemLockActivities";
|
||||
public static final String COLLECTED_QUEUE_INFO_CONTAINER_NAME = "CollectedQueueInfo";
|
||||
public static final String COLLECTED_ANALYST_INFO_CONTAINER_NAME = "CollectedAnalystInfo";
|
||||
public static final String ITEM_PLACEMENT_ACTIVITY_CONTAINER_NAME = "ItemPlacementActivities";
|
||||
public static final String QUEUE_SIZE_CALCULATION_ACTIVITY_CONTAINER_NAME = "QueueSizeCalculationActivities";
|
||||
public static final String TASK_CONTAINER_NAME = "Task";
|
||||
public static final String ALERT_CONTAINER_NAME = "Alert";
|
||||
public static final String APP_SETTINGS_CONTAINER_NAME = "ConfigurableAppSetting";
|
||||
public static final String TASK_CONTAINER_NAME = "Tasks";
|
||||
public static final String ALERT_CONTAINER_NAME = "Alerts";
|
||||
public static final String APP_SETTINGS_CONTAINER_NAME = "ConfigurableAppSettings";
|
||||
|
||||
public static final String REGISTRATION_NAME = "azure-dfp-api";
|
||||
|
||||
public static final String OVERALL_SIZE_ID = "overall";
|
||||
|
|
|
@ -136,8 +136,9 @@ public class ModelMapperConfig {
|
|||
case REJECT:
|
||||
return "OfflineManualReview_Fraud";
|
||||
case WATCH_NA:
|
||||
return "ManualReview_WatchNA";
|
||||
case WATCH_INCONCLUSIVE:
|
||||
return "OfflineManualReview_Watchlist";
|
||||
return "ManualReview_Inclusive";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
spring:
|
||||
profiles:
|
||||
active: local
|
|
@ -42,8 +42,8 @@ dependencies {
|
|||
implementation 'org.modelmapper:modelmapper:2.3.7'
|
||||
implementation 'org.springframework:spring-webflux'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.2'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.2'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
|
|
|
@ -4,21 +4,27 @@ import com.griddynamics.msd365fp.manualreview.azuregraph.client.AnalystClient;
|
|||
import com.griddynamics.msd365fp.manualreview.dfpauth.config.properties.DFPRoleExtractorProperties;
|
||||
import com.griddynamics.msd365fp.manualreview.dfpauth.security.DFPRoleExtractionFilter;
|
||||
import com.griddynamics.msd365fp.manualreview.model.exception.IncorrectConfigurationException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(DFPRoleExtractorProperties.class)
|
||||
public class DFPAuthAutoConfiguration {
|
||||
|
||||
@Value("${spring.profiles.active:}")
|
||||
private String activeProfiles;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(AnalystClient.class)
|
||||
DFPRoleExtractionFilter dfpRoleFilter(
|
||||
AnalystClient analystClient,
|
||||
DFPRoleExtractorProperties properties) throws IncorrectConfigurationException {
|
||||
return new DFPRoleExtractionFilter(analystClient, properties);
|
||||
return new DFPRoleExtractionFilter(analystClient, properties, List.of(activeProfiles.split(",")));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import com.griddynamics.msd365fp.manualreview.dfpauth.config.properties.DFPRoleE
|
|||
import com.griddynamics.msd365fp.manualreview.dfpauth.util.UserPrincipalUtility;
|
||||
import com.griddynamics.msd365fp.manualreview.model.Analyst;
|
||||
import com.griddynamics.msd365fp.manualreview.model.exception.IncorrectConfigurationException;
|
||||
import com.microsoft.azure.spring.autoconfigure.aad.UserPrincipal;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
@ -21,20 +23,24 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.griddynamics.msd365fp.manualreview.dfpauth.config.Constants.AUTH_TOKEN_PRINCIPAL_ID_CLAIM;
|
||||
|
||||
@Slf4j
|
||||
public class DFPRoleExtractionFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String VIRTUAL_USER = "Virtual-User";
|
||||
private static final String PERF_TEST_PROFILE = "perftest";
|
||||
|
||||
private final AnalystClient analystClient;
|
||||
private final Cache<String, Set<String>> roleCache;
|
||||
private final List<String> activeProfiles;
|
||||
|
||||
public DFPRoleExtractionFilter(final AnalystClient analystClient,
|
||||
final DFPRoleExtractorProperties properties) throws IncorrectConfigurationException {
|
||||
final DFPRoleExtractorProperties properties,
|
||||
final List<String> activeProfiles) throws IncorrectConfigurationException {
|
||||
if (properties.getTokenCacheSize() == null ||
|
||||
properties.getTokenCacheSize() < 1 ||
|
||||
properties.getTokenCacheRetention() == null) {
|
||||
|
@ -45,6 +51,7 @@ public class DFPRoleExtractionFilter extends OncePerRequestFilter {
|
|||
.expireAfterWrite(properties.getTokenCacheRetention())
|
||||
.maximumSize(properties.getTokenCacheSize())
|
||||
.build();
|
||||
this.activeProfiles = activeProfiles;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
|
@ -55,6 +62,7 @@ public class DFPRoleExtractionFilter extends OncePerRequestFilter {
|
|||
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
final String userId = UserPrincipalUtility.extractUserId(auth);
|
||||
final String virtualUserName = request.getHeader(VIRTUAL_USER);
|
||||
|
||||
if (authHeader != null && auth != null && userId != null) {
|
||||
log.debug("The request will be authorized by DFP roles");
|
||||
|
@ -64,19 +72,38 @@ public class DFPRoleExtractionFilter extends OncePerRequestFilter {
|
|||
.stream()
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.toSet());
|
||||
log.info("User [{}] imported from DFP RBAC policies with [{}] roles", userId, analystRoles);
|
||||
log.info("User [{}] imported from DFP RBAC policies with [{}] roles.", userId, analystRoles);
|
||||
return analystRoles;
|
||||
});
|
||||
|
||||
List<GrantedAuthority> updatedAuthorities = new LinkedList<>();
|
||||
roles.forEach(role -> updatedAuthorities.add(new SimpleGrantedAuthority(role)));
|
||||
Authentication newAuth = new PreAuthenticatedAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities);
|
||||
Object newPrincipal = null;
|
||||
if (virtualUserName != null && activeProfiles.contains(PERF_TEST_PROFILE)) {
|
||||
newPrincipal = addVirtualUserClaim(auth, virtualUserName, userId);
|
||||
}
|
||||
Authentication newAuth = new PreAuthenticatedAuthenticationToken(
|
||||
newPrincipal == null ? auth.getPrincipal() : newPrincipal, auth.getCredentials(), updatedAuthorities);
|
||||
SecurityContextHolder.getContext().setAuthentication(newAuth);
|
||||
} else {
|
||||
log.debug("No conditions have been met for DFP roles retrieving");
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private Object addVirtualUserClaim(Authentication auth, String virtualUserName, String userId) {
|
||||
try {
|
||||
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder();
|
||||
UserPrincipal principal = (UserPrincipal) auth.getPrincipal();
|
||||
for (Map.Entry<String, Object> entry : principal.getClaims().entrySet()) {
|
||||
claimsSetBuilder.claim(entry.getKey(), entry.getValue());
|
||||
}
|
||||
claimsSetBuilder.claim(AUTH_TOKEN_PRINCIPAL_ID_CLAIM, virtualUserName);
|
||||
UserPrincipal newPrincipal = new UserPrincipal(null, claimsSetBuilder.build());
|
||||
log.debug("Virtual user with ID [{}] will be used as authentication principal", virtualUserName);
|
||||
return newPrincipal;
|
||||
} catch (Exception e) {
|
||||
log.warn("User [{}] tried to authenticate with virtual user [{}].", userId, virtualUserName, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,16 +11,21 @@ import java.util.List;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.griddynamics.msd365fp.manualreview.dfpauth.config.Constants.*;
|
||||
import static org.springframework.security.core.context.SecurityContextHolder.getContext;
|
||||
|
||||
@UtilityClass
|
||||
public class UserPrincipalUtility {
|
||||
|
||||
public Authentication getAuth() {
|
||||
return SecurityContextHolder.getContext().getAuthentication();
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return extractUserId(SecurityContextHolder.getContext().getAuthentication());
|
||||
return extractUserId(getAuth());
|
||||
}
|
||||
|
||||
public List<String> getUserRoles() {
|
||||
return extractUserRoles(SecurityContextHolder.getContext().getAuthentication());
|
||||
return extractUserRoles(getAuth());
|
||||
}
|
||||
|
||||
public String extractUserId(Authentication auth) {
|
||||
|
|
|
@ -25,8 +25,8 @@ jar {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.2'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.2'
|
||||
implementation 'io.swagger.core.v3:swagger-annotations:2.1.2'
|
||||
implementation 'org.apache.commons:commons-lang3:3.9'
|
||||
implementation 'org.springframework:spring-core'
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,8 +1,10 @@
|
|||
package com.griddynamics.msd365fp.manualreview.queues.config;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@UtilityClass
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("java:S2386")
|
||||
public class Constants {
|
||||
|
||||
public static final int TOP_ELEMENT_IN_CONTAINER_PAGE_SIZE = 1;
|
||||
|
@ -16,9 +18,9 @@ public class Constants {
|
|||
|
||||
public static final String ITEMS_CONTAINER_NAME = "Items";
|
||||
public static final String QUEUES_CONTAINER_NAME = "Queues";
|
||||
public static final String TASK_CONTAINER_NAME = "Task";
|
||||
public static final String TASK_CONTAINER_NAME = "Tasks";
|
||||
public static final String DICTIONARIES_CONTAINER_NAME = "Dictionaries";
|
||||
public static final String SETTINGS_CONTAINER_NAME = "Settings";
|
||||
public static final String SETTINGS_CONTAINER_NAME = "ConfigurableAppSettings";
|
||||
|
||||
public static final int DEFAULT_CACHE_CONTROL_SECONDS = 1800;
|
||||
|
||||
|
@ -47,13 +49,13 @@ public class Constants {
|
|||
public static final String MESSAGE_ITEM_LOCKING_IN_ABSTRACT_QUEUE = "Item can't be locked under an abstract queue";
|
||||
|
||||
public static final String RESIDUAL_QUEUE_TASK_NAME = "residual-queue-reconciliation-task";
|
||||
public static final String QUEUE_STATE_TASK_NAME = "queue-size-calculation-task";
|
||||
public static final String QUEUE_SIZE_TASK_NAME = "queue-size-calculation-task";
|
||||
public static final String OVERALL_SIZE_TASK_NAME = "overall-size-calculation-task";
|
||||
public static final String ITEM_STATE_TASK_NAME = "item-assignment-reconciliation-task";
|
||||
public static final String ITEM_ASSIGNMENT_TASK_NAME = "item-assignment-reconciliation-task";
|
||||
public static final String ITEM_UNLOCK_TASK_NAME = "item-unlock-task";
|
||||
public static final String DICTIONARY_VALIDATION_TASK_NAME = "dictionary-reconciliation-task";
|
||||
public static final String DICTIONARY_TASK_NAME = "dictionary-reconciliation-task";
|
||||
public static final String ENRICHMENT_TASK_NAME = "item-enrichment-task";
|
||||
public static final String QUEUE_ASSIGNMENT_TASK = "queue-assignment-reconciliation-task";
|
||||
public static final String QUEUE_ASSIGNMENT_TASK_NAME = "queue-assignment-reconciliation-task";
|
||||
|
||||
public static final String DATETIME_PATTERN_DFP = "MM/dd/yyyy HH:mm:ss xxxxx";
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.griddynamics.msd365fp.manualreview.model.Decision;
|
|||
import com.griddynamics.msd365fp.manualreview.model.dfp.*;
|
||||
import com.griddynamics.msd365fp.manualreview.model.dfp.raw.*;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.model.persistence.Item;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.modelmapper.Converter;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.modelmapper.TypeMap;
|
||||
|
@ -62,7 +63,8 @@ public class DFPModelMapperConfig {
|
|||
Map<String, PaymentInstrument> paymentInstrumentMap = new HashMap<>();
|
||||
Map<String, Product> productMap = new HashMap<>();
|
||||
Map<String, BankEvent> bankEventMap = new HashMap<>();
|
||||
Map<String, Address> addressMap = new HashMap<>();
|
||||
Map<String, Address> shippingAddressMap = new HashMap<>();
|
||||
Map<String, Address> billingAddressMap = new HashMap<>();
|
||||
Map<String, PreviousPurchase> previousPurchaseMap = new HashMap<>();
|
||||
AssesmentResult assesmentResult = new AssesmentResult();
|
||||
Map<String, PurchaseStatus> purchaseStatusMap = new HashMap<>();
|
||||
|
@ -73,14 +75,14 @@ public class DFPModelMapperConfig {
|
|||
entity.getEdges().forEach(edge -> {
|
||||
switch (edge.getName()) {
|
||||
case "PurchaseAddress":
|
||||
modelMapper.map(edge.getData(), addressMap.computeIfAbsent(
|
||||
modelMapper.map(edge.getData(), shippingAddressMap.computeIfAbsent(
|
||||
((PurchaseAddressEdgeData) edge.getData()).getAddressId(),
|
||||
key -> new Address()));
|
||||
break;
|
||||
case "PaymentInstrumentAddress":
|
||||
PaymentInstrumentAddressEdgeData edgeData =
|
||||
((PaymentInstrumentAddressEdgeData) edge.getData());
|
||||
modelMapper.map(edge.getData(), addressMap.computeIfAbsent(
|
||||
modelMapper.map(edge.getData(), billingAddressMap.computeIfAbsent(
|
||||
edgeData.getAddressId(),
|
||||
key -> new Address()));
|
||||
paymentInstrumentMap
|
||||
|
@ -126,7 +128,12 @@ public class DFPModelMapperConfig {
|
|||
entity.getNodes().forEach(node -> {
|
||||
switch (node.getName()) {
|
||||
case "Address":
|
||||
modelMapper.map(node.getData(), addressMap.computeIfAbsent(node.getId(), key -> new Address()));
|
||||
if (shippingAddressMap.containsKey(node.getId())) {
|
||||
modelMapper.map(node.getData(), shippingAddressMap.get(node.getId()));
|
||||
}
|
||||
if (billingAddressMap.containsKey(node.getId())) {
|
||||
modelMapper.map(node.getData(), billingAddressMap.get(node.getId()));
|
||||
}
|
||||
break;
|
||||
case "BankEvent":
|
||||
modelMapper.map(node.getData(), bankEventMap.computeIfAbsent(node.getId(), key -> new BankEvent()));
|
||||
|
@ -159,7 +166,7 @@ public class DFPModelMapperConfig {
|
|||
});
|
||||
|
||||
purchase.setPreviousPurchaseList(new ArrayList<>(previousPurchaseMap.values()));
|
||||
purchase.setAddressList(new ArrayList<>(addressMap.values()));
|
||||
purchase.setAddressList(new ArrayList<>(CollectionUtils.union(shippingAddressMap.values(), billingAddressMap.values())));
|
||||
purchase.setBankEventsList(new ArrayList<>(bankEventMap.values()));
|
||||
purchase.setDeviceContext(deviceContext);
|
||||
purchase.setPaymentInstrumentList(new ArrayList<>(paymentInstrumentMap.values()));
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package com.griddynamics.msd365fp.manualreview.queues.model.persistence;
|
||||
|
||||
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document;
|
||||
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey;
|
||||
import lombok.*;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.Version;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static com.griddynamics.msd365fp.manualreview.queues.config.Constants.SETTINGS_CONTAINER_NAME;
|
||||
import static com.griddynamics.msd365fp.manualreview.queues.config.Constants.TASK_CONTAINER_NAME;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
@EqualsAndHashCode(exclude = "_etag")
|
||||
@Document(collection = SETTINGS_CONTAINER_NAME)
|
||||
public class ConfigurableAppSettings {
|
||||
@Id
|
||||
@PartitionKey
|
||||
private String id;
|
||||
private String type;
|
||||
private boolean active;
|
||||
private Map<String, Object> values;
|
||||
|
||||
@Version
|
||||
@SuppressWarnings("java:S116")
|
||||
String _etag;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.griddynamics.msd365fp.manualreview.queues.repository;
|
||||
|
||||
import com.griddynamics.msd365fp.manualreview.queues.model.persistence.ConfigurableAppSettings;
|
||||
import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ConfigurableAppSettingsRepository extends CosmosRepository<ConfigurableAppSettings, String> {
|
||||
|
||||
List<ConfigurableAppSettings> findAllByTypeAndActiveTrue(String type);
|
||||
|
||||
}
|
|
@ -65,22 +65,26 @@ public class ItemService {
|
|||
|
||||
private void saveEmptyItem(PurchaseEvent event) {
|
||||
String id = event.getEventId();
|
||||
log.info("Event with purchase ID [{}] has been received from the DFP by rule [{}].", id, event.getRuleName());
|
||||
log.info("Event [{}] has been received from the DFP rule [{}].", id, event.getRuleName());
|
||||
|
||||
// Create and save the item
|
||||
Item item = Item.builder()
|
||||
.id(id)
|
||||
.active(false)
|
||||
.imported(OffsetDateTime.now())
|
||||
._etag(id)
|
||||
.build();
|
||||
try {
|
||||
itemRepository.save(item);
|
||||
log.info("Item [{}] has been saved to the storage.", id);
|
||||
} catch (CosmosDBAccessException e){
|
||||
log.info("Item [{}] has not been saved to the storage because it's already exist.", id);
|
||||
} catch (Exception e){
|
||||
log.warn("Item [{}] has not been saved to the storage: {}", id, e.getMessage());
|
||||
if ("purchase".equalsIgnoreCase(event.getEventType())) {
|
||||
// Create and save the item
|
||||
Item item = Item.builder()
|
||||
.id(id)
|
||||
.active(false)
|
||||
.imported(OffsetDateTime.now())
|
||||
._etag(id)
|
||||
.build();
|
||||
try {
|
||||
itemRepository.save(item);
|
||||
log.info("Item [{}] has been saved to the storage.", id);
|
||||
} catch (CosmosDBAccessException e) {
|
||||
log.info("Item [{}] has not been saved to the storage because it's already exist.", id);
|
||||
} catch (Exception e) {
|
||||
log.warn("Item [{}] has not been saved to the storage: {}", id, e.getMessage());
|
||||
}
|
||||
} else {
|
||||
log.info("The event type of [{}] is [{}]. The event has been ignored.", id, event.getEventType());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,11 +264,11 @@ public class ItemService {
|
|||
unassigned = true;
|
||||
}
|
||||
boolean unlocked = false;
|
||||
if (item.getLock() != null && queue.getId().equals(item.getLock().getQueueId())){
|
||||
if (item.getLock() != null && queue.getId().equals(item.getLock().getQueueId())) {
|
||||
item.unlock();
|
||||
unlocked = true;
|
||||
}
|
||||
if (item.getHold() != null && queue.getId().equals(item.getHold().getQueueId())){
|
||||
if (item.getHold() != null && queue.getId().equals(item.getHold().getQueueId())) {
|
||||
item.setHold(null);
|
||||
item.setLabel(new ItemLabel());
|
||||
item.getNotes().add(ItemNote.builder()
|
||||
|
@ -273,7 +277,7 @@ public class ItemService {
|
|||
.userId(oldItem.getHold().getOwnerId())
|
||||
.build());
|
||||
}
|
||||
if (item.getEscalation() != null && queue.getId().equals(item.getEscalation().getQueueId())){
|
||||
if (item.getEscalation() != null && queue.getId().equals(item.getEscalation().getQueueId())) {
|
||||
item.setEscalation(null);
|
||||
item.setLabel(new ItemLabel());
|
||||
item.getNotes().add(ItemNote.builder()
|
||||
|
|
|
@ -50,7 +50,6 @@ public class PublicItemService {
|
|||
@Setter(onMethod = @__({@Value("${azure.cosmosdb.default-ttl}")}))
|
||||
private Duration defaultTtl;
|
||||
|
||||
|
||||
public ItemDTO getItem(@NonNull final String itemId, @Nullable final String queueId) throws NotFoundException {
|
||||
QueueView queueView = null;
|
||||
if (queueId != null) {
|
||||
|
|
|
@ -97,7 +97,7 @@ public class PublicQueueService {
|
|||
streamService.sendQueueUpdateEvent(queue);
|
||||
|
||||
// trigger the task for assignment recalculation
|
||||
taskService.forceTaskRunByName(ITEM_STATE_TASK_NAME);
|
||||
taskService.forceTaskRunByName(ITEM_ASSIGNMENT_TASK_NAME);
|
||||
|
||||
return QueueViewUtility.getAllQueueViews(queue, true)
|
||||
.map(qv -> modelMapper.map(qv, QueueViewDTO.class))
|
||||
|
@ -148,7 +148,7 @@ public class PublicQueueService {
|
|||
streamService.sendQueueUpdateEvent(queue);
|
||||
|
||||
// trigger the task for assignment recalculation
|
||||
taskService.forceTaskRunByName(ITEM_STATE_TASK_NAME);
|
||||
taskService.forceTaskRunByName(ITEM_ASSIGNMENT_TASK_NAME);
|
||||
|
||||
return QueueViewUtility.getAllQueueViews(queue, true)
|
||||
.map(qv -> modelMapper.map(qv, QueueViewDTO.class))
|
||||
|
|
|
@ -66,6 +66,7 @@ public class QueueService {
|
|||
.build();
|
||||
QueueViewUtility.addViewToQueue(newResidualQueue, QueueViewType.REGULAR);
|
||||
queueRepository.save(newResidualQueue);
|
||||
streamService.sendQueueUpdateEvent(newResidualQueue);
|
||||
log.info("New [{}] with ID [{}] has been created.", RESIDUAL_QUEUE_NAME, newResidualQueue.getId());
|
||||
} else if (!managers.isEmpty()) {
|
||||
for (Queue queue : residualQueues) {
|
||||
|
@ -79,6 +80,7 @@ public class QueueService {
|
|||
Objects.requireNonNullElse(queue.getSupervisors(), Collections.emptySet()));
|
||||
queue.setSupervisors(updatedSupervisors);
|
||||
queueRepository.save(queue);
|
||||
streamService.sendQueueUpdateEvent(queue);
|
||||
log.info("Managers [{}] have been assigned as supervisors to [{}] with ID [{}].",
|
||||
unassignedManagers, RESIDUAL_QUEUE_NAME, queue.getId());
|
||||
}
|
||||
|
@ -122,11 +124,13 @@ public class QueueService {
|
|||
}
|
||||
if (CollectionUtils.isEmpty(queue.getSupervisors())) {
|
||||
queue.setSupervisors(managers);
|
||||
queue.getReviewers().removeAll(managers);
|
||||
needToUpdate = true;
|
||||
}
|
||||
if (needToUpdate) {
|
||||
log.info("The queue [{}] were updated in order to reconcile assignments.", queue.getId());
|
||||
queueRepository.save(queue);
|
||||
streamService.sendQueueUpdateEvent(queue);
|
||||
}
|
||||
return needToUpdate;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package com.griddynamics.msd365fp.manualreview.queues.service;
|
|||
import com.griddynamics.msd365fp.manualreview.model.exception.NotFoundException;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.model.dto.SettingsConfigurationDTO;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.model.dto.SettingsDTO;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.model.persistence.Settings;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.repository.SettingsRepository;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.model.persistence.ConfigurableAppSettings;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.repository.ConfigurableAppSettingsRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.modelmapper.ModelMapper;
|
||||
|
@ -19,7 +19,7 @@ import java.util.stream.Collectors;
|
|||
@RequiredArgsConstructor
|
||||
public class SettingsService {
|
||||
|
||||
private final SettingsRepository settingsRepository;
|
||||
private final ConfigurableAppSettingsRepository settingsRepository;
|
||||
private final ModelMapper modelMapper;
|
||||
|
||||
public Collection<SettingsDTO> getSettings(final String type) {
|
||||
|
@ -30,7 +30,7 @@ public class SettingsService {
|
|||
|
||||
public void createSettings(final SettingsConfigurationDTO settings) {
|
||||
log.info("Trying to create new settings: [{}]", settings);
|
||||
Settings entity = modelMapper.map(settings, Settings.class);
|
||||
ConfigurableAppSettings entity = modelMapper.map(settings, ConfigurableAppSettings.class);
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setActive(true);
|
||||
settingsRepository.save(entity);
|
||||
|
@ -39,7 +39,7 @@ public class SettingsService {
|
|||
|
||||
public void updateSettings(final String id, final SettingsConfigurationDTO settings) throws NotFoundException {
|
||||
log.info("Trying to update settings by ID [{}]: [{}]", id, settings);
|
||||
Settings entity = settingsRepository.findById(id)
|
||||
ConfigurableAppSettings entity = settingsRepository.findById(id)
|
||||
.orElseThrow(NotFoundException::new);
|
||||
modelMapper.map(settings, entity);
|
||||
settingsRepository.save(entity);
|
||||
|
@ -48,7 +48,7 @@ public class SettingsService {
|
|||
|
||||
public SettingsDTO deleteSettings(final String id) throws NotFoundException {
|
||||
log.info("Trying to delete settings by ID [{}].", id);
|
||||
Settings entity = settingsRepository.findById(id)
|
||||
ConfigurableAppSettings entity = settingsRepository.findById(id)
|
||||
.orElseThrow(NotFoundException::new);
|
||||
entity.setActive(false);
|
||||
settingsRepository.save(entity);
|
||||
|
|
|
@ -57,21 +57,21 @@ public class TaskService {
|
|||
@PostConstruct
|
||||
private void initializeTasks() {
|
||||
this.taskExecutions = Map.of(
|
||||
QUEUE_ASSIGNMENT_TASK, task ->
|
||||
QUEUE_ASSIGNMENT_TASK_NAME, task ->
|
||||
queueService.reconcileQueueAssignments(),
|
||||
ENRICHMENT_TASK_NAME, task ->
|
||||
itemService.enrichAllPoorItems(false),
|
||||
OVERALL_SIZE_TASK_NAME, task ->
|
||||
streamService.sendOverallSizeEvent(itemService.countActiveItems()),
|
||||
QUEUE_STATE_TASK_NAME, task ->
|
||||
QUEUE_SIZE_TASK_NAME, task ->
|
||||
queueService.fetchSizesForQueues(),
|
||||
RESIDUAL_QUEUE_TASK_NAME, task ->
|
||||
queueService.reviseResidualQueue(),
|
||||
ITEM_UNLOCK_TASK_NAME, task ->
|
||||
itemService.unlockItemsByTimeout(),
|
||||
DICTIONARY_VALIDATION_TASK_NAME, task ->
|
||||
DICTIONARY_TASK_NAME, task ->
|
||||
dictionaryService.updateDictionariesByStorageData(),
|
||||
ITEM_STATE_TASK_NAME, this::itemStateFetch
|
||||
ITEM_ASSIGNMENT_TASK_NAME, this::itemStateFetch
|
||||
);
|
||||
|
||||
Optional<Map.Entry<String, ApplicationProperties.TaskProperties>> incorrectTimingTask = applicationProperties.getTasks().entrySet().stream()
|
||||
|
|
|
@ -5,14 +5,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.griddynamics.msd365fp.manualreview.model.dfp.raw.ExplorerEntity;
|
||||
import com.griddynamics.msd365fp.manualreview.model.dfp.raw.Node;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.config.DFPModelMapperConfig;
|
||||
import com.griddynamics.msd365fp.manualreview.queues.model.persistence.Item;
|
||||
import lombok.Setter;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -20,17 +17,15 @@ import java.util.TimeZone;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@SpringBootTest
|
||||
public class DFPPurchaseMappingTest {
|
||||
|
||||
@Setter(onMethod = @__({@Autowired, @Qualifier("dfpModelMapper")}))
|
||||
private ModelMapper dfpModelMapper;
|
||||
class DFPPurchaseMappingTest {
|
||||
|
||||
private ModelMapper dfpModelMapper = new DFPModelMapperConfig().dfpModelMapper();
|
||||
private final ObjectMapper jsonMapper = new Jackson2ObjectMapperBuilder().build()
|
||||
.setTimeZone(TimeZone.getTimeZone("UTC"))
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"SampleGraphExplorerEntity1.json,SampleInternalItem1.json",
|
||||
"SampleGraphExplorerEntity2.json,SampleInternalItem2.json",
|
||||
|
|
|
@ -1,23 +1,5 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"nodeIdAttribute": "AddressId",
|
||||
"id": "D42CDFFCDE8E935E3ECEFCE26002A997",
|
||||
"name": "Address",
|
||||
"data": {
|
||||
"AdditionalParams": {
|
||||
"NormalizedAddress": "{\n \"Status\": \"None\",\n \"MailabilityScore\": \"0\",\n \"ResultPercentage\": \"0.00\",\n \"AddressType\": \"U\",\n \"Address\": {\n \"Street1\": null,\n \"Street2\": null,\n \"Street3\": null,\n \"State\": null,\n \"Country\": \"US\",\n \"City\": \"San Ramon\",\n \"Zipcode\": \"90345\",\n \"District\": null,\n \"Latitude\": null,\n \"Longitude\": null\n }\n}"
|
||||
},
|
||||
"AddressId": "D42CDFFCDE8E935E3ECEFCE26002A997",
|
||||
"Street1": "dfgdfgdf",
|
||||
"Street2": "string",
|
||||
"Street3": "string",
|
||||
"City": "San Ramon",
|
||||
"State": "CA",
|
||||
"ZipCode": "90345",
|
||||
"CountryRegion": "US"
|
||||
}
|
||||
},
|
||||
{
|
||||
"nodeIdAttribute": "DeviceContextId",
|
||||
"id": "72ea90b57778fef7e70de53abcae6865",
|
||||
|
@ -691,10 +673,10 @@
|
|||
"name": "PurchaseAddress",
|
||||
"data": {
|
||||
"PurchaseId": "27ffe40e-a6a2-4217-a3bb-ee9df4d43234",
|
||||
"AddressId": "D42CDFFCDE8E935E3ECEFCE26002A997",
|
||||
"AddressId": "0516c562-4b1b-4110-9c7b-763520db4858_SYSGENERATED",
|
||||
"Type": "SHIPPING",
|
||||
"FirstName": "aaa",
|
||||
"LastName": "bbb"
|
||||
"FirstName": "Aleksei",
|
||||
"LastName": "Sokolov"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -63,15 +63,15 @@
|
|||
},
|
||||
"AddressList": [
|
||||
{
|
||||
"AddressId": "9cb60ea7-fb76-4ae1-8650-660b4a3f7009_SYSGENERATED",
|
||||
"Type": "BILLING",
|
||||
"AddressId": "0516c562-4b1b-4110-9c7b-763520db4858_SYSGENERATED",
|
||||
"Type": "SHIPPING",
|
||||
"FirstName": "Aleksei",
|
||||
"LastName": "Sokolov",
|
||||
"Street1": "Azina",
|
||||
"Street2": "345678",
|
||||
"City": "Saratov",
|
||||
"ZipCode": "34567",
|
||||
"CountryRegion": "AL"
|
||||
"Street2": "5645",
|
||||
"City": "London",
|
||||
"ZipCode": "32343",
|
||||
"CountryRegion": "UK"
|
||||
},
|
||||
{
|
||||
"AddressId": "0516c562-4b1b-4110-9c7b-763520db4858_SYSGENERATED",
|
||||
|
@ -85,17 +85,15 @@
|
|||
"CountryRegion": "UK"
|
||||
},
|
||||
{
|
||||
"AddressId": "D42CDFFCDE8E935E3ECEFCE26002A997",
|
||||
"Type": "SHIPPING",
|
||||
"FirstName": "aaa",
|
||||
"LastName": "bbb",
|
||||
"Street1": "dfgdfgdf",
|
||||
"Street2": "string",
|
||||
"Street3": "string",
|
||||
"City": "San Ramon",
|
||||
"State": "CA",
|
||||
"ZipCode": "90345",
|
||||
"CountryRegion": "US"
|
||||
"AddressId": "9cb60ea7-fb76-4ae1-8650-660b4a3f7009_SYSGENERATED",
|
||||
"Type": "BILLING",
|
||||
"FirstName": "Aleksei",
|
||||
"LastName": "Sokolov",
|
||||
"Street1": "Azina",
|
||||
"Street2": "345678",
|
||||
"City": "Saratov",
|
||||
"ZipCode": "34567",
|
||||
"CountryRegion": "AL"
|
||||
}
|
||||
],
|
||||
"PaymentInstrumentList": [
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"metadata": {
|
||||
"tenantId": "74143e14-36bb-4cd2-bdb9-65d0f8b5b360",
|
||||
"timestamp": "2020-08-12T14:45:07.2207134Z"
|
||||
}
|
||||
},
|
||||
"id": "0aec2421-0111-4309-af8a-30007bfd1524"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,18 +1,3 @@
|
|||
mr:
|
||||
tasks:
|
||||
residual-queue-reconciliation-task:
|
||||
enabled: false
|
||||
queue-size-calculation-task:
|
||||
enabled: false
|
||||
overall-size-calculation-task:
|
||||
enabled: false
|
||||
item-assignment-reconciliation-task:
|
||||
enabled: false
|
||||
item-unlock-task:
|
||||
enabled: false
|
||||
dictionary-reconciliation-task:
|
||||
enabled: false
|
||||
item-enrichment-task:
|
||||
enabled: false
|
||||
queue-assignment-reconciliation-task:
|
||||
enabled: false
|
||||
spring:
|
||||
profiles:
|
||||
active: local
|
|
@ -15,6 +15,7 @@
|
|||
# misc
|
||||
.DS_Store
|
||||
.idea
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
|
|
@ -14,7 +14,8 @@ For quick project setup in development mode, you can execute the following comma
|
|||
|
||||
> NOTE:
|
||||
> In order to access local Back End runtime instead of Cloud deployed version
|
||||
> you need to modify development proxy `target` configuration property under `./src/setupProxy.js:13`
|
||||
> you need to specify API_BASE_URL environment variable for instance in .env file
|
||||
> dev URL is used by default, find details in `./src/setupProxy.js`
|
||||
|
||||
```sh
|
||||
> cd ./msd365fp-manual-review/frontend
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { DEFAULT_QUEUES_PER_PAGE } from './default';
|
||||
import { COLORS } from '../styles/variables';
|
||||
|
||||
export enum PERFORMANCE_RATING {
|
||||
ALL = 'ALL',
|
||||
|
@ -98,6 +99,7 @@ export enum DASHBOARD_SEARCH_OPTIONS {
|
|||
ANALYSTS = 'ANALYSTS',
|
||||
QUEUES = 'QUEUES'
|
||||
}
|
||||
|
||||
export const DASHBOARD_SEARCH_DISPLAY_OPTIONS = {
|
||||
[DASHBOARD_SEARCH_OPTIONS.ANALYSTS]: 'Analysts',
|
||||
[DASHBOARD_SEARCH_OPTIONS.QUEUES]: 'Queues'
|
||||
|
@ -162,3 +164,79 @@ export const OVERTURNED_ACTIONS_REPORT_KEYS = {
|
|||
[OVERTURNED_ACTIONS_CHART_KEYS.REJECTED_ACCURACY]: 'rejected accuracy',
|
||||
[OVERTURNED_ACTIONS_CHART_KEYS.ACCURACY_AVERAGE]: 'accuracy average rate',
|
||||
};
|
||||
|
||||
export const OVERTURNED_CHART_DATUM_LABELS = {
|
||||
[OVERTURNED_CHART_KEYS.APPROVED_MATCHED]: 'approveMatched',
|
||||
[OVERTURNED_CHART_KEYS.APPROVED_UNMATCHED]: 'approveUnmatched',
|
||||
[OVERTURNED_CHART_KEYS.REJECTED_MATCHED]: 'rejectMatched',
|
||||
[OVERTURNED_CHART_KEYS.REJECTED_UNMATCHED]: 'rejectUnmatched'
|
||||
};
|
||||
|
||||
export enum OVERTURNED_LABELS {
|
||||
GOOD = 'GOOD',
|
||||
BAD = 'BAD',
|
||||
OVERTURNED_GOOD = 'OVERTURNED_GOOD',
|
||||
OVERTURNED_BAD = 'OVERTURNED_BAD',
|
||||
RATE_OVERTURNED_GOOD = 'RATE_OVERTURNED_GOOD',
|
||||
RATE_OVERTURNED_BAD = 'RATE_OVERTURNED_BAD',
|
||||
RATE_AVERAGE_OVERTURNED = 'RATE_AVERAGE_OVERTURNED'
|
||||
}
|
||||
export const OVERTURNED_DISPLAY_LABELS = {
|
||||
[OVERTURNED_LABELS.GOOD]: 'Good actions applied',
|
||||
[OVERTURNED_LABELS.BAD]: 'Bad actions applied',
|
||||
[OVERTURNED_LABELS.OVERTURNED_GOOD]: 'Overturned good actions',
|
||||
[OVERTURNED_LABELS.OVERTURNED_BAD]: 'Overturned bad actions',
|
||||
[OVERTURNED_LABELS.RATE_OVERTURNED_GOOD]: 'Good actions overturned rate',
|
||||
[OVERTURNED_LABELS.RATE_OVERTURNED_BAD]: 'Bad actions overturned rate',
|
||||
[OVERTURNED_LABELS.RATE_AVERAGE_OVERTURNED]: 'Average overturned rate'
|
||||
};
|
||||
|
||||
export const OVERTURNED_ACTIONS_DISPLAY_NAMES = {
|
||||
[OVERTURNED_CHART_KEYS.APPROVED_MATCHED]: {
|
||||
label: [OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.GOOD]],
|
||||
color: COLORS.barChart.approveMatched
|
||||
},
|
||||
[OVERTURNED_CHART_KEYS.APPROVED_UNMATCHED]: {
|
||||
label: [OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.OVERTURNED_GOOD]],
|
||||
color: COLORS.barChart.approvedUnmatched
|
||||
},
|
||||
[OVERTURNED_CHART_KEYS.REJECTED_MATCHED]: {
|
||||
label: [OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.BAD]],
|
||||
color: COLORS.barChart.rejectMatched
|
||||
},
|
||||
[OVERTURNED_CHART_KEYS.REJECTED_UNMATCHED]: {
|
||||
label: [OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.OVERTURNED_BAD]],
|
||||
color: COLORS.barChart.rejectUnmatched
|
||||
}
|
||||
};
|
||||
|
||||
export const OVERTURNED_CHART_DATUM_KEYS = {
|
||||
approveUnmatched: OVERTURNED_LABELS.OVERTURNED_GOOD,
|
||||
approveMatched: OVERTURNED_LABELS.GOOD,
|
||||
rejectMatched: OVERTURNED_LABELS.BAD,
|
||||
rejectUnmatched: OVERTURNED_LABELS.OVERTURNED_BAD
|
||||
};
|
||||
|
||||
export const OVERTURNED_LABELS_TO_OVERTURNED_CHART_KEYS_COLORS = new Map<OVERTURNED_LABELS, { color: string, label: string}>([
|
||||
[
|
||||
OVERTURNED_LABELS.GOOD, {
|
||||
color: COLORS.barChart.approveMatched,
|
||||
label: 'approveMatched'
|
||||
}
|
||||
], [
|
||||
OVERTURNED_LABELS.OVERTURNED_GOOD, {
|
||||
color: COLORS.barChart.approvedUnmatched,
|
||||
label: 'approveUnmatched'
|
||||
}
|
||||
], [
|
||||
OVERTURNED_LABELS.BAD, {
|
||||
color: COLORS.barChart.rejectMatched,
|
||||
label: 'rejectMatched'
|
||||
}
|
||||
], [
|
||||
OVERTURNED_LABELS.OVERTURNED_BAD, {
|
||||
color: COLORS.barChart.rejectUnmatched,
|
||||
label: 'rejectUnmatched'
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
|
|
@ -129,14 +129,14 @@ export class Item {
|
|||
const isInProgress = !!this.lockedDate;
|
||||
const isOnHold = !!this.hold;
|
||||
|
||||
if (isOnHold) {
|
||||
return ITEM_STATUS.ON_HOLD;
|
||||
}
|
||||
|
||||
if (isInProgress) {
|
||||
return ITEM_STATUS.IN_PROGRESS;
|
||||
}
|
||||
|
||||
if (isOnHold) {
|
||||
return ITEM_STATUS.ON_HOLD;
|
||||
}
|
||||
|
||||
return ITEM_STATUS.AWAITING;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* Report - represents CSV report model
|
||||
*/
|
||||
export interface Report {
|
||||
/**
|
||||
* name - report name
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@import "../../../styles/variables";
|
||||
|
||||
.accuracy-data-table {
|
||||
max-width: 1328px;
|
||||
position: relative;
|
||||
|
||||
.ms-DetailsHeader {
|
||||
|
@ -13,6 +14,7 @@
|
|||
.ms-DetailsHeader-cell:not(:first-child) {
|
||||
.ms-DetailsHeader-cellTitle {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
justify-content: flex-end;
|
||||
white-space: pre-line;
|
||||
text-align: end;
|
||||
|
@ -29,7 +31,7 @@
|
|||
&::before {
|
||||
top: 0;
|
||||
width: 12px;
|
||||
left: 5px;
|
||||
left: -14px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
content: '';
|
||||
|
|
|
@ -5,14 +5,16 @@ import cx from 'classnames';
|
|||
|
||||
import { Text } from '@fluentui/react/lib/Text';
|
||||
import { Checkbox } from '@fluentui/react/lib/Checkbox';
|
||||
import {
|
||||
DetailsListLayoutMode, IColumn, SelectionMode
|
||||
} from '@fluentui/react/lib/DetailsList';
|
||||
import { Persona, PersonaSize, IPersonaProps } from '@fluentui/react/lib/Persona';
|
||||
import { DetailsListLayoutMode, IColumn, SelectionMode } from '@fluentui/react/lib/DetailsList';
|
||||
import { IPersonaProps, Persona, PersonaSize } from '@fluentui/react/lib/Persona';
|
||||
import { ShimmeredDetailsList } from '@fluentui/react/lib/ShimmeredDetailsList';
|
||||
|
||||
import { AnalystPerformance, BasicEntityPerformance } from '../../../models/dashboard';
|
||||
import { DEFAULT_DATA_LIST_SHIMMER_LINES_NUMBER } from '../../../constants';
|
||||
import {
|
||||
DEFAULT_DATA_LIST_SHIMMER_LINES_NUMBER,
|
||||
OVERTURNED_DISPLAY_LABELS,
|
||||
OVERTURNED_LABELS
|
||||
} from '../../../constants';
|
||||
|
||||
import './accuracy-data-table.scss';
|
||||
import { User } from '../../../models/user';
|
||||
|
@ -37,14 +39,13 @@ export class AccuracyDataTable<T extends BasicEntityPerformance> extends Compone
|
|||
// eslint-disable-next-line react/destructuring-assignment
|
||||
name: this.props.isAnalystTable ? 'Analyst' : 'Queue',
|
||||
minWidth: 50,
|
||||
maxWidth: 470,
|
||||
onRender: this.renderFirstColumn,
|
||||
|
||||
},
|
||||
{
|
||||
key: 'approve-applied',
|
||||
name: 'Approve applied',
|
||||
minWidth: 50,
|
||||
name: OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.GOOD],
|
||||
minWidth: 90,
|
||||
maxWidth: 90,
|
||||
className: `${CN}__right-aligned-cell`,
|
||||
onRender: ({ approvedApplied }) => (
|
||||
|
@ -57,9 +58,9 @@ export class AccuracyDataTable<T extends BasicEntityPerformance> extends Compone
|
|||
},
|
||||
{
|
||||
key: 'approved-overturned',
|
||||
name: 'Approved overturned',
|
||||
minWidth: 50,
|
||||
maxWidth: 120,
|
||||
name: OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.OVERTURNED_GOOD],
|
||||
minWidth: 90,
|
||||
maxWidth: 90,
|
||||
className: `${CN}__right-aligned-cell`,
|
||||
onRender: ({ approvedOverturned }) => (
|
||||
<div className={`${CN}__content-row`}>
|
||||
|
@ -71,9 +72,9 @@ export class AccuracyDataTable<T extends BasicEntityPerformance> extends Compone
|
|||
},
|
||||
{
|
||||
key: 'approve-accuracy',
|
||||
name: 'Approve accuracy',
|
||||
minWidth: 50,
|
||||
maxWidth: 90,
|
||||
name: OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.RATE_OVERTURNED_GOOD],
|
||||
minWidth: 110,
|
||||
maxWidth: 110,
|
||||
className: `${CN}__right-aligned-cell`,
|
||||
onRender: ({ approvedAccuracy }) => (
|
||||
<div className={`${CN}__content-row`}>
|
||||
|
@ -92,9 +93,9 @@ export class AccuracyDataTable<T extends BasicEntityPerformance> extends Compone
|
|||
},
|
||||
{
|
||||
key: 'rejected-applied',
|
||||
name: 'Rejected applied',
|
||||
minWidth: 50,
|
||||
maxWidth: 90,
|
||||
name: OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.BAD],
|
||||
minWidth: 80,
|
||||
maxWidth: 80,
|
||||
className: `${CN}__right-aligned-cell`,
|
||||
onRender: ({ rejectedApplied }) => (
|
||||
<div className={`${CN}__content-row`}>
|
||||
|
@ -105,9 +106,9 @@ export class AccuracyDataTable<T extends BasicEntityPerformance> extends Compone
|
|||
),
|
||||
}, {
|
||||
key: 'rejected-overturned',
|
||||
name: 'Rejected overturned',
|
||||
minWidth: 50,
|
||||
maxWidth: 120,
|
||||
name: OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.OVERTURNED_BAD],
|
||||
minWidth: 80,
|
||||
maxWidth: 80,
|
||||
className: `${CN}__right-aligned-cell`,
|
||||
onRender: ({ rejectedOverturned }) => (
|
||||
<div className={`${CN}__content-row`}>
|
||||
|
@ -118,9 +119,9 @@ export class AccuracyDataTable<T extends BasicEntityPerformance> extends Compone
|
|||
),
|
||||
}, {
|
||||
key: 'rejected-accuracy',
|
||||
name: 'Rejected accuracy',
|
||||
minWidth: 50,
|
||||
maxWidth: 90,
|
||||
name: OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.RATE_OVERTURNED_BAD],
|
||||
minWidth: 110,
|
||||
maxWidth: 110,
|
||||
className: `${CN}__right-aligned-cell`,
|
||||
onRender: ({ rejectedAccuracy }) => (
|
||||
<div className={`${CN}__content-row`}>
|
||||
|
@ -138,9 +139,9 @@ export class AccuracyDataTable<T extends BasicEntityPerformance> extends Compone
|
|||
),
|
||||
}, {
|
||||
key: 'accuracy-average-rate',
|
||||
name: 'Accuracy average rate',
|
||||
minWidth: 100,
|
||||
maxWidth: 100,
|
||||
name: OVERTURNED_DISPLAY_LABELS[OVERTURNED_LABELS.RATE_AVERAGE_OVERTURNED],
|
||||
minWidth: 110,
|
||||
maxWidth: 110,
|
||||
className: `${CN}__right-aligned-cell ${CN}__accuracy-sorting-arrow`,
|
||||
onRender: ({ accuracyAverage }) => (
|
||||
<div className={`${CN}__content-row`}>
|
||||
|
|
|
@ -5,61 +5,52 @@
|
|||
position: relative;
|
||||
|
||||
&__approve-label {
|
||||
font-size: 12px;
|
||||
color: $approveMatchedColor;
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
color: $approveMatchedColor;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
filter: brightness(0.9);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
&__reject-label {
|
||||
font-size: 12px;
|
||||
color: $rejectMatchedColor;
|
||||
position: absolute;
|
||||
bottom: 30%;
|
||||
color: $rejectMatchedColor;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
transform: rotate(-90deg);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
&__top-legend {
|
||||
$width: 425px;
|
||||
$width: 600px;
|
||||
|
||||
top: -4%;
|
||||
left: calc(50% - #{$width / 2});
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-row-gap: 10px;
|
||||
position: absolute;
|
||||
|
||||
width: $width;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__overturned-approve-indicator {
|
||||
background: $approvedUnmatchedColor;
|
||||
&__legend-color-indicator {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&__overturned-approve-text {
|
||||
&__legend-label-text {
|
||||
font-size: 12px;
|
||||
color: $approveMatchedColor;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__overturned-reject-indicator {
|
||||
background: $rejectUnmatchedColor;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&__overturned-reject-text {
|
||||
font-size: 12px;
|
||||
color: $rejectMatchedColor;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
@ -83,4 +74,36 @@
|
|||
background: black;
|
||||
width: 93%;
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__tooltip-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__tooltip-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__tooltip-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
}
|
||||
|
||||
&__tooltip-value {
|
||||
margin-left: auto;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
&__color-indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,76 @@
|
|||
import React from 'react';
|
||||
|
||||
import cx from 'classnames';
|
||||
import { ResponsiveBar, BarSvgProps } from '@nivo/bar';
|
||||
import { COLORS } from '../../../styles/variables';
|
||||
import autobind from 'autobind-decorator';
|
||||
|
||||
import { BarExtendedDatum, BarSvgProps, ResponsiveBar, } from '@nivo/bar';
|
||||
|
||||
import { WarningChartMessage } from '../warning-chart-message';
|
||||
import { AccuracyChartDatum } from '../../../view-services/dashboard/base-overturned-performance-store';
|
||||
import { formatDateToFullMonthDayYear } from '../../../utils/date';
|
||||
import {
|
||||
OVERTURNED_ACTIONS_DISPLAY_NAMES,
|
||||
OVERTURNED_CHART_DATUM_KEYS,
|
||||
OVERTURNED_CHART_KEYS,
|
||||
OVERTURNED_LABELS_TO_OVERTURNED_CHART_KEYS_COLORS
|
||||
} from '../../../constants';
|
||||
|
||||
import './bar-chart.scss';
|
||||
|
||||
import { generateTicksValues } from './generate-ticks-values';
|
||||
|
||||
interface OverturnedChartDatumAccumulator {
|
||||
approveUnmatched: number, approveMatched: number, rejectMatched: number, rejectUnmatched: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns dynamically calculated keys and colors for the responsive bar chart
|
||||
*
|
||||
* This function is intentionally create in order to resolve the issue when some of the datum points
|
||||
* has 0, nullish values if that is that case, then some of the provided colors under color property
|
||||
* of Responsive bar whould be shifted in unpredictable orders, and it brakes representation of actual
|
||||
* data to be displayed on the dashboard, thus such solution will aim to avoid such issue
|
||||
*
|
||||
* For more details please refer:
|
||||
* @see https://github.com/plouc/nivo/issues/952
|
||||
* @see https://github.com/plouc/nivo/issues/952#issuecomment-688245940
|
||||
* @see https://github.com/plouc/nivo/issues/986
|
||||
* @see https://github.com/plouc/nivo/issues/1031
|
||||
*
|
||||
* @returns Object<{ colors, keys }> - colors - array of colors representing in a string, keys - array of keys
|
||||
*/
|
||||
function getBarChartKeysAndColorsValues(data: AccuracyChartDatum[]) {
|
||||
const keys: string[] = [];
|
||||
const colors: string[] = [];
|
||||
|
||||
// order of the keys must be in the defined order for the representation
|
||||
const dataSum: OverturnedChartDatumAccumulator = data.reduce((acc, next) => ({
|
||||
approveMatched: acc.approveMatched + Math.abs(next.approveMatched),
|
||||
approveUnmatched: acc.approveUnmatched + Math.abs(next.approveUnmatched),
|
||||
rejectMatched: acc.rejectMatched + Math.abs(next.rejectMatched),
|
||||
rejectUnmatched: acc.rejectUnmatched + Math.abs(next.rejectUnmatched),
|
||||
|
||||
}), {
|
||||
approveUnmatched: 0, approveMatched: 0, rejectMatched: 0, rejectUnmatched: 0
|
||||
});
|
||||
|
||||
Object.keys(dataSum).forEach(key => {
|
||||
const value = dataSum[key as keyof OverturnedChartDatumAccumulator];
|
||||
|
||||
// filter keys if every decision exists (approveUnmatched, approveMatched, rejectMatched, rejectUnmatched)
|
||||
// total sum is greater then 0
|
||||
if (value > 0) {
|
||||
const overturnedChartDatumKey = OVERTURNED_CHART_DATUM_KEYS[key as keyof OverturnedChartDatumAccumulator];
|
||||
const { color, label } = OVERTURNED_LABELS_TO_OVERTURNED_CHART_KEYS_COLORS.get(overturnedChartDatumKey)!;
|
||||
keys.push(label);
|
||||
colors.push(color);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
keys, colors
|
||||
};
|
||||
}
|
||||
|
||||
interface BarChartProps extends BarSvgProps {
|
||||
className?: string;
|
||||
isDataLoading?: boolean;
|
||||
|
@ -30,13 +94,18 @@ interface BarChartProps extends BarSvgProps {
|
|||
const CN = 'bar-chart';
|
||||
|
||||
export class BarChart extends React.Component<BarChartProps, never> {
|
||||
getPadding() {
|
||||
const { data } = this.props;
|
||||
if (data.length < 5) {
|
||||
return 0.9;
|
||||
}
|
||||
static getLabels() {
|
||||
const approve = OVERTURNED_ACTIONS_DISPLAY_NAMES[OVERTURNED_CHART_KEYS.APPROVED_MATCHED];
|
||||
const approveOverturned = OVERTURNED_ACTIONS_DISPLAY_NAMES[OVERTURNED_CHART_KEYS.APPROVED_UNMATCHED];
|
||||
const reject = OVERTURNED_ACTIONS_DISPLAY_NAMES[OVERTURNED_CHART_KEYS.REJECTED_MATCHED];
|
||||
const rejectOverturned = OVERTURNED_ACTIONS_DISPLAY_NAMES[OVERTURNED_CHART_KEYS.REJECTED_UNMATCHED];
|
||||
|
||||
return 0.65;
|
||||
return {
|
||||
approve,
|
||||
approveOverturned,
|
||||
reject,
|
||||
rejectOverturned
|
||||
};
|
||||
}
|
||||
|
||||
renderNoDataWarningMessage() {
|
||||
|
@ -73,14 +142,130 @@ export class BarChart extends React.Component<BarChartProps, never> {
|
|||
return null;
|
||||
}
|
||||
|
||||
renderTooltipColorIndicator(color: string) {
|
||||
return (
|
||||
<div
|
||||
className={`${CN}__color-indicator`}
|
||||
style={{
|
||||
background: color
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderTopLegends() {
|
||||
const {
|
||||
approve,
|
||||
approveOverturned,
|
||||
reject,
|
||||
rejectOverturned
|
||||
} = BarChart.getLabels();
|
||||
|
||||
return (
|
||||
<div className={`${CN}__top-legend`}>
|
||||
<div>
|
||||
<div style={{ background: approve.color }} className={`${CN}__legend-color-indicator`} />
|
||||
<div
|
||||
style={{ color: approve.color, filter: 'brightness(0.9)' }}
|
||||
className={`${CN}__legend-label-text`}
|
||||
>
|
||||
{approve.label}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ background: reject.color }} className={`${CN}__legend-color-indicator`} />
|
||||
<div
|
||||
style={{ color: reject.color, filter: 'brightness(0.9)' }}
|
||||
className={`${CN}__legend-label-text`}
|
||||
>
|
||||
{reject.label}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ background: approveOverturned.color }} className={`${CN}__legend-color-indicator`} />
|
||||
<div
|
||||
style={{ color: approve.color }}
|
||||
className={`${CN}__legend-label-text`}
|
||||
>
|
||||
{approveOverturned.label}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ background: rejectOverturned.color }} className={`${CN}__legend-color-indicator`} />
|
||||
<div
|
||||
style={{ color: reject.color }}
|
||||
className={`${CN}__legend-label-text`}
|
||||
>
|
||||
{rejectOverturned.label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderTooltip(extendedDatum: BarExtendedDatum) {
|
||||
const formatValue = (value: number) => `${Math.abs(value)}%`;
|
||||
const {
|
||||
id,
|
||||
data: {
|
||||
originalDate,
|
||||
approveUnmatched,
|
||||
approveMatched,
|
||||
rejectMatched,
|
||||
rejectUnmatched
|
||||
}
|
||||
} = extendedDatum as unknown as { data: AccuracyChartDatum } & BarExtendedDatum;
|
||||
const {
|
||||
approve,
|
||||
approveOverturned,
|
||||
reject,
|
||||
rejectOverturned
|
||||
} = BarChart.getLabels();
|
||||
|
||||
return (
|
||||
<div className={`${CN}__tooltip`} key={id}>
|
||||
<div className={`${CN}__tooltip-header`}>
|
||||
<strong>{formatDateToFullMonthDayYear(new Date(originalDate))}</strong>
|
||||
</div>
|
||||
<div className={`${CN}__tooltip-content`}>
|
||||
<div className={`${CN}__tooltip-row`}>
|
||||
{this.renderTooltipColorIndicator(approveOverturned.color)}
|
||||
<div>{approveOverturned.label}</div>
|
||||
<div className={`${CN}__tooltip-value`}>{formatValue(approveUnmatched)}</div>
|
||||
</div>
|
||||
<div className={`${CN}__tooltip-row`}>
|
||||
{this.renderTooltipColorIndicator(approve.color)}
|
||||
<div>{approve.label}</div>
|
||||
<div className={`${CN}__tooltip-value`}>{formatValue(approveMatched)}</div>
|
||||
</div>
|
||||
<div className={`${CN}__tooltip-row`}>
|
||||
{this.renderTooltipColorIndicator(reject.color)}
|
||||
<div>{reject.label}</div>
|
||||
<div className={`${CN}__tooltip-value`}>{formatValue(rejectMatched)}</div>
|
||||
</div>
|
||||
<div className={`${CN}__tooltip-row`}>
|
||||
{this.renderTooltipColorIndicator(rejectOverturned.color)}
|
||||
<div>{rejectOverturned.label}</div>
|
||||
<div className={`${CN}__tooltip-value`}>{formatValue(rejectUnmatched)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const { className, data } = this.props;
|
||||
const { keys, colors } = getBarChartKeysAndColorsValues(data as AccuracyChartDatum[]);
|
||||
|
||||
return (
|
||||
<div className={cx(CN, className)}>
|
||||
<ResponsiveBar
|
||||
tooltip={this.renderTooltip}
|
||||
indexBy="key"
|
||||
padding={this.getPadding()}
|
||||
padding={0.2}
|
||||
labelSkipWidth={32}
|
||||
labelSkipHeight={14}
|
||||
enableGridX
|
||||
enableGridY
|
||||
margin={{
|
||||
|
@ -90,34 +275,34 @@ export class BarChart extends React.Component<BarChartProps, never> {
|
|||
tickSize: 5, tickPadding: 5, tickRotation: 0, legend: '', legendOffset: 0, format: v => `${v}%`
|
||||
}}
|
||||
axisLeft={null}
|
||||
axisBottom={{
|
||||
tickValues: generateTicksValues(data as AccuracyChartDatum[])
|
||||
}}
|
||||
minValue={-100}
|
||||
maxValue={100}
|
||||
keys={['approveMatched', 'approveUnmatched', 'rejectMatched', 'rejectUnmatched']}
|
||||
colors={[
|
||||
COLORS.barChart.approveMatched,
|
||||
COLORS.barChart.approvedUnmatched,
|
||||
COLORS.barChart.rejectMatched,
|
||||
COLORS.barChart.rejectUnmatched
|
||||
]}
|
||||
keys={keys}
|
||||
colors={colors}
|
||||
labelFormat={v => `${v}%`}
|
||||
label={d => `${Math.abs(+d.value)}`}
|
||||
animate
|
||||
isInteractive={false}
|
||||
isInteractive
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...this.props}
|
||||
theme={{
|
||||
tooltip: {
|
||||
container: {
|
||||
minWidth: 280,
|
||||
padding: 16,
|
||||
background: 'white',
|
||||
boxShadow: '0 6.4px 14.4px rgba(0, 0, 0, 0.13),0 1.2px 3.6px rgba(0, 0, 0, 0.1)',
|
||||
borderRadius: 2
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={`${CN}__approve-label`}>Approve</div>
|
||||
<div className={`${CN}__reject-label`}>Reject</div>
|
||||
<div className={`${CN}__top-legend`}>
|
||||
<div>
|
||||
<div className={`${CN}__overturned-approve-indicator`} />
|
||||
<span className={`${CN}__overturned-approve-text`}>Overturned action (Approve)</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className={`${CN}__overturned-reject-indicator`} />
|
||||
<span className={`${CN}__overturned-reject-text`}>Overturned action (Reject)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${CN}__approve-label`}>GOOD</div>
|
||||
<div className={`${CN}__reject-label`}>BAD</div>
|
||||
{this.renderTopLegends()}
|
||||
{this.renderNoDataWarningMessage()}
|
||||
{this.renderNoSelectedItemsWarningMessage()}
|
||||
<div className={`${CN}__baseline`} />
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { AccuracyChartDatum } from '../../../view-services/dashboard/base-overturned-performance-store';
|
||||
import { isoStringToLocalMothDayFormat } from '../../../utils/date';
|
||||
|
||||
/**
|
||||
* Returns calculated number of tick values for the chart
|
||||
* depending on ratio between data length and maximum ticks count
|
||||
*
|
||||
* @param data
|
||||
* @param maxTicksCount - maximum tick values count
|
||||
*/
|
||||
export function generateTicksValues(data: AccuracyChartDatum[], maxTicksCount = 30) {
|
||||
const dataLength = data.length;
|
||||
|
||||
if (!dataLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const divisor = Math.ceil(dataLength / maxTicksCount);
|
||||
|
||||
return data.reduce((accum, next, currentIndex) => {
|
||||
if (currentIndex % divisor === 0) {
|
||||
return [...accum, isoStringToLocalMothDayFormat(next.originalDate)];
|
||||
}
|
||||
|
||||
return [...accum];
|
||||
}, [] as Array<string>);
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
@import "../../../styles/variables";
|
||||
|
||||
.switch-header {
|
||||
max-width: 1328px;
|
||||
|
||||
&__sub-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Persona, PersonaSize } from '@fluentui/react/lib/Persona';
|
|||
|
||||
import { Note } from '../../../../models';
|
||||
import { QueuesScreenStore } from '../../../../view-services';
|
||||
import { encodeStringForCSS } from '../../../../utils';
|
||||
import { TYPES } from '../../../../types';
|
||||
|
||||
import './queue-item-notes.scss';
|
||||
|
@ -47,8 +48,8 @@ export class QueueItemNote extends Component<QueueItemNoteProps, never> {
|
|||
render() {
|
||||
const { className, notes, itemId } = this.props;
|
||||
const { displayedNotesItemId } = this.queuesScreenStore;
|
||||
const iconClass = `${CN}-${itemId}`;
|
||||
const isCalloutVisible = displayedNotesItemId === itemId;
|
||||
const iconClass = `${CN}-${encodeStringForCSS(itemId)}`;
|
||||
const isCalloutVisible = displayedNotesItemId === itemId && !!notes?.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { resolve } from 'inversify-react';
|
|||
import { IconButton } from '@fluentui/react/lib/Button';
|
||||
import { Callout } from '@fluentui/react/lib/Callout';
|
||||
import { QueuesScreenStore } from '../../../../view-services';
|
||||
import { encodeStringForCSS } from '../../../../utils';
|
||||
import { TYPES } from '../../../../types';
|
||||
|
||||
import './queue-item-tags.scss';
|
||||
|
@ -49,8 +50,8 @@ export class QueueItemTags extends Component<QueueItemTagProps, never> {
|
|||
itemId
|
||||
} = this.props;
|
||||
const { displayTagsItemId } = this.queuesScreenStore;
|
||||
const iconClass = `${CN}-${itemId}`;
|
||||
const isCalloutVisible = displayTagsItemId === itemId;
|
||||
const iconClass = `${CN}-${encodeStringForCSS(itemId)}`;
|
||||
const isCalloutVisible = displayTagsItemId === itemId && !!tags?.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -15,14 +15,14 @@ export class PaymentInformation extends BaseTileRenderer<PaymentInformationProps
|
|||
renderPaymentInfo() {
|
||||
const { item } = this.props;
|
||||
|
||||
const paymentInstrument = item.purchase.paymentInstrumentList[0];
|
||||
const paymentInstrument = item.purchase?.paymentInstrumentList[0];
|
||||
|
||||
const renderingConfig: KeyValueItem[] = [
|
||||
{ key: 'Payment instrument Id', value: paymentInstrument.paymentInstrumentId },
|
||||
{ key: 'Payment method', value: paymentInstrument.type },
|
||||
{ key: 'Card type', value: paymentInstrument.cardType },
|
||||
{ key: 'Holder name', value: paymentInstrument.holderName },
|
||||
{ key: 'BIN', value: paymentInstrument.BIN }
|
||||
{ key: 'Payment instrument Id', value: paymentInstrument?.paymentInstrumentId },
|
||||
{ key: 'Payment method', value: paymentInstrument?.type },
|
||||
{ key: 'Card type', value: paymentInstrument?.cardType },
|
||||
{ key: 'Holder name', value: paymentInstrument?.holderName },
|
||||
{ key: 'BIN', value: paymentInstrument?.BIN }
|
||||
|
||||
];
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import OrderLockedIllustrationSvg from '../../assets/order-locked.svg';
|
|||
import { LABEL, ROUTES } from '../../constants';
|
||||
import { ApiServiceError } from '../../data-services/base-api-service';
|
||||
import { MrUserError } from '../../models/exceptions';
|
||||
import { Item } from '../../models';
|
||||
import { TYPES } from '../../types';
|
||||
import { CurrentUserStore, LockedItemsStore } from '../../view-services';
|
||||
import { ReviewConsoleScreenStore } from '../../view-services/review-console';
|
||||
|
@ -101,6 +102,10 @@ export class ReviewConsole extends Component<ReviewConsoleProps, ReviewConsoleSt
|
|||
}
|
||||
}
|
||||
|
||||
isItemHeldByCurrentUser(reviewItem: Item | null): boolean {
|
||||
return reviewItem?.hold?.ownerId === this.userStore?.user?.id;
|
||||
}
|
||||
|
||||
@autoBind
|
||||
handleGoToQueuesClick() {
|
||||
// if there are no more items in a queue we should proceed to the page with tiles - not the one with queue data
|
||||
|
@ -169,7 +174,14 @@ export class ReviewConsole extends Component<ReviewConsoleProps, ReviewConsoleSt
|
|||
}
|
||||
|
||||
if (queue && reviewItem) {
|
||||
this.reviewConsoleScreenStore.startReview(queue, reviewItem);
|
||||
// The user should NOT start reviewing the selected item, but the top one instead,
|
||||
// if the queue is locked and the selected item isn't held be the current user.
|
||||
const shouldTopItemBeSelectedForReview = queue.sortingLocked && !this.isItemHeldByCurrentUser(reviewItem);
|
||||
|
||||
this.reviewConsoleScreenStore.startReview(
|
||||
queue,
|
||||
shouldTopItemBeSelectedForReview ? null : reviewItem
|
||||
);
|
||||
}
|
||||
|
||||
if (queueId && itemId) {
|
||||
|
@ -261,6 +273,7 @@ export class ReviewConsole extends Component<ReviewConsoleProps, ReviewConsoleSt
|
|||
const itemReviewPermission = this.reviewPermissionStore.itemReviewPermissions(reviewItem);
|
||||
const isReviewAllowed = (permission ? permission.isAllowed : true) && itemReviewPermission.isAllowed;
|
||||
let reasonToPreventReview: JSX.Element | string = permission?.reason || '';
|
||||
|
||||
if (reasonToPreventReview === QUEUE_REVIEW_PROHIBITION_REASONS.CANNOT_LOCK_TWO_ITEMS_ON_QUEUE) {
|
||||
const lockedItemOnQueue = this.lockedItemsStore.lockedItems!.find(item => item.lockedOnQueueId === queue?.queueId);
|
||||
const goToLockedItem = () => this.handleGoToLockedItemClick(lockedItemOnQueue!.lockedOnQueueId as string, lockedItemOnQueue!.id);
|
||||
|
@ -305,7 +318,9 @@ export class ReviewConsole extends Component<ReviewConsoleProps, ReviewConsoleSt
|
|||
return (
|
||||
<StartReviewPanel
|
||||
onStartReviewCallback={this.startReviewProcess}
|
||||
isQueueSortingLocked={queue.sortingLocked}
|
||||
// Items held by the current user should be available for review
|
||||
// See MDMR-475 for more details
|
||||
isItemReviewLocked={queue.sortingLocked && !this.isItemHeldByCurrentUser(reviewItem)}
|
||||
notes={reviewItem?.notes || []}
|
||||
isReviewAllowed={isReviewAllowed}
|
||||
reasonToPreventReview={reasonToPreventReview}
|
||||
|
|
|
@ -10,9 +10,10 @@ import { Note } from '../../../models';
|
|||
|
||||
interface StartReviewPanelProps {
|
||||
/**
|
||||
* isQueueSortingLocked - indicates whether sorting in queue is locked
|
||||
* isItemReviewLocked - indicates whether item review is prohibited,
|
||||
* for instance in locked queues.
|
||||
*/
|
||||
isQueueSortingLocked: boolean
|
||||
isItemReviewLocked: boolean
|
||||
notes: Note[];
|
||||
isReviewAllowed: boolean;
|
||||
reasonToPreventReview?: JSX.Element | string;
|
||||
|
@ -25,7 +26,7 @@ const CN = 'start-review-panel';
|
|||
export const StartReviewPanel: React.FC<StartReviewPanelProps> = (
|
||||
{
|
||||
onStartReviewCallback,
|
||||
isQueueSortingLocked,
|
||||
isItemReviewLocked,
|
||||
notes,
|
||||
isReviewAllowed,
|
||||
reasonToPreventReview
|
||||
|
@ -45,7 +46,7 @@ export const StartReviewPanel: React.FC<StartReviewPanelProps> = (
|
|||
|
||||
function renderReviewBlock() {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (isQueueSortingLocked) {
|
||||
if (isItemReviewLocked) {
|
||||
return renderMessageBar(
|
||||
<>
|
||||
It is not allowed to review transactions in random order because the queue is locked.
|
||||
|
|
|
@ -8,9 +8,10 @@ module.exports = function proxyConfiguration(app) {
|
|||
app.use(
|
||||
'/api',
|
||||
createProxyMiddleware({
|
||||
// NOTE: in order to access local BackEnd installation replace target value
|
||||
// target: 'http://localhost:8080/api',
|
||||
target: 'https://dfp-manrev-dev.azurefd.net/api',
|
||||
// NOTE: in order to access local BackEnd installation
|
||||
// specify API_BASE_URL environment variable equal 'http://localhost:8080/api',
|
||||
// for example in .env file
|
||||
target: process.env.API_BASE_URL || 'https://dfp-manrev-dev.azurefd.net/api',
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
// logLevel: 'debug',
|
||||
|
|
|
@ -58,7 +58,13 @@ export function isoStringToLocalMothDayFormat(isoDateTimeString: string) {
|
|||
|
||||
const dateObj = new Date(isoDateTimeString);
|
||||
|
||||
return `${dateObj.getMonth() + 1}/${dateObj.getDate()}`;
|
||||
let month: number | string = dateObj.getMonth() + 1;
|
||||
month = month < 10 ? `0${month}` : month;
|
||||
|
||||
let day: number| string = dateObj.getDate();
|
||||
day = day < 10 ? `0${day}` : day;
|
||||
|
||||
return `${month}/${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* This function replaces all non-alphanumeric characters in the passed string
|
||||
* with underscore plus UTF-8 encoding of the character
|
||||
* and can be used for encoding strings to be used for CSS selectors.
|
||||
* @param str: string
|
||||
* @returns: string
|
||||
*/
|
||||
export function encodeStringForCSS(str: string): string {
|
||||
return str.replace(/[^\w_-]/gi, c => `_${c.charCodeAt(0).toString(16)}`);
|
||||
}
|
|
@ -3,3 +3,4 @@ export * from '../date/duration-parcers';
|
|||
export * from './digest-message-to-hex-hash';
|
||||
export * from './placehold';
|
||||
export * from './format-to-percentage-string';
|
||||
export * from './encode-string-for-css';
|
||||
|
|
|
@ -45,11 +45,7 @@ export class BaseOverturnedPerformanceStore<T extends BasicEntityPerformance> ex
|
|||
|
||||
if (data.length) {
|
||||
return BaseOverturnedPerformanceStore
|
||||
.calculateAccuracyData(toJS(data))
|
||||
.filter(datum => {
|
||||
const { key, ...metrics } = datum;
|
||||
return Object.values(metrics).some(metric => metric > 0);
|
||||
});
|
||||
.calculateAccuracyData(toJS(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export class LockedItemsStore {
|
|||
|
||||
return this.lockedItems
|
||||
.reduce((acc: ItemLock[], item: Item): ItemLock[] => {
|
||||
const seekedQueue = allQueues?.find(queue => queue.queueId === item.lockedOnQueueId);
|
||||
const seekedQueue = allQueues?.find(queue => queue.viewId === item.lockedOnQueueViewId);
|
||||
return seekedQueue
|
||||
? [...acc, { item, queue: seekedQueue }]
|
||||
: acc;
|
||||
|
|
|
@ -101,11 +101,11 @@ export class ReviewConsoleScreenStore {
|
|||
}
|
||||
|
||||
@action
|
||||
startReview(queue: Queue, item?: Item) {
|
||||
if (queue.sortingLocked) {
|
||||
this.getReviewItem(queue.viewId);
|
||||
startReview(queue: Queue, item?: Item | null) {
|
||||
if (item) {
|
||||
this.getReviewItem(queue.viewId, item.id);
|
||||
} else {
|
||||
this.getReviewItem(queue.viewId, (item as Item).id);
|
||||
this.getReviewItem(queue.viewId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
##############################
|
||||
## Java
|
||||
##############################
|
||||
.mtj.tmp/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.nar
|
||||
hs_err_pid*
|
||||
|
||||
##############################
|
||||
## Gradle
|
||||
##############################
|
||||
bin/
|
||||
build/
|
||||
.gradle
|
||||
.gradletasknamecache
|
||||
gradle-app.setting
|
||||
!gradle-wrapper.jar
|
||||
|
||||
##############################
|
||||
## IntelliJ
|
||||
##############################
|
||||
out/
|
||||
../.idea/
|
||||
.idea_modules/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
##############################
|
||||
## OS X
|
||||
##############################
|
||||
.DS_Store
|
||||
|
||||
##############################
|
||||
## App Specific
|
||||
##############################
|
||||
results/
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# Perftests
|
||||
|
||||
The perftests module written in scala uses Gatling framework for performance testing [backend](../backend) and [frontend](../frontend) modules.
|
||||
Performance tests can be split into [simulations](https://gatling.io/docs/current/general/concepts/#simulation). Each simulation represents a
|
||||
[scenario](https://gatling.io/docs/current/general/concepts/#scenario) from the user point of view. Users, such regular analyst, senior analyst
|
||||
or admin manager, might have different simulations of the same scenario. Users in Gatling are actually
|
||||
[virtual users](https://gatling.io/docs/current/general/concepts/#virtual-user), so they don't use actual credentials.
|
||||
|
||||
In order to authenticate test users and don't create them all by hands, you can use backend's backdoor which is only active when `perftests`
|
||||
profile is active in the running application. Anyway, to run a simulation you will need to provide your own access token (called idtoken) to let
|
||||
all generated virtual users to use it when calling Azure resources.
|
||||
|
||||
### Run Performance Tests
|
||||
|
||||
1. Go to the `${projectDir}/perftests` directory.
|
||||
1. Set environment variables:
|
||||
* AUTH_TOKEN - idtoken which you should obtain from [this](#create-users-for-tests) section.
|
||||
* BASE_URL - hostname with a protocol where all the requests will go (can be either `http://localhost` or `https://${prefix}.azuredf.net`).
|
||||
1. Run `./gradlew gatlingRun` Gradle task to run all simulations which are placed in the source directory.
|
||||
|
||||
If you want to run only one simulation, check the official [documentation](https://github.com/lkishalmi/gradle-gatling-plugin#default-tasks) of
|
||||
the Gradle plugin.
|
||||
|
||||
### Debug Performance Tests
|
||||
|
||||
Create run config in Intellij IDEA of type `Application` and set the main class to `com.griddynamics.msd365fp.manualreview.GatlingRunner`. Also
|
||||
define working directory and environment variables before debugging.
|
||||
|
||||
### Prepare Cloud Environment
|
||||
|
||||
Deploy dedicated resource group for performance tests:
|
||||
|
||||
1. Check if there are any existing resource group which is acceptable for running performance tests (dfp-manrev-dev should be fine). Otherwise follow
|
||||
the instructions in the [arm deployment](../arm) README file on how to create new resource group with all required resources.
|
||||
|
||||
Add `perftest` profile to application profiles:
|
||||
|
||||
1. Open App Services | Configurations which you are about to test.
|
||||
1. Click 'edit' against `JAVA_OPTS` variable
|
||||
1. If there are no `-Dspring.profiles.active` parameter in the `JAVA_OPTS`, add `-Dspring.profiles.active=perftest` in the end of the string.
|
||||
Otherwise, just add `,perftest` in the end of it.
|
||||
1. Save changes and restart app service (you need to do these steps with each replica).
|
||||
|
||||
Instantiate virtual machine where the tests will be running:
|
||||
|
||||
1. Check if there are any existing virtual machines in the same resource group as application which you are going to test. Otherwise proceed to the next steps.
|
||||
1. Create [virtual machine](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal) in the same region if none is present.
|
||||
1. Grant yourself permissions to login to this VM and connect to it via ssh.
|
||||
1. Install all required toolset (like Git and Java).
|
||||
1. Clone repository in your home directory.
|
||||
|
||||
### Create Users for Tests
|
||||
|
||||
To create users for perfomance testing, first you will need to read this
|
||||
[section](https://docs.google.com/document/d/1we5YZDPwda8MTp-6FHqsfEwEfzBZi_Wi3AeLrfzcaug/edit#heading=h.tlh7rl7b6vua)
|
||||
of technical specification. In the [MR access](https://docs.google.com/document/d/1we5YZDPwda8MTp-6FHqsfEwEfzBZi_Wi3AeLrfzcaug/edit#heading=h.cn9kcybosyyr)
|
||||
section you will find the instruction on how to create a user in DFP. You will need three users with different roles. Each role has approptiate
|
||||
simulations to run, for example, dashboards are only accessible by senior analysts and managers, so regular analyst won't be able to test them.
|
||||
|
||||
You will need to obtain idtoken somehow to run any simulation. It is required to bypass login page where unauthenticated users are redirected to.
|
||||
The easiest way to get it, is to login in the application and search for `msal.idtoken` key in the session storage - the value assotiated with this key is what
|
||||
you need.
|
||||
|
||||
### Additional Links
|
||||
|
||||
Jira Epic: https://griddynamics.atlassian.net/browse/MDMR-454
|
||||
Gatling Documentation: https://gatling.io/docs/current/general/
|
||||
Gatling Gradle plugin: https://github.com/lkishalmi/gradle-gatling-plugin
|
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id "com.github.lkishalmi.gatling" version "3.3.4"
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
Двоичный файл не отображается.
|
@ -0,0 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
|
@ -0,0 +1,84 @@
|
|||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
|
@ -0,0 +1,16 @@
|
|||
virtualUser
|
||||
perf-user-1,
|
||||
perf-user-2,
|
||||
perf-user-3,
|
||||
perf-user-4,
|
||||
perf-user-5,
|
||||
perf-user-6,
|
||||
perf-user-7,
|
||||
perf-user-8,
|
||||
perf-user-9,
|
||||
perf-user-10,
|
||||
perf-user-11,
|
||||
perf-user-12,
|
||||
perf-user-13,
|
||||
perf-user-14,
|
||||
perf-user-15
|
|
|
@ -0,0 +1,3 @@
|
|||
Manifest-Version: 1.0
|
||||
Main-Class: com.griddynamics.msd365fp.manualreview.GatlingRunner
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.griddynamics.msd365fp.manualreview
|
||||
|
||||
import io.gatling.app.Gatling
|
||||
import io.gatling.core.config.GatlingPropertiesBuilder
|
||||
|
||||
object GatlingRunner {
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
val simClass = classOf[RegularAnalystSimulation].getName
|
||||
val props = new GatlingPropertiesBuilder
|
||||
props.simulationClass(simClass)
|
||||
Gatling.fromMap(props build)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.griddynamics.msd365fp.manualreview
|
||||
|
||||
import com.griddynamics.msd365fp.manualreview.action.{
|
||||
RegularAnalystOpenQueuesView,
|
||||
RegularAnalystReviewsOrder,
|
||||
RegularAnalystUnlockOrder
|
||||
}
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class RegularAnalystSimulation extends Simulation {
|
||||
|
||||
val baseUrl = System.getenv("BASE_URL")
|
||||
|
||||
val authToken = System.getenv("AUTH_TOKEN")
|
||||
|
||||
val virtualUserFeeder = csv("virtualusers.csv").eager.queue
|
||||
|
||||
val httpProtocol = http
|
||||
.baseUrl(baseUrl)
|
||||
.header("sec-fetch-site", "same-origin")
|
||||
.header("sec-fetch-mode", "cors")
|
||||
.header("sec-fetch-dest", "empty")
|
||||
.header("origin", baseUrl)
|
||||
.authorizationHeader("Bearer ${authToken}")
|
||||
.acceptLanguageHeader("en-US,en;q=0.9,ru;q=0.8")
|
||||
.acceptEncodingHeader("gzip, deflate, br")
|
||||
.acceptHeader("application/json, text/plain, */*")
|
||||
.inferHtmlResources()
|
||||
.silentResources
|
||||
|
||||
val scn = scenario("Regular analyst behaviour")
|
||||
.exec(_.set("authToken", authToken))
|
||||
.feed(virtualUserFeeder)
|
||||
.exec(RegularAnalystOpenQueuesView.action)
|
||||
.exitHereIfFailed
|
||||
.pause(5 seconds, 30 seconds)
|
||||
.during(30 minutes) {
|
||||
exec(RegularAnalystReviewsOrder.action)
|
||||
}
|
||||
.exec(RegularAnalystUnlockOrder.action)
|
||||
.exec(RegularAnalystOpenQueuesView.action)
|
||||
|
||||
setUp(scn.inject(
|
||||
nothingFor(1 minute),
|
||||
atOnceUsers(5),
|
||||
rampUsers(10) during (5 minutes),
|
||||
)).protocols(httpProtocol)
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package com.griddynamics.msd365fp.manualreview.action
|
||||
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
object RegularAnalystOpenQueuesView {
|
||||
|
||||
val resourceRequestHeaders = Map(
|
||||
"Accept" -> "application/json, text/plain, */*",
|
||||
"authorization" -> "Bearer ${authToken}"
|
||||
)
|
||||
|
||||
val action = exec(http("Request root page").get("/"))
|
||||
.exec(http("Request current user").get("/api/users/me"))
|
||||
.exec(http("Request all users").get("/api/users"))
|
||||
.exec(
|
||||
http("Review console links").get("/api/settings/review-console-links")
|
||||
)
|
||||
.exec(http("Request self user").get("/api/users/me"))
|
||||
.exec(
|
||||
http("Get locked items for the current user")
|
||||
.get("/api/items/locked")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
)
|
||||
.exec(
|
||||
http("Get all regular queues")
|
||||
.get("/api/queues?viewType=REGULAR")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
.check(
|
||||
jsonPath("$..reviewers")
|
||||
.ofType[Seq[Any]]
|
||||
.findAll
|
||||
.transform(_.flatten)
|
||||
.saveAs("regularReviewers")
|
||||
)
|
||||
.check(
|
||||
jsonPath("$..views[?(@.viewType == 'REGULAR')].viewId").findRandom
|
||||
.saveAs("regularQueue")
|
||||
)
|
||||
)
|
||||
.foreach(session => session("regularReviewers").as[Seq[Any]], "reviewer") {
|
||||
exec(
|
||||
http("Request photo")
|
||||
.get("/api/users/${reviewer}/photo")
|
||||
.headers(resourceRequestHeaders)
|
||||
)
|
||||
}
|
||||
.exec(
|
||||
http("Get locked items for the current user")
|
||||
.get("/api/items/locked")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
)
|
||||
.exec(
|
||||
http("Get regular queue to show as autoselected queue")
|
||||
.get("/api/queues/${regularQueue}")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
.check(
|
||||
jsonPath("$..allowedLabels")
|
||||
.ofType[Seq[Any]]
|
||||
.find
|
||||
.saveAs("allowedLabels")
|
||||
)
|
||||
)
|
||||
.exec(
|
||||
http("Get N items from the regular queue to show in autoselected queue")
|
||||
.get("/api/queues/${regularQueue}/items?size=18")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
)
|
||||
.exec(
|
||||
http("Get N items from the regular queue to show in autoselected queue")
|
||||
.get("/api/queues/${regularQueue}/items?size=18")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.griddynamics.msd365fp.manualreview.action
|
||||
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object RegularAnalystReviewsOrder {
|
||||
|
||||
val action = exec(
|
||||
http("Lock top item in a queue")
|
||||
.post("/api/queues/${regularQueue}/top/lock")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
.check(
|
||||
jsonPath("$.id")
|
||||
.find
|
||||
.saveAs("reviewOrder")
|
||||
)
|
||||
)
|
||||
.exec(
|
||||
http("Get information about the order")
|
||||
.get("/api/queues/${regularQueue}/items/${reviewOrder}")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
)
|
||||
.pause(1 minute, 4 minutes)
|
||||
.tryMax(3) {
|
||||
exec(http("Label an item after review")
|
||||
.patch("/api/items/${reviewOrder}/label")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
.body(StringBody("{\"label\": \"${allowedLabels.random()}\"}"))
|
||||
.asJson
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.griddynamics.msd365fp.manualreview.action
|
||||
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
object RegularAnalystUnlockOrder {
|
||||
|
||||
val action = exec(
|
||||
http("Unlock top item in a queue")
|
||||
.delete("/api/items/${reviewOrder}/lock")
|
||||
.header("Virtual-User", "${virtualUser}")
|
||||
)
|
||||
}
|
Загрузка…
Ссылка в новой задаче