Merge pull request #2 from microsoft/amgupt/hotfixupdate

code update from GD team to MS Github
This commit is contained in:
satyamamit 2020-09-09 17:04:21 -07:00 коммит произвёл GitHub
Родитель 5d96df867b c4c87173d3
Коммит 6dc7d49af0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
74 изменённых файлов: 3281 добавлений и 1443 удалений

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

@ -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"
}

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

@ -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
}
}
}

584
arm/templates/alerts.json Normal file
Просмотреть файл

@ -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

1
frontend/.gitignore поставляемый
Просмотреть файл

@ -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);
}
}

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

@ -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/

68
perftests/README.md Normal file
Просмотреть файл

@ -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

7
perftests/build.gradle Normal file
Просмотреть файл

@ -0,0 +1,7 @@
plugins {
id "com.github.lkishalmi.gatling" version "3.3.4"
}
repositories {
jcenter()
}

Двоичные данные
perftests/gradle/wrapper/gradle-wrapper.jar поставляемый Normal file

Двоичный файл не отображается.

5
perftests/gradle/wrapper/gradle-wrapper.properties поставляемый Normal file
Просмотреть файл

@ -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

172
perftests/gradlew поставляемый Normal file
Просмотреть файл

@ -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" "$@"

84
perftests/gradlew.bat поставляемый Normal file
Просмотреть файл

@ -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
1 virtualUser
2 perf-user-1,
3 perf-user-2,
4 perf-user-3,
5 perf-user-4,
6 perf-user-5,
7 perf-user-6,
8 perf-user-7,
9 perf-user-8,
10 perf-user-9,
11 perf-user-10,
12 perf-user-11,
13 perf-user-12,
14 perf-user-13,
15 perf-user-14,
16 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}")
)
}