From ec1a1c9f8be8b693b8a7c8f7fcacfd3f75797cc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:04:05 -0700 Subject: [PATCH 01/24] Bump google-api-python-client from 2.100.0 to 2.101.0 (#3810) Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.100.0 to 2.101.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.100.0...v2.101.0) --- updated-dependencies: - dependency-name: google-api-python-client dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index e7f2d7377c99..c35e9deba7db 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -122,7 +122,7 @@ frozenlist==1.4.0 # aiosignal google-api-core==2.11.1 # via google-api-python-client -google-api-python-client==2.100.0 +google-api-python-client==2.101.0 # via -r requirements-base.in google-auth==2.22.0 # via From 8c2220e18d9b69d1f3d01b400979ec2b47ddcaef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:04:13 -0700 Subject: [PATCH 02/24] Bump openai from 0.28.0 to 0.28.1 (#3809) Bumps [openai](https://github.com/openai/openai-python) from 0.28.0 to 0.28.1. - [Release notes](https://github.com/openai/openai-python/releases) - [Commits](https://github.com/openai/openai-python/compare/v0.28.0...v0.28.1) --- updated-dependencies: - dependency-name: openai dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index c35e9deba7db..b174b8920ee9 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -235,7 +235,7 @@ oauthlib[signedtoken]==3.2.2 # atlassian-python-api # jira # requests-oauthlib -openai==0.28.0 +openai==0.28.1 # via -r requirements-base.in packaging==23.1 # via From 2ff03055087ecacbe9282c81c14bc02451cf574a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:04:21 -0700 Subject: [PATCH 03/24] Bump ruff from 0.0.290 to 0.0.291 (#3808) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.290 to 0.0.291. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.290...v0.0.291) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d15fe4be7bbd..c31768d3beee 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -90,7 +90,7 @@ python-dateutil==2.8.2 # via faker pyyaml==6.0.1 # via pre-commit -ruff==0.0.290 +ruff==0.0.291 # via -r requirements-dev.in six==1.16.0 # via From c2ff65bab04e7ccb8abe6bf5f92a7e9837385737 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:04:29 -0700 Subject: [PATCH 04/24] Bump eslint from 8.49.0 to 8.50.0 in /src/dispatch/static/dispatch (#3807) Bumps [eslint](https://github.com/eslint/eslint) from 8.49.0 to 8.50.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.49.0...v8.50.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../static/dispatch/package-lock.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/dispatch/static/dispatch/package-lock.json b/src/dispatch/static/dispatch/package-lock.json index 92cb453af538..85d77f4e0cc1 100644 --- a/src/dispatch/static/dispatch/package-lock.json +++ b/src/dispatch/static/dispatch/package-lock.json @@ -584,9 +584,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2595,15 +2595,15 @@ } }, "node_modules/eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", + "@eslint/js": "8.50.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -7053,9 +7053,9 @@ } }, "@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true }, "@humanwhocodes/config-array": { @@ -8540,15 +8540,15 @@ } }, "eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", + "@eslint/js": "8.50.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", From dc379613217825397908991a7723100b1ebfc55c Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Wed, 27 Sep 2023 22:37:14 -0700 Subject: [PATCH 05/24] Revert "Making experience textbox from oncall end-of-shift feedback optional (#3799)" (#3814) This reverts commit db544b0251d21d5ad1f7d16f7068b5a50202ba4f. --- src/dispatch/plugins/dispatch_slack/feedback/interactive.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dispatch/plugins/dispatch_slack/feedback/interactive.py b/src/dispatch/plugins/dispatch_slack/feedback/interactive.py index 307b837fa18b..f0ba81afdb54 100644 --- a/src/dispatch/plugins/dispatch_slack/feedback/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/feedback/interactive.py @@ -275,7 +275,6 @@ def oncall_shift_feedback_input( initial_value=initial_value, multiline=True, placeholder="How would you describe your experience?", - optional=True, ), label=label, **kwargs, @@ -382,7 +381,7 @@ def handle_oncall_shift_feedback_submission_event( ack_oncall_shift_feedback_submission_event(ack=ack) - feedback = form_data.get(ServiceFeedbackNotificationBlockIds.feedback_input, "") + feedback = form_data.get(ServiceFeedbackNotificationBlockIds.feedback_input) rating = form_data.get(ServiceFeedbackNotificationBlockIds.rating_select, {}).get("value") # metadata is organization_slug|project_id|schedule_id|shift_end_at|reminder_id From 5760562a654042b230d302c9e8de144c70f4b759 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:06:02 -0700 Subject: [PATCH 06/24] bump fastapi from 0.103.1 to 0.103.2 (#3821) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index b174b8920ee9..267228cd1451 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -114,7 +114,7 @@ email-validator==2.0.0.post2 # via -r requirements-base.in emails==0.6 # via -r requirements-base.in -fastapi==0.103.1 +fastapi==0.103.2 # via -r requirements-base.in frozenlist==1.4.0 # via From 0f0970501f2e2580434c86c44a0ffb4f7c8c9513 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:06:10 -0700 Subject: [PATCH 07/24] Bump msal from 1.24.0 to 1.24.1 (#3820) Bumps [msal](https://github.com/AzureAD/microsoft-authentication-library-for-python) from 1.24.0 to 1.24.1. - [Release notes](https://github.com/AzureAD/microsoft-authentication-library-for-python/releases) - [Commits](https://github.com/AzureAD/microsoft-authentication-library-for-python/compare/1.24.0...1.24.1) --- updated-dependencies: - dependency-name: msal dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index 267228cd1451..b29277cd7662 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -207,7 +207,7 @@ markupsafe==2.1.3 # jinja2 # mako # werkzeug -msal==1.24.0 +msal==1.24.1 # via -r requirements-base.in multidict==6.0.4 # via From 6b00b0f22939e05e744d52ff3f53c37dbcafca46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:06:19 -0700 Subject: [PATCH 08/24] Bump ipython from 8.15.0 to 8.16.0 (#3819) Bumps [ipython](https://github.com/ipython/ipython) from 8.15.0 to 8.16.0. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/8.15.0...8.16.0) --- updated-dependencies: - dependency-name: ipython dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c31768d3beee..c8d639b2ce32 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -44,7 +44,7 @@ identify==2.5.27 # via pre-commit iniconfig==2.0.0 # via pytest -ipython==8.15.0 +ipython==8.16.0 # via -r requirements-dev.in jedi==0.19.0 # via ipython From 0a59fa49c4d0bbf1c8d8a2776e4079617470c91f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:06:27 -0700 Subject: [PATCH 09/24] Bump psycopg2-binary from 2.9.7 to 2.9.8 (#3816) Bumps [psycopg2-binary](https://github.com/psycopg/psycopg2) from 2.9.7 to 2.9.8. - [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS) - [Commits](https://github.com/psycopg/psycopg2/compare/2.9.7...2.9.8) --- updated-dependencies: - dependency-name: psycopg2-binary dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index b29277cd7662..f3e2dc86c687 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -272,7 +272,7 @@ protobuf==4.24.3 # -r requirements-base.in # google-api-core # googleapis-common-protos -psycopg2-binary==2.9.7 +psycopg2-binary==2.9.8 # via -r requirements-base.in pyasn1==0.5.0 # via From 4bc48633b453026286895792080632e0ff312958 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:06:34 -0700 Subject: [PATCH 10/24] Bump pydantic from 1.10.12 to 1.10.13 (#3815) Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.12 to 1.10.13. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v1.10.12...v1.10.13) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index f3e2dc86c687..0775b5d9bc6e 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -286,7 +286,7 @@ pyasn1-modules==0.3.0 # oauth2client pycparser==2.21 # via cffi -pydantic==1.10.12 +pydantic==1.10.13 # via # -r requirements-base.in # blockkit From f08a22b3972f6db1f5e718f1d3a2e96a5e78f6c2 Mon Sep 17 00:00:00 2001 From: Avery Date: Fri, 29 Sep 2023 12:02:32 -0700 Subject: [PATCH 11/24] Adds functionality to manually create missing incident and case resources. (#3791) * Refactors resource creation to a separate function outside of the background task. Adds checks for resource existence. * Adds a button to the resources tab to initiate a retry for creating any missing or unsuccessfully created incident resource(s) * Adds a button to the resources tab to initiate a retry for creating any missing or unsuccessfully created case management resource(s) * Fixes lint errors. * Update src/dispatch/static/dispatch/src/case/ResourcesTab.vue Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> * Plugin retrieval returns list of all enabled plugins instead of by individual queries. Adds additional indicators that resource creation is active * Fixes lint errors. * Changes in client-side incident updates. * Removes success toast after resource creation. * Sets resource creation as a background task. * Bump jsonpath-ng from 1.5.3 to 1.6.0 (#3787) Bumps [jsonpath-ng](https://github.com/h2non/jsonpath-ng) from 1.5.3 to 1.6.0. - [Release notes](https://github.com/h2non/jsonpath-ng/releases) - [Changelog](https://github.com/h2non/jsonpath-ng/blob/master/History.md) - [Commits](https://github.com/h2non/jsonpath-ng/compare/v1.5.3...v1.6.0) --- updated-dependencies: - dependency-name: jsonpath-ng dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @playwright/test in /src/dispatch/static/dispatch (#3786) Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.37.1 to 1.38.0. - [Release notes](https://github.com/Microsoft/playwright/releases) - [Commits](https://github.com/Microsoft/playwright/compare/v1.37.1...v1.38.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump google-api-python-client from 2.98.0 to 2.99.0 (#3784) Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.98.0 to 2.99.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.98.0...v2.99.0) --- updated-dependencies: - dependency-name: google-api-python-client dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump pdpyras from 5.1.1 to 5.1.2 (#3783) Bumps [pdpyras](https://github.com/PagerDuty/pdpyras) from 5.1.1 to 5.1.2. - [Release notes](https://github.com/PagerDuty/pdpyras/releases) - [Changelog](https://github.com/PagerDuty/pdpyras/blob/main/docs/changelog.html) - [Commits](https://github.com/PagerDuty/pdpyras/compare/v5.1.1...v5.1.2) --- updated-dependencies: - dependency-name: pdpyras dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump msal from 1.23.0 to 1.24.0 (#3782) Bumps [msal](https://github.com/AzureAD/microsoft-authentication-library-for-python) from 1.23.0 to 1.24.0. - [Release notes](https://github.com/AzureAD/microsoft-authentication-library-for-python/releases) - [Commits](https://github.com/AzureAD/microsoft-authentication-library-for-python/compare/1.23.0...1.24.0) --- updated-dependencies: - dependency-name: msal dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump sentry-sdk from 1.30.0 to 1.31.0 (#3781) Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.30.0 to 1.31.0. - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/1.30.0...1.31.0) --- updated-dependencies: - dependency-name: sentry-sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Do not run playwright with safar (#3797) * Appends outstanding incident tasks in tactical reports (#3798) * Appends outstanding incident tasks to the end of user input in tactical reports. * Update src/dispatch/plugins/dispatch_slack/incident/interactive.py Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> * Update src/dispatch/static/dispatch/src/incident/store.js Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> --------- Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> * Rework the way slack api errors are propagated (to the caller) (#3789) Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> * Only use stable priority if set (#3803) Co-authored-by: David Whittaker * Bump pandas from 2.1.0 to 2.1.1 (#3801) Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Commits](https://github.com/pandas-dev/pandas/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: pandas dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump faker from 19.6.1 to 19.6.2 (#3800) Bumps [faker](https://github.com/joke2k/faker) from 19.6.1 to 19.6.2. - [Release notes](https://github.com/joke2k/faker/releases) - [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/joke2k/faker/compare/v19.6.1...v19.6.2) --- updated-dependencies: - dependency-name: faker dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump numpy from 1.25.2 to 1.26.0 (#3794) Bumps [numpy](https://github.com/numpy/numpy) from 1.25.2 to 1.26.0. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v1.25.2...v1.26.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> * Bump google-api-python-client from 2.99.0 to 2.100.0 (#3793) Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.99.0 to 2.100.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.99.0...v2.100.0) --- updated-dependencies: - dependency-name: google-api-python-client dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> * Bump ruff from 0.0.289 to 0.0.290 (#3792) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.289 to 0.0.290. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.289...v0.0.290) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> * Making experience textbox from oncall end-of-shift feedback optional (#3799) * Handles unexpected numbers of enabled plugins. * Adds front end polling for resource creation updates. Adds success notification. * Minor refactors for case and incident resource creation. * Bump google-api-python-client from 2.98.0 to 2.99.0 (#3784) Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.98.0 to 2.99.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.98.0...v2.99.0) --- updated-dependencies: - dependency-name: google-api-python-client dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump google-api-python-client from 2.100.0 to 2.101.0 (#3810) Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.100.0 to 2.101.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.100.0...v2.101.0) --- updated-dependencies: - dependency-name: google-api-python-client dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump openai from 0.28.0 to 0.28.1 (#3809) Bumps [openai](https://github.com/openai/openai-python) from 0.28.0 to 0.28.1. - [Release notes](https://github.com/openai/openai-python/releases) - [Commits](https://github.com/openai/openai-python/compare/v0.28.0...v0.28.1) --- updated-dependencies: - dependency-name: openai dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ruff from 0.0.290 to 0.0.291 (#3808) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.290 to 0.0.291. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.290...v0.0.291) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump eslint from 8.49.0 to 8.50.0 in /src/dispatch/static/dispatch (#3807) Bumps [eslint](https://github.com/eslint/eslint) from 8.49.0 to 8.50.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.49.0...v8.50.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Revert "Making experience textbox from oncall end-of-shift feedback optional (#3799)" (#3814) This reverts commit db544b0251d21d5ad1f7d16f7068b5a50202ba4f. * Bump @playwright/test in /src/dispatch/static/dispatch (#3786) Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.37.1 to 1.38.0. - [Release notes](https://github.com/Microsoft/playwright/releases) - [Commits](https://github.com/Microsoft/playwright/compare/v1.37.1...v1.38.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update src/dispatch/case/views.py Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> * Fixes lint error. * Bump @playwright/test in /src/dispatch/static/dispatch (#3786) Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.37.1 to 1.38.0. - [Release notes](https://github.com/Microsoft/playwright/releases) - [Commits](https://github.com/Microsoft/playwright/compare/v1.37.1...v1.38.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Restores original package-lock file. * Removes irrelevant files. * Removes irrelevant files. * Refactors converstion creation flow. * Fixes linting errors. --------- Signed-off-by: dependabot[bot] Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Will Sheldon <114631109+wssheldon@users.noreply.github.com> Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> Co-authored-by: kevgliss Co-authored-by: David Whittaker --- src/dispatch/case/flows.py | 206 ++++++++---------- src/dispatch/case/views.py | 28 +++ src/dispatch/conversation/flows.py | 97 ++++++++- src/dispatch/incident/flows.py | 133 ++++++----- src/dispatch/incident/messaging.py | 8 + src/dispatch/incident/views.py | 20 ++ .../dispatch_slack/case/interactive.py | 4 +- .../static/dispatch/src/case/ResourcesTab.vue | 62 +++++- src/dispatch/static/dispatch/src/case/api.js | 4 + .../static/dispatch/src/case/store.js | 66 ++++++ .../dispatch/src/incident/ResourcesTab.vue | 66 +++++- .../static/dispatch/src/incident/api.js | 4 + .../static/dispatch/src/incident/store.js | 66 ++++++ 13 files changed, 581 insertions(+), 183 deletions(-) diff --git a/src/dispatch/case/flows.py b/src/dispatch/case/flows.py index 4f64d6ee717c..6c06cb88113d 100644 --- a/src/dispatch/case/flows.py +++ b/src/dispatch/case/flows.py @@ -6,8 +6,7 @@ from dispatch.case import service as case_service from dispatch.case.models import CaseRead -from dispatch.conversation import service as conversation_service -from dispatch.conversation.models import ConversationCreate +from dispatch.conversation import flows as conversation_flows from dispatch.database.core import SessionLocal from dispatch.decorators import background_task from dispatch.document import flows as document_flows @@ -154,26 +153,6 @@ def case_add_or_reactivate_participant_flow( return participant -def create_conversation(case: Case, conversation_target: str, db_session: SessionLocal): - """Create external communication conversation.""" - plugin = plugin_service.get_active_instance( - db_session=db_session, project_id=case.project.id, plugin_type="conversation" - ) - conversation = plugin.instance.create_threaded( - case=case, conversation_id=conversation_target, db_session=db_session - ) - conversation.update({"resource_type": plugin.plugin.slug, "resource_id": conversation["id"]}) - - event_service.log_case_event( - db_session=db_session, - source=plugin.plugin.title, - description="Case conversation created", - case_id=case.id, - ) - - return conversation - - def update_conversation(case: Case, db_session: SessionLocal): """Updates external communication conversation.""" plugin = plugin_service.get_active_instance( @@ -217,6 +196,7 @@ def case_new_create_flow( case_id=case.id, individual_participants=individual_participants, team_participants=team_participants, + conversation_target=conversation_target, ) if case.case_priority.page_assignee: @@ -241,67 +221,6 @@ def case_new_create_flow( else: log.warning("Case assignee not paged. No plugin of type oncall enabled.") - conversation_plugin = plugin_service.get_active_instance( - db_session=db_session, project_id=case.project.id, plugin_type="conversation" - ) - if conversation_plugin: - if not conversation_target: - conversation_target = case.case_type.conversation_target - if conversation_target: - try: - # TODO: Refactor conversation creation using conversation_flows module - conversation = create_conversation(case, conversation_target, db_session) - conversation_in = ConversationCreate( - resource_id=conversation["resource_id"], - resource_type=conversation["resource_type"], - weblink=conversation["weblink"], - thread_id=conversation["timestamp"], - channel_id=conversation["id"], - ) - case.conversation = conversation_service.create( - db_session=db_session, conversation_in=conversation_in - ) - - event_service.log_case_event( - db_session=db_session, - source="Dispatch Core App", - description="Conversation added to case", - case_id=case.id, - ) - # wait until all resources are created before adding suggested participants - individual_participants = [x.email for x, _ in individual_participants] - - for email in individual_participants: - # we don't rely on on this flow to add folks to the conversation because in this case - # we want to do it in bulk - case_add_or_reactivate_participant_flow( - db_session=db_session, - user_email=email, - case_id=case.id, - add_to_conversation=False, - ) - # explicitly add the assignee to the conversation - all_participants = individual_participants + [case.assignee.individual.email] - conversation_plugin.instance.add_to_thread( - case.conversation.channel_id, - case.conversation.thread_id, - all_participants, - ) - event_service.log_case_event( - db_session=db_session, - source="Dispatch Core App", - description="Case participants added to conversation.", - case_id=case.id, - ) - except Exception as e: - event_service.log_case_event( - db_session=db_session, - source="Dispatch Core App", - description=f"Creation of case conversation failed. Reason: {e}", - case_id=case.id, - ) - log.exception(e) - db_session.add(case) db_session.commit() @@ -684,7 +603,12 @@ def case_assign_role_flow( def case_create_resources_flow( - db_session: Session, case_id: int, individual_participants: list, team_participants: list + db_session: Session, + case_id: int, + individual_participants: List[str], + team_participants: List[str], + conversation_target: str = None, + create_resources: bool = True, ) -> None: """Runs the case resource creation flow.""" case = get(db_session=db_session, case_id=case_id) @@ -692,43 +616,93 @@ def case_create_resources_flow( if case.assignee: individual_participants.append((case.assignee.individual, None)) - # we create the tactical group - direct_participant_emails = [i.email for i, _ in individual_participants] + if create_resources: + # we create the tactical group + direct_participant_emails = [i.email for i, _ in individual_participants] - indirect_participant_emails = [t.email for t in team_participants] + indirect_participant_emails = [t.email for t in team_participants] - group = group_flows.create_group( - subject=case, - group_type=GroupType.tactical, - group_participants=list(set(direct_participant_emails + indirect_participant_emails)), - db_session=db_session, - ) + if not case.groups: + group_flows.create_group( + subject=case, + group_type=GroupType.tactical, + group_participants=list( + set(direct_participant_emails + indirect_participant_emails) + ), + db_session=db_session, + ) - # we create the storage folder - storage_members = [] - if group: - storage_members = [group.email] + # we create the storage folder + storage_members = [] + if case.tactical_group: + storage_members = [case.tactical_group.email] + # direct add members if not group exists + else: + storage_members = direct_participant_emails - # direct add members if not group exists - else: - storage_members = direct_participant_emails + if not case.storage: + storage_flows.create_storage( + subject=case, storage_members=storage_members, db_session=db_session + ) - case.storage = storage_flows.create_storage( - subject=case, storage_members=storage_members, db_session=db_session - ) + # we create the investigation document + if not case.case_document: + document_flows.create_document( + subject=case, + document_type=DocumentResourceTypes.case, + document_template=case.case_type.case_template_document, + db_session=db_session, + ) - # we create the investigation document - document = document_flows.create_document( - subject=case, - document_type=DocumentResourceTypes.case, - document_template=case.case_type.case_template_document, - db_session=db_session, - ) + # we update the ticket + ticket_flows.update_case_ticket(case=case, db_session=db_session) - # we update the ticket - ticket_flows.update_case_ticket(case=case, db_session=db_session) + # we update the case document + document_flows.update_document( + document=case.case_document, project_id=case.project.id, db_session=db_session + ) - # we update the case document - document_flows.update_document( - document=document, project_id=case.project.id, db_session=db_session - ) + try: + # we create the conversation and add participants to the thread + conversation_flows.create_case_conversation(case, conversation_target, db_session) + + event_service.log_case_event( + db_session=db_session, + source="Dispatch Core App", + description="Conversation added to case", + case_id=case.id, + ) + # wait until all resources are created before adding suggested participants + individual_participants = [x.email for x, _ in individual_participants] + + for email in individual_participants: + # we don't rely on on this flow to add folks to the conversation because in this case + # we want to do it in bulk + case_add_or_reactivate_participant_flow( + db_session=db_session, + user_email=email, + case_id=case.id, + add_to_conversation=False, + ) + # explicitly add the assignee to the conversation + all_participants = individual_participants + [case.assignee.individual.email] + + # # we add the participant to the conversation + conversation_flows.add_case_participants( + case=case, participant_emails=all_participants, db_session=db_session + ) + + event_service.log_case_event( + db_session=db_session, + source="Dispatch Core App", + description="Case participants added to conversation.", + case_id=case.id, + ) + except Exception as e: + event_service.log_case_event( + db_session=db_session, + source="Dispatch Core App", + description=f"Creation of case conversation failed. Reason: {e}", + case_id=case.id, + ) + log.exception(e) diff --git a/src/dispatch/case/views.py b/src/dispatch/case/views.py index 1d9e68f36e94..add6f8a5037a 100644 --- a/src/dispatch/case/views.py +++ b/src/dispatch/case/views.py @@ -34,6 +34,8 @@ case_new_create_flow, case_triage_create_flow, case_update_flow, + case_create_resources_flow, + get_case_participants, ) from .models import Case, CaseCreate, CasePagination, CaseRead, CaseUpdate, CaseExpandedPagination from .service import create, delete, get, update @@ -145,6 +147,32 @@ def create_case( return case +@router.post( + "/{case_id}/resources", + response_model=CaseRead, + summary="Creates resources for an existing case.", +) +def create_case_resources( + db_session: DbSession, + case_id: PrimaryKey, + current_case: CurrentCase, + background_tasks: BackgroundTasks, +): + """Creates resources for an existing case.""" + individual_participants, team_participants = get_case_participants( + case=current_case, db_session=db_session + ) + background_tasks.add_task( + case_create_resources_flow, + db_session=db_session, + case_id=case_id, + individual_participants=individual_participants, + team_participants=team_participants, + ) + + return current_case + + @router.put( "/{case_id}", response_model=CaseRead, diff --git a/src/dispatch/conversation/flows.py b/src/dispatch/conversation/flows.py index ab14fbf4b4a3..dadba50ff470 100644 --- a/src/dispatch/conversation/flows.py +++ b/src/dispatch/conversation/flows.py @@ -2,6 +2,7 @@ from typing import TypeVar, List +from dispatch.case.models import Case from dispatch.conference.models import Conference from dispatch.database.core import SessionLocal, resolve_attr from dispatch.document.models import Document @@ -22,7 +23,57 @@ Resource = TypeVar("Resource", Document, Conference, Storage, Ticket) -def create_conversation(incident: Incident, db_session: SessionLocal): +def create_case_conversation(case: Case, conversation_target: str, db_session: SessionLocal): + """Create external communication conversation.""" + + plugin = plugin_service.get_active_instance( + db_session=db_session, project_id=case.project.id, plugin_type="conversation" + ) + if not plugin: + log.warning("Conversation not created. No conversation plugin enabled.") + return + + if not conversation_target: + conversation_target = case.case_type.conversation_target + + if conversation_target: + try: + conversation = plugin.instance.create_threaded( + case=case, conversation_id=conversation_target, db_session=db_session + ) + except Exception as e: + # TODO: consistency across exceptions + log.exception(e) + + if not conversation: + log.error(f"Conversation not created. Plugin {plugin.plugin.slug} encountered an error.") + return + + conversation.update({"resource_type": plugin.plugin.slug, "resource_id": conversation["id"]}) + + conversation_in = ConversationCreate( + resource_id=conversation["resource_id"], + resource_type=conversation["resource_type"], + weblink=conversation["weblink"], + thread_id=conversation["timestamp"], + channel_id=conversation["id"], + ) + case.conversation = create(db_session=db_session, conversation_in=conversation_in) + + event_service.log_case_event( + db_session=db_session, + source=plugin.plugin.title, + description="Case conversation created", + case_id=case.id, + ) + + db_session.add(case) + db_session.commit() + + return case.conversation + + +def create_incident_conversation(incident: Incident, db_session: SessionLocal): """Creates a conversation.""" plugin = plugin_service.get_active_instance( db_session=db_session, project_id=incident.project.id, plugin_type="conversation" @@ -59,8 +110,7 @@ def create_conversation(incident: Incident, db_session: SessionLocal): weblink=external_conversation["weblink"], channel_id=external_conversation["id"], ) - conversation = create(conversation_in=conversation_in, db_session=db_session) - incident.conversation = conversation + incident.conversation = create(conversation_in=conversation_in, db_session=db_session) db_session.add(incident) db_session.commit() @@ -72,7 +122,7 @@ def create_conversation(incident: Incident, db_session: SessionLocal): incident_id=incident.id, ) - return conversation + return incident.conversation def archive_conversation(incident: Incident, db_session: SessionLocal): @@ -262,8 +312,43 @@ def add_conversation_bookmarks(incident: Incident, db_session: SessionLocal): log.exception(e) -def add_participants(incident: Incident, participant_emails: List[str], db_session: SessionLocal): - """Adds one or more participants to the conversation.""" +def add_case_participants(case: Case, participant_emails: List[str], db_session: SessionLocal): + """Adds one or more participants to the case conversation.""" + if not case.conversation: + log.warning( + "Case participant(s) not added to conversation. No conversation available for this case." + ) + return + + plugin = plugin_service.get_active_instance( + db_session=db_session, project_id=case.project.id, plugin_type="conversation" + ) + if not plugin: + log.warning( + "Case participant(s) not added to conversation. No conversation plugin enabled." + ) + return + + try: + plugin.instance.add_to_thread( + case.conversation.channel_id, + case.conversation.thread_id, + participant_emails, + ) + except Exception as e: + event_service.log_case_event( + db_session=db_session, + source="Dispatch Core App", + description=f"Adding participant(s) to case conversation failed. Reason: {e}", + case_id=case.id, + ) + log.exception(e) + + +def add_incident_participants( + incident: Incident, participant_emails: List[str], db_session: SessionLocal +): + """Adds one or more participants to the incident conversation.""" if not incident.conversation: log.warning( "Incident participant(s) not added to conversation. No conversation available for this incident." diff --git a/src/dispatch/incident/flows.py b/src/dispatch/incident/flows.py index 84f553c20456..4585f5f5d4d6 100644 --- a/src/dispatch/incident/flows.py +++ b/src/dispatch/incident/flows.py @@ -140,68 +140,74 @@ def inactivate_incident_participants(incident: Incident, db_session: Session): ) -@background_task -def incident_create_flow(*, organization_slug: str, incident_id: int, db_session=None) -> Incident: - """Creates all resources required for new incidents.""" - # we get the incident - incident = incident_service.get(db_session=db_session, incident_id=incident_id) - +def incident_create_resources(*, incident: Incident, db_session=None) -> Incident: + """Creates all resources required for incidents.""" # we create the incident ticket - ticket_flows.create_incident_ticket(incident=incident, db_session=db_session) + if not incident.ticket: + ticket_flows.create_incident_ticket(incident=incident, db_session=db_session) # we resolve individual and team participants individual_participants, team_participants = get_incident_participants(incident, db_session) + tactical_participant_emails = [i.email for i, _ in individual_participants] # we create the tactical group - tactical_participant_emails = [i.email for i, _ in individual_participants] - tactical_group = group_flows.create_group( - subject=incident, - group_type=GroupType.tactical, - group_participants=tactical_participant_emails, - db_session=db_session, - ) + if not incident.tactical_group: + group_flows.create_group( + subject=incident, + group_type=GroupType.tactical, + group_participants=tactical_participant_emails, + db_session=db_session, + ) # we create the notifications group - notification_participant_emails = [t.email for t in team_participants] - notifications_group = group_flows.create_group( - subject=incident, - group_type=GroupType.notifications, - group_participants=notification_participant_emails, - db_session=db_session, - ) + if not incident.notifications_group: + notification_participant_emails = [t.email for t in team_participants] + group_flows.create_group( + subject=incident, + group_type=GroupType.notifications, + group_participants=notification_participant_emails, + db_session=db_session, + ) # we create the storage folder - storage_members = [] - if tactical_group and notifications_group: - storage_members = [tactical_group.email, notifications_group.email] - else: - storage_members = tactical_participant_emails + if not incident.storage: + storage_members = [] + if incident.tactical_group and incident.notifications_group: + storage_members = [incident.tactical_group.email, incident.notifications_group.email] + else: + storage_members = tactical_participant_emails - storage_flows.create_storage( - subject=incident, storage_members=storage_members, db_session=db_session - ) + storage_flows.create_storage( + subject=incident, storage_members=storage_members, db_session=db_session + ) # we create the incident document - document_flows.create_document( - subject=incident, - document_type=DocumentResourceTypes.incident, - document_template=incident.incident_type.incident_template_document, - db_session=db_session, - ) + if not incident.incident_document: + document_flows.create_document( + subject=incident, + document_type=DocumentResourceTypes.incident, + document_template=incident.incident_type.incident_template_document, + db_session=db_session, + ) # we create the conference room - conference_participants = [] - if tactical_group and notifications_group: - conference_participants = [tactical_group.email, notifications_group.email] - else: - conference_participants = tactical_participant_emails + if not incident.conference: + conference_participants = [] + if incident.tactical_group and incident.notifications_group: + conference_participants = [ + incident.tactical_group.email, + incident.notifications_group.email, + ] + else: + conference_participants = tactical_participant_emails - conference_flows.create_conference( - incident=incident, participants=conference_participants, db_session=db_session - ) + conference_flows.create_conference( + incident=incident, participants=conference_participants, db_session=db_session + ) # we create the conversation - conversation_flows.create_conversation(incident=incident, db_session=db_session) + if not incident.conversation: + conversation_flows.create_incident_conversation(incident=incident, db_session=db_session) # we update the incident ticket ticket_flows.update_incident_ticket(incident_id=incident.id, db_session=db_session) @@ -238,7 +244,7 @@ def incident_create_flow(*, organization_slug: str, incident_id: int, db_session ) # we add the participant to the conversation - conversation_flows.add_participants( + conversation_flows.add_incident_participants( incident=incident, participant_emails=[user_email], db_session=db_session ) @@ -272,14 +278,31 @@ def incident_create_flow(*, organization_slug: str, incident_id: int, db_session incident_id=incident.id, ) - send_incident_created_notifications(incident, db_session) + return incident - event_service.log_incident_event( - db_session=db_session, - source="Dispatch Core App", - description="Incident notifications sent", - incident_id=incident.id, - ) + +@background_task +def incident_create_resources_flow( + *, organization_slug: str, incident_id: int, db_session=None +) -> Incident: + """Creates all resources required for an existing incident.""" + # we get the incident + incident = incident_service.get(db_session=db_session, incident_id=incident_id) + + # we create the incident resources + return incident_create_resources(incident=incident, db_session=db_session) + + +@background_task +def incident_create_flow(*, organization_slug: str, incident_id: int, db_session=None) -> Incident: + """Creates all resources required for new incidents and initiates incident response workflow.""" + # we get the incident + incident = incident_service.get(db_session=db_session, incident_id=incident_id) + + # we create the incident resources + incident_create_resources(incident=incident, db_session=db_session) + + send_incident_created_notifications(incident, db_session) # we page the incident commander based on incident priority if incident.incident_priority.page_commander: @@ -902,7 +925,7 @@ def incident_add_or_reactivate_participant_flow( if incident.status != IncidentStatus.closed: # we add the participant to the conversation - conversation_flows.add_participants( + conversation_flows.add_incident_participants( incident=incident, participant_emails=[user_email], db_session=db_session ) @@ -942,7 +965,7 @@ def incident_remove_participant_flow( for assignee in task.assignees: if assignee == participant: # we add the participant to the conversation - conversation_flows.add_participants( + conversation_flows.add_incident_participants( incident=incident, participant_emails=[user_email], db_session=db_session ) @@ -954,7 +977,7 @@ def incident_remove_participant_flow( if user_email == incident.commander.individual.email: # we add the participant to the conversation - conversation_flows.add_participants( + conversation_flows.add_incident_participants( incident=incident, participant_emails=[user_email], db_session=db_session ) diff --git a/src/dispatch/incident/messaging.py b/src/dispatch/incident/messaging.py index 1c6f1d4b2575..994ae45d4564 100644 --- a/src/dispatch/incident/messaging.py +++ b/src/dispatch/incident/messaging.py @@ -11,6 +11,7 @@ from dispatch.conversation.enums import ConversationCommands from dispatch.database.core import SessionLocal, resolve_attr from dispatch.document import service as document_service +from dispatch.event import service as event_service from dispatch.incident.enums import IncidentStatus from dispatch.incident.models import Incident, IncidentRead from dispatch.notification import service as notification_service @@ -347,6 +348,13 @@ def send_incident_created_notifications(incident: Incident, db_session: SessionL notification_params=notification_params, ) + event_service.log_incident_event( + db_session=db_session, + source="Dispatch Core App", + description="Incident notifications sent", + incident_id=incident.id, + ) + log.debug("Incident created notifications sent.") diff --git a/src/dispatch/incident/views.py b/src/dispatch/incident/views.py index 031909017f47..f8c740a70230 100644 --- a/src/dispatch/incident/views.py +++ b/src/dispatch/incident/views.py @@ -36,6 +36,7 @@ incident_delete_flow, incident_subscribe_participant_flow, incident_update_flow, + incident_create_resources_flow, ) from .metrics import create_incident_metric_query, make_forecast from .models import ( @@ -139,6 +140,25 @@ def create_incident( return incident +@router.post( + "/{incident_id}/resources", + response_model=IncidentRead, + summary="Creates resources for an existing incident.", +) +def create_incident_resources( + organization: OrganizationSlug, + incident_id: PrimaryKey, + current_incident: CurrentIncident, + background_tasks: BackgroundTasks, +): + """Creates resources for an existing incident.""" + background_tasks.add_task( + incident_create_resources_flow, organization_slug=organization, incident_id=incident_id + ) + + return current_incident + + @router.put( "/{incident_id}", response_model=IncidentRead, diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py index e4797c36d368..a49ad567497b 100644 --- a/src/dispatch/plugins/dispatch_slack/case/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py @@ -1032,7 +1032,7 @@ def handle_escalation_submission_event( case=case, organization_slug=context["subject"].organization_slug, db_session=db_session ) - conversation_flows.add_participants( + conversation_flows.add_incident_participants( incident=incident, participant_emails=[user.email], db_session=db_session ) @@ -1082,7 +1082,7 @@ def join_incident_button_click( case = case_service.get(db_session=db_session, case_id=context["subject"].id) # we add the user to the incident conversation - conversation_flows.add_participants( + conversation_flows.add_incident_participants( # TODO: handle case where there are multiple related incidents incident=case.incidents[0], participant_emails=[user.email], diff --git a/src/dispatch/static/dispatch/src/case/ResourcesTab.vue b/src/dispatch/static/dispatch/src/case/ResourcesTab.vue index ad7403449c82..a9eb2b5d5df4 100644 --- a/src/dispatch/static/dispatch/src/case/ResourcesTab.vue +++ b/src/dispatch/static/dispatch/src/case/ResourcesTab.vue @@ -65,23 +65,83 @@ + + + + Recreate Missing Resources + Initiate a retry for creating any missing or unsuccesfully created + resource(s). + + + + refresh + + + + + + Creating resources... + Initiate a retry for creating any missing or unsuccesfully created + resource(s). + + + diff --git a/src/dispatch/static/dispatch/src/case/api.js b/src/dispatch/static/dispatch/src/case/api.js index 8a3b0d2abb61..2fc1a01f27e3 100644 --- a/src/dispatch/static/dispatch/src/case/api.js +++ b/src/dispatch/static/dispatch/src/case/api.js @@ -44,4 +44,8 @@ export default { delete(caseId) { return API.delete(`/${resource}/${caseId}`) }, + + createAllResources(caseId, payload) { + return API.post(`/${resource}/${caseId}/resources`, payload) + }, } diff --git a/src/dispatch/static/dispatch/src/case/store.js b/src/dispatch/static/dispatch/src/case/store.js index bbe3ec901dac..6eda8cc9b416 100644 --- a/src/dispatch/static/dispatch/src/case/store.js +++ b/src/dispatch/static/dispatch/src/case/store.js @@ -3,6 +3,7 @@ import { debounce } from "lodash" import SearchUtils from "@/search/utils" import CaseApi from "@/case/api" +import PluginApi from "@/plugin/api" import router from "@/router" const getDefaultSelectedState = () => { @@ -253,6 +254,40 @@ const actions = { commit("SET_SELECTED_LOADING", false) }) }, + createAllResources({ commit, dispatch }) { + commit("SET_SELECTED_LOADING", true) + return CaseApi.createAllResources(state.selected.id) + .then(() => { + CaseApi.get(state.selected.id).then((response) => { + commit("SET_SELECTED", response.data) + dispatch("getEnabledPlugins").then((enabledPlugins) => { + // Poll the server for resource creation updates. + var interval = setInterval(function () { + if ( + state.selected.conversation ^ enabledPlugins.includes("conversation") || + state.selected.documents ^ enabledPlugins.includes("document") || + state.selected.storage ^ enabledPlugins.includes("storage") || + state.selected.groups ^ enabledPlugins.includes("participant-group") || + state.selected.ticket ^ enabledPlugins.includes("ticket") + ) { + dispatch("get").then(() => { + clearInterval(interval) + commit("SET_SELECTED_LOADING", false) + commit( + "notification_backend/addBeNotification", + { text: "Resources(s) created successfully.", type: "success" }, + { root: true } + ) + }) + } + }, 5000) + }) + }) + }) + .catch(() => { + commit("SET_SELECTED_LOADING", false) + }) + }, save({ commit, dispatch }) { commit("SET_SELECTED_LOADING", true) if (!state.selected.id) { @@ -330,6 +365,37 @@ const actions = { ) }) }, + getEnabledPlugins() { + if (!state.selected.project) { + return false + } + return PluginApi.getAllInstances({ + filter: JSON.stringify({ + and: [ + { + model: "PluginInstance", + field: "enabled", + op: "==", + value: "true", + }, + { + model: "Project", + field: "name", + op: "==", + value: state.selected.project.name, + }, + ], + }), + itemsPerPage: 50, + }).then((response) => { + return response.data.items.reduce((result, item) => { + if (item.plugin) { + result.push(item.plugin.type) + } + return result + }, []) + }) + }, } const mutations = { diff --git a/src/dispatch/static/dispatch/src/incident/ResourcesTab.vue b/src/dispatch/static/dispatch/src/incident/ResourcesTab.vue index 9c1a3d2c9f15..b478fb9ecb6e 100644 --- a/src/dispatch/static/dispatch/src/incident/ResourcesTab.vue +++ b/src/dispatch/static/dispatch/src/incident/ResourcesTab.vue @@ -62,23 +62,83 @@ + + + + Recreate Missing Resources + Initiate a retry for creating any missing or unsuccesfully created + resource(s). + + + + refresh + + + + + + Creating resources... + Initiate a retry for creating any missing or unsuccesfully created + resource(s). + + + diff --git a/src/dispatch/static/dispatch/src/incident/api.js b/src/dispatch/static/dispatch/src/incident/api.js index 0516aaa84cdf..1f77a9053460 100644 --- a/src/dispatch/static/dispatch/src/incident/api.js +++ b/src/dispatch/static/dispatch/src/incident/api.js @@ -58,4 +58,8 @@ export default { createReport(incidentId, type, payload) { return API.post(`/${resource}/${incidentId}/report/${type}`, payload) }, + + createAllResources(incidentId, payload) { + return API.post(`/${resource}/${incidentId}/resources`, payload) + }, } diff --git a/src/dispatch/static/dispatch/src/incident/store.js b/src/dispatch/static/dispatch/src/incident/store.js index bf57d004f858..f43befd9717d 100644 --- a/src/dispatch/static/dispatch/src/incident/store.js +++ b/src/dispatch/static/dispatch/src/incident/store.js @@ -3,6 +3,7 @@ import { debounce } from "lodash" import SearchUtils from "@/search/utils" import IncidentApi from "@/incident/api" +import PluginApi from "@/plugin/api" import router from "@/router" const getDefaultSelectedState = () => { @@ -347,6 +348,40 @@ const actions = { ) }) }, + createAllResources({ commit, dispatch }) { + commit("SET_SELECTED_LOADING", true) + return IncidentApi.createAllResources(state.selected.id) + .then(() => { + IncidentApi.get(state.selected.id).then((response) => { + commit("SET_SELECTED", response.data) + dispatch("getEnabledPlugins").then((enabledPlugins) => { + // Poll the server for resource creation updates. + var interval = setInterval(function () { + if ( + state.selected.conversation ^ enabledPlugins.includes("conversation") || + state.selected.documents ^ enabledPlugins.includes("document") || + state.selected.storage ^ enabledPlugins.includes("storage") || + state.selected.conference ^ enabledPlugins.includes("conference") || + state.selected.ticket ^ enabledPlugins.includes("ticket") + ) { + dispatch("get").then(() => { + clearInterval(interval) + commit("SET_SELECTED_LOADING", false) + commit( + "notification_backend/addBeNotification", + { text: "Resources(s) created successfully.", type: "success" }, + { root: true } + ) + }) + } + }, 5000) + }) + }) + }) + .catch(() => { + commit("SET_SELECTED_LOADING", false) + }) + }, resetSelected({ commit }) { commit("RESET_SELECTED") }, @@ -371,6 +406,37 @@ const actions = { ) }) }, + getEnabledPlugins() { + if (!state.selected.project) { + return false + } + return PluginApi.getAllInstances({ + filter: JSON.stringify({ + and: [ + { + model: "PluginInstance", + field: "enabled", + op: "==", + value: "true", + }, + { + model: "Project", + field: "name", + op: "==", + value: state.selected.project.name, + }, + ], + }), + itemsPerPage: 50, + }).then((response) => { + return response.data.items.reduce((result, item) => { + if (item.plugin) { + result.push(item.plugin.type) + } + return result + }, []) + }) + }, } const mutations = { From 2e3f8e0efe9176ef1d8eacd176c87bd4caaa728f Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:06:58 -0700 Subject: [PATCH 12/24] Fixes errors in oncall end-of-shift feedback reminders plus opens api to fetch feedback (#3817) --- src/dispatch/api.py | 4 ++++ src/dispatch/feedback/service/models.py | 12 +++++++----- .../feedback/service/reminder/service.py | 10 +++++----- src/dispatch/feedback/service/scheduled.py | 16 +++++++++++----- src/dispatch/feedback/service/views.py | 14 ++++++++++++++ .../plugins/dispatch_pagerduty/plugin.py | 3 ++- .../plugins/dispatch_pagerduty/service.py | 5 ++++- .../dispatch_slack/feedback/interactive.py | 7 ++++--- .../static/dispatch/src/service_feedback/api.js | 9 +++++++++ 9 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 src/dispatch/feedback/service/views.py create mode 100644 src/dispatch/static/dispatch/src/service_feedback/api.js diff --git a/src/dispatch/api.py b/src/dispatch/api.py index 882a08d99b4d..21c84e50de7d 100644 --- a/src/dispatch/api.py +++ b/src/dispatch/api.py @@ -24,6 +24,7 @@ from dispatch.entity.views import router as entity_router from dispatch.entity_type.views import router as entity_type_router from dispatch.feedback.incident.views import router as feedback_router +from dispatch.feedback.service.views import router as service_feedback_router from dispatch.incident.priority.views import router as incident_priority_router from dispatch.incident.severity.views import router as incident_severity_router from dispatch.incident.type.views import router as incident_type_router @@ -203,6 +204,9 @@ def get_organization_path(organization: OrganizationSlug): authenticated_organization_api_router.include_router( feedback_router, prefix="/feedback", tags=["feedback"] ) +authenticated_organization_api_router.include_router( + service_feedback_router, prefix="/service_feedback", tags=["service_feedback"] +) authenticated_organization_api_router.include_router( notification_router, prefix="/notifications", tags=["notifications"] ) diff --git a/src/dispatch/feedback/service/models.py b/src/dispatch/feedback/service/models.py index e1d09e9f51ca..e2773f347bac 100644 --- a/src/dispatch/feedback/service/models.py +++ b/src/dispatch/feedback/service/models.py @@ -5,10 +5,11 @@ from sqlalchemy import Column, Integer, ForeignKey, DateTime, String from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy_utils import TSVectorType +from sqlalchemy.orm import relationship from dispatch.database.core import Base -from dispatch.individual.models import IndividualContactRead -from dispatch.models import DispatchBase, TimeStampMixin, FeedbackMixin, PrimaryKey +from dispatch.individual.models import IndividualContactReadMinimal +from dispatch.models import DispatchBase, TimeStampMixin, FeedbackMixin, PrimaryKey, Pagination from dispatch.project.models import ProjectRead from .enums import ServiceFeedbackRating @@ -26,6 +27,7 @@ class ServiceFeedback(TimeStampMixin, FeedbackMixin, Base): # Relationships individual_contact_id = Column(Integer, ForeignKey("individual_contact.id")) + individual = relationship("IndividualContact") search_vector = Column( TSVectorType( @@ -37,14 +39,14 @@ class ServiceFeedback(TimeStampMixin, FeedbackMixin, Base): @hybrid_property def project(self): - return self.service.project + return self.individual.project # Pydantic models class ServiceFeedbackBase(DispatchBase): feedback: Optional[str] = Field(None, nullable=True) hours: Optional[int] - individual: Optional[IndividualContactRead] + individual: Optional[IndividualContactReadMinimal] rating: ServiceFeedbackRating = ServiceFeedbackRating.little_effort schedule: Optional[str] shift_end_at: Optional[datetime] @@ -64,6 +66,6 @@ class ServiceFeedbackRead(ServiceFeedbackBase): project: Optional[ProjectRead] -class ServiceFeedbackPagination(DispatchBase): +class ServiceFeedbackPagination(Pagination): items: List[ServiceFeedbackRead] total: int diff --git a/src/dispatch/feedback/service/reminder/service.py b/src/dispatch/feedback/service/reminder/service.py index ae264e1d403d..248f729e0f73 100644 --- a/src/dispatch/feedback/service/reminder/service.py +++ b/src/dispatch/feedback/service/reminder/service.py @@ -3,7 +3,6 @@ from .models import ( ServiceFeedbackReminder, - ServiceFeedbackReminderCreate, ServiceFeedbackReminderUpdate, ) from dispatch.individual.models import IndividualContact @@ -24,11 +23,11 @@ def get_all_expired_reminders_by_project_id( ) -def create(*, db_session, reminder_in: ServiceFeedbackReminderCreate) -> ServiceFeedbackReminder: +def create(*, db_session, reminder_in: ServiceFeedbackReminder) -> ServiceFeedbackReminder: """Creates a new service feedback reminder.""" reminder = ServiceFeedbackReminder(**reminder_in.dict()) - db_session.add(reminder_in) + db_session.add(reminder) db_session.commit() return reminder @@ -55,5 +54,6 @@ def delete(*, db_session, reminder_id: int): .filter(ServiceFeedbackReminder.id == reminder_id) .one_or_none() ) - db_session.delete(reminder) - db_session.commit() + if reminder: + db_session.delete(reminder) + db_session.commit() diff --git a/src/dispatch/feedback/service/scheduled.py b/src/dispatch/feedback/service/scheduled.py index 6f5968659559..3c7a3c48f079 100644 --- a/src/dispatch/feedback/service/scheduled.py +++ b/src/dispatch/feedback/service/scheduled.py @@ -28,7 +28,7 @@ @timer @scheduled_project_task def oncall_shift_feedback_ucan(db_session: SessionLocal, project: Project): - oncall_shift_feedback(db_session=db_session, project=project) + oncall_shift_feedback(db_session=db_session, project=project, hour=16) find_expired_reminders_and_send(db_session=db_session, project=project) @@ -36,7 +36,7 @@ def oncall_shift_feedback_ucan(db_session: SessionLocal, project: Project): @timer @scheduled_project_task def oncall_shift_feedback_emea(db_session: SessionLocal, project: Project): - oncall_shift_feedback(db_session=db_session, project=project) + oncall_shift_feedback(db_session=db_session, project=project, hour=6) find_expired_reminders_and_send(db_session=db_session, project=project) @@ -60,13 +60,18 @@ def find_expired_reminders_and_send(*, db_session: SessionLocal, project: Projec def find_schedule_and_send( - *, db_session: SessionLocal, project: Project, oncall_plugin: PluginInstance, schedule_id: str + *, + db_session: SessionLocal, + project: Project, + oncall_plugin: PluginInstance, + schedule_id: str, + hour: int, ): """ Given PagerDuty schedule_id, determine if the shift ended for the previous oncall person and send the health metrics feedback request """ - current_oncall = oncall_plugin.instance.did_oncall_just_go_off_shift(schedule_id) + current_oncall = oncall_plugin.instance.did_oncall_just_go_off_shift(schedule_id, hour) individual = individual_service.get_by_email_and_project( db_session=db_session, email=current_oncall["email"], project_id=project.id @@ -82,7 +87,7 @@ def find_schedule_and_send( ) -def oncall_shift_feedback(db_session: SessionLocal, project: Project): +def oncall_shift_feedback(db_session: SessionLocal, project: Project, hour: int): """ Experimental: collects feedback from individuals participating in an oncall service that has health metrics enabled when their oncall shift ends. For now, only for one project and schedule. @@ -115,4 +120,5 @@ def oncall_shift_feedback(db_session: SessionLocal, project: Project): project=project, oncall_plugin=oncall_plugin, schedule_id=schedule_id, + hour=hour, ) diff --git a/src/dispatch/feedback/service/views.py b/src/dispatch/feedback/service/views.py new file mode 100644 index 000000000000..280ec390ac5e --- /dev/null +++ b/src/dispatch/feedback/service/views.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter + +from dispatch.database.service import search_filter_sort_paginate, CommonParameters + +from .models import ServiceFeedbackPagination + + +router = APIRouter() + + +@router.get("", response_model=ServiceFeedbackPagination) +def get_feedback_entries(commons: CommonParameters): + """Get all feedback entries, or only those matching a given search term.""" + return search_filter_sort_paginate(model="ServiceFeedback", **commons) diff --git a/src/dispatch/plugins/dispatch_pagerduty/plugin.py b/src/dispatch/plugins/dispatch_pagerduty/plugin.py index 1e7303d8bb32..b30b0456bdd2 100644 --- a/src/dispatch/plugins/dispatch_pagerduty/plugin.py +++ b/src/dispatch/plugins/dispatch_pagerduty/plugin.py @@ -83,12 +83,13 @@ def page( incident_description=incident_description, ) - def did_oncall_just_go_off_shift(self, schedule_id: str) -> Optional[dict]: + def did_oncall_just_go_off_shift(self, schedule_id: str, hour: int) -> Optional[dict]: client = APISession(self.configuration.api_key.get_secret_value()) client.url = self.configuration.pagerduty_api_url return oncall_shift_check( client=client, schedule_id=schedule_id, + hour=hour, ) def get_schedule_id_from_service_id(self, service_id: str) -> Optional[str]: diff --git a/src/dispatch/plugins/dispatch_pagerduty/service.py b/src/dispatch/plugins/dispatch_pagerduty/service.py index 130401e841e2..89079192ff40 100644 --- a/src/dispatch/plugins/dispatch_pagerduty/service.py +++ b/src/dispatch/plugins/dispatch_pagerduty/service.py @@ -177,9 +177,12 @@ def get_oncall_at_time(client: APISession, schedule_id: str, utctime: str) -> Op raise e -def oncall_shift_check(client: APISession, schedule_id: str) -> Optional[dict]: +def oncall_shift_check(client: APISession, schedule_id: str, hour: int) -> Optional[dict]: """Determines whether the oncall person just went off shift and returns their email.""" now = datetime.utcnow() + # in case scheduler is late, replace hour with exact one for shift comparison + now = now.replace(hour=hour, minute=0, second=0, microsecond=0) + # compare oncall person scheduled 18 hours ago vs 2 hours from now previous_shift = (now - timedelta(hours=18)).isoformat(timespec="minutes") + "Z" next_shift = (now + timedelta(hours=2)).isoformat(timespec="minutes") + "Z" diff --git a/src/dispatch/plugins/dispatch_slack/feedback/interactive.py b/src/dispatch/plugins/dispatch_slack/feedback/interactive.py index f0ba81afdb54..ea61d7665323 100644 --- a/src/dispatch/plugins/dispatch_slack/feedback/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/feedback/interactive.py @@ -186,7 +186,7 @@ def handle_incident_feedback_submission_event( ack_incident_feedback_submission_event(ack=ack) incident = incident_service.get(db_session=db_session, incident_id=context["subject"].id) - feedback = form_data.get(IncidentFeedbackNotificationBlockIds.feedback_input) + feedback = form_data.get(IncidentFeedbackNotificationBlockIds.feedback_input, "") rating = form_data.get(IncidentFeedbackNotificationBlockIds.rating_select, {}).get("value") feedback_in = FeedbackCreate( @@ -276,6 +276,7 @@ def oncall_shift_feedback_input( multiline=True, placeholder="How would you describe your experience?", ), + optional=True, label=label, **kwargs, ) @@ -381,7 +382,7 @@ def handle_oncall_shift_feedback_submission_event( ack_oncall_shift_feedback_submission_event(ack=ack) - feedback = form_data.get(ServiceFeedbackNotificationBlockIds.feedback_input) + feedback = form_data.get(ServiceFeedbackNotificationBlockIds.feedback_input, "") rating = form_data.get(ServiceFeedbackNotificationBlockIds.rating_select, {}).get("value") # metadata is organization_slug|project_id|schedule_id|shift_end_at|reminder_id @@ -447,7 +448,7 @@ def handle_oncall_shift_feedback_submission_event( ] try: plugin.instance.send_direct( - individual.email, + user.email, notification_text, notification_template, MessageType.service_feedback, diff --git a/src/dispatch/static/dispatch/src/service_feedback/api.js b/src/dispatch/static/dispatch/src/service_feedback/api.js new file mode 100644 index 000000000000..95b15f247874 --- /dev/null +++ b/src/dispatch/static/dispatch/src/service_feedback/api.js @@ -0,0 +1,9 @@ +import API from "@/api" + +const resource = "/service_feedback" + +export default { + getAll(options) { + return API.get(`${resource}`, { params: { ...options } }) + }, +} From a4a322ee8e7bc2af4e765cf232e9470742ec5a36 Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:12:36 -0700 Subject: [PATCH 13/24] Allowing filtering in incident dashboard by participant or commander (#3802) --- src/dispatch/database/service.py | 6 ++- .../incident/IncidentDialogFilter.vue | 36 ++++++++++++++ .../static/dispatch/src/router/utils.js | 48 ++++++++++++++++--- .../static/dispatch/src/search/utils.js | 9 +++- 4 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/dispatch/database/service.py b/src/dispatch/database/service.py index 03a3c8c39d7a..a7f6d297d640 100644 --- a/src/dispatch/database/service.py +++ b/src/dispatch/database/service.py @@ -504,12 +504,14 @@ def search_filter_sort_paginate( sort = False if sort_by else True query = search(query_str=query_str, query=query, model=model, sort=sort) - query = apply_model_specific_filters(model_cls, query, current_user, role) - if filter_spec: query = apply_filter_specific_joins(model_cls, filter_spec, query) query = apply_filters(query, filter_spec, model_cls) + if model == "Incident": + query_restricted = apply_model_specific_filters(model_cls, query, current_user, role) + query = query.intersect(query_restricted) + if sort_by: sort_spec = create_sort_spec(model, sort_by, descending) query = apply_sort(query, sort_spec) diff --git a/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue b/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue index 4c9bc4394fd8..28532489bf0a 100644 --- a/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue +++ b/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue @@ -45,6 +45,28 @@ + + + + Incident Participant + Show only incidents with this participant + + + + + @@ -70,6 +92,7 @@ import ProjectCombobox from "@/project/ProjectCombobox.vue" import RouterUtils from "@/router/utils" import SearchUtils from "@/search/utils" import TagFilterAutoComplete from "@/tag/TagFilterAutoComplete.vue" +import ParticipantSelect from "@/incident/ParticipantSelect.vue" let today = function () { let now = new Date() @@ -86,6 +109,7 @@ export default { IncidentTypeCombobox, ProjectCombobox, TagFilterAutoComplete, + ParticipantSelect, }, props: { @@ -118,6 +142,8 @@ export default { end: null, }, }, + local_participant_is_commander: false, + local_participant: null, } }, @@ -130,6 +156,7 @@ export default { this.filters.project.length, this.filters.status.length, this.filters.tag.length, + this.local_participant == null ? 0 : 1, 1, ]) }, @@ -138,6 +165,15 @@ export default { methods: { applyFilters() { + if (this.local_participant) { + if (this.local_participant_is_commander) { + this.filters.commander = this.local_participant + this.filters.participant = null + } else { + this.filters.commander = null + this.filters.participant = this.local_participant + } + } RouterUtils.updateURLFilters(this.filters) this.fetchData() // we close the dialog diff --git a/src/dispatch/static/dispatch/src/router/utils.js b/src/dispatch/static/dispatch/src/router/utils.js index 081d8b8f64e1..ccd5c72c20ef 100644 --- a/src/dispatch/static/dispatch/src/router/utils.js +++ b/src/dispatch/static/dispatch/src/router/utils.js @@ -14,17 +14,33 @@ export default { return } each(value, function (item) { - if (has(flatFilters, key)) { - if (typeof item === "string" || item instanceof String) { - flatFilters[key].push(item) + if (["commander", "participant"].includes(key)) { + if (has(flatFilters, key)) { + if (typeof item === "string" || item instanceof String) { + flatFilters[key].push(item) + } else { + flatFilters[key].push(item.email) + } } else { - flatFilters[key].push(item.name) + if (typeof item === "string" || item instanceof String) { + flatFilters[key] = [item] + } else { + flatFilters[key] = [item.email] + } } } else { - if (typeof item === "string" || item instanceof String) { - flatFilters[key] = [item] + if (has(flatFilters, key)) { + if (typeof item === "string" || item instanceof String) { + flatFilters[key].push(item) + } else { + flatFilters[key].push(item.name) + } } else { - flatFilters[key] = [item.name] + if (typeof item === "string" || item instanceof String) { + flatFilters[key] = [item] + } else { + flatFilters[key] = [item.name] + } } } }) @@ -69,6 +85,24 @@ export default { } return } + if (["commander", "participant"].includes(key)) { + if (typeof value === "string" || value instanceof String) { + if (has(filters, key)) { + filters[key].push({ email: value }) + } else { + filters[key] = [{ email: value }] + } + } else { + each(value, function (item) { + if (has(filters, key)) { + filters[key].push({ email: item }) + } else { + filters[key] = [{ email: item }] + } + }) + } + return + } if (typeof value === "string" || value instanceof String) { if (has(filters, key)) { filters[key].push({ name: value }) diff --git a/src/dispatch/static/dispatch/src/search/utils.js b/src/dispatch/static/dispatch/src/search/utils.js index 3d1be804615f..20438a2c5183 100644 --- a/src/dispatch/static/dispatch/src/search/utils.js +++ b/src/dispatch/static/dispatch/src/search/utils.js @@ -118,7 +118,14 @@ export default { if (!value) { return } - if (has(value, "id")) { + if (["commander", "participant"].includes(key) && has(value, "email")) { + subFilter.push({ + model: toPascalCase(key), + field: "email", + op: "==", + value: value.email, + }) + } else if (has(value, "id")) { subFilter.push({ model: toPascalCase(key), field: "id", From 8dfae58663d9ff9828c0bf9bfa7de8468f467a26 Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:28:27 -0700 Subject: [PATCH 14/24] Fixing restricted query statement (#3822) --- src/dispatch/database/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dispatch/database/service.py b/src/dispatch/database/service.py index a7f6d297d640..e404b96a7d7b 100644 --- a/src/dispatch/database/service.py +++ b/src/dispatch/database/service.py @@ -504,12 +504,13 @@ def search_filter_sort_paginate( sort = False if sort_by else True query = search(query_str=query_str, query=query, model=model, sort=sort) + query_restricted = apply_model_specific_filters(model_cls, query, current_user, role) + if filter_spec: query = apply_filter_specific_joins(model_cls, filter_spec, query) query = apply_filters(query, filter_spec, model_cls) if model == "Incident": - query_restricted = apply_model_specific_filters(model_cls, query, current_user, role) query = query.intersect(query_restricted) if sort_by: From d63554e40a436a5dcb439fdab9f39ffdbc7bedbb Mon Sep 17 00:00:00 2001 From: Will Sheldon <114631109+wssheldon@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:42:14 -0700 Subject: [PATCH 15/24] Adds `Experimental Features` toggle for built-in feature flags (#3818) * Add db revision * Adds experimental toggle to User model, and front-end toggle, for feature flag * Update color, margin --- src/dispatch/auth/models.py | 3 ++ src/dispatch/auth/service.py | 3 ++ .../core/versions/2023-09-27_5c60513d6e5e.py | 28 +++++++++++++++++++ .../static/dispatch/src/auth/store.js | 13 +++++++++ .../dispatch/src/components/AppToolbar.vue | 25 ++++++++++++++++- 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/dispatch/database/revisions/core/versions/2023-09-27_5c60513d6e5e.py diff --git a/src/dispatch/auth/models.py b/src/dispatch/auth/models.py index d6cd1828d2d8..56827f9ca588 100644 --- a/src/dispatch/auth/models.py +++ b/src/dispatch/auth/models.py @@ -54,6 +54,7 @@ class DispatchUser(Base, TimeStampMixin): email = Column(String, unique=True) password = Column(LargeBinary, nullable=False) last_mfa_time = Column(DateTime, nullable=True) + experimental_features = Column(Boolean, default=False) # relationships events = relationship("Event", backref="dispatch_user") @@ -157,6 +158,7 @@ class UserLoginResponse(DispatchBase): class UserRead(UserBase): id: PrimaryKey role: Optional[str] = Field(None, nullable=True) + experimental_features: Optional[bool] class UserUpdate(DispatchBase): @@ -164,6 +166,7 @@ class UserUpdate(DispatchBase): password: Optional[str] = Field(None, nullable=True) projects: Optional[List[UserProject]] organizations: Optional[List[UserOrganization]] + experimental_features: Optional[bool] role: Optional[str] = Field(None, nullable=True) @validator("password", pre=True) diff --git a/src/dispatch/auth/service.py b/src/dispatch/auth/service.py index 29f8ca5be8ee..8d2b57ef1385 100644 --- a/src/dispatch/auth/service.py +++ b/src/dispatch/auth/service.py @@ -245,6 +245,9 @@ def update(*, db_session, user: DispatchUser, user_in: UserUpdate) -> DispatchUs ) ) + if experimental_features := user_in.experimental_features: + user.experimental_features = experimental_features + db_session.commit() return user diff --git a/src/dispatch/database/revisions/core/versions/2023-09-27_5c60513d6e5e.py b/src/dispatch/database/revisions/core/versions/2023-09-27_5c60513d6e5e.py new file mode 100644 index 000000000000..c124e41afa3c --- /dev/null +++ b/src/dispatch/database/revisions/core/versions/2023-09-27_5c60513d6e5e.py @@ -0,0 +1,28 @@ +"""Adds last_mfa_time to DispatchUser + +Revision ID: 5c60513d6e5e +Revises: 3dd4d12844dc +Create Date: 2023-09-27 15:17:00.450716 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "5c60513d6e5e" +down_revision = "3dd4d12844dc" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("dispatch_user", sa.Column("experimental_features", sa.Boolean(), default=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("dispatch_user", "experimental_features") + # ### end Alembic commands ### diff --git a/src/dispatch/static/dispatch/src/auth/store.js b/src/dispatch/static/dispatch/src/auth/store.js index 1bf94015d960..b8cafad588ee 100644 --- a/src/dispatch/static/dispatch/src/auth/store.js +++ b/src/dispatch/static/dispatch/src/auth/store.js @@ -25,6 +25,7 @@ const state = { email: "", projects: [], role: null, + experimental_features: false, }, selected: { ...getDefaultSelectedState(), @@ -148,6 +149,15 @@ const actions = { commit("SET_USER_LOGOUT") router.go() }, + getExperimentalFeatures({ commit }) { + UserApi.getUserInfo() + .then((response) => { + commit("SET_EXPERIMENTAL_FEATURES", response.data.experimental_features) + }) + .catch((error) => { + console.error("Error occurred while updating experimental features: ", error) + }) + }, createExpirationCheck({ state, commit }) { // expiration time minus 10 min let expire_at = subMinutes(fromUnixTime(state.currentUser.exp), 10) @@ -195,6 +205,9 @@ const mutations = { } localStorage.setItem("token", token) }, + SET_EXPERIMENTAL_FEATURES(state, value) { + state.currentUser.experimental_features = value + }, SET_USER_LOGOUT(state) { state.currentUser = { loggedIn: false } }, diff --git a/src/dispatch/static/dispatch/src/components/AppToolbar.vue b/src/dispatch/static/dispatch/src/components/AppToolbar.vue index f98c0e80d440..78aaad9c1a61 100644 --- a/src/dispatch/static/dispatch/src/components/AppToolbar.vue +++ b/src/dispatch/static/dispatch/src/components/AppToolbar.vue @@ -112,6 +112,16 @@ + Experimental Features + + Organizations @@ -159,6 +169,7 @@ import { mapActions, mapGetters, mapMutations, mapState } from "vuex" import Util from "@/util" import OrganizationApi from "@/organization/api" import OrganizationCreateEditDialog from "@/organization/CreateEditDialog.vue" +import UserApi from "@/auth/api" export default { name: "AppToolbar", @@ -181,6 +192,16 @@ export default { }, }, methods: { + updateExperimentalFeatures() { + UserApi.getUserInfo() + .then((response) => { + let userId = response.data.id + UserApi.update(userId, { id: userId, experimental_features: this.experimental_features }) + }) + .catch((error) => { + console.error("Error occurred while updating experimental features: ", error) + }) + }, handleDrawerToggle() { this.$store.dispatch("app/toggleDrawer") }, @@ -203,7 +224,7 @@ export default { }, ...mapState("auth", ["currentUser"]), ...mapState("app", ["currentVersion"]), - ...mapActions("auth", ["logout"]), + ...mapActions("auth", ["logout", "getExperimentalFeatures"]), ...mapActions("search", ["setQuery"]), ...mapActions("organization", ["showCreateEditDialog"]), ...mapActions("app", ["showCommitMessage"]), @@ -233,6 +254,8 @@ export default { this.organizations = response.data.items this.loading = false }) + + this.getExperimentalFeatures() }, } From e2d6ec7ce1c7d37d1da8f2c2282a87a1796593de Mon Sep 17 00:00:00 2001 From: kevgliss Date: Sat, 30 Sep 2023 21:04:02 -0700 Subject: [PATCH 16/24] Enhancement/case reminders (#3796) * Adds case status reminders * Adds scheduling * Update src/dispatch/case/messaging.py Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> * Update src/dispatch/messaging/strings.py Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> * Update src/dispatch/cli.py Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> * Update src/dispatch/messaging/strings.py Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> --------- Co-authored-by: Marc Vilanova <39573146+mvilanova@users.noreply.github.com> Co-authored-by: David Whittaker <84562015+whitdog47@users.noreply.github.com> --- src/dispatch/case/messaging.py | 87 +++++++++++++++++++++++++++++++ src/dispatch/case/scheduled.py | 47 +++++++++++++++++ src/dispatch/cli.py | 1 + src/dispatch/messaging/strings.py | 49 +++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 src/dispatch/case/messaging.py create mode 100644 src/dispatch/case/scheduled.py diff --git a/src/dispatch/case/messaging.py b/src/dispatch/case/messaging.py new file mode 100644 index 000000000000..9cb7780f6e1d --- /dev/null +++ b/src/dispatch/case/messaging.py @@ -0,0 +1,87 @@ +""" +.. module: dispatch.case.messaging + :platform: Unix + :copyright: (c) 2019 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import logging + +from dispatch.database.core import SessionLocal +from dispatch.case.models import Case +from dispatch.messaging.strings import ( + CASE_CLOSE_REMINDER, + CASE_TRIAGE_REMINDER, + MessageType, +) +from dispatch.plugin import service as plugin_service + + +log = logging.getLogger(__name__) + + +def send_case_close_reminder(case: Case, db_session: SessionLocal): + """ + Sends a direct message to the assignee reminding them to close the case if possible. + """ + message_text = "Case Close Reminder" + message_template = CASE_CLOSE_REMINDER + + plugin = plugin_service.get_active_instance( + db_session=db_session, project_id=case.project.id, plugin_type="conversation" + ) + if not plugin: + log.warning("Case close reminder message not sent. No conversation plugin enabled.") + return + + items = [ + { + "name": case.name, + "ticket_weblink": case.ticket.weblink, + "title": case.title, + "status": case.status, + } + ] + + plugin.instance.send_direct( + case.assignee.individual.email, + message_text, + message_template, + MessageType.case_status_reminder, + items=items, + ) + + log.debug(f"Case close reminder sent to {case.assignee.individual.email}.") + + +def send_case_triage_reminder(case: Case, db_session: SessionLocal): + """ + Sends a direct message to the assignee reminding them to triage the case if possible. + """ + message_text = "Case Triage Reminder" + message_template = CASE_TRIAGE_REMINDER + + plugin = plugin_service.get_active_instance( + db_session=db_session, project_id=case.project.id, plugin_type="conversation" + ) + if not plugin: + log.warning("Case triage reminder message not sent. No conversation plugin enabled.") + return + + items = [ + { + "name": case.name, + "ticket_weblink": case.ticket.weblink, + "title": case.title, + "status": case.status, + } + ] + + plugin.instance.send_direct( + case.assignee.individual.email, + message_text, + message_template, + MessageType.case_status_reminder, + items=items, + ) + + log.debug(f"Case triage reminder sent to {case.assignee.individual.email}.") diff --git a/src/dispatch/case/scheduled.py b/src/dispatch/case/scheduled.py new file mode 100644 index 000000000000..fa386bcf085e --- /dev/null +++ b/src/dispatch/case/scheduled.py @@ -0,0 +1,47 @@ +from datetime import datetime, date +from schedule import every + +from dispatch.database.core import SessionLocal +from dispatch.decorators import scheduled_project_task, timer +from dispatch.project.models import Project +from dispatch.scheduler import scheduler + +from .enums import CaseStatus +from .messaging import send_case_close_reminder, send_case_triage_reminder +from .service import ( + get_all_by_status, +) + + +@scheduler.add(every(1).day.at("18:00"), name="case-close-reminder") +@timer +@scheduled_project_task +def case_close_reminder(db_session: SessionLocal, project: Project): + """Sends a reminder to the case assignee to close out their case.""" + cases = get_all_by_status( + db_session=db_session, project_id=project.id, status=CaseStatus.triage + ) + + for case in cases: + span = datetime.utcnow() - case.triage_at + q, r = divmod(span.days, 7) + if q >= 1 and date.today().isoweekday() == 1: + # we only send the reminder for cases that have been triaging + # longer than a week and only on Mondays + send_case_close_reminder(case, db_session) + + +@scheduler.add(every(1).day.at("18:00"), name="case-triage-reminder") +@timer +@scheduled_project_task +def case_triage_reminder(db_session: SessionLocal, project: Project): + """Sends a reminder to the case assignee to triage their case.""" + cases = get_all_by_status(db_session=db_session, project_id=project.id, status=CaseStatus.new) + + # if we want more specific SLA reminders, we would need to add additional data model + for case in cases: + span = datetime.utcnow() - case.created + q, r = divmod(span.days, 1) + if q >= 1: + # we only send one reminder per case per day + send_case_triage_reminder(case, db_session) diff --git a/src/dispatch/cli.py b/src/dispatch/cli.py index 95ae7d5a3f96..689e3ec9c66b 100644 --- a/src/dispatch/cli.py +++ b/src/dispatch/cli.py @@ -640,6 +640,7 @@ def dispatch_scheduler(): ) from .term.scheduled import sync_terms # noqa from .workflow.scheduled import sync_workflows # noqa + from .case.scheduled import case_triage_reminder, case_close_reminder # noqa @dispatch_scheduler.command("list") diff --git a/src/dispatch/messaging/strings.py b/src/dispatch/messaging/strings.py index 0facbef81ce4..877dac1ce69b 100644 --- a/src/dispatch/messaging/strings.py +++ b/src/dispatch/messaging/strings.py @@ -5,6 +5,7 @@ from dispatch.messaging.email.filters import env from dispatch.conversation.enums import ConversationButtonActions from dispatch.incident.enums import IncidentStatus +from dispatch.case.enums import CaseStatus from dispatch.enums import Visibility from dispatch import config @@ -34,6 +35,7 @@ class MessageType(DispatchEnum): incident_tactical_report = "incident-tactical-report" incident_task_list = "incident-task-list" incident_task_reminder = "incident-task-reminder" + case_status_reminder = "case-status-reminder" service_feedback = "service-feedback" @@ -43,6 +45,13 @@ class MessageType(DispatchEnum): IncidentStatus.closed: "This no longer requires additional involvement, long term incident action items have been assigned to their respective owners.", } +CASE_STATUS_DESCRIPTIONS = { + CaseStatus.new: "This case is new and needs triaging.", + CaseStatus.triage: "This case is being triaged.", + CaseStatus.escalated: "This case has been escalated.", + CaseStatus.closed: "This case has been closed.", +} + INCIDENT_VISIBILITY_DESCRIPTIONS = { Visibility.open: "We ask that you use your best judgment while sharing details about this incident outside of the dedicated channels of communication. Please reach out to the Incident Commander if you have any questions.", Visibility.restricted: "This incident is restricted to immediate participants of this incident. We ask that you exercise extra caution and discretion while talking about this incident outside of the dedicated channels of communication. Only invite new participants that are strictly necessary. Please reach out to the Incident Commander if you have any questions.", @@ -236,6 +245,16 @@ class MessageType(DispatchEnum): "\n", " " ).strip() +CASE_TRIAGE_REMINDER_DESCRIPTION = """The status of this case hasn't been updated recently. +Please ensure you triage the case based on its priority.""".replace( + "\n", " " +).strip() + +CASE_CLOSE_REMINDER_DESCRIPTION = """The status of this case hasn't been updated recently. +You can use the case 'Resolve' button if it has been resolved and can be closed.""".replace( + "\n", " " +).strip() + INCIDENT_TASK_NEW_DESCRIPTION = """ The following incident task has been created and assigned to you by {{task_creator}}: {{task_description}}""" @@ -390,6 +409,14 @@ class MessageType(DispatchEnum): INCIDENT_TITLE = {"title": "Title", "text": "{{title}}"} +CASE_TITLE = {"title": "Title", "text": "{{title}}"} + +CASE_STATUS = { + "title": "Status - {{status}}", + "status_mapping": CASE_STATUS_DESCRIPTIONS, +} + + if config.DISPATCH_MARKDOWN_IN_INCIDENT_DESC: INCIDENT_DESCRIPTION = {"title": "Description", "text": "{{description | markdown}}"} else: @@ -596,6 +623,28 @@ class MessageType(DispatchEnum): INCIDENT_STATUS, ] + +CASE_CLOSE_REMINDER = [ + { + "title": "{{name}} Case - Close Reminder", + "title_link": "{{ticket_weblink}}", + "text": CASE_CLOSE_REMINDER_DESCRIPTION, + }, + CASE_TITLE, + CASE_STATUS, +] + +CASE_TRIAGE_REMINDER = [ + { + "title": "{{name}} Case - Triage Reminder", + "title_link": "{{ticket_weblink}}", + "text": CASE_TRIAGE_REMINDER_DESCRIPTION, + }, + CASE_TITLE, + CASE_STATUS, +] + + INCIDENT_TASK_REMINDER = [ {"title": "Incident - {{ name }}", "text": "{{ title }}"}, {"title": "Creator", "text": "{{ creator }}"}, From 445049c3d5156000412eb4e1a1cdf6b9c181b7d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:34:31 -0700 Subject: [PATCH 17/24] Bump ipython from 8.16.0 to 8.16.1 (#3826) Bumps [ipython](https://github.com/ipython/ipython) from 8.16.0 to 8.16.1. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/commits) --- updated-dependencies: - dependency-name: ipython dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c8d639b2ce32..e348450adb31 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -44,7 +44,7 @@ identify==2.5.27 # via pre-commit iniconfig==2.0.0 # via pytest -ipython==8.16.0 +ipython==8.16.1 # via -r requirements-dev.in jedi==0.19.0 # via ipython From a2cc301764dc7f6eb6a23e1b3d5705459c9c59d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:34:39 -0700 Subject: [PATCH 18/24] Bump schedule from 1.2.0 to 1.2.1 (#3825) Bumps [schedule](https://github.com/dbader/schedule) from 1.2.0 to 1.2.1. - [Changelog](https://github.com/dbader/schedule/blob/master/HISTORY.rst) - [Commits](https://github.com/dbader/schedule/compare/1.2.0...1.2.1) --- updated-dependencies: - dependency-name: schedule dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index 0775b5d9bc6e..c6599d9ec3fa 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -358,7 +358,7 @@ rsa==4.9 # google-auth # oauth2client # python-jose -schedule==1.2.0 +schedule==1.2.1 # via -r requirements-base.in schemathesis==3.19.7 # via -r requirements-base.in From c2ff7e9e7409da1bd1f8280cacfba0a3b30b4604 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:34:48 -0700 Subject: [PATCH 19/24] Bump apexcharts from 3.42.0 to 3.43.0 in /src/dispatch/static/dispatch (#3824) Bumps [apexcharts](https://github.com/apexcharts/apexcharts.js) from 3.42.0 to 3.43.0. - [Release notes](https://github.com/apexcharts/apexcharts.js/releases) - [Commits](https://github.com/apexcharts/apexcharts.js/compare/v3.42.0...v3.43.0) --- updated-dependencies: - dependency-name: apexcharts dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/dispatch/static/dispatch/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dispatch/static/dispatch/package-lock.json b/src/dispatch/static/dispatch/package-lock.json index 85d77f4e0cc1..1b261d3beff4 100644 --- a/src/dispatch/static/dispatch/package-lock.json +++ b/src/dispatch/static/dispatch/package-lock.json @@ -1386,9 +1386,9 @@ } }, "node_modules/apexcharts": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.42.0.tgz", - "integrity": "sha512-hYhzZqh2Efny9uiutkGU2M/EarJ4Nn8s6dxZ0C7E7N+SV4d1xjTioXi2NLn4UKVJabZkb3HnpXDoumXgtAymwg==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.43.0.tgz", + "integrity": "sha512-YPw1aLatPQMUqVLMp5d+LDaXFi4QrRQND72/XO7/2NJdg+R5MjE9sifJ0GzOfgoZM7ltBUTjwfSxIvwR/9V8yw==", "dependencies": { "@yr/monotone-cubic-spline": "^1.0.3", "svg.draggable.js": "^2.2.2", @@ -7743,9 +7743,9 @@ } }, "apexcharts": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.42.0.tgz", - "integrity": "sha512-hYhzZqh2Efny9uiutkGU2M/EarJ4Nn8s6dxZ0C7E7N+SV4d1xjTioXi2NLn4UKVJabZkb3HnpXDoumXgtAymwg==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.43.0.tgz", + "integrity": "sha512-YPw1aLatPQMUqVLMp5d+LDaXFi4QrRQND72/XO7/2NJdg+R5MjE9sifJ0GzOfgoZM7ltBUTjwfSxIvwR/9V8yw==", "requires": { "@yr/monotone-cubic-spline": "^1.0.3", "svg.draggable.js": "^2.2.2", From 5488b0b647034e83ee6bd21672c9209f734a8cdf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:34:56 -0700 Subject: [PATCH 20/24] Bump @monaco-editor/loader in /src/dispatch/static/dispatch (#3823) Bumps [@monaco-editor/loader](https://github.com/suren-atoyan/monaco-loader) from 1.3.3 to 1.4.0. - [Release notes](https://github.com/suren-atoyan/monaco-loader/releases) - [Changelog](https://github.com/suren-atoyan/monaco-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/suren-atoyan/monaco-loader/compare/v1.3.3...v1.4.0) --- updated-dependencies: - dependency-name: "@monaco-editor/loader" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/dispatch/static/dispatch/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dispatch/static/dispatch/package-lock.json b/src/dispatch/static/dispatch/package-lock.json index 1b261d3beff4..dadfccd0d4e6 100644 --- a/src/dispatch/static/dispatch/package-lock.json +++ b/src/dispatch/static/dispatch/package-lock.json @@ -757,9 +757,9 @@ "optional": true }, "node_modules/@monaco-editor/loader": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz", - "integrity": "sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", "dependencies": { "state-local": "^1.0.6" }, @@ -7201,9 +7201,9 @@ "optional": true }, "@monaco-editor/loader": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz", - "integrity": "sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", "requires": { "state-local": "^1.0.6" } From 62c3ff3840119bfd5a202a3283dd5bb560aeb3f2 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 2 Oct 2023 11:45:07 -0700 Subject: [PATCH 21/24] Fixing var (#3827) --- src/dispatch/case/scheduled.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatch/case/scheduled.py b/src/dispatch/case/scheduled.py index fa386bcf085e..e3f419cc5265 100644 --- a/src/dispatch/case/scheduled.py +++ b/src/dispatch/case/scheduled.py @@ -40,7 +40,7 @@ def case_triage_reminder(db_session: SessionLocal, project: Project): # if we want more specific SLA reminders, we would need to add additional data model for case in cases: - span = datetime.utcnow() - case.created + span = datetime.utcnow() - case.created_at q, r = divmod(span.days, 1) if q >= 1: # we only send one reminder per case per day From beec5de2e3080175e9a6877ee7fb03f2364a30df Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 2 Oct 2023 13:02:22 -0700 Subject: [PATCH 22/24] Process Signal SQS (#3813) * Initial work * Fixes entities undefined * Fixes * Removes scheduled consumers in favor of long running processes * Fix name --------- Co-authored-by: Will Sheldon <114631109+wssheldon@users.noreply.github.com> --- requirements-base.in | 3 +- setup.py | 1 + src/dispatch/cli.py | 112 +++++++++++------- src/dispatch/plugins/dispatch_aws/__init__.py | 1 + src/dispatch/plugins/dispatch_aws/_version.py | 1 + src/dispatch/plugins/dispatch_aws/config.py | 29 +++++ src/dispatch/plugins/dispatch_aws/plugin.py | 78 ++++++++++++ src/dispatch/signal/exceptions.py | 13 ++ src/dispatch/signal/models.py | 3 +- src/dispatch/signal/scheduled.py | 50 -------- src/dispatch/signal/service.py | 62 ++++++++++ 11 files changed, 261 insertions(+), 92 deletions(-) create mode 100644 src/dispatch/plugins/dispatch_aws/__init__.py create mode 100644 src/dispatch/plugins/dispatch_aws/_version.py create mode 100644 src/dispatch/plugins/dispatch_aws/config.py create mode 100644 src/dispatch/plugins/dispatch_aws/plugin.py create mode 100644 src/dispatch/signal/exceptions.py delete mode 100644 src/dispatch/signal/scheduled.py diff --git a/requirements-base.in b/requirements-base.in index e6834e4d73dd..95003484d6f4 100644 --- a/requirements-base.in +++ b/requirements-base.in @@ -6,6 +6,7 @@ atlassian-python-api==3.32.0 attrs==22.1.0 bcrypt blockkit +boto3 cachetools chardet click @@ -44,8 +45,8 @@ schemathesis sentry-asgi sentry-sdk sh -slack-bolt slack_sdk +slack-bolt slowapi spacy sqlalchemy-filters diff --git a/setup.py b/setup.py index 0314fe703c4e..ef838342801a 100644 --- a/setup.py +++ b/setup.py @@ -404,6 +404,7 @@ def run(self): "dispatch.plugins": [ "dispatch_atlassian_confluence = dispatch.plugins.dispatch_atlassian_confluence.plugin:ConfluencePagePlugin", "dispatch_atlassian_confluence_document = dispatch.plugins.dispatch_atlassian_confluence.docs.plugin:ConfluencePageDocPlugin", + "dispatch_aws_sqs = dispatch.plugins.dispatch_aws.plugin:AWSSQSSignalConsumerPlugin", "dispatch_basic_auth = dispatch.plugins.dispatch_core.plugin:BasicAuthProviderPlugin", "dispatch_contact = dispatch.plugins.dispatch_core.plugin:DispatchContactPlugin", "dispatch_document_resolver = dispatch.plugins.dispatch_core.plugin:DispatchDocumentResolverPlugin", diff --git a/src/dispatch/cli.py b/src/dispatch/cli.py index 689e3ec9c66b..0675e157cbec 100644 --- a/src/dispatch/cli.py +++ b/src/dispatch/cli.py @@ -3,13 +3,13 @@ import click import uvicorn + from dispatch import __version__, config from dispatch.enums import UserRoles from dispatch.plugin.models import PluginInstance -from .scheduler import scheduler from .extensions import configure_extensions - +from .scheduler import scheduler os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" @@ -80,10 +80,10 @@ def list_plugins(): ) def install_plugins(force): """Installs all plugins, or only one.""" + from dispatch.common.utils.cli import install_plugins from dispatch.database.core import SessionLocal from dispatch.plugin import service as plugin_service from dispatch.plugin.models import Plugin - from dispatch.common.utils.cli import install_plugins from dispatch.plugins.base import plugins install_plugins() @@ -162,9 +162,9 @@ def dispatch_user(): ) def register_user(email: str, role: str, password: str, organization: str): """Registers a new user.""" - from dispatch.database.core import refetch_db_session from dispatch.auth import service as user_service - from dispatch.auth.models import UserRegister, UserOrganization + from dispatch.auth.models import UserOrganization, UserRegister + from dispatch.database.core import refetch_db_session db_session = refetch_db_session(organization_slug=organization) user = user_service.get_by_email(email=email, db_session=db_session) @@ -198,9 +198,9 @@ def register_user(email: str, role: str, password: str, organization: str): ) def update_user(email: str, role: str, organization: str): """Updates a user's roles.""" - from dispatch.database.core import SessionLocal from dispatch.auth import service as user_service - from dispatch.auth.models import UserUpdate, UserOrganization + from dispatch.auth.models import UserOrganization, UserUpdate + from dispatch.database.core import SessionLocal db_session = SessionLocal() user = user_service.get_by_email(email=email, db_session=db_session) @@ -222,9 +222,9 @@ def update_user(email: str, role: str, organization: str): @click.password_option() def reset_user_password(email: str, password: str): """Resets a user's password.""" - from dispatch.database.core import SessionLocal from dispatch.auth import service as user_service from dispatch.auth.models import UserUpdate + from dispatch.database.core import SessionLocal db_session = SessionLocal() user = user_service.get_by_email(email=email, db_session=db_session) @@ -249,9 +249,7 @@ def database_init(): """Initializes a new database.""" click.echo("Initializing new database...") from .database.core import engine - from .database.manage import ( - init_database, - ) + from .database.manage import init_database init_database(engine) click.secho("Success.", fg="green") @@ -265,12 +263,13 @@ def database_init(): ) def restore_database(dump_file): """Restores the database via psql.""" - from sh import psql, createdb, ErrorReturnCode_1 + from sh import ErrorReturnCode_1, createdb, psql + from dispatch.config import ( + DATABASE_CREDENTIALS, DATABASE_HOSTNAME, DATABASE_NAME, DATABASE_PORT, - DATABASE_CREDENTIALS, ) username, password = str(DATABASE_CREDENTIALS).split(":") @@ -318,11 +317,12 @@ def restore_database(dump_file): def dump_database(dump_file): """Dumps the database via pg_dump.""" from sh import pg_dump + from dispatch.config import ( + DATABASE_CREDENTIALS, DATABASE_HOSTNAME, DATABASE_NAME, DATABASE_PORT, - DATABASE_CREDENTIALS, ) username, password = str(DATABASE_CREDENTIALS).split(":") @@ -345,7 +345,7 @@ def dump_database(dump_file): @click.option("--yes", is_flag=True, help="Silences all confirmation prompts.") def drop_database(yes): """Drops all data in database.""" - from sqlalchemy_utils import drop_database, database_exists + from sqlalchemy_utils import database_exists, drop_database if database_exists(str(config.SQLALCHEMY_DATABASE_URI)): if yes: @@ -378,10 +378,10 @@ def drop_database(yes): def upgrade_database(tag, sql, revision, revision_type): """Upgrades database schema to newest version.""" import sqlalchemy - from sqlalchemy import inspect - from sqlalchemy_utils import database_exists from alembic import command as alembic_command from alembic.config import Config as AlembicConfig + from sqlalchemy import inspect + from sqlalchemy_utils import database_exists from .database.core import engine from .database.manage import init_database @@ -570,6 +570,7 @@ def revision_database( ): """Create new database revision.""" import types + from alembic import command as alembic_command from alembic.config import Config as AlembicConfig @@ -623,20 +624,15 @@ def dispatch_scheduler(): from .evergreen.scheduled import create_evergreen_reminders # noqa from .feedback.incident.scheduled import feedback_report_daily # noqa from .feedback.service.scheduled import oncall_shift_feedback # noqa - from .incident_cost.scheduled import calculate_incidents_response_cost # noqa - from .incident.scheduled import ( # noqa - incident_auto_tagger, - incident_close_reminder, - incident_report_daily, + from .incident.scheduled import ( + incident_auto_tagger, # noqa ) + from .incident_cost.scheduled import calculate_incidents_response_cost # noqa from .monitor.scheduled import sync_active_stable_monitors # noqa from .report.scheduled import incident_report_reminders # noqa - from .signal.scheduled import consume_signals # noqa - from .tag.scheduled import sync_tags, build_tag_models # noqa - from .task.scheduled import ( # noqa - create_incident_tasks_reminders, - sync_incident_tasks_daily, - sync_active_stable_incident_tasks, + from .tag.scheduled import build_tag_models, sync_tags # noqa + from .task.scheduled import ( + create_incident_tasks_reminders, # noqa ) from .term.scheduled import sync_terms # noqa from .workflow.scheduled import sync_workflows # noqa @@ -662,6 +658,7 @@ def list_tasks(): def start_tasks(tasks, exclude, eager): """Starts the scheduler.""" import signal + from dispatch.common.utils.cli import install_plugins install_plugins() @@ -705,6 +702,7 @@ def dispatch_server(): def show_routes(): """Prints all available routes.""" from tabulate import tabulate + from dispatch.main import api_router table = [] @@ -717,9 +715,11 @@ def show_routes(): @dispatch_server.command("config") def show_config(): """Prints the current config as dispatch sees it.""" - import sys import inspect + import sys + from tabulate import tabulate + from dispatch import config func_members = inspect.getmembers(sys.modules[config.__name__]) @@ -772,15 +772,51 @@ def signals_group(): pass +@signals_group.command("consume") +@click.argument("organization") +@click.argument("project") +@click.argument("plugin_name") +def consume_signals(organization, project, plugin_name): # TODO support multiple from one command + """Runs a continuous process that consumes signals from the specified plugin.""" + from dispatch.common.utils.cli import install_plugins + from dispatch.database.core import refetch_db_session + from dispatch.project import service as project_service + from dispatch.project.models import ProjectRead + from dispatch.plugin import service as plugin_service + + install_plugins() + + session = refetch_db_session(organization) + + project = project_service.get_by_name_or_raise( + db_session=session, project_in=ProjectRead(name=project) + ) + + plugins = plugin_service.get_active_instances( + db_session=session, plugin_type="signal-consumer", project_id=project.id + ) + + if not plugins: + log.debug( + "No signals consumed. No signal-consumer plugins enabled. Project: {project.name}. Organization: {project.organization.name}" + ) + return + + for plugin in plugins: + if plugin.plugin.slug == plugin_name: + plugin.instance.consume(db_session=session, project=project) + + @signals_group.command("process") def process_signals(): """Runs a continuous process that does additional processing on newly created signals.""" from sqlalchemy import asc - from dispatch.database.core import sessionmaker, engine, SessionLocal - from dispatch.signal.models import SignalInstance + + from dispatch.common.utils.cli import install_plugins + from dispatch.database.core import SessionLocal, engine, sessionmaker from dispatch.organization.service import get_all as get_all_organizations from dispatch.signal import flows as signal_flows - from dispatch.common.utils.cli import install_plugins + from dispatch.signal.models import SignalInstance install_plugins() @@ -819,20 +855,15 @@ def process_signals(): @click.argument("project") def run_slack_websocket(organization: str, project: str): """Runs the slack websocket process.""" - from sqlalchemy import true - from slack_bolt.adapter.socket_mode import SocketModeHandler + from sqlalchemy import true - from dispatch.database.core import refetch_db_session from dispatch.common.utils.cli import install_plugins + from dispatch.database.core import refetch_db_session from dispatch.plugins.dispatch_slack.bolt import app + from dispatch.plugins.dispatch_slack.case.interactive import configure as case_configure from dispatch.plugins.dispatch_slack.incident.interactive import configure as incident_configure - from dispatch.plugins.dispatch_slack.feedback.interactive import ( # noqa - configure as feedback_configure, - ) from dispatch.plugins.dispatch_slack.workflow import configure as workflow_configure - from dispatch.plugins.dispatch_slack.case.interactive import configure as case_configure - from dispatch.project import service as project_service from dispatch.project.models import ProjectRead @@ -884,6 +915,7 @@ def run_slack_websocket(organization: str, project: str): def shell(ipython_args): """Starts an ipython shell importing our app. Useful for debugging.""" import sys + import IPython from IPython.terminal.ipapp import load_default_config diff --git a/src/dispatch/plugins/dispatch_aws/__init__.py b/src/dispatch/plugins/dispatch_aws/__init__.py new file mode 100644 index 000000000000..ad5cc752c07b --- /dev/null +++ b/src/dispatch/plugins/dispatch_aws/__init__.py @@ -0,0 +1 @@ +from ._version import __version__ # noqa diff --git a/src/dispatch/plugins/dispatch_aws/_version.py b/src/dispatch/plugins/dispatch_aws/_version.py new file mode 100644 index 000000000000..3dc1f76bc69e --- /dev/null +++ b/src/dispatch/plugins/dispatch_aws/_version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/src/dispatch/plugins/dispatch_aws/config.py b/src/dispatch/plugins/dispatch_aws/config.py new file mode 100644 index 000000000000..d14e3f58ae2a --- /dev/null +++ b/src/dispatch/plugins/dispatch_aws/config.py @@ -0,0 +1,29 @@ +from pydantic import Field +from dispatch.config import BaseConfigurationModel + + +class AWSSQSConfiguration(BaseConfigurationModel): + """Signal SQS configuration""" + + queue_name: str = Field( + title="Queue Name", + description="Queue Name, not the ARN.", + ) + + queue_owner: str = Field( + title="Queue Owner", + description="Queue Owner Account ID.", + ) + + region: str = Field( + title="AWS Region", + description="AWS Region.", + default="us-east-1", + ) + + batch_size: int = Field( + title="Batch Size", + description="Number of messages to retrieve from SQS.", + default=10, + le=10, + ) diff --git a/src/dispatch/plugins/dispatch_aws/plugin.py b/src/dispatch/plugins/dispatch_aws/plugin.py new file mode 100644 index 000000000000..fb6902e83cc1 --- /dev/null +++ b/src/dispatch/plugins/dispatch_aws/plugin.py @@ -0,0 +1,78 @@ +""" +.. module: dispatch.plugins.dispatchaws.plugin + :platform: Unix + :copyright: (c) 2023 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +import boto3 +import json +import logging + +from dispatch.metrics import provider as metrics_provider +from dispatch.plugins.bases import SignalConsumerPlugin +from dispatch.signal import service as signal_service +from dispatch.signal.models import SignalInstanceCreate +from dispatch.plugins.dispatch_aws.config import AWSSQSConfiguration + +from . import __version__ + +log = logging.getLogger(__name__) + + +class AWSSQSSignalConsumerPlugin(SignalConsumerPlugin): + title = "AWS SQS - Signal Consumer" + slug = "aws-sqs-signal-consumer" + description = "Uses sqs to consume signals" + version = __version__ + + author = "Netflix" + author_url = "https://github.com/netflix/dispatch.git" + + def __init__(self): + self.configuration_schema = AWSSQSConfiguration + + def consume(self, db_session, project): + client = boto3.client("sqs", region_name=self.configuration.region) + queue_url: str = client.get_queue_url( + QueueName=self.configuration.queue_name, + QueueOwnerAWSAccountId=self.configuration.queue_owner, + )["QueueUrl"] + + while True: + response = client.receive_message( + QueueUrl=queue_url, + MaxNumberOfMessages=self.configuration.batch_size, + VisibilityTimeout=40, + WaitTimeSeconds=20, + ) + if response.get("Messages") and len(response.get("Messages")) > 0: + entries = [] + for message in response["Messages"]: + try: + body = json.loads(message["Body"]) + signal_data = json.loads(body["Message"]) + + signal_instance = signal_service.create_signal_instance( + db_session=db_session, + signal_instance_in=SignalInstanceCreate( + project=project, raw=signal_data, **signal_data + ), + ) + metrics_provider.counter( + "aws-sqs-signal-consumer.signal.received", + tags={ + "signalName": signal_instance.signal.name, + "externalId": signal_instance.signal.external_id, + }, + ) + log.debug( + f"Received signal: SignalName: {signal_instance.signal.name} ExernalId: {signal_instance.signal.external_id}" + ) + entries.append( + {"Id": message["MessageId"], "ReceiptHandle": message["ReceiptHandle"]} + ) + except Exception as e: + log.exception(e) + + client.delete_message_batch(QueueUrl=queue_url, Entries=entries) diff --git a/src/dispatch/signal/exceptions.py b/src/dispatch/signal/exceptions.py new file mode 100644 index 000000000000..867412acd0d4 --- /dev/null +++ b/src/dispatch/signal/exceptions.py @@ -0,0 +1,13 @@ +from dispatch.exceptions import DispatchException + + +class SignalNotIdentifiedException(DispatchException): + pass + + +class SignalNotDefinedException(DispatchException): + pass + + +class SignalNotEnabledException(DispatchException): + pass diff --git a/src/dispatch/signal/models.py b/src/dispatch/signal/models.py index d35154a8c325..a295c02fb486 100644 --- a/src/dispatch/signal/models.py +++ b/src/dispatch/signal/models.py @@ -363,11 +363,12 @@ class AdditionalMetadata(DispatchBase): class SignalInstanceBase(DispatchBase): - project: ProjectRead + project: Optional[ProjectRead] case: Optional[CaseReadMinimal] canary: Optional[bool] = False entities: Optional[List[EntityRead]] = [] raw: dict[str, Any] + external_id: Optional[str] filter_action: SignalFilterAction = None created_at: Optional[datetime] = None diff --git a/src/dispatch/signal/scheduled.py b/src/dispatch/signal/scheduled.py deleted file mode 100644 index f09d5415f396..000000000000 --- a/src/dispatch/signal/scheduled.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -.. module: dispatch.signal.scheduled - :platform: Unix - :copyright: (c) 2022 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. -""" -import logging - -from schedule import every - -from dispatch.database.core import SessionLocal -from dispatch.decorators import scheduled_project_task, timer -from dispatch.plugin import service as plugin_service -from dispatch.project.models import Project -from dispatch.scheduler import scheduler -from dispatch.signal import flows as signal_flows - -log = logging.getLogger(__name__) - - -# TODO do we want per signal source flexibility? -@scheduler.add(every(1).minutes, name="signal-consume") -@timer -@scheduled_project_task -def consume_signals(db_session: SessionLocal, project: Project): - """Consume signals from external sources.""" - plugins = plugin_service.get_active_instances( - db_session=db_session, plugin_type="signal-consumer", project_id=project.id - ) - - if not plugins: - log.debug( - "No signals consumed. No signal-consumer plugins enabled. Project: {project.name}. Organization: {project.organization.name}" - ) - return - - for plugin in plugins: - log.debug(f"Consuming signals using signal-consumer plugin: {plugin.plugin.slug}") - signal_instances = plugin.instance.consume() - for signal_instance_data in signal_instances: - log.info(f"Attempting to process the following signal: {signal_instance_data}") - try: - signal_flows.create_signal_instance( - db_session=db_session, - project=project, - signal_instance_data=signal_instance_data, - ) - except Exception as e: - log.debug(signal_instance_data) - log.exception(e) diff --git a/src/dispatch/signal/service.py b/src/dispatch/signal/service.py index f445763af842..eadc7e0924e1 100644 --- a/src/dispatch/signal/service.py +++ b/src/dispatch/signal/service.py @@ -18,6 +18,15 @@ from dispatch.tag import service as tag_service from dispatch.workflow import service as workflow_service from dispatch.entity.models import Entity +from sqlalchemy.exc import IntegrityError +from dispatch.entity_type.models import EntityScopeEnum +from dispatch.entity import service as entity_service + +from .exceptions import ( + SignalNotDefinedException, + SignalNotEnabledException, + SignalNotIdentifiedException, +) from .models import ( Signal, @@ -109,6 +118,58 @@ def get_signal_engagement_by_name_or_raise( return signal_engagement +def create_signal_instance(*, db_session: Session, signal_instance_in: SignalInstanceCreate): + if not signal_instance_in.signal: + external_id = signal_instance_in.external_id + + # this assumes the external_ids are uuids + if external_id: + signal = ( + db_session.query(Signal).filter(Signal.external_id == external_id).one_or_none() + ) + signal_instance_in.signal = signal + else: + msg = "An externalId must be provided." + raise SignalNotIdentifiedException(msg) + + if not signal: + msg = f"No signal definition found. ExternalId: {external_id}" + raise SignalNotDefinedException(msg) + + if not signal.enabled: + msg = f"Signal definition not enabled. SignalName: {signal.name} ExternalId: {signal.external_id}" + raise SignalNotEnabledException(msg) + + try: + signal_instance = create_instance( + db_session=db_session, signal_instance_in=signal_instance_in + ) + signal_instance.signal = signal + db_session.commit() + except IntegrityError: + db_session.rollback() + signal_instance = update_instance( + db_session=db_session, signal_instance_in=signal_instance_in + ) + # Note: we can do this because it's still relatively cheap, if we add more logic here + # this will need to be moved to a background function (similar to case creation) + # fetch `all` entities that should be associated with all signal definitions + entity_types = entity_type_service.get_all( + db_session=db_session, scope=EntityScopeEnum.all + ).all() + entity_types = signal_instance.signal.entity_types + entity_types + + if entity_types: + entities = entity_service.find_entities( + db_session=db_session, + signal_instance=signal_instance, + entity_types=entity_types, + ) + signal_instance.entities = entities + db_session.commit() + return signal_instance + + def create_signal_filter( *, db_session: Session, creator: DispatchUser, signal_filter_in: SignalFilterCreate ) -> SignalFilter: @@ -451,6 +512,7 @@ def create_instance( "project", "entities", "raw", + "external_id", } ), raw=json.loads(json.dumps(signal_instance_in.raw)), From 3940a5aeb1b718da3e272bdbce8cc20eebcb7074 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 2 Oct 2023 14:05:55 -0700 Subject: [PATCH 23/24] Enhancement/clearer signal consume msg (#3828) * Adds a message to the signal consume cli * Adding multi process --- src/dispatch/cli.py | 50 ++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/dispatch/cli.py b/src/dispatch/cli.py index 0675e157cbec..1b11c602b563 100644 --- a/src/dispatch/cli.py +++ b/src/dispatch/cli.py @@ -773,38 +773,42 @@ def signals_group(): @signals_group.command("consume") -@click.argument("organization") -@click.argument("project") -@click.argument("plugin_name") -def consume_signals(organization, project, plugin_name): # TODO support multiple from one command +def consume_signals(): """Runs a continuous process that consumes signals from the specified plugin.""" + import concurrent.futures + from dispatch.common.utils.cli import install_plugins - from dispatch.database.core import refetch_db_session from dispatch.project import service as project_service - from dispatch.project.models import ProjectRead from dispatch.plugin import service as plugin_service - install_plugins() - - session = refetch_db_session(organization) + from dispatch.organization.service import get_all as get_all_organizations + from dispatch.database.core import SessionLocal, engine, sessionmaker - project = project_service.get_by_name_or_raise( - db_session=session, project_in=ProjectRead(name=project) - ) + install_plugins() + organizations = get_all_organizations(db_session=SessionLocal()) + for organization in organizations: + schema_engine = engine.execution_options( + schema_translate_map={ + None: f"dispatch_organization_{organization.slug}", + } + ) + session = sessionmaker(bind=schema_engine)() - plugins = plugin_service.get_active_instances( - db_session=session, plugin_type="signal-consumer", project_id=project.id - ) + projects = project_service.get_all(db_session=session) + for project in projects: + plugins = plugin_service.get_active_instances( + db_session=session, plugin_type="signal-consumer", project_id=project.id + ) - if not plugins: - log.debug( - "No signals consumed. No signal-consumer plugins enabled. Project: {project.name}. Organization: {project.organization.name}" - ) - return + if not plugins: + log.warning( + f"No signals consumed. No signal-consumer plugins enabled. Project: {project.name}. Organization: {project.organization.name}" + ) - for plugin in plugins: - if plugin.plugin.slug == plugin_name: - plugin.instance.consume(db_session=session, project=project) + for plugin in plugins: + log.debug(f"Consuming signals for plugin {plugin.plugin.slug}") + with concurrent.futures.ProcessPoolExecutor() as executor: + executor.submit(plugin.instance.consume, session, project) @signals_group.command("process") From d277d5b4f1c7e8965fb01f5417ea6d7461799c01 Mon Sep 17 00:00:00 2001 From: Will Sheldon <114631109+wssheldon@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:00:37 -0700 Subject: [PATCH 24/24] Passes the current state of `experimental_features` to `UserApi` (#3829) * Passes the current state of experimental_features to UserApi * Use a better variable name for currentUserExperimentalFeatures --- src/dispatch/static/dispatch/src/components/AppToolbar.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dispatch/static/dispatch/src/components/AppToolbar.vue b/src/dispatch/static/dispatch/src/components/AppToolbar.vue index 78aaad9c1a61..b0f47c660b6d 100644 --- a/src/dispatch/static/dispatch/src/components/AppToolbar.vue +++ b/src/dispatch/static/dispatch/src/components/AppToolbar.vue @@ -196,7 +196,11 @@ export default { UserApi.getUserInfo() .then((response) => { let userId = response.data.id - UserApi.update(userId, { id: userId, experimental_features: this.experimental_features }) + let newUserExperimentalFeatures = this.currentUser().experimental_features + UserApi.update(userId, { + id: userId, + experimental_features: newUserExperimentalFeatures, + }) }) .catch((error) => { console.error("Error occurred while updating experimental features: ", error)